pax_global_header00006660000000000000000000000064143417747460014533gustar00rootroot0000000000000052 comment=2a97be5d87741386a2006b115bb9bf356239eb0c python-oracledb-1.2.1/000077500000000000000000000000001434177474600146265ustar00rootroot00000000000000python-oracledb-1.2.1/.github/000077500000000000000000000000001434177474600161665ustar00rootroot00000000000000python-oracledb-1.2.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001434177474600203515ustar00rootroot00000000000000python-oracledb-1.2.1/.github/ISSUE_TEMPLATE/announcements.md000066400000000000000000000006361434177474600235550ustar00rootroot00000000000000--- name: Announcements about: Use this if you are sharing something interesting title: '' labels: announcement assignees: '' --- python-oracledb-1.2.1/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000027741434177474600230550ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- 1. What versions are you using? 2. Is it an error or a hang or a crash? 3. What error(s) or behavior you are seeing? 4. Does your application call init_oracle_client()? 5. Include a runnable Python script that shows the problem. python-oracledb-1.2.1/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000000341434177474600223360ustar00rootroot00000000000000blank_issues_enabled: false python-oracledb-1.2.1/.github/ISSUE_TEMPLATE/documentation-and-example-improvements.md000066400000000000000000000010731434177474600304640ustar00rootroot00000000000000--- name: Documentation and Example Improvements about: Use this to suggest changes to documentation and examples title: '' labels: enhancement assignees: '' --- 1. What is the link to the documentation section that needs improving? 2. Describe the confusion 3. Suggest changes that would help python-oracledb-1.2.1/.github/ISSUE_TEMPLATE/enhancement-requests.md000066400000000000000000000012001434177474600250220ustar00rootroot00000000000000--- name: Enhancement Requests about: Use this for enhancement requests title: '' labels: enhancement assignees: '' --- 1. Describe your new request in detail 2. Give supporting information about tools and operating systems. Give relevant product version numbers python-oracledb-1.2.1/.github/ISSUE_TEMPLATE/general-questions-and-runtime-problems.md000066400000000000000000000005521434177474600304040ustar00rootroot00000000000000--- name: Questions and Runtime Problems about: For general python-oracledb questions title: '' labels: question assignees: '' --- python-oracledb-1.2.1/.github/ISSUE_TEMPLATE/installation-questions.md000066400000000000000000000012041434177474600254210ustar00rootroot00000000000000--- name: Installation Problems about: Use this for python-oracledb installation questions title: '' labels: install & configuration assignees: '' --- python-oracledb-1.2.1/.github/SECURITY.md000066400000000000000000000033161434177474600177620ustar00rootroot00000000000000# Reporting security vulnerabilities Oracle values the independent security research community and believes that responsible disclosure of security vulnerabilities helps us ensure the security and privacy of all our users. Please do NOT raise a GitHub Issue to report a security vulnerability. If you believe you have found a security vulnerability, please submit a report to [secalert_us@oracle.com][1] preferably with a proof of concept. Please review some additional information on [how to report security vulnerabilities to Oracle][2]. We encourage people who contact Oracle Security to use email encryption using [our encryption key][3]. We ask that you do not use other channels or contact the project maintainers directly. Non-vulnerability related security issues including ideas for new or improved security features are welcome on GitHub Issues. ## Security updates, alerts and bulletins Security updates will be released on a regular cadence. Many of our projects will typically release security fixes in conjunction with the [Oracle Critical Patch Update][3] program. Additional information, including past advisories, is available on our [security alerts][4] page. ## Security-related information We will provide security related information such as a threat model, considerations for secure use, or any known security issues in our documentation. Please note that labs and sample code are intended to demonstrate a concept and may not be sufficiently hardened for production use. [1]: mailto:secalert_us@oracle.com [2]: https://www.oracle.com/corporate/security-practices/assurance/vulnerability/reporting.html [3]: https://www.oracle.com/security-alerts/encryptionkey.html [4]: https://www.oracle.com/security-alerts/ python-oracledb-1.2.1/.github/SUPPORT.md000066400000000000000000000015471434177474600176730ustar00rootroot00000000000000# Python python-oracledb Support python-oracledb is an Open Source project, so do some searching and reading before asking questions. ## python-oracledb Installation issues Read the [Installation instructions](http://python-oracledb.readthedocs.io/en/latest/installation.html) ## SQL and PL/SQL Questions Ask SQL and PL/SQL questions at [AskTOM](https://asktom.oracle.com/) Try out SQL and find code snippets on our hosted database with [LIVE SQL](https://livesql.oracle.com/) ## Database and other Oracle Issues Ask Database and other Oracle issues at [Oracle Communities](https://community.oracle.com/hub/). ## python-oracledb Documentation The python-oracledb documentation is [here](http://python-oracledb.readthedocs.io/en/latest/) ## Got a python-oracledb question? Start a [Discussion](https://github.com/oracle/python-oracledb/discussions) on GitHub. python-oracledb-1.2.1/.github/pull_request_template.md000066400000000000000000000011351434177474600231270ustar00rootroot00000000000000Thanks for contributing! Before submitting PRs for python-oracledb you must have your signed *Oracle Contributor Agreement* accepted. See https://oca.opensource.oracle.com If the problem solved is small, you may find it easier to open an Issue describing the problem and its cause so we can create the fix. The bottom of your commit message must have the following line using your name and e-mail address as it appears in the OCA Signatories list. ``` Signed-off-by: Your Name ``` This can be automatically added to pull requests by committing with: ``` git commit --signoff ```` python-oracledb-1.2.1/.github/stale.yml000066400000000000000000000015341434177474600200240ustar00rootroot00000000000000# https://probot.github.io/apps/stale/ # Number of days of inactivity before an issue becomes stale daysUntilStale: 30 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - pinned - enhancement - bug - announcement - OCA accepted # Label to use when marking an issue as stale staleLabel: inactive # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as inactive because it has not been updated recently. It will be closed if no further activity occurs. Thank you for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: > This issue has been automatically closed because it has not been updated for a month. python-oracledb-1.2.1/.gitignore000066400000000000000000000001161434177474600166140ustar00rootroot00000000000000__pycache__/ .eggs/ .tox/ *.egg-info/ build/ dist/ doc/build src/oracledb/*.c python-oracledb-1.2.1/.gitmodules000066400000000000000000000001431434177474600170010ustar00rootroot00000000000000[submodule "src/oracledb/impl/thick/odpi"] path = src/oracledb/impl/thick/odpi url = ../odpi.git python-oracledb-1.2.1/.readthedocs.yaml000066400000000000000000000004441434177474600200570ustar00rootroot00000000000000# required version: 2 build: os: ubuntu-20.04 tools: python: "3.9" # Build documentation in the doc/src directory with Sphinx sphinx: configuration: doc/src/conf.py # declare Python requirements required to build docs python: install: - requirements: doc/requirements.txt python-oracledb-1.2.1/CONTRIBUTING.md000066400000000000000000000035241434177474600170630ustar00rootroot00000000000000# Contributing We welcome your contributions! There are multiple ways to contribute. ## Issues For bugs or enhancement requests, please file a GitHub issue unless it's security related. When filing a bug remember that the better written the bug is, the more likely it is to be fixed. If you think you've found a security vulnerability, do not raise a GitHub issue and follow the instructions on our [Security Policy](./.github/SECURITY.md). ## Contributing Code To get started, you will need to sign the [Oracle Contributor Agreement](https://oca.opensource.oracle.com) (OCA). For pull requests to be accepted, the bottom of your commit message must have the following line using the name and e-mail address you used for the OCA. ```text Signed-off-by: Your Name ``` This can be automatically added to pull requests by committing with: ```text git commit --signoff ``` Only pull requests from committers that can be verified as having signed the OCA can be accepted. ### Pull request process 1. Fork this repository 1. Create a branch in your fork to implement the changes. We recommend using the issue number as part of your branch name, e.g. `1234-fixes` 1. Ensure that any documentation is updated with the changes that are required by your fix. 1. Ensure that any samples are updated if the base image has been changed. 1. Submit the pull request. *Do not leave the pull request blank*. Explain exactly what your changes are meant to do and provide simple steps on how to validate your changes. Ensure that you reference the issue you created as well. 1. We will review your PR before it is merged. ## Code of Conduct Follow the [Golden Rule](https://en.wikipedia.org/wiki/Golden_Rule). If you'd like more specific guidelines see the [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/1/4/code-of-conduct/) python-oracledb-1.2.1/LICENSE.txt000066400000000000000000000304321434177474600164530ustar00rootroot00000000000000Copyright (c) 2016, 2022 Oracle and/or its affiliates. This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. If you elect to accept the software under the Apache License, Version 2.0, the following applies: Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. The Universal Permissive License (UPL), Version 1.0 =================================================== Subject to the condition set forth below, permission is hereby granted to any person obtaining a copy of this software, associated documentation and/or data (collectively the "Software"), free of charge and under any and all copyright rights in the Software, and any and all patent rights owned or freely licensable by each licensor hereunder covering either (i) the unmodified Software as contributed to or provided by such licensor, or (ii) the Larger Works (as defined below), to deal in both (a) the Software, and (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if one is included with the Software (each a "Larger Work" to which the Software is contributed by such licensors), without restriction, including without limitation the rights to copy, create derivative works of, display, perform, and distribute the Software and make, use, sell, offer for sale, import, export, have made, and have sold the Software and the Larger Work(s), and to sublicense the foregoing rights on either these or other terms. This license is subject to the following condition: The above copyright notice and either this complete permission notice or at a minimum a reference to the UPL must be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Apache License ============== Version 2.0, January 2004 TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. **Definitions**. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. **Grant of Copyright License.** Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. **Grant of Patent License.** Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. **Redistribution.** You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and 2. You must cause any modified files to carry prominent notices stating that You changed the files; and 3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. **Submission of Contributions.** Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. **Trademarks.** This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. **Disclaimer of Warranty.** Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. **Limitation of Liability.** In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. **Accepting Warranty or Additional Liability.** While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS python-oracledb-1.2.1/MANIFEST.in000066400000000000000000000006161434177474600163670ustar00rootroot00000000000000include MANIFEST.in include pyproject.toml include tox.ini include *.txt recursive-include src/oracledb *.pxd recursive-include src/oracledb *.pxi recursive-include src/oracledb *.pyx recursive-include src/oracledb/impl/thick/odpi *.c recursive-include src/oracledb/impl/thick/odpi *.h prune src/oracledb/impl/thick/odpi/test prune src/oracledb/impl/thick/odpi/samples recursive-include tests *.py python-oracledb-1.2.1/NOTICE.txt000066400000000000000000000000701434177474600163450ustar00rootroot00000000000000Copyright (c) 2016, 2022, Oracle and/or its affiliates. python-oracledb-1.2.1/README.md000066400000000000000000000065461434177474600161200ustar00rootroot00000000000000# python-oracledb python-oracledb is a [Python programming language][python] extension module allowing Python programs to connect to [Oracle Database][oracledb]. It is the renamed, new major release of the popular cx_Oracle driver. The module conforms to the [Python Database API 2.0 specification][pep249] with a considerable number of additions and a couple of minor exclusions, see the [feature list][features]. ## Installation Run `python -m pip install oracledb` See [python-oracledb Installation][installation]. ## Dependencies and Interoperability - Python versions 3.6 through 3.11. Prebuilt packages are available on Windows for Python 3.7 or later, on macOS for Python 3.7 or later, and on Linux for Python 3.6 or later. Source code is also available. - Oracle Client libraries are *optional*. **Thin mode**: By default python-oracledb runs in a 'Thin' mode which connects directly to Oracle Database. **Thick mode**: Some advanced Oracle Database functionality is currently only available when optional Oracle Client libraries are loaded by python-oracledb. Libraries are available in the free [Oracle Instant Client][instantclient] packages. Python-oracledb can use Oracle Client libraries 11.2 through 21c. - Oracle Database **Thin mode**: Oracle Database 12.1 (or later) is required. **Thick mode**: Oracle Database 11.2 (or later) is required, depending on the Oracle Client library version. Oracle Database's standard client-server version interoperability allows connection to both older and newer databases. For example when python-oracledb uses Oracle Client 19c libraries, then it can connect to Oracle Database 11.2 or later. ## Documentation See the [python-oracledb Documentation][documentation] and [Release Notes][relnotes]. ## Samples Examples can be found in the [/samples][samples] directory and the [Python and Oracle Database Tutorial][tutorial]. ## Help Questions can be asked in [Github Discussions][ghdiscussions]. Problem reports can be raised in [GitHub Issues][ghissues]. ## Tests See [/tests][tests] ## Contributing See [CONTRIBUTING](https://github.com/oracle/python-oracledb/blob/main/CONTRIBUTING.md) ## License See [LICENSE][license], [THIRD_PARTY_LICENSES][tplicense], and [NOTICE][notice]. [python]: https://www.python.org/ [oracledb]: https://www.oracle.com/database/ [instantclient]: https://www.oracle.com/database/technologies/instant-client.html [pep249]: https://peps.python.org/pep-0249/ [documentation]: http://python-oracledb.readthedocs.io [relnotes]: https://python-oracledb.readthedocs.io/en/latest/release_notes.html [license]: https://github.com/oracle/python-oracledb/blob/main/LICENSE.txt [tplicense]: https://github.com/oracle/python-oracledb/blob/main/THIRD_PARTY_LICENSES.txt [notice]: https://github.com/oracle/python-oracledb/blob/main/NOTICE.txt [tutorial]: https://oracle.github.io/python-oracledb/samples/tutorial/Python-and-Oracle-Database-The-New-Wave-of-Scripting.html [ghdiscussions]: https://github.com/oracle/python-oracledb/discussions [ghissues]: https://github.com/oracle/python-oracledb/issues [tests]: https://github.com/oracle/python-oracledb/tree/main/tests [samples]: https://github.com/oracle/python-oracledb/tree/main/samples [installation]: https://python-oracledb.readthedocs.io/en/latest/user_guide/installation.html [features]: https://oracle.github.io/python-oracledb/#features python-oracledb-1.2.1/README.txt000066400000000000000000000002471434177474600163270ustar00rootroot00000000000000Please see the python-oracledb home page for links to documentation, installation instructions, and source code: https://oracle.github.io/python-oracledb/index.html python-oracledb-1.2.1/THIRD_PARTY_LICENSES.txt000066400000000000000000000667101434177474600205170ustar00rootroot00000000000000The following attribution text was taken from Component Cython Version 0.24.1 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ------------------------------------------------------------------------------- LICENSE: https://github.com/pyca/cryptography/blob/3.4.x/LICENSE This software is made available under the terms of *either* of the licenses found in LICENSE.APACHE or LICENSE.BSD. Contributions to cryptography are made under the terms of *both* these licenses. The code used in the OS random engine is derived from CPython, and is licensed under the terms of the PSF License Agreement. ___________________________________________________________________________ LICENSE.APACHE: https://github.com/pyca/cryptography/blob/3.4.x/LICENSE.APACHE Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ___________________________________________________________________________ LICENSE.BSD: https://github.com/pyca/cryptography/blob/3.4.x/LICENSE.BSD Copyright (c) Individual contributors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of PyCA Cryptography nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ============================================================================================= Fourth-party dependencies: six LICENSE - MIT: https://github.com/benjaminp/six/blob/1.15.0/LICENSE Copyright (c) 2010-2020 Benjamin Peterson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ___________________________________________________________________________________________ cffi LICENSE - MIT: https://github.com/python-cffi/release-doc/blob/master/LICENSE MIT License Copyright (c) 2020 CFFI, Python's C Foreign Function Interface Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ___________________________________________________________________________________________ pycparser LICENSE: https://github.com/eliben/pycparser/blob/master/LICENSE pycparser -- A C parser in Python Copyright (c) 2008-2017, Eli Bendersky All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Eli Bendersky nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. python-oracledb-1.2.1/doc/000077500000000000000000000000001434177474600153735ustar00rootroot00000000000000python-oracledb-1.2.1/doc/Makefile000066400000000000000000000007041434177474600170340ustar00rootroot00000000000000# Makefile to generate python-oracledb documentation using Sphinx SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = src BUILDDIR = build .PHONY: html html: @$(SPHINXBUILD) -M html $(SOURCEDIR) $(BUILDDIR) $(SPHINXOPTS) .PHONY: epub epub: @$(SPHINXBUILD) -M epub $(SOURCEDIR) $(BUILDDIR) $(SPHINXOPTS) .PHONY: pdf pdf: @$(SPHINXBUILD) -M latexpdf $(SOURCEDIR) $(BUILDDIR) $(SPHINXOPTS) .PHONY: clean clean: rm -rf $(BUILDDIR)/* python-oracledb-1.2.1/doc/README.md000066400000000000000000000021101434177474600166440ustar00rootroot00000000000000Sphinx is used to generate the HTML for the python-oracledb documentation. The generated python-oracledb documentation is at https://python-oracledb.readthedocs.io/ This directory contains the documentation source. It is written using reST (re-Structured Text). The source files are processed using Sphinx and can be turned into HTML, PDF or ePub documentation. If you wish to build documentation yourself, install Sphinx and the Read the Docs theme. Sphinx is available on many Linux distributions as a pre-built package. You can also install Sphinx and the Read the Docs theme using the Python package manager "pip", for example:: python -m pip install -r requirements.txt For more information on Sphinx, please visit this page: http://www.sphinx-doc.org Once Sphinx is installed, the supplied Makefile can be used to build the different targets, for example to build the HTML documentation, run:: make To make ePub documentation, run:: make epub To make PDF documentation, run:: make pdf The program ``latexmk`` may be required by Sphinx to generate PDF output. python-oracledb-1.2.1/doc/requirements.txt000066400000000000000000000000371434177474600206570ustar00rootroot00000000000000sphinx>=4.2.0 sphinx-rtd-theme python-oracledb-1.2.1/doc/src/000077500000000000000000000000001434177474600161625ustar00rootroot00000000000000python-oracledb-1.2.1/doc/src/.static/000077500000000000000000000000001434177474600175275ustar00rootroot00000000000000python-oracledb-1.2.1/doc/src/.static/custom.css000066400000000000000000000002331434177474600215510ustar00rootroot00000000000000/* Added code to display tables without horizontal scrollbars */ .wy-table-responsive table td, .wy-table-responsive table th { white-space: normal; } python-oracledb-1.2.1/doc/src/_ext/000077500000000000000000000000001434177474600171215ustar00rootroot00000000000000python-oracledb-1.2.1/doc/src/_ext/oracle_deprecated.py000066400000000000000000000045601434177474600231250ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # oracle_deprecated.py # # Overrides the 'deprecated' directive so that it can a componment name can be # used in conjunction with the version. #------------------------------------------------------------------------------ from docutils import nodes from docutils.parsers.rst import Directive from sphinx import addnodes class DriverDeprecated(Directive): has_content = True required_arguments = 1 optional_arguments = 0 final_argument_whitespace = True def run(self): node = addnodes.versionmodified() node.document = self.state.document node['type'] = self.name node['version'] = self.arguments[0] text = 'Deprecated since {}.'.format(self.arguments[0]) classes = ['versionmodified', 'deprecated'] para = nodes.paragraph('', '', nodes.inline('', text, classes=classes), translatable=False) node.append(para) ret: List[Node] = [node] return ret def setup(app): app.add_directive("deprecated", DriverDeprecated, override=True) return { 'version': '0.1', 'parallel_read_safe': True, 'parallel_write_safe': True, } python-oracledb-1.2.1/doc/src/_ext/table_with_summary.py000066400000000000000000000056001434177474600233730ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # table_with_summary.py # # Defines a directive (list-table-with-summary) that adds support for a summary # option on top of the regular "list-table" directive. #------------------------------------------------------------------------------ import sphinx from docutils.parsers.rst import directives from docutils.parsers.rst.directives import tables from packaging import version class ListTableWithSummary(tables.ListTable): option_spec = {'summary': directives.unchanged} option_spec.update(tables.ListTable.option_spec) def run(self): result = super().run() summary = self.options.get('summary') if summary: table_node = result[0] table_node["summary"] = summary return result class HTMLTranslator(sphinx.writers.html5.HTML5Translator): def visit_table(self, node): if version.parse(sphinx.__version__) > version.parse('4.2.0'): self._table_row_indices = [0] else: self._table_row_index = 0 atts = {} classes = [cls.strip(' \t\n') \ for cls in self.settings.table_style.split(',')] classes.insert(0, "docutils") # compat # set align-default if align not specified to give a default style classes.append('align-%s' % node.get('align', 'default')) if 'width' in node: atts['style'] = 'width: %s' % node['width'] if 'summary' in node: atts['summary'] = node['summary'] tag = self.starttag(node, 'table', CLASS=' '.join(classes), **atts) self.body.append(tag) def setup(app): app.add_directive('list-table-with-summary', ListTableWithSummary) app.set_translator("html", HTMLTranslator, override=True) python-oracledb-1.2.1/doc/src/api_manual/000077500000000000000000000000001434177474600202705ustar00rootroot00000000000000python-oracledb-1.2.1/doc/src/api_manual/aq.rst000066400000000000000000000311171434177474600214260ustar00rootroot00000000000000.. _aq: *************************** API: Advanced Queuing (AQ) *************************** See :ref:`aqusermanual` for more information about using AQ in python-oracledb. .. note:: All of these objects are extensions to the DB API. .. _queue: Queue Objects ============= These objects are created using the :meth:`Connection.queue()` method and are used to enqueue and dequeue messages. Queue Methods ------------- .. method:: Queue.deqmany(maxMessages) Dequeues up to the specified number of messages from the queue and returns a list of these messages. Each element of the returned list is a :ref:`message property` object. For consistency and compliance with the PEP 8 naming style, the name of the method was changed from `deqMany()`. The old name will continue to work for a period of time. .. method:: Queue.deqone() Dequeues at most one message from the queue. If a message is dequeued, it will be a :ref:`message property` object; otherwise, it will be the value None. For consistency and compliance with the PEP 8 naming style, the name of the method was changed from `deqOne()`. The old name will continue to work for a period of time. .. method:: Queue.enqmany(messages) Enqueues multiple messages into the queue. The ``messages`` parameter must be a sequence containing :ref:`message property ` objects which have all had their payload attribute set to a value that the queue supports. .. warning:: Calling this function in parallel on different connections acquired from the same pool may fail due to Oracle bug 29928074. Ensure that this function is not run in parallel, use standalone connections or connections from different pools, or make multiple calls to :meth:`Queue.enqone()` instead. The function :meth:`Queue.deqmany()` call is not affected. For consistency and compliance with the PEP 8 naming style, the name of the method was changed from `enqMany()`. The old name will continue to work for a period of time. .. method:: Queue.enqone(message) Enqueues a single message into the queue. The message must be a :ref:`message property` object which has had its payload attribute set to a value that the queue supports. For consistency and compliance with the PEP 8 naming style, the name of the method was changed from `enqOne()`. The old name will continue to work for a period of time. Queue Attributes ---------------- .. attribute:: Queue.connection This read-only attribute returns a reference to the connection object on which the queue was created. .. attribute:: Queue.deqoptions This read-only attribute returns a reference to the :ref:`options ` that will be used when dequeuing messages from the queue. For consistency and compliance with the PEP 8 naming style, the name of the attribute was changed from `deqOptions`. The old name will continue to work for a period of time. .. attribute:: Queue.enqoptions This read-only attribute returns a reference to the :ref:`options ` that will be used when enqueuing messages into the queue. For consistency and compliance with the PEP 8 naming style, the name of the attribute was changed from `enqOptions`. The old name will continue to work for a period of time. .. attribute:: Queue.name This read-only attribute returns the name of the queue. .. attribute:: Queue.payload_type This read-only attribute returns the object type for payloads that can be enqueued and dequeued. If using a JSON queue, this returns the value ``"JSON"``. If using a raw queue, this returns the value ``None``. For consistency and compliance with the PEP 8 naming style, the name of the attribute was changed from `payloadType`. The old name will continue to work for a period of time. .. _deqoptions: Dequeue Options =============== .. note:: These objects are used to configure how messages are dequeued from queues. An instance of this object is found in the attribute :attr:`Queue.deqOptions`. .. attribute:: DeqOptions.condition This read-write attribute specifies a boolean expression similar to the where clause of a SQL query. The boolean expression can include conditions on message properties, user data properties and PL/SQL or SQL functions. The default is to have no condition specified. .. attribute:: DeqOptions.consumername This read-write attribute specifies the name of the consumer. Only messages matching the consumer name will be accessed. If the queue is not set up for multiple consumers this attribute should not be set. The default is to have no consumer name specified. .. attribute:: DeqOptions.correlation This read-write attribute specifies the correlation identifier of the message to be dequeued. Special pattern-matching characters, such as the percent sign (%) and the underscore (_), can be used. If multiple messages satisfy the pattern, the order of dequeuing is indeterminate. The default is to have no correlation specified. .. attribute:: DeqOptions.deliverymode This write-only attribute specifies what types of messages should be dequeued. It should be one of the values :data:`~oracledb.MSG_PERSISTENT` (default), :data:`~oracledb.MSG_BUFFERED` or :data:`~oracledb.MSG_PERSISTENT_OR_BUFFERED`. .. attribute:: DeqOptions.mode This read-write attribute specifies the locking behaviour associated with the dequeue operation. It should be one of the values :data:`~oracledb.DEQ_BROWSE`, :data:`~oracledb.DEQ_LOCKED`, :data:`~oracledb.DEQ_REMOVE` (default), or :data:`~oracledb.DEQ_REMOVE_NODATA`. .. attribute:: DeqOptions.msgid This read-write attribute specifies the identifier of the message to be dequeued. The default is to have no message identifier specified. .. attribute:: DeqOptions.navigation This read-write attribute specifies the position of the message that is retrieved. It should be one of the values :data:`~oracledb.DEQ_FIRST_MSG`, :data:`~oracledb.DEQ_NEXT_MSG` (default), or :data:`~oracledb.DEQ_NEXT_TRANSACTION`. .. attribute:: DeqOptions.transformation This read-write attribute specifies the name of the transformation that must be applied after the message is dequeued from the database but before it is returned to the calling application. The transformation must be created using dbms_transform. The default is to have no transformation specified. .. attribute:: DeqOptions.visibility This read-write attribute specifies the transactional behavior of the dequeue request. It should be one of the values :data:`~oracledb.DEQ_ON_COMMIT` (default) or :data:`~oracledb.DEQ_IMMEDIATE`. This attribute is ignored when using the :data:`~oracledb.DEQ_BROWSE` mode. Note the value of :attr:`~Connection.autocommit` is always ignored. .. attribute:: DeqOptions.wait This read-write attribute specifies the time to wait, in seconds, for a message matching the search criteria to become available for dequeuing. One of the values :data:`~oracledb.DEQ_NO_WAIT` or :data:`~oracledb.DEQ_WAIT_FOREVER` can also be used. The default is :data:`~oracledb.DEQ_WAIT_FOREVER`. .. _enqoptions: Enqueue Options =============== .. note:: These objects are used to configure how messages are enqueued into queues. An instance of this object is found in the attribute :attr:`Queue.enqOptions`. .. attribute:: EnqOptions.deliverymode This write-only attribute specifies what type of messages should be enqueued. It should be one of the values :data:`~oracledb.MSG_PERSISTENT` (default) or :data:`~oracledb.MSG_BUFFERED`. .. attribute:: EnqOptions.transformation This read-write attribute specifies the name of the transformation that must be applied before the message is enqueued into the database. The transformation must be created using dbms_transform. The default is to have no transformation specified. .. attribute:: EnqOptions.visibility This read-write attribute specifies the transactional behavior of the enqueue request. It should be one of the values :data:`~oracledb.ENQ_ON_COMMIT` (default) or :data:`~oracledb.ENQ_IMMEDIATE`. Note the value of :attr:`~Connection.autocommit` is ignored. .. _msgproperties: Message Properties ================== .. note:: These objects are used to identify the properties of messages that are enqueued and dequeued in queues. They are created by the method :meth:`Connection.msgproperties()`. They are used by the methods :meth:`Queue.enqone()` and :meth:`Queue.enqmany()` and returned by the methods :meth:`Queue.deqone()` and :meth:`Queue.deqmany()`. .. attribute:: MessageProperties.attempts This read-only attribute specifies the number of attempts that have been made to dequeue the message. .. attribute:: MessageProperties.correlation This read-write attribute specifies the correlation used when the message was enqueued. .. attribute:: MessageProperties.delay This read-write attribute specifies the number of seconds to delay an enqueued message. Any integer is acceptable but the constant :data:`~oracledb.MSG_NO_DELAY` can also be used indicating that the message is available for immediate dequeuing. .. attribute:: MessageProperties.deliverymode This read-only attribute specifies the type of message that was dequeued. It will be one of the values :data:`~oracledb.MSG_PERSISTENT` or :data:`~oracledb.MSG_BUFFERED`. .. attribute:: MessageProperties.enqtime This read-only attribute specifies the time that the message was enqueued. .. attribute:: MessageProperties.exceptionq This read-write attribute specifies the name of the queue to which the message is moved if it cannot be processed successfully. Messages are moved if the number of unsuccessful dequeue attempts has exceeded the maximum number of retries or if the message has expired. All messages in the exception queue are in the :data:`~oracledb.MSG_EXPIRED` state. The default value is the name of the exception queue associated with the queue table. .. attribute:: MessageProperties.expiration This read-write attribute specifies, in seconds, how long the message is available for dequeuing. This attribute is an offset from the delay attribute. Expiration processing requires the queue monitor to be running. Any integer is accepted but the constant :data:`~oracledb.MSG_NO_EXPIRATION` can also be used indicating that the message never expires. .. attribute:: MessageProperties.msgid This read-only attribute specifies the id of the message in the last queue that enqueued or dequeued the message. If the message has never been dequeued or enqueued, the value will be `None`. .. attribute:: MessageProperties.payload This read-write attribute identifies the payload that will be enqueued or the payload that was dequeued when using a :ref:`queue `. When enqueuing, the value is checked to ensure that it conforms to the type expected by that queue. For RAW queues, the value can be a bytes object or a string. If the value is a string it will first be converted to bytes by encoding in the encoding identified by the attribute :attr:`Connection.encoding`. .. attribute:: MessageProperties.priority This read-write attribute specifies the priority of the message. A smaller number indicates a higher priority. The priority can be any integer, including negative numbers. The default value is zero. .. attribute:: MessageProperties.state This read-only attribute specifies the state of the message at the time of the dequeue. It will be one of the values :data:`~oracledb.MSG_WAITING`, :data:`~oracledb.MSG_READY`, :data:`~oracledb.MSG_PROCESSED` or :data:`~oracledb.MSG_EXPIRED`. .. attribute:: Messageproperties.recipient This read-write attribute specifies a list of recipient names that can be associated with a message at the time of enqueuing the message. This allows a limited set of recipients to dequeue each message. The recipient list associated with the message overrides the queue subscriber list, if there is one. The recipient names need not be in the subscriber list but can be, if desired. To dequeue a message, the consumername attribute can be set to one of the recipient names. The original message recipient list is not available on dequeued messages. All recipients have to dequeue a message before it gets removed from the queue. python-oracledb-1.2.1/doc/src/api_manual/connect_param.rst000066400000000000000000000277361434177474600236520ustar00rootroot00000000000000.. _connparam: ************************** API: ConnectParams Objects ************************** .. note:: This object is an extension to the DB API. These objects are created by :meth:`oracledb.ConnectParams()`. See :ref:`usingconnparams` for more information. .. _connparamsmeth: ConnectParams Methods ===================== .. method:: ConnectParams.copy() Creates a copy of the ConnectParams instance and returns it. .. method:: ConnectParams.get_connect_string() Returns the connection string associated with the ConnectParams instance. .. method:: ConnectParams.parse_connect_string(connect_string) Parses the connect string into its components and stores the parameters. The ``connect string`` parameter can be an Easy Connect string, name-value pairs, or a simple alias which is looked up in ``tnsnames.ora``. Parameters that are found in the connect string override any currently stored values. .. method:: ConnectParams.set(user=None, proxy_user=None, password=None, \ newpassword=None, wallet_password=None, access_token=None, host=None, \ port=None, protocol=None, https_proxy=None, https_proxy_port=None, service_name=None, \ sid=None, server_type=None, cclass=None, purity=None, expire_time=None, retry_count=None, \ retry_delay=None, tcp_connect_timeout=None, ssl_server_dn_match=None, \ ssl_server_cert_dn=None, wallet_location=None, events=None, externalauth=None, \ mode=None, disable_oob=None, stmtcachesize=None, edition=None, tag=None, \ matchanytag=None, config_dir=None, appcontext=[], shardingkey=[], supershardingkey=[], \ debug_jdwp=None, handle=None) Sets one or more of the parameters. .. _connparamsattr: ConnectParams Attributes ======================== .. attribute:: ConnectParams.appcontext This read-only attribute is a list that specifies the application context used by the connection. It is a list of 3-tuples that includes the namespace, name, and value. Each entry in the tuple is a string. This attribute is only supported in the python-oracledb Thick mode. .. attribute:: ConnectParams.cclass This read-only attribute is a string that specifies the connection class to use for Database Resident Connection Pooling (DRCP). This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: ConnectParams.config_dir This read-only attribute is a string that identifies the directory in which the configuration files such as tnsnames.ora are found. The default is the value of :attr:`defaults.config_dir`. This attribute is only supported in the python-oracledb Thin mode. For the python-oracledb Thick mode, use the ``config_dir`` parameter of :func:`oracledb.init_oracle_client`. .. attribute:: ConnectParams.debug_jdwp This read-only attribute is a string with the format "host=;port=" that specifies the host and port of the PL/SQL debugger. This allows the Java Debug Wire Protocol (JDWP) to debug the PL/SQL code invoked by the python-oracledb driver. The default value is the value of the environment variable ``ORA_DEBUG_JDWP``. This attribute is only supported in the python-oracledb Thin mode. For the python-oracledb Thick mode, set the ``ORA_DEBUG_JDWP`` environment variable which has the same syntax. See :ref:`applntracing` for more information. .. attribute:: ConnectParams.disable_oob This read-only attribute is a boolean that indicates whether out-of-band breaks should be disabled. The default value is False. Note that this value has no effect on Windows, which does not support this functionality. This attribute is only supported in the python-oracledb Thin mode. For the python-oracledb Thick mode, set the equivalent option in a ``sqlnet.ora`` file. .. attribute:: ConnectParams.edition This read-only attribute is a string that specifies the edition to use for the connection. This attribute cannot be used simultaneously with the :attr:`ConnectParams.cclass` attribute. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: ConnectParams.events This read-only attribute is a boolean that specifies whether the events mode should be enabled. This attribute is needed for continuous query notification (CQN) and high availability event notifications. The default value is False. This attribute is only supported in the python-oracledb Thick mode. .. attribute:: ConnectParams.expire_time This read-only attribute is an integer that returns the number of minutes between the sending of keepalive probes. The default value is 0. If this attribute is set to a value greater than zero, it enables keepalive. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: ConnectParams.externalauth This read-only attribute is a boolean that specifies whether external authentication should be used. The default value is False. For standalone connections, external authentication occurs when the ``user`` and ``password`` attributes are not used. If these attributes, are not used, you can optionally set the ``externalauth`` attribute to True, which may aid code auditing. This attribute is only supported in the python-oracledb Thick mode. .. attribute:: ConnectParams.host This read-only attribute is a string that returns the name or IP address of the machine hosting the database. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: ConnectParams.https_proxy This read-only attribute is a string that returns the name or IP address of a proxy host that is to be used for tunneling secure connections. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: ConnectParams.https_proxy_port This read-only attribute is an integer that returns the port to be used to communicate with the proxy host. The default value is 0. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: ConnectParams.matchanytag This read-only attribute is a boolean that specifies whether any tag can be used when acquiring a connection from the pool. The default value is False. This attribute is only supported in the python-oracledb Thick mode. .. attribute:: ConnectParams.mode This read-only attribute is an integer that specifies the authorization mode to use. The default value is :data:`~oracledb.AUTH_MODE_DEFAULT`. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: ConnectParams.port This read-only attribute is an integer that returns the port number on which the database listener is listening. The default value is 1521. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: ConnectParams.protocol This read-only attribute is a string that indicates whether unencrypted network traffic or encrypted network traffic (TLS) is used and it can have the value tcp or tcps. The default value is tcp. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: ConnectParams.proxy_user This read-only attribute is a string that specifies the name of the proxy user to connect to. If this value is not specified, then it will be parsed out of the user if the user attribute is in the form "user[proxy_user]". This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: ConnectParams.purity This read-only attribute is an integer that returns the purity used for DRCP. When the value of this attribute is :attr:`oracledb.PURITY_DEFAULT`, then any standalone connection will use :attr:`oracledb.PURITY_NEW` and any pooled connection will use :attr:`oracledb.PURITY_SELF`. The default value is :data:`~oracledb.PURITY_DEFAULT`. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: ConnectParams.retry_count This read-only attribute is an integer that returns the number of times that a connection attempt should be retried before the attempt is terminated. The default value is 0. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: ConnectParams.retry_delay This read-only attribute is an integer that returns the number of seconds to wait before making a new connection attempt. The default value is 0. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: ConnectParams.server_type This read-only attribute is a string that returns the type of server connection that should be established. If specified, it should be one of `dedicated`, `shared`, or `pooled`. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: ConnectParams.service_name This read-only attribute is a string that returns the service name of the database. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: ConnectParams.shardingkey This read-only attribute is a list that specifies a sequence of strings, numbers, bytes, or dates that identify the database shard to connect to. This attribute is only supported in the python-oracledb Thick mode. .. attribute:: ConnectParams.sid This read-only attribute is a string that returns the SID of the database. It is recommended to use the :attr:`ConnectParams.service_name` instead. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: ConnectParams.ssl_server_cert_dn This read-only attribute is a string that returns the distinguished name (DN), which should be matched with the server. If this value is specified, then it is used for any verification. Otherwise, the hostname will be used. This value is ignored if the :attr:`~ConnectParams.ssl_server_dn_match` attribute is not set to the value `True`. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: ConnectParams.ssl_server_dn_match This read-only attribute is a boolean that indicates whether the server certificate distinguished name (DN) should be matched in addition to the regular certificate verification that is performed. The default value is True. Note that if the :attr:`~ConnectParams.ssl_server_cert_dn` attribute is not specified, then host name matching is performed instead. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: ConnectParams.stmtcachesize This read-only attribute is an integer that identifies the initial size of the statement cache. The default is the value of :attr:`defaults.stmtcachesize`. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: ConnectParams.supershardingkey This read-only attribute is a list that specifies a sequence of strings, numbers, bytes, or dates that identify the database shard to connect to. This attribute is only supported in python-oracledb Thick mode. .. attribute:: ConnectParams.tag This read-only attribute is a string that identifies the type of connection that should be returned from a pool. This attribute is only supported in python-oracledb Thick mode. .. attribute:: ConnectParams.tcp_connect_timeout This read-only attribute is a float that indicates the maximum number of seconds to wait for a connection to be established to the database host. The default value is 60.0. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: ConnectParams.user This read-only attribute is a string that specifies the name of the user to connect to. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: ConnectParams.wallet_location This read-only attribute is a string that specifies the directory where the wallet can be found. In python-oracledb Thin mode, this attribute is the directory containing the PEM-encoded wallet file, ewallet.pem. In python-oracledb Thick mode, this attribute is the directory containing the file, cwallet.sso. This attribute is supported in the python-oracledb Thin and Thick modes. python-oracledb-1.2.1/doc/src/api_manual/connection.rst000066400000000000000000000746331434177474600231760ustar00rootroot00000000000000.. _connobj: *********************** API: Connection Objects *********************** A Connection object can be created with :meth:`oracledb.connect()`. .. note:: Any outstanding database transaction will be rolled back when the connection object is destroyed or closed. You must perform a commit first if you want data to persist in the database, see :ref:`txnmgmnt`. Connection Methods ================== .. method:: Connection.__enter__() The entry point for the connection as a context manager. It returns itself. .. note:: This method is an extension to the DB API definition. .. method:: Connection.__exit__() The exit point for the connection as a context manager. This will close the connection and roll back any uncommitted transaction. .. note:: This method is an extension to the DB API definition. .. method:: Connection.begin([formatId, transactionId, branchId]) Explicitly begins a new transaction. Without parameters, this explicitly begins a local transaction; otherwise, this explicitly begins a distributed (global) transaction with the given parameters. See the Oracle documentation for more details. Note that in order to make use of global (distributed) transactions, the :attr:`~Connection.internal_name` and :attr:`~Connection.external_name` attributes must be set. .. deprecated:: 1.0 Use the method :meth:`Connection.tpc_begin()` instead. .. note:: This method is an extension to the DB API definition. .. method:: Connection.cancel() Breaks a long-running statement. .. note:: This method is an extension to the DB API definition. .. method:: Connection.changepassword(oldpassword, newpassword) Changes the password for the user to which the connection is connected. .. note:: This method is an extension to the DB API definition. .. method:: Connection.close() Closes the connection now and makes it unusable for further operations. An Error exception will be raised if any operation is attempted with this connection after this method is completed successfully. All open cursors and LOBs created by the connection will be closed and will also no longer be usable. Internally, references to the connection are held by cursor objects, LOB objects, subscription objects, etc. Once all of these references are released, the connection itself will be closed automatically. Either control references to these related objects carefully or explicitly close connections in order to ensure sufficient resources are available. .. method:: Connection.commit() Commits any pending transactions to the database. .. method:: Connection.createlob(lobType) Creates and returns a new temporary :ref:`LOB object ` of the specified type. The ``lobType`` parameter should be one of :data:`oracledb.CLOB`, :data:`oracledb.BLOB` or :data:`oracledb.NCLOB`. .. note:: This method is an extension to the DB API definition. .. method:: Connection.cursor() Returns a new :ref:`cursor object ` using the connection. .. method:: Connection.getSodaDatabase() Returns a :ref:`SodaDatabase ` object for Simple Oracle Document Access (SODA). All SODA operations are performed either on the returned SodaDatabase object or from objects created by the returned SodaDatabase object. See `here `__ for additional information on SODA. .. note:: This method is an extension to the DB API definition. .. method:: Connection.gettype(name) Returns a :ref:`type object ` given its name. This can then be used to create objects which can be bound to cursors created by this connection. .. note:: This method is an extension to the DB API definition. .. method:: Connection.is_healthy() This function returns a boolean indicating the health status of a connection. Connections may become unusable in several cases, such as, if the network socket is broken, if an Oracle error indicates the connection is unusable, or, after receiving a planned down notification from the database. This function is best used before starting a new database request on an existing standalone connection. Pooled connections internally perform this check before returning a connection to the application. If this function returns False, the connection should be not be used by the application and a new connection should be established instead. This function performs a local check. To fully check a connection's health, use :meth:`Connection.ping()` which performs a round-trip to the database. .. method:: Connection.msgproperties(payload, correlation, delay, exceptionq, expiration, priority) Returns an object specifying the properties of messages used in advanced queuing. See :ref:`msgproperties` for more information. Each of the parameters are optional. If specified, they act as a shortcut for setting each of the equivalently named properties. .. note:: This method is an extension to the DB API definition. .. method:: Connection.ping() Pings the database to verify if the connection is valid. .. note:: This method is an extension to the DB API definition. .. method:: Connection.prepare() Prepares the distributed (global) transaction for commit. Return a boolean indicating if a transaction was actually prepared in order to avoid the error ORA-24756 (transaction does not exist). .. deprecated:: python-oracledb 1.0 Use the method :meth:`Connection.tpc_prepare()` instead. .. note:: This method is an extension to the DB API definition. .. method:: Connection.queue(name, payload_type=None) Creates a :ref:`queue ` which is used to enqueue and dequeue messages in Advanced Queuing. The ``name`` parameter is expected to be a string identifying the queue in which messages are to be enqueued or dequeued. The ``payload_type`` parameter, if specified, is expected to be an :ref:`object type ` that identifies the type of payload the queue expects. If the string "JSON" is specified, JSON data is enqueued and dequeued. If not specified, RAW data is enqueued and dequeued. For consistency and compliance with the PEP 8 naming style, the parameter `payloadType` was renamed to `payload_type`. The old name will continue to work as a keyword parameter for a period of time. .. note:: This method is an extension to the DB API definition. .. method:: Connection.rollback() Rolls back any pending transactions. .. method:: Connection.shutdown([mode]) Shuts down the database. In order to do this the connection must be connected as :data:`~oracledb.SYSDBA` or :data:`~oracledb.SYSOPER`. Two calls must be made unless the mode specified is :data:`~oracledb.DBSHUTDOWN_ABORT`. An example is shown below: :: import oracledb connection = oracledb.connect(mode = oracledb.SYSDBA) connection.shutdown(mode = oracledb.DBSHUTDOWN_IMMEDIATE) cursor = connection.cursor() cursor.execute("alter database close normal") cursor.execute("alter database dismount") connection.shutdown(mode = oracledb.DBSHUTDOWN_FINAL) .. note:: This method is an extension to the DB API definition. .. method:: Connection.startup(force=False, restrict=False, pfile=None) Starts up the database. This is equivalent to the SQL\*Plus command "startup nomount". The connection must be connected as :data:`~oracledb.SYSDBA` or :data:`~oracledb.SYSOPER` with the :data:`~oracledb.PRELIM_AUTH` option specified for this to work. The ``pfile`` parameter, if specified, is expected to be a string identifying the location of the parameter file (PFILE) which will be used instead of the stored parameter file (SPFILE). An example is shown below: :: import oracledb connection = oracledb.connect( mode=oracledb.SYSDBA | oracledb.PRELIM_AUTH) connection.startup() connection = oracledb.connect(mode=oracledb.SYSDBA) cursor = connection.cursor() cursor.execute("alter database mount") cursor.execute("alter database open") .. note:: This method is an extension to the DB API definition. .. method:: Connection.subscribe(namespace=oracledb.SUBSCR_NAMESPACE_DBCHANGE, \ protocol=oracledb.SUBSCR_PROTO_OCI, callback=None, timeout=0, \ operations=OPCODE_ALLOPS, port=0, qos=0, ip_address=None, grouping_class=0, \ grouping_value=0, grouping_type=oracledb.SUBSCR_GROUPING_TYPE_SUMMARY, \ name=None, client_initiated=False) Returns a new :ref:`subscription object ` that receives notifications for events that take place in the database that match the given parameters. The ``namespace`` parameter specifies the namespace the subscription uses. It can be one of :data:`oracledb.SUBSCR_NAMESPACE_DBCHANGE` or :data:`oracledb.SUBSCR_NAMESPACE_AQ`. The ``protocol`` parameter specifies the protocol to use when notifications are sent. Currently the only valid value is :data:`oracledb.SUBSCR_PROTO_OCI`. The ``callback`` is expected to be a callable that accepts a single parameter. A :ref:`message object ` is passed to this callback whenever a notification is received. The ``timeout`` value specifies that the subscription expires after the given time in seconds. The default value of 0 indicates that the subscription never expires. The ``operations`` parameter enables filtering of the messages that are sent (insert, update, delete). The default value will send notifications for all operations. This parameter is only used when the namespace is set to :data:`oracledb.SUBSCR_NAMESPACE_DBCHANGE`. The ``port`` parameter specifies the listening port for callback notifications from the database server. If not specified, an unused port will be selected by the Oracle Client libraries. The ``qos`` parameter specifies quality of service options. It should be one or more of the following flags, OR'ed together: :data:`oracledb.SUBSCR_QOS_RELIABLE`, :data:`oracledb.SUBSCR_QOS_DEREG_NFY`, :data:`oracledb.SUBSCR_QOS_ROWIDS`, :data:`oracledb.SUBSCR_QOS_QUERY`, :data:`oracledb.SUBSCR_QOS_BEST_EFFORT`. The ``ip_address`` parameter specifies the IP address (IPv4 or IPv6) in standard string notation to bind for callback notifications from the database server. If not specified, the client IP address will be determined by the Oracle Client libraries. The ``grouping_class`` parameter specifies what type of grouping of notifications should take place. Currently, if set, this value can only be set to the value :data:`oracledb.SUBSCR_GROUPING_CLASS_TIME`, which will group notifications by the number of seconds specified in the ``grouping_value`` parameter. The ``grouping_type`` parameter should be one of the values :data:`oracledb.SUBSCR_GROUPING_TYPE_SUMMARY` (the default) or :data:`oracledb.SUBSCR_GROUPING_TYPE_LAST`. The ``name`` parameter is used to identify the subscription and is specific to the selected namespace. If the namespace parameter is :data:`oracledb.SUBSCR_NAMESPACE_DBCHANGE` then the name is optional and can be any value. If the namespace parameter is :data:`oracledb.SUBSCR_NAMESPACE_AQ`, however, the name must be in the format '' for single consumer queues and ':' for multiple consumer queues, and identifies the queue that will be monitored for messages. The queue name may include the schema, if needed. The ``client_initiated`` parameter is used to determine if client initiated connections or server initiated connections (the default) will be established. Client initiated connections are only available in Oracle Client 19.4 and Oracle Database 19.4 and higher. For consistency and compliance with the PEP 8 naming style, the parameter `ipAddress` was renamed to `ip_address`, the parameter `groupingClass` was renamed to `grouping_class`, the parameter `groupingValue` was renamed to `grouping_value`, the parameter `groupingType` was renamed to `grouping_type` and the parameter `clientInitiated` was renamed to `client_initiated`. The old names will continue to work as keyword parameters for a period of time. .. note:: This method is an extension to the DB API definition. .. note:: The subscription can be deregistered in the database by calling the function :meth:`~Connection.unsubscribe()`. If this method is not called and the connection that was used to create the subscription is explicitly closed using the function :meth:`~Connection.close()`, the subscription will not be deregistered in the database. .. method:: Connection.tpc_begin(xid, flags) Begins a Two-Phase Commit (TPC) on a global transaction using the specified transaction identifier (xid). The ``xid`` parameter should be an object returned by the :meth:`~Connection.xid()` method. The ``flags`` parameter is one of the constants :data:`oracledb.TPC_BEGIN_JOIN`, :data:`oracledb.TPC_BEGIN_NEW`, :data:`oracledb.TPC_BEGIN_PROMOTE`, or :data:`oracledb.TPC_BEGIN_RESUME`. The default is :data:`oracledb.TPC_BEGIN_NEW`. The following code sample demonstrates the ``tpc_begin()`` function:: connection.tpc_begin(xid=x, flags=oracledb.TPC_BEGIN_NEW) x = connection.xid(format_id=1, global_transaction_id="tx1", branch_qualifier="br1") See :ref:`tcp` for information on TPC. .. method:: Connection.tpc_commit(xid, one_phase) Commits a global transaction. When called with no arguments, this method commits a transaction previously prepared with :meth:`~Connection.tpc_begin()` and optionally prepared with :meth:`~Connection.tpc_prepare()`. If :meth:`~Connection.tpc_prepare()` is not called, a single phase commit is performed. A transaction manager may choose to do this if only a single resource is participating in the global transaction. If an ``xid`` parameter is passed, then an object should be returned by the :meth:`~Connection.xid()` function. This form should be called outside of a transaction and is intended for use in recovery. The ``one_phase`` parameter is a boolean identifying whether to perform a one-phase or two-phase commit. If ``one_phase`` parameter is True, a single-phase commit is performed. The default value is False. This parameter is only examined if a value is provided for the ``xid`` parameter. Otherwise, the driver already knows whether :meth:`~Connection.tpc_prepare()` was called for the transaction and whether a one-phase or two-phase commit is required. The following code sample demonstrates the ``tpc_commit()`` function:: connection.tpc_commit(xid=x, one_phase=False) x = connection.xid(format_id=1, global_transaction_id="tx1", branch_qualifier="br1") See :ref:`tcp` for information on TPC. .. method:: Connection.tpc_end(xid, flags) Ends or suspends work on a global transaction. This function is only intended for use by transaction managers. If an ``xid`` parameter is passed, then an object should be returned by the :meth:`~Connection.xid()` function. If no xid parameter is passed, then the transaction identifier used by the previous :meth:`~Connection.tpc_begin()` is used. The ``flags`` parameter is one of the constants :data:`oracledb.TPC_END_NORMAL` or :data:`oracledb.TPC_END_SUSPEND`. The default is :data:`oracledb.TPC_END_NORMAL`. If the flag is :data:`oracledb.TPC_END_SUSPEND` then the transaction may be resumed later by calling :meth:`Connection.tpc_begin()` with the flag :data:`oracledb.TPC_BEGIN_RESUME`. The following code sample demonstrates the ``tpc_end()`` function:: connection.tpc_end(xid=x, flags=oracledb.TPC_END_NORMAL) x = connection.xid(format_id=1, global_transaction_id="tx1", branch_qualifier="br1") See :ref:`tcp` for information on TPC. .. method:: Connection.tpc_forget(xid) Causes the database to forget a heuristically completed TPC transaction. This function is only intended to be called by transaction managers. The ``xid`` parameter is mandatory and should be an object should be returned by the :meth:`~Connection.xid()` function. The following code sample demonstrates the ``tpc_forget()`` function:: connection.tpc_forget(xid=x) x = connection.xid(format_id=1, global_transaction_id="tx1", branch_qualifier="br1") See :ref:`tcp` for information on TPC. .. method:: Connection.tpc_prepare(xid) Prepares a two-phase transaction for commit. After this function is called, no further activity should take place on this connection until either :meth:`~Connection.tpc_commit()` or :meth:`~Connection.tpc_rollback()` have been called. Returns a boolean indicating whether a commit is needed or not. If you attempt to commit when not needed, then it results in the error ``ORA-24756: transaction does not exist``. If an ``xid`` parameter is passed, then an object should be returned by the :meth:`~Connection.xid()` function. If an xid parameter is not passed, then the transaction identifier used by the previous :meth:`~Connection.tpc_begin()` is used. The following code sample demonstrates the ``tpc_prepare()`` function:: connection.tpc_prepare(xid=x) x = connection.xid(format_id=1, global_transaction_id="tx1", branch_qualifier="br1") See :ref:`tcp` for information on TPC. .. method:: Connection.tpc_recover() Returns a list of pending transaction identifiers that require recovery. Objects of type ``Xid`` (as returned by the :meth:`~Connection.xid()` function) are returned and these can be passed to :meth:`~Connection.tpc_commit()` or :meth:`~Connection.tpc_rollback()` as needed. This function queries the view ``DBA_PENDING_TRANSACTIONS`` and requires ``SELECT`` privilege on that view. The following code sample demonstrates the ``tpc_recover()`` function:: connection.tpc_recover() See :ref:`tcp` for information on TPC. .. method:: Connection.tpc_rollback(xid) Rolls back a global transaction. If an ``xid`` parameter is not passed, then it rolls back the transaction that was previously started with :meth:`~Connection.tpc_begin()`. If an ``xid`` parameter is passed, then an object should be returned by :meth:`~Connection.xid()` and the specified transaction is rolled back. This form should be called outside of a transaction and is intended for use in recovery. The following code sample demonstrates the ``tpc_rollback()`` function:: connection.tpc_rollback(xid=x) x = connection.xid(format_id=1, global_transaction_id="tx1", branch_qualifier="br1") See :ref:`tcp` for information on TPC. .. method:: Connection.unsubscribe(subscr) Unsubscribe from events in the database that were originally subscribed to using :meth:`~Connection.subscribe()`. The connection used to unsubscribe should be the same one used to create the subscription, or should access the same database and be connected as the same user name. .. method:: Connection.xid (format_id, global_transaction_id, branch_qualifier) Returns a global transaction identifier (xid) that can be used with the Two-Phase Commit (TPC) functions. The ``xid`` contains a format identifier, a global transaction identifier, and a branch identifier. There are no checks performed at the Python level. The values are checked by ODPI-C when they are passed to the relevant functions. .. When this functionality is also supported in the thin driver the checks will be performed at the Python level as well. The ``format_id`` parameter should be a positive 32-bit integer. This value identifies the format of the global_transaction_id and branch_qualifier parameters and the value is determined by the Transaction Manager (TM), if one is in use. The ``global_transaction_id`` and branch_qualifier parameters should be of type bytes or string. If a value of type string is passed, then this value will be UTF-8 encoded to bytes. The values cannot exceed 64 bytes in length. The following code sample demonstrates the ``xid()`` function:: connection.xid(format_id=1, global_transaction_id="tx1", branch_qualifier="br1") See :ref:`tcp` for information on TPC. .. _connattrs: Connection Attributes ===================== .. attribute:: Connection.action This write-only attribute sets the action column in the v$session table. It is a string attribute and cannot be set to None -- use the empty string instead. .. note:: This attribute is an extension to the DB API definition. .. attribute:: Connection.autocommit This read-write attribute determines whether autocommit mode is on or off. When autocommit mode is on, all statements are committed as soon as they have completed executing. .. note:: This attribute is an extension to the DB API definition. .. attribute:: Connection.call_timeout This read-write attribute specifies the amount of time (in milliseconds) that a single round-trip to the database may take before a timeout will occur. A value of 0 means that no timeout will take place. If a timeout occurs, the error *DPI-1067* will be returned if the connection is still usable. Alternatively the error *DPI-1080* will be returned if the connection has become invalid and can no longer be used. For consistency and compliance with the PEP 8 naming style, the attribute `callTimeout` was renamed to `call_timeout`. The old name will continue to work for a period of time. The error *DPI-1080* was also introduced in this release. .. note:: This attribute is an extension to the DB API definition and is only available in Oracle Client 18c and higher. .. attribute:: Connection.client_identifier This write-only attribute sets the client_identifier column in the v$session table. .. note:: This attribute is an extension to the DB API definition. .. attribute:: Connection.clientinfo This write-only attribute sets the client_info column in the v$session table. .. note:: This attribute is an extension to the DB API definition. .. attribute:: Connection.current_schema This read-write attribute sets the current schema attribute for the session. Setting this value is the same as executing the SQL statement ``ALTER SESSION SET CURRENT_SCHEMA``. The attribute is set (and verified) on the next call that does a round trip to the server. The value is placed before unqualified database objects in SQL statements you then execute. .. note:: This attribute is an extension to the DB API definition. .. attribute:: Connection.dbop This write-only attribute sets the database operation that is to be monitored. This can be viewed in the ``DBOP_NAME`` column of the ``V$SQL_MONITOR`` table. .. note:: This attribute is an extension to the DB API definition. .. attribute:: Connection.dsn This read-only attribute returns the TNS entry of the database to which a connection has been established. .. note:: This attribute is an extension to the DB API definition. .. attribute:: Connection.econtext_id This write-only attribute specifies the execution context id. This value can be found as ecid in the v$session table and econtext_id in the auditing tables. The maximum length is 64 bytes. .. attribute:: Connection.edition This read-only attribute gets the session edition and is only available in Oracle Database 11.2 (both client and server must be at this level or higher for this to work). .. note:: This attribute is an extension to the DB API definition. .. attribute:: Connection.encoding This read-only attribute returns the IANA character set name of the character set in use by the Oracle client for regular strings. The encodings in use are always UTF-8. .. deprecated:: cx_Oracle 8.2 .. note:: This attribute is an extension to the DB API definition. .. attribute:: Connection.external_name This read-write attribute specifies the external name that is used by the connection when logging distributed transactions. .. note:: This attribute is an extension to the DB API definition. .. attribute:: Connection.handle This read-only attribute returns the Oracle Call Interface (OCI) service context handle for the connection. It is primarily provided to facilitate testing the creation of a connection using the OCI service context handle. This property is only relevant in the python-oracledb Thick mode. .. note:: This attribute is an extension to the DB API definition. .. attribute:: Connection.inputtypehandler This read-write attribute specifies a method called for each value that is bound to a statement executed on any cursor associated with this connection. The method signature is handler(cursor, value, arraysize) and the return value is expected to be a variable object or None in which case a default variable object will be created. If this attribute is None, the default behavior will take place for all values bound to statements. .. note:: This attribute is an extension to the DB API definition. .. attribute:: Connection.internal_name This read-write attribute specifies the internal name that is used by the connection when logging distributed transactions. .. note:: This attribute is an extension to the DB API definition. .. attribute:: Connection.ltxid This read-only attribute returns the logical transaction id for the connection. It is used within Oracle Transaction Guard as a means of ensuring that transactions are not duplicated. See the Oracle documentation and the provided sample for more information. .. note: This attribute is an extension to the DB API definition. It is only available when Oracle Database 12.1 or higher is in use on both the server and the client. .. attribute:: Connection.maxBytesPerCharacter This deprecated, read-only attribute returns the value 4 since encodings are always UTF-8. Previously it returned the maximum number of bytes each character can use for the client character set. .. deprecated:: cx_Oracle 8.2 .. note:: This attribute is an extension to the DB API definition. .. attribute:: Connection.module This write-only attribute sets the module column in the v$session table. The maximum length for this string is 48 and if you exceed this length you will get ORA-24960. .. note: This attribute is an extension to the DB API definition. .. attribute:: Connection.nencoding This read-only attribute returns the IANA character set name of the national character set in use by the Oracle client. This is always the value "UTF-8". .. deprecated:: cx_Oracle 8.2 .. note:: This attribute is an extension to the DB API definition. .. attribute:: Connection.outputtypehandler This read-write attribute specifies a method called for each column that is going to be fetched from any cursor associated with this connection. The method signature is handler(cursor, name, defaultType, length, precision, scale) and the return value is expected to be a variable object or None in which case a default variable object will be created. If this attribute is None, the default behavior will take place for all columns fetched from cursors. See :ref:`outputtypehandlers`. .. note:: This attribute is an extension to the DB API definition. .. attribute:: Connection.stmtcachesize This read-write attribute specifies the size of the statement cache. This value can make a significant difference in performance if you have a small number of statements that you execute repeatedly. The default value is 20. See :ref:`Statement Caching ` for more information. .. note:: This attribute is an extension to the DB API definition. .. attribute:: Connection.tag This read-write attribute initially contains the actual tag of the session that was acquired from a pool by :meth:`ConnectionPool.acquire()`. If the connection was not acquired from a pool or no tagging parameters were specified (``tag`` and ``matchanytag``) when the connection was acquired from the pool, this value will be None. If the value is changed, it must be a string containing name=value pairs like "k1=v1;k2=v2". If this value is not None when the connection is released back to the pool it will be used to retag the session. This value can be overridden in the call to :meth:`ConnectionPool.release()`. .. note:: This attribute is an extension to the DB API definition. .. attribute:: Connection.thin This read-only attribute returns a boolean indicating if the connection was established with the python-oracledb Thin mode (True) or python-oracledb Thick mode (False). .. note:: This attribute is an extension to the DB API definition. .. attribute:: Connection.tnsentry This read-only attribute returns the TNS entry of the database to which a connection has been established. .. deprecated:: cx_Oracle 8.2 Use the attribute :attr:`~Connection.dsn` instead. .. note:: This attribute is an extension to the DB API definition. .. attribute:: Connection.username This read-only attribute returns the name of the user which established the connection to the database. .. note:: This attribute is an extension to the DB API definition. .. attribute:: Connection.version This read-only attribute returns the version of the database to which a connection has been established. .. note:: This attribute is an extension to the DB API definition. .. note:: If you connect to Oracle Database 18 or higher with client libraries 12.2 or lower that you will only receive the base version (such as 18.0.0.0.0) instead of the full version (18.3.0.0.0). python-oracledb-1.2.1/doc/src/api_manual/connection_pool.rst000066400000000000000000000333561434177474600242240ustar00rootroot00000000000000.. _connpool: *************************** API: ConnectionPool Objects *************************** .. note:: This object is an extension to the DB API. The new ConnectionPool class is synonymous with SessionPool. The SessionPool class is deprecated in python-oracledb. The preferred function to create pools is now :meth:`oracledb.create_pool()`. (The name SessionPool came from the `Oracle Call Interface (OCI) session pool `__. This implementation is only used in the python-oracledb Thick mode and is not available in the python-oracledb Thin mode). In python-oracledb, the type `pool` will show the class `oracledb.ConnectionPool`. This only affects the name. The following code will continue to work providing backward compatibility with cx_Oracle: .. code-block:: python issubclass(cls, oracledb.SessionPool) == True isinstance(pool, oracledb.SessionPool) == True The following code will also work: .. code-block:: python issubclass(cls, oracledb.ConnectionPool) == True isinstance(pool, oracledb.ConnectionPool) == True The function :meth:`oracledb.SessionPool` that is used to create pools is deprecated in python-oracledb 1.0 and has been deprecated by the function :meth:`oracledb.create_pool`. ConnectionPool Methods ====================== .. method:: ConnectionPool.acquire(user=None, password=None, cclass=None, \ purity=oracledb.PURITY_DEFAULT, tag=None, matchanytag=False, \ shardingkey=[], supershardingkey=[]) Acquires a connection from the session pool and returns a :ref:`connection object `. If the pool is :ref:`homogeneous `, the ``user`` and ``password`` parameters cannot be specified. If they are, an exception will be raised. The ``cclass`` parameter, if specified, should be a string corresponding to the connection class for Database Resident Connection Pooling (DRCP). The ``purity`` parameter is expected to be one of :data:`~oracledb.PURITY_NEW`, :data:`~oracledb.PURITY_ANY`, or :data:`~oracledb.PURITY_DEFAULT`. The ``tag`` parameter, if specified, is expected to be a string with name=value pairs like "k1=v1;k2=v2" and will limit the connections that can be returned from a connection pool unless the ``matchanytag`` parameter is set to True. In that case, connections with the specified tag will be preferred over others, but if no such connections are available, then a connection with a different tag may be returned instead. In any case, untagged connections will always be returned if no connections with the specified tag are available. Connections are tagged when they are :meth:`released ` back to the pool. The ``shardingkey`` and ``supershardingkey`` parameters, if specified, are expected to be a sequence of values which will be used to identify the database shard to connect to. The key values can be strings, numbers, bytes or dates. .. method:: ConnectionPool.close(force=False) Closes the pool now, rather than when the last reference to it is released, which makes it unusable for further work. If any connections have been acquired and not released back to the pool, this method will fail unless the ``force`` parameter is set to True. .. method:: ConnectionPool.drop(connection) Drops the connection from the pool which is useful if the connection is no longer usable (such as when the session is killed). .. method:: ConnectionPool.reconfigure([min, max, increment, getmode, \ timeout, wait_timeout, max_lifetime_session, max_sessions_per_shard, \ soda_metadata_cache, stmtcachesize, ping_interval]) Reconfigures various parameters of a connection pool. The pool size can be altered with ``reconfigure()`` by passing values for :data:`~ConnectionPool.min`, :data:`~ConnectionPool.max` or :data:`~ConnectionPool.increment`. The :data:`~ConnectionPool.getmode`, :data:`~ConnectionPool.timeout`, :data:`~ConnectionPool.wait_timeout`, :data:`~ConnectionPool.max_lifetime_session`, :data:`~ConnectionPool.max_sessions_per_shard`, :data:`~ConnectionPool.soda_metadata_cache`, :data:`~ConnectionPool.stmtcachesize` and :data:`~ConnectionPool.ping_interval` attributes can be set directly or with ``reconfigure()``. All parameters are optional. Unspecified parameters will leave those pool attributes unchanged. The parameters are processed in two stages. After any size change has been processed, reconfiguration on the other parameters is done sequentially. If an error such as an invalid value occurs when changing one attribute, then an exception will be generated but any already changed attributes will retain their new values. During reconfiguration of a pool's size, the behavior of :meth:`ConnectionPool.acquire()` depends on the ``getmode`` in effect when ``acquire()`` is called: * With mode :data:`~oracledb.POOL_GETMODE_FORCEGET`, an ``acquire()`` call will wait until the pool has been reconfigured. * With mode :data:`~oracledb.POOL_GETMODE_TIMEDWAIT`, an ``acquire()`` call will try to acquire a connection in the time specified by pool.wait_timeout and return an error if the time taken exceeds that value. * With mode :data:`~oracledb.POOL_GETMODE_WAIT`, an ``acquire()`` call will wait until after the pool has been reconfigured and a connection is available. * With mode :data:`~oracledb.POOL_GETMODE_NOWAIT`, if the number of busy connections is less than the pool size, ``acquire()`` will return a new connection after pool reconfiguration is complete. Closing connections with :meth:`ConnectionPool.release()` or :meth:`Connection.close()` will wait until any pool size reconfiguration is complete. Closing the connection pool with :meth:`ConnectionPool.close()` will wait until reconfiguration is complete. See :ref:`Connection Pool Reconfiguration `. .. method:: ConnectionPool.release(connection, tag=None) Releases the connection back to the pool now, rather than whenever __del__ is called. The connection will be unusable from this point forward; an Error exception will be raised if any operation is attempted with the connection. Any cursors or LOBs created by the connection will also be marked unusable and an Error exception will be raised if any operation is attempted with them. Internally, references to the connection are held by cursor objects, LOB objects, etc. Once all of these references are released, the connection itself will be released back to the pool automatically. Either control references to these related objects carefully or explicitly release connections back to the pool in order to ensure sufficient resources are available. If the tag is not None, it is expected to be a string with name=value pairs like "k1=v1;k2=v2" and will override the value in the property :attr:`Connection.tag`. If either :attr:`Connection.tag` or the tag parameter are not None, the connection will be retagged when it is released back to the pool. ConnectionPool Attributes ========================= .. attribute:: ConnectionPool.busy This read-only attribute returns the number of connections currently acquired. .. attribute:: ConnectionPool.dsn This read-only attribute returns the TNS entry of the database to which a connection has been established. .. attribute:: ConnectionPool.getmode This read-write attribute determines how connections are returned from the pool. If :data:`~oracledb.POOL_GETMODE_FORCEGET` is specified, a new connection will be returned even if there are no free connections in the pool. :data:`~oracledb.POOL_GETMODE_NOWAIT` will raise an exception if there are no free connections are available in the pool. If :data:`~oracledb.POOL_GETMODE_WAIT` is specified and there are no free connections in the pool, the caller will wait until a free connection is available. :data:`~oracledb.POOL_GETMODE_TIMEDWAIT` uses the value of :data:`~ConnectionPool.wait_timeout` to determine how long the caller should wait for a connection to become available before returning an error. .. attribute:: ConnectionPool.homogeneous This read-write boolean attribute indicates whether the pool is considered :ref:`homogeneous ` or not. If the pool is not homogeneous, different authentication can be used for each connection acquired from the pool. .. attribute:: ConnectionPool.increment This read-only attribute returns the number of connections that will be established when additional connections need to be created. .. attribute:: ConnectionPool.max This read-only attribute returns the maximum number of connections that the pool can control. .. attribute:: ConnectionPool.max_lifetime_session This read-write attribute returns the maximum length of time (in seconds) that a pooled connection may exist. Connections that are in use will not be closed. They become candidates for termination only when they are released back to the pool and have existed for longer than max_lifetime_session seconds. Note that termination only occurs when the pool is accessed. A value of 0 means that there is no maximum length of time that a pooled connection may exist. This attribute is only available in Oracle Database 12.1. .. attribute:: ConnectionPool.max_sessions_per_shard This read-write attribute returns the number of sessions that can be created per shard in the pool. Setting this attribute greater than zero specifies the maximum number of sessions in the pool that can be used for any given shard in a sharded database. This lets connections in the pool be balanced across the shards. A value of zero will not set any maximum number of sessions for each shard. This attribute is only available in Oracle Client 18.3 and higher. .. attribute:: ConnectionPool.min This read-only attribute returns the number of connections with which the connection pool was created and the minimum number of connections that will be controlled by the connection pool. .. attribute:: ConnectionPool.name This read-only attribute returns the name assigned to the pool by Oracle. .. attribute:: ConnectionPool.opened This read-only attribute returns the number of connections currently opened by the pool. .. attribute:: ConnectionPool.ping_interval This read-write integer attribute specifies the pool ping interval in seconds. When a connection is acquired from the pool, a check is first made to see how long it has been since the connection was put into the pool. If this idle time exceeds ``ping_interval``, then a :ref:`round-trip ` ping to the database is performed. If the connection is unusable, it is discarded and a different connection is selected to be returned by :meth:`ConnectionPool.acquire()`. Setting ``ping_interval`` to a negative value disables pinging. Setting it to 0 forces a ping for every :meth:`ConnectionPool.acquire()` and is not recommended. Prior to cx_Oracle 8.2, the ping interval was fixed at 60 seconds. .. attribute:: ConnectionPool.soda_metadata_cache This read-write boolean attribute returns whether the SODA metadata cache is enabled or not. Enabling the cache significantly improves the performance of methods :meth:`SodaDatabase.createCollection()` (when not specifying a value for the metadata parameter) and :meth:`SodaDatabase.openCollection()`. Note that the cache can become out of date if changes to the metadata of cached collections are made externally. .. attribute:: ConnectionPool.stmtcachesize This read-write attribute specifies the size of the statement cache that will be used for connections obtained from the pool. Once a connection is created, that connection’s statement cache size can only be changed by setting the stmtcachesize attribute on the connection itself. See :ref:`Statement Caching ` for more information. .. attribute:: ConnectionPool.thin This attribute returns a boolean which indicates the python-oracledb mode in which the pool was created. If the value of this attribute is True, it indicates that the pool was created in the python-oracledb Thin mode. If the value of this attribute is False, it indicates that the pool was created in the python-oracledb Thick mode. .. attribute:: ConnectionPool.timeout This read-write attribute specifies the time (in seconds) after which idle connections will be terminated in order to maintain an optimum number of open connections. A value of 0 means that no idle connections are terminated. Note that in python-oracledb Thick mode with older Oracle Client Libraries, the termination only occurs when the pool is accessed. .. attribute:: ConnectionPool.tnsentry This read-only attribute returns the TNS entry of the database to which a connection has been established. .. deprecated:: cx_Oracle 8.2 Use the attribute :attr:`~ConnectionPool.dsn` instead. .. attribute:: ConnectionPool.username This read-only attribute returns the name of the user which established the connection to the database. .. attribute:: ConnectionPool.wait_timeout This read-write attribute specifies the time (in milliseconds) that the caller should wait for a connection to become available in the pool before returning with an error. This value is only used if the ``getmode`` parameter to :meth:`oracledb.create_pool()` was the value :data:`oracledb.POOL_GETMODE_TIMEDWAIT`. python-oracledb-1.2.1/doc/src/api_manual/cursor.rst000066400000000000000000000606471434177474600223540ustar00rootroot00000000000000.. _cursorobj: ******************* API: Cursor Objects ******************* A cursor object can be created with :meth:`Connection.cursor`. Cursor Methods ============== .. method:: Cursor.__enter__() The entry point for the cursor as a context manager. It returns itself. .. note:: This method is an extension to the DB API definition. .. method:: Cursor.__exit__() The exit point for the cursor as a context manager. It closes the cursor. .. note:: This method is an extension to the DB API definition. .. method:: Cursor.arrayvar(typ, value, [size]) Creates an array variable associated with the cursor of the given type and size and return a :ref:`variable object `. The value is either an integer specifying the number of elements to allocate or it is a list and the number of elements allocated is drawn from the size of the list. If the value is a list, the variable is also set with the contents of the list. If the size is not specified and the type is a string or binary, 4000 bytes is allocated. This is needed for passing arrays to PL/SQL (in cases where the list might be empty and the type cannot be determined automatically) or returning arrays from PL/SQL. Array variables can only be used for PL/SQL associative arrays with contiguous keys. For PL/SQL associative arrays with sparsely populated keys or for varrays and nested tables, the approach shown in this `example `__ needs to be used. .. note:: The DB API definition does not define this method. .. method:: Cursor.bindnames() Returns the list of bind variable names bound to the statement. Note that a statement must have been prepared first. .. note:: The DB API definition does not define this method. .. method:: Cursor.callfunc(name, returnType, parameters=[], \ keyword_parameters={}) Calls a function with the given name. The return type is specified in the same notation as is required by :meth:`~Cursor.setinputsizes()`. The sequence of parameters must contain one entry for each parameter that the function expects. Any keyword parameters will be included after the positional parameters. The result of the call is the return value of the function. See :ref:`plsqlfunc` for an example. For consistency and compliance with the PEP 8 naming style, the parameter `keywordParameters` was renamed to `keyword_parameters`. The old name will continue to work as a keyword parameter for a period of time. .. note:: The DB API definition does not define this method. .. note:: If you intend to call :meth:`Cursor.setinputsizes()` on the cursor prior to making this call, then note that the first item in the parameter list refers to the return value of the function. .. method:: Cursor.callproc(name, parameters=[], keyword_parameters={}) Calls a procedure with the given name. The sequence of parameters must contain one entry for each parameter that the procedure expects. The result of the call is a modified copy of the input sequence. Input parameters are left untouched; output and input/output parameters are replaced with possibly new values. Keyword parameters will be included after the positional parameters and are not returned as part of the output sequence. See :ref:`plsqlproc` for an example. For consistency and compliance with the PEP 8 naming style, the parameter `keywordParameters` was renamed to `keyword_parameters`. The old name will continue to work as a keyword parameter for a period of time. .. note:: The DB API definition does not allow for keyword parameters. .. method:: Cursor.close() Closes the cursor now, rather than whenever __del__ is called. The cursor will be unusable from this point forward; an Error exception will be raised if any operation is attempted with the cursor. .. method:: Cursor.execute(statement, parameters=[], ** keyword_parameters) Executes a statement against the database. See :ref:`sqlexecution`. Parameters may be passed as a dictionary or sequence or as keyword parameters. If the parameters are a dictionary, the values will be bound by name and if the parameters are a sequence the values will be bound by position. Note that if the values are bound by position, the order of the variables is from left to right as they are encountered in the statement and SQL statements are processed differently than PL/SQL statements. For this reason, it is generally recommended to bind parameters by name instead of by position. Parameters passed as a dictionary are name and value pairs. The name maps to the bind variable name used by the statement and the value maps to the Python value you wish bound to that bind variable. A reference to the statement will be retained by the cursor. If None or the same string object is passed in again, the cursor will execute that statement again without performing a prepare or rebinding and redefining. This is most effective for algorithms where the same statement is used, but different parameters are bound to it (many times). Note that parameters that are not passed in during subsequent executions will retain the value passed in during the last execution that contained them. For maximum efficiency when reusing an statement, it is best to use the :meth:`~Cursor.setinputsizes()` method to specify the parameter types and sizes ahead of time; in particular, None is assumed to be a string of length 1 so any values that are later bound as numbers or dates will raise a TypeError exception. If the statement is a query, the cursor is returned as a convenience to the caller (so it can be used directly as an iterator over the rows in the cursor); otherwise, ``None`` is returned. .. note:: The DB API definition does not define the return value of this method. .. method:: Cursor.executemany(statement, parameters, batcherrors=False, \ arraydmlrowcounts=False) Prepares a statement for execution against a database and then execute it against all parameter mappings or sequences found in the sequence parameters. See :ref:`batchstmnt`. The ``statement`` parameter is managed in the same way as the :meth:`~Cursor.execute()` method manages it. If the size of the buffers allocated for any of the parameters exceeds 2 GB, you will receive the error "DPI-1015: array size of is too large", where varies with the size of each element being allocated in the buffer. If you receive this error, decrease the number of elements in the sequence parameters. If there are no parameters, or parameters have previously been bound, the number of iterations can be specified as an integer instead of needing to provide a list of empty mappings or sequences. When True, the ``batcherrors`` parameter enables batch error support within Oracle and ensures that the call succeeds even if an exception takes place in one or more of the sequence of parameters. The errors can then be retrieved using :meth:`~Cursor.getbatcherrors()`. When True, the ``arraydmlrowcounts`` parameter enables DML row counts to be retrieved from Oracle after the method has completed. The row counts can then be retrieved using :meth:`~Cursor.getarraydmlrowcounts()`. Both the ``batcherrors`` parameter and the ``arraydmlrowcounts`` parameter can only be true when executing an insert, update, delete or merge statement; in all other cases an error will be raised. For maximum efficiency, it is best to use the :meth:`~Cursor.setinputsizes()` method to specify the parameter types and sizes ahead of time; in particular, None is assumed to be a string of length 1 so any values that are later bound as numbers or dates will raise a TypeError exception. .. method:: Cursor.fetchall() Fetches all (remaining) rows of a query result, returning them as a list of tuples. An empty list is returned if no more rows are available. Note that the cursor's arraysize attribute can affect the performance of this operation, as internally reads from the database are done in batches corresponding to the arraysize. An exception is raised if the previous call to :meth:`~Cursor.execute()` did not produce any result set or no call was issued yet. See :ref:`fetching` for an example. .. method:: Cursor.fetchmany(size=cursor.arraysize) Fetches the next set of rows of a query result, returning a list of tuples. An empty list is returned if no more rows are available. Note that the cursor's arraysize attribute can affect the performance of this operation. The number of rows to fetch is specified by the parameter. If it is not given, the cursor's arraysize attribute determines the number of rows to be fetched. If the number of rows available to be fetched is fewer than the amount requested, fewer rows will be returned. An exception is raised if the previous call to :meth:`~Cursor.execute()` did not produce any result set or no call was issued yet. See :ref:`fetching` for an example. .. method:: Cursor.fetchone() Fetches the next row of a query result set, returning a single tuple or None when no more data is available. An exception is raised if the previous call to :meth:`~Cursor.execute()` did not produce any result set or no call was issued yet. See :ref:`fetching` for an example. .. method:: Cursor.getarraydmlrowcounts() Retrieves the DML row counts after a call to :meth:`~Cursor.executemany()` with arraydmlrowcounts enabled. This will return a list of integers corresponding to the number of rows affected by the DML statement for each element of the array passed to :meth:`~Cursor.executemany()`. .. note:: The DB API definition does not define this method and it is only available for Oracle 12.1 and higher. .. method:: Cursor.getbatcherrors() Retrieves the exceptions that took place after a call to :meth:`~Cursor.executemany()` with batcherrors enabled. This will return a list of Error objects, one error for each iteration that failed. The offset can be determined by looking at the offset attribute of the error object. .. note:: The DB API definition does not define this method. .. method:: Cursor.getimplicitresults() Returns a list of cursors which correspond to implicit results made available from a PL/SQL block or procedure without the use of OUT ref cursor parameters. The PL/SQL block or procedure opens the cursors and marks them for return to the client using the procedure dbms_sql.return_result. Cursors returned in this fashion should not be closed. They will be closed automatically by the parent cursor when it is closed. Closing the parent cursor will invalidate the cursors returned by this method. .. note:: The DB API definition does not define this method and it is only available for Oracle Database 12.1 (both client and server must be at this level or higher). It is most like the DB API method nextset(), but unlike that method (which requires that the next result set overwrite the current result set), this method returns cursors which can be fetched independently of each other. .. method:: Cursor.__iter__() Returns the cursor itself to be used as an iterator. .. note:: This method is an extension to the DB API definition but it is mentioned in PEP 249 as an optional extension. .. method:: Cursor.parse(statement) This can be used to parse a statement without actually executing it (this step is done automatically by Oracle when a statement is executed). .. note:: The DB API definition does not define this method. .. note:: You can parse any DML or DDL statement. DDL statements are executed immediately and an implied commit takes place. .. method:: Cursor.prepare(statement, tag, cache_statement=True) This can be used before a call to :meth:`~Cursor.execute()` to define the statement that will be executed. When this is done, the prepare phase will not be performed when the call to :meth:`~Cursor.execute()` is made with None or the same string object as the statement. If the ``tag`` parameter is specified and the ``cache_statement`` parameter is True, the statement will be returned to the statement cache with the given tag. If the ``cache_statement`` parameter is False, the statement will be removed from the statement cache (if it was found there) or will simply not be cached. See :ref:`Statement Caching ` for more information. .. note:: The DB API definition does not define this method. .. method:: Cursor.scroll(value=0, mode="relative") Scrolls the cursor in the result set to a new position according to the mode. If mode is "relative" (the default value), the value is taken as an offset to the current position in the result set. If set to "absolute", value states an absolute target position. If set to "first", the cursor is positioned at the first row and if set to "last", the cursor is set to the last row in the result set. An error is raised if the mode is "relative" or "absolute" and the scroll operation would position the cursor outside of the result set. .. note:: This method is an extension to the DB API definition but it is mentioned in PEP 249 as an optional extension. .. method:: Cursor.setinputsizes(\*args, \*\*keywordArgs) This can be used before a call to :meth:`~Cursor.execute()`, :meth:`~Cursor.callfunc()` or :meth:`~Cursor.callproc()` to predefine memory areas for the operation's parameters. Each parameter should be a type object corresponding to the input that will be used or it should be an integer specifying the maximum length of a string parameter. Use keyword parameters when binding by name and positional parameters when binding by position. The singleton None can be used as a parameter when using positional parameters to indicate that no space should be reserved for that position. .. note:: If you plan to use :meth:`~Cursor.callfunc()` then be aware that the first parameter in the list refers to the return value of the function. .. method:: Cursor.setoutputsize(size, [column]) This method does nothing and is retained solely for compatibility with the DB API. The module automatically allocates as much space as needed to fetch LONG and LONG RAW columns (or CLOB as string and BLOB as bytes). .. method:: Cursor.var(typ, [size, arraysize, inconverter, outconverter, \ typename, encoding_errors, bypass_decode]) Creates a variable with the specified characteristics. This method was designed for use with PL/SQL in/out variables where the length or type cannot be determined automatically from the Python object passed in or for use in input and output type handlers defined on cursors or connections. The ``typ`` parameter specifies the type of data that should be stored in the variable. This should be one of the :ref:`database type constants `, :ref:`DB API constants `, an object type returned from the method :meth:`Connection.gettype()` or one of the following Python types: .. list-table-with-summary:: :header-rows: 1 :class: wy-table-responsive :align: center :summary: The first column is the Python Type. The second column is the corresponding Database Type. * - Python Type - Database Type * - bool - :attr:`oracledb.DB_TYPE_BOOLEAN` * - bytes - :attr:`oracledb.DB_TYPE_RAW` * - datetime.date - :attr:`oracledb.DB_TYPE_DATE` * - datetime.datetime - :attr:`oracledb.DB_TYPE_DATE` * - datetime.timedelta - :attr:`oracledb.DB_TYPE_INTERVAL_DS` * - decimal.Decimal - :attr:`oracledb.DB_TYPE_NUMBER` * - float - :attr:`oracledb.DB_TYPE_NUMBER` * - int - :attr:`oracledb.DB_TYPE_NUMBER` * - str - :attr:`oracledb.DB_TYPE_VARCHAR` The ``size`` parameter specifies the length of string and raw variables and is ignored in all other cases. If not specified for string and raw variables, the value 4000 is used. The ``arraysize`` parameter specifies the number of elements the variable will have. If not specified the bind array size (usually 1) is used. When a variable is created in an output type handler this parameter should be set to the cursor's array size. The ``inconverter`` and ``outconverter`` parameters specify methods used for converting values to/from the database. More information can be found in the section on :ref:`variable objects`. The ``typename`` parameter specifies the name of a SQL object type and must be specified when using type :data:`oracledb.OBJECT` unless the type object was passed directly as the first parameter. The ``encoding_errors`` parameter specifies what should happen when decoding byte strings fetched from the database into strings. It should be one of the values noted in the builtin `decode `__ function. The ``bypass_decode`` parameter, if specified, should be passed as a boolean value. Passing a `True` value causes values of database types :data:`~oracledb.DB_TYPE_VARCHAR`, :data:`~oracledb.DB_TYPE_CHAR`, :data:`~oracledb.DB_TYPE_NVARCHAR`, :data:`~oracledb.DB_TYPE_NCHAR` and :data:`~oracledb.DB_TYPE_LONG` to be returned as `bytes` instead of `str`, meaning that python-oracledb does not do any decoding. See :ref:`Fetching raw data ` for more information. For consistency and compliance with the PEP 8 naming style, the parameter `encodingErrors` was renamed to `encoding_errors`. The old name will continue to work as a keyword parameter for a period of time. .. note:: The DB API definition does not define this method. Cursor Attributes ================= .. attribute:: Cursor.arraysize This read-write attribute can be used to tune the number of rows internally fetched and buffered by internal calls to the database when fetching rows from SELECT statements and REF CURSORS. The value can drastically affect the performance of a query since it directly affects the number of network round trips between Python and the database. For methods like :meth:`~Cursor.fetchone()` and :meth:`~Cursor.fetchall()` it does not change how many rows are returned to the application. For :meth:`~Cursor.fetchmany()` it is the default number of rows to fetch. The attribute is only used for tuning row and SODA document fetches from the database. It does not affect data inserts. Due to the performance benefits, the default ``Cursor.arraysize`` is 100 instead of the 1 that the Python DB API recommends. See :ref:`Tuning Fetch Performance ` for more information. .. attribute:: Cursor.bindvars This read-only attribute provides the bind variables used for the last execute. The value will be either a list or a dictionary depending on whether binding was done by position or name. Care should be taken when referencing this attribute. In particular, elements should not be removed or replaced. .. note:: The DB API definition does not define this attribute. .. attribute:: Cursor.connection This read-only attribute returns a reference to the connection object on which the cursor was created. .. note:: This attribute is an extension to the DB API definition but it is mentioned in PEP 249 as an optional extension. .. data:: Cursor.description This read-only attribute is a sequence of 7-item sequences. Each of these sequences contains information describing one result column: (name, type, display_size, internal_size, precision, scale, null_ok). This attribute will be None for operations that do not return rows or if the cursor has not had an operation invoked via the :meth:`~Cursor.execute()` method yet. The type will be one of the :ref:`database type constants ` defined at the module level. .. attribute:: Cursor.fetchvars This read-only attribute specifies the list of variables created for the last query that was executed on the cursor. Care should be taken when referencing this attribute. In particular, elements should not be removed or replaced. .. note:: The DB API definition does not define this attribute. .. attribute:: Cursor.inputtypehandler This read-write attribute specifies a method called for each value that is bound to a statement executed on the cursor and overrides the attribute with the same name on the connection if specified. The method signature is handler(cursor, value, arraysize) and the return value is expected to be a variable object or None in which case a default variable object will be created. If this attribute is None, the default behavior will take place for all values bound to the statements. .. note:: This attribute is an extension to the DB API definition. .. data:: Cursor.lastrowid This read-only attribute returns the rowid of the last row modified by the cursor. If no row was modified by the last operation performed on the cursor, the value None is returned. .. attribute:: Cursor.outputtypehandler This read-write attribute specifies a method called for each column that is to be fetched from this cursor. The method signature is handler(cursor, name, defaultType, length, precision, scale) and the return value is expected to be a variable object or None in which case a default variable object will be created. If this attribute is None, then the default behavior will take place for all columns fetched from this cursor. See :ref:`outputtypehandlers`. .. note:: This attribute is an extension to the DB API definition. .. attribute:: Cursor.prefetchrows This read-write attribute can be used to tune the number of rows that the Oracle Client library fetches when a SELECT statement is executed. This value can reduce the number of round-trips to the database that are required to fetch rows but at the cost of additional memory. Setting this value to 0 can be useful when the timing of fetches must be explicitly controlled. The attribute is only used for tuning row fetches from the database. It does not affect data inserts. See :ref:`Tuning Fetch Performance ` for more information. .. note:: The DB API definition does not define this method. .. attribute:: Cursor.rowcount This read-only attribute specifies the number of rows that have currently been fetched from the cursor (for select statements), that have been affected by the operation (for insert, update, delete and merge statements), or the number of successful executions of the statement (for PL/SQL statements). .. attribute:: Cursor.rowfactory This read-write attribute specifies a method to call for each row that is retrieved from the database. Ordinarily, a tuple is returned for each row but if this attribute is set, the method is called with the tuple that would normally be returned, and the result of the method is returned instead. See :ref:`rowfactories`. .. note:: The DB API definition does not define this attribute. .. attribute:: Cursor.scrollable This read-write boolean attribute specifies whether the cursor can be scrolled or not. By default, cursors are not scrollable, as the server resources and response times are greater than nonscrollable cursors. This attribute is checked and the corresponding mode set in Oracle when calling the method :meth:`~Cursor.execute()`. .. note:: The DB API definition does not define this attribute. .. attribute:: Cursor.statement This read-only attribute provides the string object that was previously prepared with :meth:`~Cursor.prepare()` or executed with :meth:`~Cursor.execute()`. .. note:: The DB API definition does not define this attribute. python-oracledb-1.2.1/doc/src/api_manual/dbobject_type.rst000066400000000000000000000130471434177474600236440ustar00rootroot00000000000000.. _dbobjecttype: ************************* API: DbObjectType Objects ************************* .. note:: This object is an extension to the DB API. It is returned by the :meth:`Connection.gettype()` call and is available as the :data:`Variable.type` for variables containing Oracle Database objects. DbObjectType Methods ==================== .. method:: DbObjectType([sequence]) The object type may be called directly and serves as an alternative way of calling :meth:`~DbObjectType.newobject()`. .. method:: DbObjectType.newobject([sequence]) Returns a new Oracle object of the given type. This object can then be modified by setting its attributes and then bound to a cursor for interaction with Oracle. If the object type refers to a collection, a sequence may be passed and the collection will be initialized with the items in that sequence. DbObjectType Attributes ======================= .. attribute:: DbObjectType.attributes This read-only attribute returns a list of the :ref:`attributes ` that make up the object type. .. attribute:: DbObjectType.element_type This read-only attribute returns the type of elements found in collections of this type, if :attr:`~DbObjectType.iscollection` is True; otherwise, it returns None. If the collection contains objects, this will be another object type; otherwise, it will be one of the :ref:`database type constants `. .. attribute:: DbObjectType.iscollection This read-only attribute returns a boolean indicating if the object type refers to a collection or not. .. attribute:: DbObjectType.name This read-only attribute returns the name of the type. .. attribute:: DbObjectType.package_name This read-only attribute returns the name of the package, if the type refers to a PL/SQL type (otherwise, it returns the value `None`). .. attribute:: DbObjectType.schema This read-only attribute returns the name of the schema that owns the type. .. _dbobject: DbObject Objects ================ .. note:: This object is an extension to the DB API. It is returned by the :meth:`DbObjectType.newobject()` call and can be bound to variables of type :data:`~oracledb.OBJECT`. Attributes can be retrieved and set directly. DbObject Methods ++++++++++++++++ .. method:: DbObject.append(element) Appends an element to the collection object. If no elements exist in the collection, this creates an element at index 0; otherwise, it creates an element immediately following the highest index available in the collection. .. method:: DbObject.asdict() Returns a dictionary where the collection's indexes are the keys and the elements are its values. .. method:: DbObject.aslist() Returns a list of each of the collection's elements in index order. .. method:: DbObject.copy() Creates a copy of the object and returns it. .. method:: DbObject.delete(index) Deletes the element at the specified index of the collection. If the element does not exist or is otherwise invalid, an error is raised. Note that the indices of the remaining elements in the collection are not changed. In other words, the delete operation creates holes in the collection. .. method:: DbObject.exists(index) Returns True or False indicating if an element exists in the collection at the specified index. .. method:: DbObject.extend(sequence) Appends all of the elements in the sequence to the collection. This is the equivalent of performing :meth:`~DbObject.append()` for each element found in the sequence. .. method:: DbObject.first() Returns the index of the first element in the collection. If the collection is empty, None is returned. .. method:: DbObject.getelement(index) Returns the element at the specified index of the collection. If no element exists at that index, an exception is raised. .. method:: DbObject.last() Returns the index of the last element in the collection. If the collection is empty, None is returned. .. method:: DbObject.next(index) Returns the index of the next element in the collection following the specified index. If there are no elements in the collection following the specified index, None is returned. .. method:: DbObject.prev(index) Returns the index of the element in the collection preceding the specified index. If there are no elements in the collection preceding the specified index, None is returned. .. method:: DbObject.setelement(index, value) Sets the value in the collection at the specified index to the given value. .. method:: DbObject.size() Returns the number of elements in the collection. .. method:: DbObject.trim(num) Removes the specified number of elements from the end of the collection. DbObject Attributes +++++++++++++++++++ .. attribute:: DbObject.Type This read-only attribute returns an ObjectType corresponding to the type of object. .. _dbobjectattr: DbObjectAttribute Objects ========================= .. note:: This object is an extension to the DB API. The elements of :attr:`DbObjectType.attributes` are instances of this type. .. attribute:: DbObjectAttribute.name This read-only attribute returns the name of the attribute. .. attribute:: DbObjectAttribute.type This read-only attribute returns the type of the attribute. This will be an :ref:`Oracle Object Type ` if the variable binds Oracle objects; otherwise, it will be one of the :ref:`database type constants `. python-oracledb-1.2.1/doc/src/api_manual/defaults.rst000066400000000000000000000064301434177474600226340ustar00rootroot00000000000000.. _defaults: ******************** API: Defaults Object ******************** This object contains attributes that can be used to adjust the behavior of the python-oracledb driver. All attributes are supported in Thin and Thick modes, subject to noted details. An example of changing a default value is: .. code-block:: python import oracledb oracledb.defaults.fetch_lobs = False # return LOBs directly as strings or bytes Defaults Attributes =================== .. attribute:: defaults.arraysize The default value for :attr:`Cursor.arraysize`. This is a query tuning attribute, see :ref:`Tuning Fetch Performance `. This attribute has an initial value of 100. .. attribute:: defaults.config_dir The directory in which optional configuration files such as ``tnsnames.ora`` will be read in python-oracledb Thin mode. This attribute takes its initial value from the environment variable ``TNS_ADMIN``. This attribute is not used by the python-oracledb Thick mode: the usual Oracle Client search path behavior for configuration files is followed, see :ref:`optnetfiles`. .. attribute:: defaults.fetch_decimals Identifies whether numbers should be fetched as ``decimal.Decimal`` values. This can help avoid issues with converting numbers from Oracle Database's decimal format to Python's binary format. An output type handler such as previously required in cx_Oracle (see `return_numbers_as_decimals.py `__) can alternatively be used to adjust the returned type. If a type handler exists and returns a variable (that is, `cursor.var(...)`), then that return variable is used. If the type handler returns None, then the value of ``oracledb.defaults.fetch_decimals`` is used to determine whether to return ``decimal.Decimal`` values. This attribute has an initial value of False. .. attribute:: defaults.fetch_lobs When the value of this attribute is True, then queries to LOB columns return LOB locators. When the value of this attribute is False, then strings or bytes are fetched. If LOBs are larger than 1 GB, then this attribute should be set to True and the LOBs should be streamed. See :ref:`lobdata`. An output type handler such as the one previously required in cx_Oracle (see `return_lobs_as_strings.py `__) can alternatively be used to adjust the returned type. If a type handler exists and returns a variable (that is, `cursor.var(...)`), then that return variable is used. If the type handler returns None, then the value of ``oracledb.defaults.fetch_lobs`` is used. This attribute has an initial value of True. .. attribute:: defaults.prefetchrows The default value for :attr:`Cursor.prefetchrows`. This is a query tuning attribute, see :ref:`Tuning Fetch Performance `. This attribute has an initial value of 2. .. attribute:: defaults.stmtcachesize The default value for :attr:`Connection.stmtcachesize` and :attr:`ConnectionPool.stmtcachesize`. This is a tuning attribute, see :ref:`stmtcache`. This attribute has an initial value of 20. python-oracledb-1.2.1/doc/src/api_manual/deprecations.rst000066400000000000000000000470461434177474600235150ustar00rootroot00000000000000.. _deprecations: ************ Deprecations ************ The following tables contain all of the deprecations in the python-oracledb API, when they were first deprecated and a comment on what should be used instead, if applicable. The most recent deprecations are listed first. .. list-table-with-summary:: Deprecated in python-oracledb 1.0 :header-rows: 1 :class: wy-table-responsive :summary: The first column, Name, displays the deprecated API name. The second column, Comments, includes information about when the API was deprecated and what API to use, if applicable. :name: _deprecations_1 * - Name - Comments * - `SessionPool class `_ and use of `cx_Oracle.SessionPool() `_. - Replace by the equivalent :ref:`ConnectionPool Class `. Use the new method :meth:`oracledb.create_pool()` to create connection pools. * - :meth:`Connection.begin()` - Replace by the new :ref:`tcp` functionality. * - :meth:`Connection.prepare()` - Replace by the new :ref:`tcp` functionality. * - Parameters ``encoding`` and ``nencoding`` of the :func:`oracledb.connect()`, :func:`oracledb.create_pool()` and ``oracledb.SessionPool()`` methods - The encodings in use are always UTF-8. * - Parameter ``threaded`` of the :meth:`oracledb.connect()` method - This was used to allow the Oracle Client libraries to support threaded applications. This value is ignored in python-oracledb because the threaded OCI is always enabled in the Thick mode, and the option is not relevant to the Thin mode. The equivalent parameter was already deprecated for `cx_Oracle.SessionPool() `_ in cx_Oracle 8.2. * - Attribute :attr:`Connection.maxBytesPerCharacter` of the Connection object - This was previously deprecated. In python-oracledb 1.0 it will return a constant value of 4 since encodings are always UTF-8. * - Size argument, ``numRows`` of the :meth:`Cursor.fetchmany()` method - Rename the parameter to ``size``. * - `cx_Oracle.makedsn() `_ - Pass the connection string components as connection creation, or pool creation, parameters. Or use a :ref:`ConnectParams Class ` object. * - oracledb.Connection() - This method is no longer recommended for creating connections. Use the equivalent function :meth:`oracledb.connect()` instead. * - Attribute ``Cursor.bindarraysize`` of the Cursor object - Remove this attribute since it is no longer needed. * - Constant :data:`~oracledb.ATTR_PURITY_DEFAULT` - Replace by :data:`~oracledb.PURITY_DEFAULT`. * - Constant :data:`~oracledb.ATTR_PURITY_NEW` - Replace by :data:`~oracledb.PURITY_NEW`. * - Constant :data:`~oracledb.ATTR_PURITY_SELF` - Replace by :data:`~oracledb.PURITY_SELF`. * - Constant :data:`~oracledb.SPOOL_ATTRVAL_WAIT` - Replace by :data:`~oracledb.POOL_GETMODE_WAIT`. * - Constant :data:`~oracledb.SPOOL_ATTRVAL_NOWAIT` - Replace by :data:`~oracledb.POOL_GETMODE_NOWAIT`. * - Constant :data:`~oracledb.SPOOL_ATTRVAL_FORCEGET` - Replace by :data:`~oracledb.POOL_GETMODE_FORCEGET`. * - Constant :data:`~oracledb.SPOOL_ATTRVAL_TIMEDWAIT` - Replace by :data:`~oracledb.POOL_GETMODE_TIMEDWAIT`. * - Constant :data:`~oracledb.DEFAULT_AUTH` - Replace by :data:`~oracledb.AUTH_MODE_DEFAULT`. * - Constant :data:`~oracledb.SYSASM` - Replace by :data:`~oracledb.AUTH_MODE_SYSASM`. * - Constant :data:`~oracledb.SYSBKP` - Replace by :data:`~oracledb.AUTH_MODE_SYSBKP`. * - Constant :data:`~oracledb.SYSDBA` - Replace by :data:`~oracledb.AUTH_MODE_SYSDBA`. * - Constant :data:`~oracledb.SYSDGD` - Replace by :data:`~oracledb.AUTH_MODE_SYSDGD`. * - Constant :data:`~oracledb.SYSKMT` - Replace by :data:`~oracledb.AUTH_MODE_SYSKMT`. * - Constant :data:`~oracledb.SYSOPER` - Replace by :data:`~oracledb.AUTH_MODE_SYSOPER`. * - Constant :data:`~oracledb.SYSRAC` - Replace by :data:`~oracledb.AUTH_MODE_SYSRAC`. * - Constant :data:`~oracledb.PRELIM_AUTH` - Replace by :data:`~oracledb.AUTH_MODE_PRELIM`. * - Constant :data:`~oracledb.SUBSCR_PROTO_OCI` - Replace by :data:`~oracledb.SUBSCR_PROTO_CALLBACK`. * - Class name `ObjectType `_ - Replace by the equivalent :ref:`DbObjectType`. * - Class name `Object `_ - Replace by the equivalent :ref:`DbObject `. Many of the usages deprecated in cx_Oracle (see tables below) are still supported by python-oracledb to ease upgrade from cx_Oracle. However, these previous cx_Oracle deprecation announcements remain in force for python-oracledb. The relevant functionality may be removed in a future version of python-oracledb. Some of the previous deprecations that have been removed and are not available in python-oracledb are listed below: - The previously deprecated function `Cursor.fetchraw() `__ has been removed in python-oracledb. Use one of the other fetch methods such as :meth:`Cursor.fetchmany()` instead. - The previously deprecated function `Cursor.executemanyprepared() `__ has been removed in python-oracledb. Use :meth:`Cursor.executemany()` instead. - The previously deprecated function `Cursor.rowcount() `__ has been removed in python-oracledb. Use :meth:`Cursor.executemany()` instead. - The previously deprecated Advanced Queuing (AQ) API has been removed in python-oracledb. Use the new AQ API instead. AQ is only available in the python-oracledb Thick mode. - Replace `Connection.deq() `__ with :meth:`Queue.deqone()` or :meth:`Queue.deqmany()`. - Replace `Connection.deqoptions() `__ with :meth:`Queue.deqoptions()`. - Replace `Connection.enq() `__ with :meth:`Queue.enqone()` or :meth:`Queue.enqmany()`. - Replace `Connection.enqoptions() `__ with :meth:`Queue.enqoptions()`. .. list-table-with-summary:: Deprecated in cx_Oracle 8.2 :header-rows: 1 :class: wy-table-responsive :summary: The first column, Name, displays the deprecated API name. The second column, Comments, includes information about when the API was deprecated and what API to use, if applicable. :name: _deprecations_8_2 * - Name - Comments * - ``encoding`` parameter to `cx_Oracle.connect() `_ - No longer needed as the use of encodings other than UTF-8 is deprecated. Encoding is handled internally between python-oracledb and Oracle Database. * - ``nencoding`` parameter to `cx_Oracle.connect() `_ - No longer needed as the use of encodings other than UTF-8 is deprecated. * - ``encoding`` parameter to `cx_Oracle.SessionPool() `_ - No longer needed as the use of encodings other than UTF-8 is deprecated. * - ``nencoding`` parameter to `cx_Oracle.SessionPool() `_ - No longer needed as the use of encodings other than UTF-8 is deprecated. * - Connection.maxBytesPerCharacter - No longer needed as the use of encodings other than UTF-8 is deprecated. The constant value 4 can be used instead. * - Positional parameters to `cx_Oracle.connect() `_ - Replace with keyword parameters in order to comply with the Python database API. * - Positional parameters to `cx_Oracle.SessionPool() `_ - Replace with keyword parameters in order to comply with the Python database API. * - ``threaded`` parameter to `cx_Oracle.SessionPool() `_ - The value of this parameter is ignored. Threading is now always used. * - ``waitTimeout`` parameter to `cx_Oracle.SessionPool() `_ - Replace with parameter name ``wait_timeout`` * - ``maxLifetimeSession`` parameter to `cx_Oracle.SessionPool() `_ - Replace with parameter name ``max_lifetime_session`` * - ``sessionCallback`` parameter to `cx_Oracle.SessionPool() `_ - Replace with parameter name ``session_callback`` * - ``maxSessionsPerShard`` parameter to `cx_Oracle.SessionPool() `_ - Replace with parameter name ``max_sessions_per_shard`` * - ``SessionPool.tnsentry`` - Replace with `SessionPool.dsn `_ * - ``payloadType`` parameter to `Connection.queue() `_ - Replace with parameter name ``payload_type`` if using keyword parameters. * - ``ipAddress`` parameter to `Connection.subscribe() `_ - Replace with parameter name ``ip_address`` * - ``groupingClass`` parameter to `Connection.subscribe() `_ - Replace with parameter name ``grouping_class`` * - ``groupingValue`` parameter to `Connection.subscribe() `_ - Replace with parameter name ``grouping_value`` * - ``groupingType`` parameter to `Connection.subscribe() `_ - Replace with parameter name ``grouping_type`` * - ``clientInitiated`` parameter to `Connection.subscribe() `_ - Replace with parameter name ``client_initiated`` * - ``Connection.callTimeout`` - Replace with `Connection.call_timeout `_ * - ``Connection.tnsentry`` - Replace with `Connection.dsn `_ * - `keywordParameters` parameter to `Cursor.callfunc() `_ - Replace with parameter name ``keyword_parameters`` * - ``keywordParameters`` parameter to `Cursor.callproc() `_ - Replace with parameter name ``keyword_parameters`` * - ``encodingErrors`` parameter to `Cursor.var() `_ - Replace with parameter name ``encoding_errors`` * - ``Cursor.fetchraw()`` - Replace with `Cursor.fetchmany() `_ * - ``newSize`` parameter to `LOB.trim() `_ - Replace with parameter name ``new_size`` * - ``Queue.deqMany`` - Replace with `Queue.deqmany() `_ * - ``Queue.deqOne`` - Replace with `Queue.deqone() `_ * - ``Queue.enqMany`` - Replace with `Queue.enqmany() `_ * - ``Queue.enqOne`` - Replace with `Queue.enqone() `_ * - ``Queue.deqOptions`` - Replace with `Queue.deqoptions `_ * - ``Queue.enqOptions`` - Replace with `Queue.enqoptions `_ * - ``Queue.payloadType`` - Replace with `Queue.payload_type `_ * - ``Subscription.ipAddress`` - Replace with `Subscription.ip_address `_ * - ``Message.consumerName`` - Replace with `Message.consumer_name `_ * - ``Message.queueName`` - Replace with `Message.queue_name `_ * - ``Variable.actualElements`` - Replace with `Variable.actual_elements `_ * - ``Variable.bufferSize`` - Replace with `Variable.buffer_size `_ * - ``Variable.numElements`` - Replace with `Variable.num_elements `_ .. list-table-with-summary:: Deprecated in cx_Oracle 8.0 :header-rows: 1 :class: wy-table-responsive :summary: The first column, Name, displays the deprecated API name. The second column, Comments, includes information about when the API was deprecated and what API to use, if applicable. :name: _deprecations_8_0 * - Name - Comments * - ``cx_Oracle.BFILE`` - Replace with `cx_Oracle.DB_TYPE_BFILE `_ * - ``cx_Oracle.BLOB`` - Replace with `cx_Oracle.DB_TYPE_BLOB `_ * - ``cx_Oracle.BOOLEAN`` - Replace with `cx_Oracle.DB_TYPE_BOOLEAN `_ * - ``cx_Oracle.CLOB`` - Replace with `cx_Oracle.DB_TYPE_CLOB `_ * - ``cx_Oracle.CURSOR`` - Replace with `cx_Oracle.DB_TYPE_CURSOR `_ * - ``cx_Oracle.FIXED_CHAR`` - Replace with `cx_Oracle.DB_TYPE_CHAR `_ * - ``cx_Oracle.FIXED_NCHAR`` - Replace with `cx_Oracle.DB_TYPE_NCHAR `_ * - ``cx_Oracle.INTERVAL`` - Replace with `cx_Oracle.DB_TYPE_INTERVAL_DS `_ * - ``cx_Oracle.LONG_BINARY`` - Replace with `cx_Oracle.DB_TYPE_LONG_RAW `_ * - ``cx_Oracle.LONG_STRING`` - Replace with `cx_Oracle.DB_TYPE_LONG `_ * - ``cx_Oracle.NATIVE_FLOAT`` - Replace with `cx_Oracle.DB_TYPE_BINARY_DOUBLE `_ * - ``cx_Oracle.NATIVE_INT`` - Replace with `cx_Oracle.DB_TYPE_BINARY_INTEGER `_ * - ``cx_Oracle.NCHAR`` - Replace with `cx_Oracle.DB_TYPE_NVARCHAR `_ * - ``cx_Oracle.NCLOB`` - Replace with `cx_Oracle.DB_TYPE_NCLOB `_ * - ``cx_Oracle.OBJECT`` - Replace with `cx_Oracle.DB_TYPE_OBJECT `_ * - ``cx_Oracle.TIMESTAMP`` - Replace with `cx_Oracle.DB_TYPE_TIMESTAMP `_ .. list-table-with-summary:: Deprecated in cx_Oracle 7.2 :header-rows: 1 :class: wy-table-responsive :summary: The first column, Name, displays the deprecated API name. The second column, Comments, includes information about when the API was deprecated and what API to use, if applicable. :name: _deprecations_7_2 * - Name - Comments * - ``Connection.deq()`` - Replace with `Queue.deqone() `_ or `Queue.deqmany() `_. * - ``Connection.deqoptions()`` - Replace with attribute `Queue.deqoptions `_. * - ``Connection.enq()`` - Replace with `Queue.enqone() `_ or `Queue.enqmany() `_. * - ``Connection.enqoptions()`` - Replace with attribute `Queue.enqoptions `_. .. list-table-with-summary:: Deprecated in cx_Oracle 6.4 :header-rows: 1 :class: wy-table-responsive :summary: The first column, Name, displays the deprecated API name. The second column, Comments, includes information about when the API was deprecated and what API to use, if applicable. :name: _deprecations_6_4 * - Name - Comments * - ``Cursor.executemanyprepared()`` - Replace with `~Cursor.executemany() `_ with None for the statement argument and an integer for the parameters argument. python-oracledb-1.2.1/doc/src/api_manual/lob.rst000066400000000000000000000057571434177474600216140ustar00rootroot00000000000000.. _lobobj: **************** API: LOB Objects **************** See :ref:`lobdata` for more information about using LOBs. .. note:: This object is an extension the DB API. It is returned whenever Oracle :data:`CLOB`, :data:`BLOB` and :data:`BFILE` columns are fetched. LOB Methods =========== .. method:: LOB.close() Closes the LOB. Call this when writing is completed so that the indexes associated with the LOB can be updated -- but only if :meth:`~LOB.open()` was called first. .. method:: LOB.fileexists() Returns a boolean indicating if the file referenced by the BFILE type LOB exists. .. method:: LOB.getchunksize() Returns the chunk size for the internal LOB. Reading and writing to the LOB in chunks of multiples of this size will improve performance. .. method:: LOB.getfilename() Returns a two-tuple consisting of the directory alias and file name for a BFILE type LOB. .. method:: LOB.isopen() Returns a boolean indicating if the LOB has been opened using the method :meth:`~LOB.open()`. .. method:: LOB.open() Opens the LOB for writing. This will improve performance when writing to a LOB in chunks and there are functional or extensible indexes associated with the LOB. If this method is not called, each write will perform an open internally followed by a close after the write has been completed. .. method:: LOB.read([offset=1, [amount]]) Returns a portion (or all) of the data in the LOB object. Note that the amount and offset are in bytes for BLOB and BFILE type LOBs and in UCS-2 code points for CLOB and NCLOB type LOBs. UCS-2 code points are equivalent to characters for all but supplemental characters. If supplemental characters are in the LOB, the offset and amount will have to be chosen carefully to avoid splitting a character. .. method:: LOB.setfilename(dirAlias, name) Sets the directory alias and name of the BFILE type LOB. .. method:: LOB.size() Returns the size of the data in the LOB object. For BLOB and BFILE type LOBs, this is the number of bytes. For CLOB and NCLOB type LOBs, this is the number of UCS-2 code points. UCS-2 code points are equivalent to characters for all but supplemental characters. .. method:: LOB.trim(new_size=0) Trims the LOB to the new size. .. method:: LOB.write(data, offset=1) Writes the data to the LOB object at the given offset. The offset is in bytes for BLOB type LOBs and in UCS-2 code points for CLOB and NCLOB type LOBs. UCS-2 code points are equivalent to characters for all but supplemental characters. If supplemental characters are in the LOB, the offset will have to be chosen carefully to avoid splitting a character. Note that if you want to make the LOB value smaller, you must use the :meth:`~LOB.trim()` function. LOB Attributes =============== .. attribute:: LOB.type This read-only attribute returns the type of the LOB as one of the :ref:`database type constants `. python-oracledb-1.2.1/doc/src/api_manual/module.rst000066400000000000000000003174431434177474600223230ustar00rootroot00000000000000.. module:: oracledb .. _module: **************************** API: python-oracledb Module **************************** .. _modmeth: Oracledb Methods ================ .. data:: __future__ Special object which contains attributes which control the behavior of python-oracledb, allowing for opting in for new features. No attributes are currently supported so all attributes will silently ignore being set and will always appear to have the value None. .. note:: This method is an extension to the DB API definition. .. function:: Binary(string) Constructs an object holding a binary (long) string value. .. function:: clientversion() Returns the version of the client library being used as a 5-tuple. The five values are the major version, minor version, update number, patch number, and port update number. .. note:: This function can only be called when python-oracledb is in Thick mode. See :ref:`enablingthick`. If ``clientversion()`` is called when in python-oracledb Thin mode, that is, if :func:`oracledb.init_oracle_client()` is not called first, then an exception will be thrown. .. note:: This method is an extension to the DB API definition. .. function:: connect(dsn=None, pool=None, conn_class=None, params=None, user=None, \ proxy_user=None, password=None, newpassword=None, wallet_password=None, access_token=None, \ host=None, port=1521, protocol="tcp", https_proxy=None, https_proxy_port=0, \ service_name=None, sid=None, server_type=None, cclass=None, purity=oracledb.PURITY_DEFAULT, \ expire_time=0, retry_count=0, retry_delay=0, tcp_connect_timeout=60.0, \ ssl_server_dn_match=True, ssl_server_cert_dn=None, wallet_location=None, events=False, \ externalauth=False, mode=oracledb.AUTH_MODE_DEFAULT, disable_oob=False, \ stmtcachesize=oracledb.defaults.stmtcachesize, edition=None, tag=None, matchanytag=False, \ config_dir=oracledb.defaults.config_dir, appcontext=[], shardingkey=[], supershardingkey=[], \ debug_jdwp=None, handle=0) Constructor for creating a connection to the database. Returns a :ref:`Connection Object `. All parameters are optional and can be specified as keyword parameters. See :ref:`standaloneconnection` information about connections. Not all parameters apply to both python-oracledb Thin and :ref:`Thick ` modes. Some values, such as the database host name, can be specified as parameters, as part of the connect string, and in the params object. If a ``dsn`` (data source name) parameter is passed, the python-oracledb Thick mode will use the string to connect, otherwise a connection string is internally constructed from the individual parameters and params object values, with the individual parameters having precedence. In python-oracledb's default Thin mode, a connection string is internally used that contains all relevant values specified. The precedence in Thin mode is that values in any ``dsn`` parameter override values passed as individual parameters, which themselves override values set in the ``params`` parameter object. Similar precedence rules also apply to other values. The ``dsn`` (data source name) parameter can be a string in the format ``user/password@connect_string`` or can simply be the connect string (in which case authentication credentials such as the username and password need to be specified separately). See :ref:`connstr` for more information. The ``pool`` parameter is expected to be a pool object. The use of this parameter is the equivalent of calling :meth:`ConnectionPool.acquire()`. The ``conn_class`` parameter is expected to be Connection or a subclass of Connection. The ``params`` parameter is expected to be of type :ref:`ConnectParams ` and contains connection parameters that will be used when establishing the connection. If this parameter is not specified, the additional keyword parameters will be used to create an instance of ConnectParams. If both the params parameter and additional keyword parameters are specified, the values in the keyword parameters have precedence. Note that if a ``dsn`` is also supplied, then in the python-oracledb Thin mode, the values of the parameters specified (if any) within the ``dsn`` will override the values passed as additional keyword parameters, which themselves override the values set in the ``params`` parameter object. The ``user`` parameter is expected to be a string which indicates the name of the user to connect to. This value is used in both the python-oracledb Thin and Thick modes. The ``proxy_user`` parameter is expected to be a string which indicates the name of the proxy user to connect to. If this value is not specified, it will be parsed out of user if user is in the form "user[proxy_user]". This value is used in both the python-oracledb Thin and Thick modes. The ``password`` parameter expected to be a string which indicates the password for the user. This value is used in both the python-oracledb Thin and Thick modes. The ``newpassword`` parameter is expected to be a string which indicates the new password for the user. The new password will take effect immediately upon a successful connection to the database. This value is used in both the python-oracledb Thin and Thick modes. The ``wallet_password`` parameter is expected to be a string which indicates the password to use to decrypt the PEM-encoded wallet, if it is encrypted. This value is only used in python-oracledb Thin mode. The ``wallet_password`` parameter is not needed for cwallet.sso files that are used in the python-oracledb Thick mode. The ``access_token`` parameter is expected to be a string or a 2-tuple or a callable. If it is a string, it specifies an Azure AD OAuth2 token used for Open Authorization (OAuth 2.0) token based authentication. If it is a 2-tuple, it specifies the token and private key strings used for Oracle Cloud Infrastructure (OCI) Identity and Access Management (IAM) token based authentication. If it is a callable, it returns either a string or a 2-tuple used for OAuth 2.0 or OCI IAM token based authentication and is useful when the pool needs to expand and create new connections but the current authentication token has expired. This value is used in both the python-oracledb Thin and Thick modes. The ``host`` parameter is expected to be a string which specifies the name or IP address of the machine hosting the listener, which handles the initial connection to the database. This value is used in both the python-oracledb Thin and Thick modes. The ``port`` parameter is expected to be an integer which indicates the port number on which the listener is listening. The default value is 1521. This value is used in both the python-oracledb Thin and Thick modes. The ``protocol`` parameter is expected to be one of the strings "tcp" or "tcps" which indicates whether to use unencrypted network traffic or encrypted network traffic (TLS). The default value is tcp. This value is used in both the python-oracledb Thin and Thick modes. The ``https_proxy`` parameter is expected to be a string which indicates the name or IP address of a proxy host to use for tunneling secure connections. This value is used in both the python-oracledb Thin and Thick modes. The ``https_proxy_port`` parameter is expected to be an integer which indicates the port that is to be used to communicate with the proxy host. The default value is 0. This value is used in both the python-oracledb Thin and Thick modes. The ``service_name`` parameter is expected to be a string which indicates the service name of the database. This value is used in both the python-oracledb Thin and Thick modes. The ``sid`` parameter is expected to be a string which indicates the SID of the database. It is recommended to use ``service_name`` instead. This value is used in both the python-oracledb Thin and Thick modes. The ``server_type`` parameter is expected to be a string that indicates the type of server connection that should be established. If specified, it should be one of `dedicated`, `shared`, or `pooled`. This value is used in both the python-oracledb Thin and Thick modes. The ``cclass`` parameter is expected to be a string that identifies the connection class to use for Database Resident Connection Pooling (DRCP). This value is used in both the python-oracledb Thin and Thick modes. The ``purity`` parameter is expected to be one of the :ref:`oracledb.PURITY_* ` constants that identifies the purity to use for DRCP. This value is used in both the python-oracledb Thin and Thick modes. The purity will internally default to :data:`~oracledb.PURITY_SELF` for pooled connections. For standalone connections, the purity will internally default to :data:`~oracledb.PURITY_NEW`. The ``expire_time`` parameter is expected to be an integer which indicates the number of minutes between the sending of keepalive probes. If this parameter is set to a value greater than zero it enables keepalive. This value is used in both the python-oracledb Thin and Thick modes. The default value is 0. The ``retry_count`` parameter is expected to be an integer that identifies the number of times that a connection attempt should be retried before the attempt is terminated. This value is used in both the python-oracledb Thin and Thick modes. The default value is 0. The ``retry_delay`` parameter is expected to be an integer that identifies the number of seconds to wait before making a new connection attempt. This value is used in both the python-oracledb Thin and Thick modes. The default value is 0. The ``tcp_connect_timeout`` parameter is expected to be a float that indicates the maximum number of seconds to wait for establishing a connection to the database host. This value is used in both the python-oracledb Thin and Thick modes. The default value is 60.0. The ``ssl_server_dn_match`` parameter is expected to be a boolean that indicates whether the server certificate distinguished name (DN) should be matched in addition to the regular certificate verification that is performed. Note that if the ``ssl_server_cert_dn`` parameter is not provided, host name matching is performed instead. This value is used in both the python-oracledb Thin and Thick modes. The default value is True. The ``ssl_server_cert_dn`` parameter is expected to be a string that indicates the distinguished name (DN) which should be matched with the server. This value is ignored if the ``ssl_server_dn_match`` parameter is not set to the value True. This value is used in both the python-oracledb Thin and Thick modes. The ``wallet_location`` parameter is expected to be a string that identifies the directory where the wallet can be found. In python-oracledb Thin mode, this must be the directory of the PEM-encoded wallet file, ewallet.pem. In python-oracledb Thick mode, this must be the directory of the file, cwallet.sso. This value is used in both the python-oracledb Thin and Thick modes. The ``events`` parameter is expected to be a boolean that specifies whether the events mode should be enabled. This value is only used in the python-oracledb Thick mode. This parameter is needed for continuous query notification and high availability event notifications. The default value is False. The ``externalauth`` parameter is a boolean that specifies whether external authentication should be used. This value is only used in the python-oracledb Thick mode. The default value is False. For standalone connections, external authentication occurs when the ``user`` and ``password`` attributes are not used. If these attributes are not used, you can optionally set the ``externalauth`` attribute to True, which may aid code auditing. If the ``mode`` parameter is specified, it must be one of the :ref:`connection authorization modes ` which are defined at the module level. This value is used in both the python-oracledb Thin and Thick modes. The default value is :data:`oracledb.AUTH_MODE_DEFAULT`. The ``disable_oob`` parameter is expected to be a boolean that indicates whether out-of-band breaks should be disabled. This value is only used in the python-oracledb Thin mode and has no effect on Windows which does not support this functionality. The default value is False. The ``stmtcachesize`` parameter is expected to be an integer which specifies the initial size of the statement cache. This value is used in both the python-oracledb Thin and Thick modes. The default is the value of :attr:`defaults.stmtcachesize`. The ``edition`` parameter is expected to be a string that indicates the edition to use for the connection. This parameter cannot be used simultaneously with the ``cclass`` parameter. This value is used in the python-oracledb Thick mode. The ``tag`` parameter is expected to be a string that identifies the type of connection that should be returned from a pool. This value is only used in the python-oracledb Thick mode. The ``matchanytag`` parameter is expected to be a boolean specifying whether any tag can be used when acquiring a connection from the pool. This value is only used in the python-oracledb Thick mode when acquiring a connection from a pool. The default value is False. The ``config_dir`` parameter is expected to be a string that indicates the directory in which configuration files (tnsnames.ora) are found. This value is only used in python-oracledb Thin mode. The default is the value of :attr:`defaults.config_dir`. For python-oracledb Thick mode, use the ``config_dir`` parameter of :func:`oracledb.init_oracle_client()`. The ``appcontext`` parameter is expected to be a list of 3-tuples that identifies the application context used by the connection. This parameter should contain namespace, name, and value and each entry in the tuple should be a string. This value is only used in the python-oracledb Thick mode. The ``shardingkey`` parameter and ``supershardingkey`` parameters, if specified, are expected to be a sequence of values which identifies the database shard to connect to. The key values can be a list of strings, numbers, bytes, or dates. This value is only used in the python-oracledb Thick mode. The ``debug_jdwp`` parameter is expected to be a string with the format `host=;port=` that specifies the host and port of the PL/SQL debugger. This allows using the Java Debug Wire Protocol (JDWP) to debug PL/SQL code called by python-oracledb. This value is only used in the python-oracledb Thin mode. For python-oracledb Thick mode, set the ``ORA_DEBUG_JDWP`` environment variable which has the same syntax. For more information, see :ref:`applntracing`. If the ``handle`` parameter is specified, it must be of type OCISvcCtx\* and is only of use when embedding Python in an application (like PowerBuilder) which has already made the connection. The connection thus created should *never* be used after the source handle has been closed or destroyed. This value is only used in the python-oracledb Thick mode. It should be used with extreme caution. The default value is 0. .. function:: ConnectParams(user=None, proxy_user=None, password=None, \ newpassword=None, wallet_password=None, access_token=None, host=None, \ port=1521, protocol="tcp", https_proxy=None, https_proxy_port=0, service_name=None, \ sid=None, server_type=None, cclass=None, purity=oracledb.PURITY_DEFAULT, expire_time=0, \ retry_count=0, retry_delay=0, tcp_connect_timeout=60.0, ssl_server_dn_match=True, \ ssl_server_cert_dn=None, wallet_location=None, events=False, externalauth=False, \ mode=oracledb.AUTH_MODE_DEFAULT, disable_oob=False, stmtcachesize=oracledb.defaults.stmtcachesize, \ edition=None, tag=None, matchanytag=False, config_dir=oracledb.defaults.config_dir, \ appcontext=[], shardingkey=[], supershardingkey=[], debug_jdwp=None, handle=0) Contains all the parameters that can be used to establish a connection to the database. Creates and returns a :ref:`ConnectParams Object `. The object can be passed to :meth:`oracledb.connect()`. All the parameters are optional. The ``user`` parameter is expected to be a string which indicates the name of the user to connect to. This value is used in both the python-oracledb Thin and :ref:`Thick ` modes. The ``proxy_user`` parameter is expected to be a string which indicates the name of the proxy user to connect to. If this value is not specified, it will be parsed out of user if user is in the form "user[proxy_user]". This value is used in both the python-oracledb Thin and Thick modes. The ``password`` parameter expected to be a string which indicates the password for the user. This value is used in both the python-oracledb Thin and Thick modes. The ``newpassword`` parameter is expected to be a string which indicates the new password for the user. The new password will take effect immediately upon a successful connection to the database. This value is used in both the python-oracledb Thin and Thick modes. The ``wallet_password`` parameter is expected to be a string which indicates the password to use to decrypt the PEM-encoded wallet, if it is encrypted. This value is only used in python-oracledb Thin mode. The ``wallet_password`` parameter is not needed for cwallet.sso files that are used in the python-oracledb Thick mode. The ``access_token`` parameter is expected to be a string or a 2-tuple or a callable. If it is a string, it specifies an Azure AD OAuth2 token used for Open Authorization (OAuth 2.0) token based authentication. If it is a 2-tuple, it specifies the token and private key strings used for Oracle Cloud Infrastructure (OCI) Identity and Access Management (IAM) token based authentication. If it is a callable, it returns either a string or a 2-tuple used for OAuth 2.0 or OCI IAM token based authentication and is useful when the pool needs to expand and create new connections but the current authentication token has expired. This value is used in both the python-oracledb Thin and Thick modes. The ``host`` parameter is expected to be a string which specifies the name or IP address of the machine hosting the listener, which handles the initial connection to the database. This value is used in both the python-oracledb Thin and Thick modes. The ``port`` parameter is expected to be an integer which indicates the port number on which the listener is listening. The default value is 1521. This value is used in both the python-oracledb Thin and Thick modes. The ``protocol`` parameter is expected to be one of the strings "tcp" or "tcps" which indicates whether to use unencrypted network traffic or encrypted network traffic (TLS). The default value is tcp. This value is used in both the python-oracledb Thin and Thick modes. The ``https_proxy`` parameter is expected to be a string which indicates the name or IP address of a proxy host to use for tunneling secure connections. This value is used in both the python-oracledb Thin and Thick modes. The ``https_proxy_port`` parameter is expected to be an integer which indicates the port that is to be used to communicate with the proxy host. The default value is 0. This value is used in both the python-oracledb Thin and Thick modes. The ``service_name`` parameter is expected to be a string which indicates the service name of the database. This value is used in both the python-oracledb Thin and Thick modes. The ``sid`` parameter is expected to be a string which indicates the SID of the database. It is recommended to use ``service_name`` instead. This value is used in both the python-oracledb Thin and Thick modes. The ``server_type`` parameter is expected to be a string that indicates the type of server connection that should be established. If specified, it should be one of "dedicated", "shared", or "pooled". This value is used in both the python-oracledb Thin and Thick modes. The ``cclass`` parameter is expected to be a string that identifies the connection class to use for Database Resident Connection Pooling (DRCP). This value is used in both the python-oracledb Thin and Thick modes. The ``purity`` parameter is expected to be one of the :ref:`oracledb.PURITY_* ` constants that identifies the purity to use for DRCP. This value is used in both the python-oracledb Thin and Thick modes. The purity will internally default to :data:`~oracledb.PURITY_SELF` for pooled connections . For standalone connections, the purity will internally default to :data:`~oracledb.PURITY_NEW`. The ``expire_time`` parameter is expected to be an integer which indicates the number of minutes between the sending of keepalive probes. If this parameter is set to a value greater than zero it enables keepalive. This value is used in both the python-oracledb Thin and Thick modes. The default value is 0. The ``retry_count`` parameter is expected to be an integer that identifies the number of times that a connection attempt should be retried before the attempt is terminated. This value is used in both the python-oracledb Thin and Thick modes. The default value is 0. The ``retry_delay`` parameter is expected to be an integer that identifies the number of seconds to wait before making a new connection attempt. This value is used in both the python-oracledb Thin and Thick modes. The default value is 0. The ``tcp_connect_timeout`` parameter is expected to be a float that indicates the maximum number of seconds to wait for establishing a connection to the database host. This value is used in both the python-oracledb Thin and Thick modes. The default value is 60.0. The ``ssl_server_dn_match`` parameter is expected to be a boolean that indicates whether the server certificate distinguished name (DN) should be matched in addition to the regular certificate verification that is performed. Note that if the ``ssl_server_cert_dn`` parameter is not provided, host name matching is performed instead. This value is used in both the python-oracledb Thin and Thick modes. The default value is True. The ``ssl_server_cert_dn`` parameter is expected to be a string that indicates the distinguished name (DN) which should be matched with the server. This value is ignored if the ``ssl_server_dn_match`` parameter is not set to the value True. This value is used in both the python-oracledb Thin and Thick modes. The ``wallet_location`` parameter is expected to be a string that identifies the directory where the wallet can be found. In python-oracledb Thin mode, this must be the directory of the PEM-encoded wallet file, ewallet.pem. In python-oracledb Thick mode, this must be the directory of the file, cwallet.sso. This value is used in both the python-oracledb Thin and Thick modes. The ``events`` parameter is expected to be a boolean that specifies whether the events mode should be enabled. This value is only used in the python-oracledb Thick mode. This parameter is needed for continuous query notification and high availability event notifications. The default value is False. The ``externalauth`` parameter is a boolean that specifies whether external authentication should be used. This value is only used in the python-oracledb Thick mode. The default value is False. For standalone connections, external authentication occurs when the ``user`` and ``password`` attributes are not used. If these attributes are not used, you can optionally set the ``externalauth`` attribute to True, which may aid code auditing. The ``mode`` parameter is expected to be an integer that identifies the authorization mode to use. This value is used in both the python-oracledb Thin and Thick modes.The default value is :data:`oracledb.AUTH_MODE_DEFAULT`. The ``disable_oob`` parameter is expected to be a boolean that indicates whether out-of-band breaks should be disabled. This value is only used in the python-oracledb Thin mode and has no effect on Windows which does not support this functionality. The default value is False. The ``stmtcachesize`` parameter is expected to be an integer that identifies the initial size of the statement cache. This value is used in both the python-oracledb Thin and Thick modes. The default is the value of :attr:`defaults.stmtcachesize`. The ``edition`` parameter is expected to be a string that indicates the edition to use for the connection. This parameter cannot be used simultaneously with the ``cclass`` parameter. This value is used in the python-oracledb Thick mode. The ``tag`` parameter is expected to be a string that identifies the type of connection that should be returned from a pool. This value is only used in the python-oracledb Thick mode. The ``matchanytag`` parameter is expected to be a boolean specifying whether any tag can be used when acquiring a connection from the pool. This value is only used in the python-oracledb Thick mode when acquiring a connection from a pool. The default value is False. The ``config_dir`` parameter is expected to be a string that indicates the directory in which configuration files (tnsnames.ora) are found. This value is only used in python-oracledb Thin mode. The default is the value of :attr:`defaults.config_dir`. For python-oracledb Thick mode, use the ``config_dir`` parameter of :func:`oracledb.init_oracle_client()`. The ``appcontext`` parameter is expected to be a list of 3-tuples that identifies the application context used by the connection. This parameter should contain namespace, name, and value and each entry in the tuple should be a string. This value is only used inthe python-oracledb Thick mode. The ``shardingkey`` parameter is expected to be a list of strings, numbers, bytes or dates that identifies the database shard to connect to. This value is only used in the python-oracledb Thick mode. The ``supershardingkey`` parameter is expected to be a list of strings, numbers, bytes or dates that identifies the database shard to connect to. This value is only used in the python-oracledb Thick mode. The ``debug_jdwp`` parameter is expected to be a string with the format `host=;port=` that specifies the host and port of the PL/SQL debugger. This allows using the Java Debug Wire Protocol (JDWP) to debug PL/SQL code invoked by python-oracledb. This value is only used in the python-oracledb Thin mode. For python-oracledb Thick mode, set the ``ORA_DEBUG_JDWP`` environment variable which has the same syntax. For more information, see :ref:`applntracing`. The ``handle`` parameter is expected to be an integer which represents a pointer to a valid service context handle. This value is only used in the python-oracledb Thick mode. It should be used with extreme caution. The default value is 0. .. function:: create_pool(dsn=None, pool_class=oracledb.ConnectionPool, \ params=None, min=1, max=2, increment=1, connectiontype=oracledb.Connection, \ getmode=oracledb.POOL_GETMODE_WAIT, homogeneous=True, timeout=0, \ wait_timeout=0, max_lifetime_session=0, session_callback=None, \ max_sessions_per_shard=0, soda_metadata_cache=False, ping_interval=60, \ user=None, proxy_user=None, password=None, newpassword=None, \ wallet_password=None, access_token=None, host=None, port=1521, \ protocol="tcp", https_proxy=None, https_proxy_port=0, service_name=None, \ sid=None, server_type=None, cclass=None, purity=oracledb.PURITY_DEFAULT, \ expire_time=0, retry_count=0, retry_delay=0, tcp_connect_timeout=60.0, \ ssl_server_dn_match=True, ssl_server_cert_dn=None, wallet_location=None, \ events=False, externalauth=False, mode=oracledb.AUTH_MODE_DEFAULT, \ disable_oob=False, stmtcachesize=oracledb.defaults.stmtcachesize, edition=None, \ tag=None, matchanytag=False, config_dir=oracledb.defaults.config_dir, \ appcontext=[], shardingkey=[], supershardingkey=[], debug_jdwp=None, handle=0) Creates a connection pool with the supplied parameters and returns the :ref:`ConnectionPool object ` for the pool. See :ref:`Connection pooling ` for more information. This function is the equivalent of the `cx_Oracle.SessionPool() `__ function. The use of ``SessionPool()`` has been deprecated in python-oracledb. Not all parameters apply to both python-oracledb Thin and :ref:`Thick ` modes. Some values, such as the database host name, can be specified as parameters, as part of the connect string, and in the params object. If a ``dsn`` (data source name) parameter is passed, the python-oracledb Thick mode will use the string to connect, otherwise a connection string is internally constructed from the individual parameters and params object values, with the individual parameters having precedence. In python-oracledb's default Thin mode, a connection string is internally used that contains all relevant values specified. The precedence in Thin mode is that values in any ``dsn`` parameter override values passed as individual parameters, which themselves override values set in the ``params`` parameter object. Similar precedence rules also apply to other values. The ``user``, ``password``, and ``dsn`` parameters are the same as for :meth:`oracledb.connect()`. The ``pool_class`` parameter is expected to be a :ref:`ConnectionPool Object ` or a subclass of ConnectionPool. The ``params`` parameter is expected to be of type :ref:`PoolParams ` and contains parameters that are used to create the pool. If this parameter is not specified, the additional keyword parameters will be used to create an instance of PoolParams. If both the params parameter and additional keyword parameters are specified, the values in the keyword parameters have precedence. Note that if a ``dsn`` is also supplied, then in the python-oracledb Thin mode, the values of the parameters specified (if any) within the ``dsn`` will override the values passed as additional keyword parameters, which themselves override the values set in the ``params`` parameter object. The ``min``, ``max`` and ``increment`` parameters control pool growth behavior. A fixed pool size where ``min`` equals ``max`` is :ref:`recommended ` to help prevent connection storms and to help overall system stability. The ``min`` parameter is the number of connections opened when the pool is created. The default value of the ``min`` parameter is 1. The ``increment`` parameter is the number of connections that are opened whenever a connection request exceeds the number of currently open connections. The default value of the ``increment`` parameter is 1. The ``max`` parameter is the maximum number of connections that can be open in the connection pool. The default value of the ``max`` parameter is 2. If the ``connectiontype`` parameter is specified, all calls to :meth:`ConnectionPool.acquire()` will create connection objects of that type, rather than the base type defined at the module level. The ``getmode`` parameter determines the behavior of :meth:`ConnectionPool.acquire()`. One of the constants :data:`oracledb.POOL_GETMODE_WAIT`, :data:`oracledb.POOL_GETMODE_NOWAIT`, :data:`oracledb.POOL_GETMODE_FORCEGET`, or :data:`oracledb.POOL_GETMODE_TIMEDWAIT`. The default value is :data:`oracledb.POOL_GETMODE_WAIT`. The ``homogeneous`` parameter is a boolean that indicates whether the connections are homogeneous (same user) or heterogeneous (multiple users). The default value is True. The ``timeout`` parameter is the length of time (in seconds) that a connection may remain idle in the pool before it is terminated. If the value of this parameter is 0, then the connections are never terminated. The default value is 0. The ``wait_timeout`` parameter is the length of time (in milliseconds) that a caller should wait when acquiring a connection from the pool with ``getmode`` set to :data:`oracledb.POOL_GETMODE_TIMEDWAIT`. The default value is 0. The ``max_lifetime_session`` parameter is the length of time (in seconds) that connections can remain in the pool. If the value of this parameter is 0, then the connections may remain in the pool indefinitely. The default value is 0. The ``session_callback`` parameter is a callable that is invoked when a connection is returned from the pool for the first time, or when the connection tag differs from the one requested. The ``max_sessions_per_shard`` parameter is the maximum number of connections that may be associated with a particular shard. The default value is 0. The ``soda_metadata_cache`` parameter is a boolean that indicates whether or not the SODA metadata cache should be enabled. The default value is False. The ``ping_interval`` parameter is the length of time (in seconds) after which an unused connection in the pool will be a candidate for pinging when :meth:`ConnectionPool.acquire()` is called. If the ping to the database indicates the connection is not alive a replacement connection will be returned by :meth:`~ConnectionPool.acquire()`. If ``ping_interval`` is a negative value, then the ping functionality will be disabled. The default value is 60 seconds. The ``proxy_user`` parameter is expected to be a string which indicates the name of the proxy user to connect to. If this value is not specified, it will be parsed out of user if user is in the form "user[proxy_user]". This value is used in both the python-oracledb Thin and Thick modes. The ``newpassword`` parameter is expected to be a string which indicates the new password for the user. The new password will take effect immediately upon a successful connection to the database. This value is used in both the python-oracledb Thin and Thick modes. The ``wallet_password`` parameter is expected to be a string which indicates the password to use to decrypt the PEM-encoded wallet, if it is encrypted. This value is only used in python-oracledb Thin mode. The ``wallet_password`` parameter is not needed for cwallet.sso files that are used in the python-oracledb Thick mode. The ``access_token`` parameter is expected to be a string or a 2-tuple or a callable. If it is a string, it specifies an Azure AD OAuth2 token used for Open Authorization (OAuth 2.0) token based authentication. If it is a 2-tuple, it specifies the token and private key strings used for Oracle Cloud Infrastructure (OCI) Identity and Access Management (IAM) token based authentication. If it is a callable, it returns either a string or a 2-tuple used for OAuth 2.0 or OCI IAM token based authentication and is useful when the pool needs to expand and create new connections but the current authentication token has expired. This value is used in both the python-oracledb Thin and Thick modes. The ``host`` parameter is expected to be a string which specifies the name or IP address of the machine hosting the listener, which handles the initial connection to the database. This value is used in both the python-oracledb Thin and Thick modes. The ``port`` parameter is expected to be an integer which indicates the port number on which the listener is listening. The default value is 1521. This value is used in both the python-oracledb Thin and Thick modes. The ``protocol`` parameter is expected to be one of the strings "tcp" or "tcps" which indicates whether to use unencrypted network traffic or encrypted network traffic (TLS). The default value is tcp. This value is used in both the python-oracledb Thin and Thick modes. The ``https_proxy`` parameter is expected to be a string which indicates the name or IP address of a proxy host to use for tunneling secure connections. This value is used in both the python-oracledb Thin and Thick modes. The ``https_proxy_port`` parameter is expected to be an integer which indicates the port that is to be used to communicate with the proxy host. The default value is 0. This value is used in both the python-oracledb Thin and Thick modes. The ``service_name`` parameter is expected to be a string which indicates the service name of the database. This value is used in both the python-oracledb Thin and Thick modes. The ``sid`` parameter is expected to be a string which indicates the SID of the database. It is recommended to use ``service_name`` instead. This value is used in both the python-oracledb Thin and Thick modes. The ``server_type`` parameter is expected to be a string that indicates the type of server connection that should be established. If specified, it should be one of `dedicated`, `shared`, or `pooled`. This value is used in both the python-oracledb Thin and Thick modes. The ``cclass`` parameter is expected to be a string that identifies the connection class to use for Database Resident Connection Pooling (DRCP). This value is used in both the python-oracledb Thin and Thick modes. The ``purity`` parameter is expected to be one of the :ref:`oracledb.PURITY_* ` constants that identifies the purity to use for DRCP. This value is used in both the python-oracledb Thin and Thick modes. The purity will internally default to :data:`~oracledb.PURITY_SELF` for pooled connections. The ``expire_time`` parameter is expected to be an integer which indicates the number of minutes between the sending of keepalive probes. If this parameter is set to a value greater than zero it enables keepalive. This value is used in both the python-oracledb Thin and Thick modes. The default value is 0. The ``retry_count`` parameter is expected to be an integer that identifies the number of times that a connection attempt should be retried before the attempt is terminated. This value is used in both the python-oracledb Thin and Thick modes. The default value is 0. The ``retry_delay`` parameter is expected to be an integer that identifies the number of seconds to wait before making a new connection attempt. This value is used in both the python-oracledb Thin and Thick modes. The default value is 0. The ``tcp_connect_timeout`` parameter is expected to be a float that indicates the maximum number of seconds to wait for establishing a connection to the database host. This value is used in both the python-oracledb Thin and Thick modes. The default value is 60.0. The ``ssl_server_dn_match`` parameter is expected to be a boolean that indicates whether the server certificate distinguished name (DN) should be matched in addition to the regular certificate verification that is performed. Note that if the ``ssl_server_cert_dn`` parameter is not provided, host name matching is performed instead. This value is used in both the python-oracledb Thin and Thick modes. The default value is True. The ``ssl_server_cert_dn`` parameter is expected to be a string that indicates the distinguished name (DN) which should be matched with the server. This value is ignored if the ``ssl_server_dn_match`` parameter is not set to the value True. This value is used in both the python-oracledb Thin and Thick modes. The ``wallet_location`` parameter is expected to be a string that identifies the directory where the wallet can be found. In python-oracledb Thin mode, this must be the directory of the PEM-encoded wallet file, ewallet.pem. In python-oracledb Thick mode, this must be the directory of the file, cwallet.sso. This value is used in both the python-oracledb Thin and Thick modes. The ``events`` parameter is expected to be a boolean that specifies whether the events mode should be enabled. This value is only used in the python-oracledb Thick mode. This parameter is needed for continuous query notification and high availability event notifications. The default value is False. The ``externalauth`` parameter is a boolean that determines whether to use external authentication. This value is only used in the python-oracledb Thick mode. The default value is False. If the ``mode`` parameter is specified, it must be one of the :ref:`connection authorization modes ` which are defined at the module level. This value is used in both the python-oracledb Thin and Thick modes.The default value is :data:`oracledb.AUTH_MODE_DEFAULT`. The ``disable_oob`` parameter is expected to be a boolean that indicates whether out-of-band breaks should be disabled. This value is only used in the python-oracledb Thin mode and has no effect on Windows which does not support this functionality. The default value is False. The ``stmtcachesize`` parameter is expected to be an integer which specifies the initial size of the statement cache. This value is used in both the python-oracledb Thin and Thick modes. The default is the value of :attr:`defaults.stmtcachesize`. The ``edition`` parameter is expected to be a string that indicates the edition to use for the connection. This parameter cannot be used simultaneously with the ``cclass`` parameter. This value is used in the python-oracledb Thick mode. The ``tag`` parameter is expected to be a string that identifies the type of connection that should be returned from a pool. This value is only used in the python-oracledb Thick mode. The ``matchanytag`` parameter is expected to be a boolean specifying whether any tag can be used when acquiring a connection from the pool. This value is only used in the python-oracledb Thick mode when acquiring a connection from a pool. The default value is False. The ``config_dir`` parameter is expected to be a string that indicates the directory in which configuration files (tnsnames.ora) are found. This value is only used in python-oracledb Thin mode. The default is the value of :attr:`defaults.config_dir`. For python-oracledb Thick mode, use the ``config_dir`` parameter of :func:`oracledb.init_oracle_client()`. The ``appcontext`` parameter is expected to be a list of 3-tuples that identifies the application context used by the connection. This parameter should contain namespace, name, and value and each entry in the tuple should be a string. This value is only used inthe python-oracledb Thick mode. The ``shardingkey`` parameter and ``supershardingkey`` parameters, if specified, are expected to be a sequence of values which identifies the database shard to connect to. The key values can be a list of strings, numbers, bytes, or dates. This value is only used in the python-oracledb Thick mode. The ``debug_jdwp`` parameter is expected to be a string with the format `host=;port=` that specifies the host and port of the PL/SQL debugger. This allows using the Java Debug Wire Protocol (JDWP) to debug PL/SQL code invoked by python-oracledb. This value is only used in the python-oracledb Thin mode. For python-oracledb Thick mode, set the ``ORA_DEBUG_JDWP`` environment variable which has the same syntax. For more information, see :ref:`applntracing`. If the ``handle`` parameter is specified, it must be of type OCISvcCtx\* and is only of use when embedding Python in an application (like PowerBuilder) which has already made the connection. The connection thus created should *never* be used after the source handle has been closed or destroyed. This value is only used in the python-oracledb Thick mode. It should be used with extreme caution. The deault value is 0. In the python-oracledb Thick mode, connection pooling is handled by Oracle's `Session pooling `__ technology. This allows python-oracledb applications to support features like `Application Continuity `__. .. function:: Cursor(connection) Constructor for creating a cursor. Returns a new :ref:`cursor object ` using the connection. .. note:: This method is an extension to the DB API definition. .. function:: Date(year, month, day) Constructs an object holding a date value. .. function:: DateFromTicks(ticks) Constructs an object holding a date value from the given ticks value (number of seconds since the epoch; see the documentation of the standard Python time module for details). .. function:: init_oracle_client(lib_dir=None, config_dir=None, \ error_url=None, driver_name=None) Enables python-oracledb Thick mode by initializing the Oracle Client library, see :ref:`enablingthick`. The method must be called before any standalone connection or pool is created. If a connection or pool is first created in Thin mode, then ``init_oracle_client()`` will raise an exception and Thick mode cannot be enabled. The ``init_oracle_client()`` method can be called multiple times in each Python process as long as the arguments are the same each time. See :ref:`initialization` for more information. If the ``lib_dir`` parameter is not None or the empty string, the specified directory is the only one searched for the Oracle Client libraries; otherwise, the standard way of locating the Oracle Client library is used. If the ``config_dir`` parameter is not None or the empty string, the specified directory is used to find Oracle Client library configuration files. This is equivalent to setting the environment variable ``TNS_ADMIN`` and overrides any value already set in ``TNS_ADMIN``. If this parameter is not set, the standard way of locating Oracle Client library configuration files is used. If the ``error_url`` parameter is not None or the empty string, the specified value is included in the message of the exception raised when the Oracle Client library cannot be loaded; otherwise, the :ref:`installation` URL is included. If the ``driver_name`` parameter is not None or the empty string, the specified value can be found in database views that give information about connections. For example, it is in the ``CLIENT_DRIVER`` column of ``V$SESSION_CONNECT_INFO``. The standard is to set this value to ``" : version>"``, where is the name of the driver and is its version. There should be a single space character before and after the colon. If this value is not specified, then the default value in python-oracledb Thick mode is like "python-oracledb thk : ". .. note:: This method is an extension to the DB API definition. .. function:: is_thin_mode() Returns a boolean indicating if Thin mode is in use. Immediately after python-oracledb is imported, this function will return True indicating that python-oracledb defaults to Thin mode. If :func:`oracledb.init_oracle_client()` is called, then a subsequent call to ``is_thin_mode()`` will return False indicating that Thick mode is enabled. Once the first standalone connection or connection pool is created, or a call to ``oracledb.init_oracle_client()`` is made, then python-oracledb’s mode is fixed and the value returned by ``is_thin_mode()`` will never change for the lifetime of the process. The attribute :attr:`Connection.thin` can be used to check a connection's mode. .. note:: This method is an extension to the DB API definition. .. versionadded:: 1.1.0 .. function:: makedsn(host, port, sid=None, service_name=None, region=None, \ sharding_key=None, super_sharding_key=None) Returns a string suitable for use as the ``dsn`` parameter for :meth:`~oracledb.connect()`. This string is identical to the strings that are defined by the Oracle names server or defined in the tnsnames.ora file. .. deprecated:: python-oracledb 1.0 Use :ref:`ConnectParams class ` instead. .. note:: This method is an extension to the DB API definition. .. function:: PoolParams(min=1, max=2, increment=1, connectiontype=None, \ getmode=oracledb.POOL_GETMODE_WAIT, homogeneous=True, timeout=0, \ wait_timeout=0, max_lifetime_session=0, session_callback=None, \ max_sessions_per_shard=0, soda_metadata_cache=False, ping_interval=60, \ user=None, proxy_user=Nonde, password=None, newpassword=None, \ wallet_password=None, access_token=None, host=None, port=1521, protocol="tcp", \ https_proxy=None, https_proxy_port=0, service_name=None, sid=None, \ server_type=None, cclass=None, purity=oracledb.PURITY_DEFAULT, \ expire_time=0, retry_count=0, retry_delay=0, tcp_connect_timeout=60.0, \ ssl_server_dn_match=True, ssl_server_cert_dn=None, wallet_location=None, \ events=False, externalauth=False, mode=oracledb.AUTH_MODE_DEFAULT, \ disable_oob=False, stmtcachesize=oracledb.defaults.stmtcachesize, edition=None, \ tag=None, matchanytag=False, config_dir=oracledb.defaults.config_dir, \ appcontext=[], shardingkey=[], supershardingkey=[], debug_jdwp=None, handle=0) Creates and returns a :ref:`PoolParams Object `. The object can be passed to :meth:`oracledb.create_pool()`. All the parameters are optional. The ``min`` parameter is the minimum number of connections that the pool should contain. The default value is 1. The ``max`` parameter is the maximum number of connections that the pool should contain. The default value is 2. The ``increment`` parameter is the number of connections that should be added to the pool whenever a new connection needs to be created. The default value is 1. The ``connectiontype`` parameter is the class of the connection that should be returned during calls to :meth:`ConnectionPool.acquire()`. It must be a Connection or a subclass of Connection. The ``getmode`` parameter determines the behavior of :meth:`ConnectionPool.acquire()`. One of the constants :data:`oracledb.POOL_GETMODE_WAIT`, :data:`oracledb.POOL_GETMODE_NOWAIT`, :data:`oracledb.POOL_GETMODE_FORCEGET`, or :data:`oracledb.POOL_GETMODE_TIMEDWAIT`. The default value is :data:`oracledb.POOL_GETMODE_WAIT`. The ``homogeneous`` parameter is a boolean that indicates whether the connections are homogeneous (same user) or heterogeneous (multiple users). The default value is True. The ``timeout`` parameter is the length of time (in seconds) that a connection may remain idle in the pool before it is terminated. If the value of this parameter is 0, then the connections are never terminated. The default value is 0. The ``wait_timeout`` parameter is the length of time (in milliseconds) that a caller should wait when acquiring a connection from the pool with ``getmode`` set to :data:`oracledb.POOL_GETMODE_TIMEDWAIT`. The default value is 0. The ``max_lifetime_session`` parameter is the length of time (in seconds) that connections can remain in the pool. If the value of this parameter is 0, then the connections may remain in the pool indefinitely. The default value is 0. The ``session_callback`` parameter is a callable that is invoked when a connection is returned from the pool for the first time, or when the connection tag differs from the one requested. The ``max_sessions_per_shard`` parameter is the maximum number of connections that may be associated with a particular shard. The default value is 0. The ``soda_metadata_cache`` parameter is a boolean that indicates whether or not the SODA metadata cache should be enabled. The default value is False. The ``ping_interval`` parameter is the length of time (in seconds) after which an unused connection in the pool will be a candidate for pinging when :meth:`ConnectionPool.acquire()` is called. If the ping to the database indicates the connection is not alive a replacement connection will be returned by :meth:`ConnectionPool.acquire()`. If ping_interval is a negative value, then the ping functionality will be disabled. The default value is 60 seconds. The ``user`` parameter is expected to be a string which indicates the name of the user to connect to. This value is used in both the python-oracledb Thin and Thick modes. The ``proxy_user`` parameter is expected to be a string which indicates the name of the proxy user to connect to. If this value is not specified, it will be parsed out of user if user is in the form "user[proxy_user]". This value is used in both the python-oracledb Thin and Thick modes. The ``password`` parameter expected to be a string which indicates the password for the user. This value is used in both the python-oracledb Thin and Thick modes. The ``newpassword`` parameter is expected to be a string which indicates the new password for the user. The new password will take effect immediately upon a successful connection to the database. This value is used in both the python-oracledb Thin and Thick modes. The ``wallet_password`` parameter is expected to be a string which indicates the password to use to decrypt the PEM-encoded wallet, if it is encrypted. This value is only used in python-oracledb Thin mode. The ``wallet_password`` parameter is not needed for cwallet.sso files that are used in the python-oracledb Thick mode. The ``access_token`` parameter is expected to be a string or a 2-tuple or a callable. If it is a string, it specifies an Azure AD OAuth2 token used for Open Authorization (OAuth 2.0) token based authentication. If it is a 2-tuple, it specifies the token and private key strings used for Oracle Cloud Infrastructure (OCI) Identity and Access Management (IAM) token based authentication. If it is a callable, it returns either a string or a 2-tuple used for OAuth 2.0 or OCI IAM token based authentication and is useful when the pool needs to expand and create new connections but the current authentication token has expired. This value is used in both the python-oracledb Thin and Thick modes. The ``host`` parameter is expected to be a string which specifies the name or IP address of the machine hosting the listener, which handles the initial connection to the database. This value is used in both the python-oracledb Thin and Thick modes. The ``port`` parameter is expected to be an integer which indicates the port number on which the listener is listening. The default value is 1521. This value is used in both the python-oracledb Thin and Thick modes. The ``protocol`` parameter is expected to be one of the strings "tcp" or "tcps" which indicates whether to use unencrypted network traffic or encrypted network traffic (TLS). The default value is tcp. This value is used in both the python-oracledb Thin and Thick modes. The ``https_proxy`` parameter is expected to be a string which indicates the name or IP address of a proxy host to use for tunneling secure connections. This value is used in both the python-oracledb Thin and Thick modes. The ``https_proxy_port`` parameter is expected to be an integer which indicates the port that is to be used to communicate with the proxy host. The default value is 0. This value is used in both the python-oracledb Thin and Thick modes. The ``service_name`` parameter is expected to be a string which indicates the service name of the database. This value is used in both the python-oracledb Thin and Thick modes. The ``sid`` parameter is expected to be a string which indicates the SID of the database. It is recommended to use ``service_name`` instead. This value is used in both the python-oracledb Thin and Thick modes. The ``server_type`` parameter is expected to be a string that indicates the type of server connection that should be established. If specified, it should be one of `dedicated`, `shared`, or `pooled`. This value is used in both the python-oracledb Thin and Thick modes. The ``cclass`` parameter is expected to be a string that identifies the connection class to use for Database Resident Connection Pooling (DRCP). This value is used in both the python-oracledb Thin and Thick modes. The ``purity`` parameter is expected to be one of the :ref:`oracledb.PURITY_* ` constants that identifies the purity to use for DRCP. This value is used in both the python-oracledb Thin and Thick modes. Internally pooled connections will default to a purity of :data:`~oracledb.PURITY_SELF`. The ``expire_time`` parameter is expected to be an integer which indicates the number of minutes between the sending of keepalive probes. If this parameter is set to a value greater than zero it enables keepalive. This value is used in both the python-oracledb Thin and Thick modes. The default value is 0. The ``retry_count`` parameter is expected to be an integer that identifies the number of times that a connection attempt should be retried before the attempt is terminated. This value is used in both the python-oracledb Thin and Thick modes. The default value is 0. The ``retry_delay`` parameter is expected to be an integer that identifies the number of seconds to wait before making a new connection attempt. This value is used in both the python-oracledb Thin and Thick modes. The default value is 0. The ``tcp_connect_timeout`` parameter is expected to be a float that indicates the maximum number of seconds to wait for establishing a connection to the database host. This value is used in both the python-oracledb Thin and Thick modes. The default value is 60.0. The ``ssl_server_dn_match`` parameter is expected to be a boolean that indicates whether the server certificate distinguished name (DN) should be matched in addition to the regular certificate verification that is performed. Note that if the ssl_server_cert_dn parameter is not provided, host name matching is performed instead. This value is used in both the python-oracledb Thin and Thick modes. The default value is True. The ``ssl_server_cert_dn`` parameter is expected to be a string that indicates the distinguished name (DN) which should be matched with the server. This value is ignored if the ssl_server_dn_match parameter is not set to the value True. This value is used in both the python-oracledb Thin and Thick modes. The ``wallet_location`` parameter is expected to be a string that identifies the directory where the wallet can be found. In python-oracledb Thin mode, this must be the directory of the PEM-encoded wallet file, ewallet.pem. In python-oracledb Thick mode, this must be the directory of the file, cwallet.sso. This value is used in both the python-oracledb Thin and Thick modes. The ``externalauth`` parameter is a boolean that determines whether to use external authentication. This value is only used in the python-oracledb Thick mode. The default value is False. The ``events`` parameter is expected to be a boolean that specifies whether the events mode should be enabled. This value is only used in the python-oracledb Thick mode. This parameter is needed for continuous query notification and high availability event notifications. The default value is False. The ``mode`` parameter is expected to be an integer that identifies the authorization mode to use. This value is used in both the python-oracledb Thin and Thick modes.The default value is :data:`oracledb.AUTH_MODE_DEFAULT`. The ``disable_oob`` parameter is expected to be a boolean that indicates whether out-of-band breaks should be disabled. This value is only used in the python-oracledb Thin mode and has no effect on Windows which does not support this functionality. The default value is False. The ``stmtcachesize`` parameter is expected to be an integer that identifies the initial size of the statement cache. This value is used in both the python-oracledb Thin and Thick modes. The default is the value of :attr:`defaults.stmtcachesize`. The ``edition`` parameter is expected to be a string that indicates the edition to use for the connection. This parameter cannot be used simultaneously with the ``cclass`` parameter. This value is used in the python-oracledb Thick mode. The ``tag`` parameter is expected to be a string that identifies the type of connection that should be returned from a pool. This value is only used in the python-oracledb Thick mode. The ``matchanytag`` parameter is expected to be a boolean specifying whether any tag can be used when acquiring a connection from the pool. This value is only used in the python-oracledb Thick mode when acquiring a connection from a pool. The default value is False. The ``config_dir`` parameter is expected to be a string that indicates the directory in which configuration files (tnsnames.ora) are found. This value is only used in python-oracledb Thin mode. The default is the value of :attr:`defaults.config_dir`. For python-oracledb Thick mode, use the ``config_dir`` parameter of :func:`oracledb.init_oracle_client()`. The ``appcontext`` parameter is expected to be a list of 3-tuples that identifies the application context used by the connection. This parameter should contain namespace, name, and value and each entry in the tuple should be a string. This value is only used inthe python-oracledb Thick mode. The ``shardingkey`` parameter is expected to be a list of strings, numbers, bytes or dates that identifies the database shard to connect to. This value is only used in the python-oracledb Thick mode. The ``supershardingkey`` parameter is expected to be a list of strings, numbers, bytes or dates that identifies the database shard to connect to. This value is only used in the python-oracledb Thick mode. The ``debug_jdwp`` parameter is expected to be a string with the format `host=;port=` that specifies the host and port of the PL/SQL debugger. This allows using the Java Debug Wire Protocol (JDWP) to debug PL/SQL code invoked by python-oracledb. This value is only used in the python-oracledb Thin mode. For python-oracledb Thick mode, set the ``ORA_DEBUG_JDWP`` environment variable which has the same syntax. For more information, see :ref:`jdwp`. The ``handle`` parameter is expected to be an integer which represents a pointer to a valid service context handle. This value is only used in the python-oracledb Thick mode. It should be used with extreme caution. The default value is 0. .. function:: Time(hour, minute, second) Constructs an object holding a time value. .. note:: The time only data type is not supported by Oracle. Calling this function will raise a NotSupportedError exception. .. function:: TimeFromTicks(ticks) Constructs an object holding a time value from the given ticks value (number of seconds since the epoch; see the documentation of the standard Python time module for details). .. note:: The time only data type is not supported by Oracle. Calling this function will raise a NotSupportedError exception. .. function:: Timestamp(year, month, day, hour, minute, second) Constructs an object holding a time stamp value. .. function:: TimestampFromTicks(ticks) Constructs an object holding a time stamp value from the given ticks value (number of seconds since the epoch; see the documentation of the standard Python time module for details). .. _constants: Oracledb Constants ================== General ------- .. data:: apilevel String constant stating the supported DB API level. Currently '2.0'. .. data:: buildtime String constant stating the time when the binary was built. .. note:: This constant is an extension to the DB API definition. .. data:: paramstyle String constant stating the type of parameter marker formatting expected by the interface. Currently 'named' as in 'where name = :name'. .. data:: threadsafety Integer constant stating the level of thread safety that the interface supports. Currently 2, which means that threads may share the module and connections, but not cursors. Sharing means that a thread may use a resource without wrapping it using a mutex semaphore to implement resource locking. Note that in order to make use of multiple threads in a program which intends to connect and disconnect in different threads, the ``threaded`` parameter to :meth:`connect()` must be True. .. data:: version .. data:: __version__ String constant stating the version of the module. Currently '|release|'. .. note:: This attribute is an extension to the DB API definition. Advanced Queuing: Delivery Modes -------------------------------- These constants are extensions to the DB API definition. They are possible values for the :attr:`~DeqOptions.deliverymode` attribute of the :ref:`dequeue options object ` passed as the ``options`` parameter to the :meth:`Queue.deqone()` or :meth:`Queue.deqmany()` methods as well as the :attr:`~EnqOptions.deliverymode` attribute of the :ref:`enqueue options object ` passed as the ``options`` parameter to the :meth:`Queue.enqone()` or :meth:`Queue.enqmany()` methods. They are also possible values for the :attr:`~MessageProperties.deliverymode` attribute of the :ref:`message properties object ` passed as the ``msgproperties`` parameter to the :meth:`Queue.deqone()` or :meth:`Queue.deqmany()` and :meth:`Queue.enqone()` or :meth:`Queue.enqmany()` methods. .. data:: MSG_BUFFERED This constant is used to specify that enqueue/dequeue operations should enqueue or dequeue buffered messages. .. data:: MSG_PERSISTENT This constant is used to specify that enqueue/dequeue operations should enqueue or dequeue persistent messages. This is the default value. .. data:: MSG_PERSISTENT_OR_BUFFERED This constant is used to specify that dequeue operations should dequeue either persistent or buffered messages. Advanced Queuing: Dequeue Modes ------------------------------- These constants are extensions to the DB API definition. They are possible values for the :attr:`~DeqOptions.mode` attribute of the :ref:`dequeue options object `. This object is the ``options`` parameter for the :meth:`Queue.deqone()` or :meth:`Queue.deqmany()` methods. .. data:: DEQ_BROWSE This constant is used to specify that dequeue should read the message without acquiring any lock on the message (equivalent to a select statement). .. data:: DEQ_LOCKED This constant is used to specify that dequeue should read and obtain a write lock on the message for the duration of the transaction (equivalent to a select for update statement). .. data:: DEQ_REMOVE This constant is used to specify that dequeue should read the message and update or delete it. This is the default value. .. data:: DEQ_REMOVE_NODATA This constant is used to specify that dequeue should confirm receipt of the message but not deliver the actual message content. Advanced Queuing: Dequeue Navigation Modes ------------------------------------------ These constants are extensions to the DB API definition. They are possible values for the :attr:`~DeqOptions.navigation` attribute of the :ref:`dequeue options object `. This object is the ``options`` parameter for the :meth:`Queue.deqone()` or :meth:`Queue.deqmany()` methods. .. data:: DEQ_FIRST_MSG This constant is used to specify that dequeue should retrieve the first available message that matches the search criteria. This resets the position to the beginning of the queue. .. data:: DEQ_NEXT_MSG This constant is used to specify that dequeue should retrieve the next available message that matches the search criteria. If the previous message belongs to a message group, AQ retrieves the next available message that matches the search criteria and belongs to the message group. This is the default. .. data:: DEQ_NEXT_TRANSACTION This constant is used to specify that dequeue should skip the remainder of the transaction group and retrieve the first message of the next transaction group. This option can only be used if message grouping is enabled for the current queue. Advanced Queuing: Dequeue Visibility Modes ------------------------------------------ These constants are extensions to the DB API definition. They are possible values for the :attr:`~DeqOptions.visibility` attribute of the :ref:`dequeue options object `. This object is the ``options`` parameter for the :meth:`Queue.deqone()` or :meth:`Queue.deqmany()` methods. .. data:: DEQ_IMMEDIATE This constant is used to specify that dequeue should perform its work as part of an independent transaction. .. data:: DEQ_ON_COMMIT This constant is used to specify that dequeue should be part of the current transaction. This is the default value. Advanced Queuing: Dequeue Wait Modes ------------------------------------ These constants are extensions to the DB API definition. They are possible values for the :attr:`~DeqOptions.wait` attribute of the :ref:`dequeue options object `. This object is the ``options`` parameter for the :meth:`Queue.deqone()` or :meth:`Queue.deqmany()` methods. .. data:: DEQ_NO_WAIT This constant is used to specify that dequeue not wait for messages to be available for dequeuing. .. data:: DEQ_WAIT_FOREVER This constant is used to specify that dequeue should wait forever for messages to be available for dequeuing. This is the default value. Advanced Queuing: Enqueue Visibility Modes ------------------------------------------ These constants are extensions to the DB API definition. They are possible values for the :attr:`~EnqOptions.visibility` attribute of the :ref:`enqueue options object `. This object is the ``options`` parameter for the :meth:`Queue.enqone()` or :meth:`Queue.enqmany()` methods. .. data:: ENQ_IMMEDIATE This constant is used to specify that enqueue should perform its work as part of an independent transaction. .. data:: ENQ_ON_COMMIT This constant is used to specify that enqueue should be part of the current transaction. This is the default value. Advanced Queuing: Message States -------------------------------- These constants are extensions to the DB API definition. They are possible values for the :attr:`~MessageProperties.state` attribute of the :ref:`message properties object `. This object is the ``msgproperties`` parameter for the :meth:`Connection.deq()` and :meth:`Queue.enqone()` or :meth:`Queue.enqmany()` methods. .. data:: MSG_EXPIRED This constant is used to specify that the message has been moved to the exception queue. .. data:: MSG_PROCESSED This constant is used to specify that the message has been processed and has been retained. .. data:: MSG_READY This constant is used to specify that the message is ready to be processed. .. data:: MSG_WAITING This constant is used to specify that the message delay has not yet been reached. Advanced Queuing: Other ----------------------- These constants are extensions to the DB API definition. They are special constants used in advanced queuing. .. data:: MSG_NO_DELAY This constant is a possible value for the :attr:`~MessageProperties.delay` attribute of the :ref:`message properties object ` passed as the ``msgproperties`` parameter to the :meth:`Queue.deqone()` or :meth:`Queue.deqmany()` and :meth:`Queue.enqone()` or :meth:`Queue.enqmany()` methods. It specifies that no delay should be imposed and the message should be immediately available for dequeuing. This is also the default value. .. data:: MSG_NO_EXPIRATION This constant is a possible value for the :attr:`~MessageProperties.expiration` attribute of the :ref:`message properties object ` passed as the ``msgproperties`` parameter to the :meth:`Queue.deqone()` or :meth:`Queue.deqmany()` and :meth:`Queue.enqone()` or :meth:`Queue.enqmany()` methods. It specifies that the message never expires. This is also the default value. .. _connection-authorization-modes: Connection Authorization Modes ------------------------------ These constants are extensions to the DB API definition and have deprecated the `authorization modes `_ used in cx_Oracle 8.3. They are possible values for the ``mode`` parameter of the :meth:`connect()` method. .. data:: AUTH_MODE_DEFAULT This constant is used to specify that default authentication is to take place. This is the default value if no mode is passed at all. .. note:: This constant can be used for standalone and pooled connections in the python-oracledb Thin mode, and for standalone connections in the Thick mode. This constant deprecates the ``DEFAULT_AUTH`` constant that was used in cx_Oracle 8.3. .. data:: AUTH_MODE_PRELIM This constant is used to specify that preliminary authentication is to be used. This is needed for performing database startup and shutdown. .. note:: This constant can only be used in the python-oracledb Thick mode for standalone connections. This constant deprecates the ``PRELIM_AUTH`` constant that was used in cx_Oracle 8.3. .. data:: AUTH_MODE_SYSASM This constant is used to specify that SYSASM access is to be acquired. .. note:: This constant can be used for standalone and pooled connections in the python-oracledb Thin mode, and for standalone connections in the Thick mode. This constant deprecates the ``SYSASM`` constant that was used in cx_Oracle 8.3. .. data:: AUTH_MODE_SYSBKP This constant is used to specify that SYSBACKUP access is to be acquired. .. note:: This constant can be used for standalone and pooled connections in the python-oracledb Thin mode, and for standalone connections in the Thick mode. This constant deprecates the ``SYSBKP`` constant that was used in cx_Oracle 8.3. .. data:: AUTH_MODE_SYSDBA This constant is used to specify that SYSDBA access is to be acquired. .. note:: This constant can be used for standalone and pooled connections in the python-oracledb Thin mode, and for standalone connections in the Thick mode. This constant deprecates the ``SYSDBA`` constant that was used in cx_Oracle 8.3. .. data:: AUTH_MODE_SYSDGD This constant is used to specify that SYSDG access is to be acquired. .. note:: This constant can be used for standalone and pooled connections in the python-oracledb Thin mode, and for standalone connections in the Thick mode. This constant deprecates the ``SYSDGD`` constant that was used in cx_Oracle 8.3. .. data:: AUTH_MODE_SYSKMT This constant is used to specify that SYSKM access is to be acquired. .. note:: This constant can be used for standalone and pooled connections in the python-oracledb Thin mode, and for standalone connections in the Thick mode. This constant deprecates the ``SYSKMT`` constant that was used in cx_Oracle 8.3. .. data:: AUTH_MODE_SYSOPER This constant is used to specify that SYSOPER access is to be acquired. .. note:: This constant can be used for standalone and pooled connections in the python-oracledb Thin mode, and for standalone connections in the Thick mode. This constant deprecates the ``SYSOPER`` constant that was used in cx_Oracle 8.3. .. data:: AUTH_MODE_SYSRAC This constant is used to specify that SYSRAC access is to be acquired. .. note:: This constant can be used for standalone and pooled connections in the python-oracledb Thin mode, and for standalone connections in the Thick mode. This constant deprecates the ``SYSRAC`` constant that was used in cx_Oracle 8.3. Database Shutdown Modes ----------------------- These constants are extensions to the DB API definition. They are possible values for the ``mode`` parameter of the :meth:`Connection.shutdown()` method. .. data:: DBSHUTDOWN_ABORT This constant is used to specify that the caller should not wait for current processing to complete or for users to disconnect from the database. This should only be used in unusual circumstances since database recovery may be necessary upon next startup. .. data:: DBSHUTDOWN_FINAL This constant is used to specify that the instance can be truly halted. This should only be done after the database has been shutdown with one of the other modes (except abort) and the database has been closed and dismounted using the appropriate SQL commands. .. data:: DBSHUTDOWN_IMMEDIATE This constant is used to specify that all uncommitted transactions should be rolled back and any connected users should be disconnected. .. data:: DBSHUTDOWN_TRANSACTIONAL This constant is used to specify that further connections to the database should be prohibited and no new transactions should be allowed. It then waits for all active transactions to complete. .. data:: DBSHUTDOWN_TRANSACTIONAL_LOCAL This constant is used to specify that further connections to the database should be prohibited and no new transactions should be allowed. It then waits for only local active transactions to complete. Event Types ----------- These constants are extensions to the DB API definition. They are possible values for the :attr:`Message.type` attribute of the messages that are sent for subscriptions created by the :meth:`Connection.subscribe()` method. .. data:: EVENT_AQ This constant is used to specify that one or more messages are available for dequeuing on the queue specified when the subscription was created. .. data:: EVENT_DEREG This constant is used to specify that the subscription has been deregistered and no further notifications will be sent. .. data:: EVENT_NONE This constant is used to specify no information is available about the event. .. data:: EVENT_OBJCHANGE This constant is used to specify that a database change has taken place on a table registered with the :meth:`Subscription.registerquery()` method. .. data:: EVENT_QUERYCHANGE This constant is used to specify that the result set of a query registered with the :meth:`Subscription.registerquery()` method has been changed. .. data:: EVENT_SHUTDOWN This constant is used to specify that the instance is in the process of being shut down. .. data:: EVENT_SHUTDOWN_ANY This constant is used to specify that any instance (when running RAC) is in the process of being shut down. .. data:: EVENT_STARTUP This constant is used to specify that the instance is in the process of being started up. .. _cqn-operation-codes: Operation Codes --------------- These constants are extensions to the DB API definition. They are possible values for the ``operations`` parameter for the :meth:`Connection.subscribe()` method. One or more of these values can be OR'ed together. These values are also used by the :attr:`MessageTable.operation` or :attr:`MessageQuery.operation` attributes of the messages that are sent. .. data:: OPCODE_ALLOPS This constant is used to specify that messages should be sent for all operations. .. data:: OPCODE_ALLROWS This constant is used to specify that the table or query has been completely invalidated. .. data:: OPCODE_ALTER This constant is used to specify that messages should be sent when a registered table has been altered in some fashion by DDL, or that the message identifies a table that has been altered. .. data:: OPCODE_DELETE This constant is used to specify that messages should be sent when data is deleted, or that the message identifies a row that has been deleted. .. data:: OPCODE_DROP This constant is used to specify that messages should be sent when a registered table has been dropped, or that the message identifies a table that has been dropped. .. data:: OPCODE_INSERT This constant is used to specify that messages should be sent when data is inserted, or that the message identifies a row that has been inserted. .. data:: OPCODE_UPDATE This constant is used to specify that messages should be sent when data is updated, or that the message identifies a row that has been updated. .. _connpoolmodes: Connection Pool Get Modes ------------------------- These constants are extensions to the DB API definition and have deprecated the `Session Pool Get Modes `_ constants that were used in cx_Oracle 8.3. They are possible values for the ``getmode`` parameter of the :meth:`oracledb.create_pool()` method. .. data:: POOL_GETMODE_FORCEGET This constant is used to specify that a new connection will be returned if there are no free sessions available in the pool. .. note:: This constant deprecates the ``SPOOL_ATTRVAL_FORCEGET`` constant that was used in cx_Oracle 8.3. .. data:: POOL_GETMODE_NOWAIT This constant is used to specify that an exception should be raised if there are no free sessions available in the pool. .. note:: This constant deprecates the ``SPOOL_ATTRVAL_NOWAIT`` constant that was used in cx_Oracle 8.3. .. data:: POOL_GETMODE_WAIT This constant is used to specify that the caller should wait until a session is available if there are no free sessions available in the pool. This is the default value. .. note:: This constant deprecates the ``SPOOL_ATTRVAL_WAIT`` constant that was used in cx_Oracle 8.3. .. data:: POOL_GETMODE_TIMEDWAIT This constant is used to specify that the caller should wait for a period of time (defined by the ``wait_timeout`` parameter) for a session to become available before returning with an error. .. note:: This constant deprecates the ``SPOOL_ATTRVAL_TIMEDWAIT`` constant that was used in cx_Oracle 8.3. .. _drcppurityconsts: Connection Pool Purity Constants -------------------------------- These constants are extensions to the DB API definition and have deprecated the `Session Pool Purity `_ constants that were used in cx_Oracle 8.3. They are possible values for the ``purity`` parameter of the :meth:`connect()` method, which is used in Database Resident Connection Pooling (DRCP). .. data:: PURITY_DEFAULT This constant is used to specify that the purity of the session is the default value identified by Oracle (see Oracle's documentation for more information). This is the default value. .. note:: This constant deprecates the ``ATTR_PURITY_DEFAULT`` constant that was used in cx_Oracle 8.3. .. data:: PURITY_NEW This constant is used to specify that the session acquired from the pool should be new and not have any prior session state. .. note:: This constant deprecates the ``ATTR_PURITY_NEW`` constant that was used in cx_Oracle 8.3. .. data:: PURITY_SELF This constant is used to specify that the session acquired from the pool need not be new and may have prior session state. .. note:: This constant deprecates the ``ATTR_PURITY_SELF`` constant that was used in cx_Oracle 8.3. Subscription Grouping Classes ----------------------------- These constants are extensions to the DB API definition. They are possible values for the ``groupingClass`` parameter of the :meth:`Connection.subscribe()` method. .. data:: SUBSCR_GROUPING_CLASS_TIME This constant is used to specify that events are to be grouped by the period of time in which they are received. Subscription Grouping Types --------------------------- These constants are extensions to the DB API definition. They are possible values for the ``groupingType`` parameter of the :meth:`Connection.subscribe()` method. .. data:: SUBSCR_GROUPING_TYPE_SUMMARY This constant is used to specify that when events are grouped a summary of the events should be sent instead of the individual events. This is the default value. .. data:: SUBSCR_GROUPING_TYPE_LAST This constant is used to specify that when events are grouped the last event that makes up the group should be sent instead of the individual events. .. _subscr-namespaces: Subscription Namespaces ----------------------- These constants are extensions to the DB API definition. They are possible values for the ``namespace`` parameter of the :meth:`Connection.subscribe()` method. .. data:: SUBSCR_NAMESPACE_AQ This constant is used to specify that notifications should be sent when a queue has messages available to dequeue. .. data:: SUBSCR_NAMESPACE_DBCHANGE This constant is used to specify that database change notification or query change notification messages are to be sent. This is the default value. .. _subscr-protocols: Subscription Protocols ---------------------- These constants are extensions to the DB API definition. They are possible values for the ``protocol`` parameter of the :meth:`Connection.subscribe()` method. .. data:: SUBSCR_PROTO_CALLBACK This constant is used to specify that notifications will be sent to the callback routine identified when the subscription was created. It is the default value and the only value currently supported. .. data:: SUBSCR_PROTO_HTTP This constant is used to specify that notifications will be sent to an HTTP URL when a message is generated. This value is currently not supported. .. data:: SUBSCR_PROTO_MAIL This constant is used to specify that notifications will be sent to an e-mail address when a message is generated. This value is currently not supported. .. data:: SUBSCR_PROTO_OCI This constant is used to specify that notifications will be sent to the callback routine identified when the subscription was created. It is the default value and the only value currently supported. .. deprecated:: python-oracledb 1.0 Use :data:`~oracledb.SUBSCR_PROTO_CALLBACK` instead. .. data:: SUBSCR_PROTO_SERVER This constant is used to specify that notifications will be sent to a PL/SQL procedure when a message is generated. This value is currently not supported. .. _subscr-qos: Subscription Quality of Service ------------------------------- These constants are extensions to the DB API definition. They are possible values for the ``qos`` parameter of the :meth:`Connection.subscribe()` method. One or more of these values can be OR'ed together. .. data:: SUBSCR_QOS_BEST_EFFORT This constant is used to specify that best effort filtering for query result set changes is acceptable. False positive notifications may be received. This behaviour may be suitable for caching applications. .. data:: SUBSCR_QOS_DEREG_NFY This constant is used to specify that the subscription should be automatically unregistered after the first notification is received. .. data:: SUBSCR_QOS_QUERY This constant is used to specify that notifications should be sent if the result set of the registered query changes. By default, no false positive notifications will be generated. .. data:: SUBSCR_QOS_RELIABLE This constant is used to specify that notifications should not be lost in the event of database failure. .. data:: SUBSCR_QOS_ROWIDS This constant is used to specify that the rowids of the inserted, updated or deleted rows should be included in the message objects that are sent. .. _types: DB API Types ------------ .. data:: BINARY This type object is used to describe columns in a database that contain binary data. The database types :data:`DB_TYPE_RAW` and :data:`DB_TYPE_LONG_RAW` will compare equal to this value. If a variable is created with this type, the database type :data:`DB_TYPE_RAW` will be used. .. data:: DATETIME This type object is used to describe columns in a database that are dates. The database types :data:`DB_TYPE_DATE`, :data:`DB_TYPE_TIMESTAMP`, :data:`DB_TYPE_TIMESTAMP_LTZ` and :data:`DB_TYPE_TIMESTAMP_TZ` will all compare equal to this value. If a variable is created with this type, the database type :data:`DB_TYPE_DATE` will be used. .. data:: NUMBER This type object is used to describe columns in a database that are numbers. The database types :data:`DB_TYPE_BINARY_DOUBLE`, :data:`DB_TYPE_BINARY_FLOAT`, :data:`DB_TYPE_BINARY_INTEGER` and :data:`DB_TYPE_NUMBER` will all compare equal to this value. If a variable is created with this type, the database type :data:`DB_TYPE_NUMBER` will be used. .. data:: ROWID This type object is used to describe the pseudo column "rowid". The database types :data:`DB_TYPE_ROWID` and :data:`DB_TYPE_UROWID` will compare equal to this value. If a variable is created with this type, the database type :data:`DB_TYPE_VARCHAR` will be used. .. data:: STRING This type object is used to describe columns in a database that are strings. The database types :data:`DB_TYPE_CHAR`, :data:`DB_TYPE_LONG`, :data:`DB_TYPE_NCHAR`, :data:`DB_TYPE_NVARCHAR` and :data:`DB_TYPE_VARCHAR` will all compare equal to this value. If a variable is created with this type, the database type :data:`DB_TYPE_VARCHAR` will be used. .. _dbtypes: Database Types -------------- All of these types are extensions to the DB API definition. They are found in query and object metadata. They can also be used to specify the database type when binding data. .. data:: DB_TYPE_BFILE Describes columns, attributes or array elements in a database that are of type BFILE. It will compare equal to the DB API type :data:`BINARY`. .. note:: DB_TYPE_BFILE database type is only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. .. data:: DB_TYPE_BINARY_DOUBLE Describes columns, attributes or array elements in a database that are of type BINARY_DOUBLE. It will compare equal to the DB API type :data:`NUMBER`. .. data:: DB_TYPE_BINARY_FLOAT Describes columns, attributes or array elements in a database that are of type BINARY_FLOAT. It will compare equal to the DB API type :data:`NUMBER`. .. data:: DB_TYPE_BINARY_INTEGER Describes attributes or array elements in a database that are of type BINARY_INTEGER. It will compare equal to the DB API type :data:`NUMBER`. .. data:: DB_TYPE_BLOB Describes columns, attributes or array elements in a database that are of type BLOB. It will compare equal to the DB API type :data:`BINARY`. .. data:: DB_TYPE_BOOLEAN Describes attributes or array elements in a database that are of type BOOLEAN. It is only available in Oracle 12.1 and higher and only within PL/SQL. .. data:: DB_TYPE_CHAR Describes columns, attributes or array elements in a database that are of type CHAR. It will compare equal to the DB API type :data:`STRING`. Note that these are fixed length string values and behave differently from VARCHAR2. .. data:: DB_TYPE_CLOB Describes columns, attributes or array elements in a database that are of type CLOB. It will compare equal to the DB API type :data:`STRING`. .. data:: DB_TYPE_CURSOR Describes columns in a database that are of type CURSOR. In PL/SQL, these are known as REF CURSOR. .. data:: DB_TYPE_DATE Describes columns, attributes or array elements in a database that are of type DATE. It will compare equal to the DB API type :data:`DATETIME`. .. data:: DB_TYPE_INTERVAL_DS Describes columns, attributes or array elements in a database that are of type INTERVAL DAY TO SECOND. .. data:: DB_TYPE_INTERVAL_YM Describes columns, attributes or array elements in a database that are of type INTERVAL YEAR TO MONTH. This database type is not currently supported by python-oracledb. .. data:: DB_TYPE_JSON Describes columns in a database that are of type JSON (with Oracle Database 21 or later). .. note:: DB_TYPE_JSON database type is only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. In python-oracledb Thin mode, the JSON database type can be fetched with an output type handler as described in :ref:`Fetching JSON `. .. data:: DB_TYPE_LONG Describes columns, attributes or array elements in a database that are of type LONG. It will compare equal to the DB API type :data:`STRING`. .. data:: DB_TYPE_LONG_RAW Describes columns, attributes or array elements in a database that are of type LONG RAW. It will compare equal to the DB API type :data:`BINARY`. .. data:: DB_TYPE_LONG_NVARCHAR This constant can be used in output type handlers when fetching NCLOB columns as a string. (Note a type handler is not needed if :ref:`oracledb.defaults.fetch_lobs ` is set to False). For IN binds, this constant can be used to create a bind variable in :meth:`Cursor.var()` or via :meth:`Cursor.setinputsizes()`. The ``DB_TYPE_LONG_NVARCHAR`` value won't be shown in query metadata since it is not a database type. It will compare equal to the DB API type :data:`STRING`. .. data:: DB_TYPE_NCHAR Describes columns, attributes or array elements in a database that are of type NCHAR. It will compare equal to the DB API type :data:`STRING`. Note that these are fixed length string values and behave differently from NVARCHAR2. .. data:: DB_TYPE_NCLOB Describes columns, attributes or array elements in a database that are of type NCLOB. It will compare equal to the DB API type :data:`STRING`. .. data:: DB_TYPE_NUMBER Describes columns, attributes or array elements in a database that are of type NUMBER. It will compare equal to the DB API type :data:`NUMBER`. .. data:: DB_TYPE_NVARCHAR Describes columns, attributes or array elements in a database that are of type NVARCHAR2. It will compare equal to the DB API type :data:`STRING`. .. data:: DB_TYPE_OBJECT Describes columns, attributes or array elements in a database that are an instance of a named SQL or PL/SQL type. .. data:: DB_TYPE_RAW Describes columns, attributes or array elements in a database that are of type RAW. It will compare equal to the DB API type :data:`BINARY`. .. data:: DB_TYPE_ROWID Describes columns, attributes or array elements in a database that are of type ROWID or UROWID. It will compare equal to the DB API type :data:`ROWID`. .. data:: DB_TYPE_TIMESTAMP Describes columns, attributes or array elements in a database that are of type TIMESTAMP. It will compare equal to the DB API type :data:`DATETIME`. .. data:: DB_TYPE_TIMESTAMP_LTZ Describes columns, attributes or array elements in a database that are of type TIMESTAMP WITH LOCAL TIME ZONE. It will compare equal to the DB API type :data:`DATETIME`. .. data:: DB_TYPE_TIMESTAMP_TZ Describes columns, attributes or array elements in a database that are of type TIMESTAMP WITH TIME ZONE. It will compare equal to the DB API type :data:`DATETIME`. .. data:: DB_TYPE_UROWID Describes columns, attributes or array elements in a database that are of type UROWID. It will compare equal to the DB API type :data:`ROWID`. .. note:: This type is not supported in python-oracledb Thick mode. See :ref:`querymetadatadiff`. .. data:: DB_TYPE_VARCHAR Describes columns, attributes or array elements in a database that are of type VARCHAR2. It will compare equal to the DB API type :data:`STRING`. .. _dbtypesynonyms: Database Type Synonyms ---------------------- All of the following constants are deprecated and will be removed in a future version of python-oracledb. .. data:: BFILE A synonym for :data:`DB_TYPE_BFILE`. .. deprecated:: cx_Oracle 8.0 .. data:: BLOB A synonym for :data:`DB_TYPE_BLOB`. .. deprecated:: cx_Oracle 8.0 .. data:: BOOLEAN A synonym for :data:`DB_TYPE_BOOLEAN`. .. deprecated:: cx_Oracle 8.0 .. data:: CLOB A synonym for :data:`DB_TYPE_CLOB`. .. deprecated:: cx_Oracle 8.0 .. data:: CURSOR A synonym for :data:`DB_TYPE_CURSOR`. .. deprecated:: cx_Oracle 8.0 .. data:: FIXED_CHAR A synonym for :data:`DB_TYPE_CHAR`. .. deprecated:: cx_Oracle 8.0 .. data:: FIXED_NCHAR A synonym for :data:`DB_TYPE_NCHAR`. .. deprecated:: cx_Oracle 8.0 .. data:: INTERVAL A synonym for :data:`DB_TYPE_INTERVAL_DS`. .. deprecated:: cx_Oracle 8.0 .. data:: LONG_BINARY A synonym for :data:`DB_TYPE_LONG_RAW`. .. deprecated:: cx_Oracle 8.0 .. data:: LONG_STRING A synonym for :data:`DB_TYPE_LONG`. .. deprecated:: cx_Oracle 8.0 .. data:: NATIVE_FLOAT A synonym for :data:`DB_TYPE_BINARY_DOUBLE`. .. deprecated:: cx_Oracle 8.0 .. data:: NATIVE_INT A synonym for :data:`DB_TYPE_BINARY_INTEGER`. .. deprecated:: cx_Oracle 8.0 .. data:: NCHAR A synonym for :data:`DB_TYPE_NVARCHAR`. .. deprecated:: cx_Oracle 8.0 .. data:: NCLOB A synonym for :data:`DB_TYPE_NCLOB`. .. deprecated:: cx_Oracle 8.0 .. data:: OBJECT A synonym for :data:`DB_TYPE_OBJECT`. .. deprecated:: cx_Oracle 8.0 .. data:: TIMESTAMP A synonym for :data:`DB_TYPE_TIMESTAMP`. .. deprecated:: cx_Oracle 8.0 Other Types ----------- All of these types are extensions to the DB API definition. .. data:: ApiType This type object is the Python type of the database API type constants :data:`BINARY`, :data:`DATETIME`, :data:`NUMBER`, :data:`ROWID` and :data:`STRING`. .. data:: DbType This type object is the Python type of the :ref:`database type constants `. .. data:: LOB This type object is the Python type of :data:`DB_TYPE_BLOB`, :data:`DB_TYPE_BFILE`, :data:`DB_TYPE_CLOB` and :data:`DB_TYPE_NCLOB` data that is returned from cursors. .. _tpcconstants: Two-Phase Commit (TPC) Constants --------------------------------- The constants for the two-phase commit (TPC) functions :meth:`~Connection.tpc_begin()` and :meth:`~Connection.tpc_end()` are listed below. .. data:: TPC_BEGIN_JOIN Joins an existing TPC transaction. .. data:: TPC_BEGIN_NEW Creates a new TPC transaction. .. data:: TPC_BEGIN_PROMOTE Promotes a local transaction to a TPC transaction. .. data:: TPC_BEGIN_RESUME Resumes an existing TPC transaction. .. data:: TPC_END_NORMAL Ends the TPC transaction participation normally. .. data:: TPC_END_SUSPEND Suspends the TPC transaction. .. _exceptions: Oracledb Exceptions =================== See :ref:`exception` for usage information. .. exception:: Warning Exception raised for important warnings and defined by the DB API but not actually used by python-oracledb. .. exception:: Error Exception that is the base class of all other exceptions defined by python-oracledb and is a subclass of the Python StandardError exception (defined in the module exceptions). .. exception:: InterfaceError Exception raised for errors that are related to the database interface rather than the database itself. It is a subclass of Error. Exception messages of this class will have the prefix DPY and an error number in the range 1000 - 1999. .. exception:: DatabaseError Exception raised for errors that are related to the database. It is a subclass of Error. Exception messages of this class will have the prefix DPY and an error number in the range 4000 - 4999. .. exception:: DataError Exception raised for errors that are due to problems with the processed data. It is a subclass of DatabaseError. Exception messages of this class are generated by the database and will have a prefix such as ORA .. exception:: OperationalError Exception raised for errors that are related to the operation of the database but are not necessarily under the control of the programmer. It is a subclass of DatabaseError. Exception messages of this class will have the prefix DPY and an error number in the range 6000 - 6999. .. exception:: IntegrityError Exception raised when the relational integrity of the database is affected. It is a subclass of DatabaseError. Exception messages of this class are generated by the database and will have a prefix such as ORA .. exception:: InternalError Exception raised when the database encounters an internal error. It is a subclass of DatabaseError. Exception messages of this class will have the prefix DPY and an error number in the range 5000 - 5999. .. exception:: ProgrammingError Exception raised for programming errors. It is a subclass of DatabaseError. Exception messages of this class will have the prefix DPY and an error number in the range 2000 - 2999. .. exception:: NotSupportedError Exception raised when a method or database API was used which is not supported by the database. It is a subclass of DatabaseError. Exception messages of this class will have the prefix DPY and an error number in the range 3000 - 3999. .. _exchandling: Oracledb._Error Objects ======================= See :ref:`exception` for usage information. .. note:: PEP 249 (Python Database API Specification v2.0) says the following about exception values: [...] The values of these exceptions are not defined. They should give the user a fairly good idea of what went wrong, though. [...] With python-oracledb every exception object has exactly one argument in the ``args`` tuple. This argument is an ``oracledb._Error`` object which has the following six read-only attributes. .. attribute:: _Error.code Integer attribute representing the Oracle error number (ORA-XXXXX). .. attribute:: _Error.offset Integer attribute representing the error offset when applicable. .. attribute:: _Error.full_code String attribute representing the top-level error prefix and the code that is shown in the :attr:`_Error.message`. .. attribute:: _Error.message String attribute representing the Oracle message of the error. This message may be localized by the environment of the Oracle connection. .. attribute:: _Error.context String attribute representing the context in which the exception was raised. .. attribute:: _Error.isrecoverable Boolean attribute representing whether the error is recoverable or not. This is False in all cases unless both Oracle Database 12.1 (or later) and Oracle Client 12.1 (or later) are being used. python-oracledb-1.2.1/doc/src/api_manual/pool_params.rst000066400000000000000000000152661434177474600233500ustar00rootroot00000000000000.. _poolparam: *********************** API: PoolParams Objects *********************** A PoolParams object can be created with :meth:`oracledb.PoolParams()`. The PoolParams class is a subclass of the :ref:`ConnectParams Class `. In addition to the parameters and attributes of the ConnectParams class, the PoolParams class also contains new parameters and attributes. See :ref:`usingpoolparams` for more information. .. _poolparamsmeth: PoolParams Methods ================== .. method:: PoolParams.copy() Creates a copy of the parameters and returns it. .. method:: PoolParams.get_connect_string() Returns the connection string associated with the PoolParams instance. .. method:: PoolParams.parse_connect_string(connect_string) Parses the connect string into its components and stores the parameters. The connect string can be an Easy Connect string, name-value pairs, or a simple alias which is looked up in ``tnsnames.ora``. Parameters that are found in the connect string override any currently stored values. .. method:: PoolParams.set(min=None, max=None, increment=None, connectiontype=None, \ getmode=None, homogeneous=None, timeout=None, wait_timeout=None, \ max_lifetime_session=None, session_callback=None, max_sessions_per_shard=None, \ soda_metadata_cache=None, ping_interval=None, user=None, proxy_user=None,\ password=None, newpassword=None, wallet_password=None, access_token=None, \ host=None, port=None, protocol=None, https_proxy=None, https_proxy_port=None, \ service_name=None, sid=None, server_type=None, cclass=None, purity=None, \ expire_time=None, retry_count=None, retry_delay=None, tcp_connect_timeout=None, \ ssl_server_dn_match=None, ssl_server_cert_dn=None, wallet_location=None, \ events=None, externalauth=None, mode=None, disable_oob=None, stmtcachesize=None, \ edition=None, tag=None, matchanytag=None, config_dir=None, appcontext=[], \ shardingkey=[], supershardingkey=[], debug_jdwp=None, handle=None) Sets one or more of the parameters. .. _poolparamsattr: PoolParams Attributes ===================== .. attribute:: PoolParams.connectiontype This read-only attribute specifies the class of the connection that should be returned during calls to :meth:`ConnectionPool.acquire()`. It must be Connection or a subclass of Connection. This attribute is of type Type["oracledb.connection"]. The default value is ``oracledb.Connection``. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: PoolParams.getmode This read-write attribute is an integer that determines the behavior of :meth:`ConnectionPool.acquire()`. The value of this attribute can be one of the constants :data:`oracledb.POOL_GETMODE_WAIT`, :data:`oracledb.POOL_GETMODE_NOWAIT`, :data:`oracledb.POOL_GETMODE_FORCEGET`, or :data:`oracledb.POOL_GETMODE_TIMEDWAIT`. The default value is :data:`oracledb.POOL_GETMODE_WAIT`. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: PoolParams.homogeneous This read-only attribute is a boolean which indicates whether the connections are :ref:`homogeneous ` (same user) or heterogeneous (multiple users). The default value is True. This attribute is only supported in the python-oracledb Thick mode. The python-oracledb Thin mode supports only homogeneous modes. .. attribute:: PoolParams.increment This read-only attribute specifies the number of connections that should be added to the pool whenever a new connection needs to be created. The default value is 1. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: PoolParams.min This read-only attribute is an integer that specifies the minimum number of connections that the pool should contain. The default value is 1. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: PoolParams.max This read-only attribute specifies the maximum number of connections that the pool should contain. The default value is 2. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: PoolParams.max_lifetime_session This read-only attribute is an integer that determines the length of time (in seconds) that connections can remain in the pool. If the value of this attribute is 0, then the connections may remain in the pool indefinitely. The default value is 0 seconds. This attribute is only supported in the python-oracledb Thick mode. .. attribute:: PoolParams.max_sessions_per_shard This read-only attribute is an integer that determines the maximum number of connections that may be associated with a particular shard. The default value is 0. This attribute is only supported in the python-oracledb Thick mode. .. attribute:: PoolParams.ping_interval This read-only attribute is an integer that specifies the length of time (in seconds) after which an unused connection in the pool will be a candidate for pinging when :meth:`ConnectionPool.acquire()` is called. If the ping to the database indicates that the connection is not alive, then a replacement connection will be returned by :meth:`ConnectionPool.acquire()`. If the ``ping_interval`` is a negative value, then the ping functionality will be disabled. The default value is 60 seconds. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: PoolParams.session_callback This read-only attribute specifies a callback that is invoked when a connection is returned from the pool for the first time, or when the connection tag differs from the one requested. This attribute is supported in the python-oracledb Thin and Thick modes. .. attribute:: PoolParams.soda_metadata_cache This read-only attribute is a boolean that indicates whether SODA metadata cache should be enabled or not. The default value is False. This attribute is only supported in the python-oracledb Thick mode. .. attribute:: PoolParams.timeout This read-only attribute is an integer that specifies the length of time (in seconds) that a connection may remain idle in the pool before it is terminated. If the value of this attribute is 0, then the connections are never terminated. The default value is 0 seconds. This attribute is only supported in the python-oracledb Thick mode. .. attribute:: PoolParams.wait_timeout This read-only attribute is an integer that specifies the length of time (in milliseconds) that a caller should wait when acquiring a connection from the pool with :attr:`~PoolParams.getmode` set to :data:`~oracledb.POOLGETMODE_TIMEDWAIT`. The default value is 0 milliseconds. This attribute is supported in the python-oracledb Thin and Thick modes. python-oracledb-1.2.1/doc/src/api_manual/soda.rst000066400000000000000000000555651434177474600217700ustar00rootroot00000000000000.. _soda: ********** API: SODA ********** `Oracle Database Simple Oracle Document Access (SODA) `__ allows documents to be inserted, queried, and retrieved from Oracle Database using a set of NoSQL-style python-oracledb methods. By default, documents are JSON strings. See the :ref:`user manual ` for examples. .. _sodarequirements: SODA Requirements ================= To use SODA, the role SODA_APP must be granted to the user. To create collections, users need the CREATE TABLE privilege. These can be granted by a DBA: .. code-block:: sql SQL> grant soda_app, create table to myuser; Advanced users who are using Oracle sequences for keys will also need the CREATE SEQUENCE privilege. SODA requires Oracle Client 18.3 or higher and Oracle Database 18.1 and higher. .. note:: If you are using Oracle Database 21c (or later) and create new collections you need to do one of the following: - Use Oracle Client libraries 21c (or later) - Or explicitly use collection metadata when creating collections and set the data storage type to BLOB, for example:: { "keyColumn": { "name":"ID" }, "contentColumn": { "name": "JSON_DOCUMENT", "sqlType": "BLOB" }, "versionColumn": { "name": "VERSION", "method": "UUID" }, "lastModifiedColumn": { "name": "LAST_MODIFIED" }, "creationTimeColumn": { "name": "CREATED_ON" } } - Or, set the database initialization parameter `compatible `__ to 19 or lower. Otherwise, you may get errors such as ``ORA-40842: unsupported value JSON in the metadata for the field sqlType`` or ``ORA-40659: Data type does not match the specification in the collection metadata``. .. _sodadb: SodaDatabase Objects ==================== .. note:: The SODA Database object is an extension the DB API. It is returned by the method :meth:`Connection.getSodaDatabase()`. SodaDatabase Methods -------------------- .. method:: SodaDatabase.createCollection(name, metadata=None, mapMode=False) Creates a SODA collection with the given name and returns a new :ref:`SODA collection object `. If you try to create a collection, and a collection with the same name and metadata already exists, then that existing collection is opened without error. If ``metadata`` is specified, it is expected to be a string containing valid JSON or a dictionary that will be transformed into a JSON string. This JSON permits you to specify the configuration of the collection including storage options; specifying the presence or absence of columns for creation timestamp, last modified timestamp and version; whether the collection can store only JSON documents; and methods of key and version generation. The default metadata creates a collection that only supports JSON documents and uses system generated keys. See this `collection metadata reference `__ for more information. If the ``mapMode`` parameter is set to True, the new collection is mapped to an existing table instead of creating a table. If a collection is created in this way, dropping the collection will not drop the existing table either. .. method:: SodaDatabase.createDocument(content, key=None, mediaType="application/json") Creates a :ref:`SODA document ` usable for SODA write operations. You only need to use this method if your collection requires client-assigned keys or has non-JSON content; otherwise, you can pass your content directly to SODA write operations. SodaDocument attributes 'createdOn', 'lastModified' and 'version' will be None. The ``content`` parameter can be a dictionary or list which will be transformed into a JSON string and then UTF-8 encoded. It can also be a string which will be UTF-8 encoded or it can be a bytes object which will be stored unchanged. If a bytes object is provided and the content is expected to be JSON, note that SODA only supports UTF-8, UTF-16LE and UTF-16BE encodings. The ``key`` parameter should only be supplied if the collection in which the document is to be placed requires client-assigned keys. The ``mediaType`` parameter should only be supplied if the collection in which the document is to be placed supports non-JSON documents and the content for this document is non-JSON. Using a standard MIME type for this value is recommended but any string will be accepted. .. method:: SodaDatabase.getCollectionNames(startName=None, limit=0) Returns a list of the names of collections in the database that match the criteria, in alphabetical order. If the ``startName`` parameter is specified, the list of names returned will start with this value and also contain any names that fall after this value in alphabetical order. If the ``limit`` parameter is specified and is non-zero, the number of collection names returned will be limited to this value. .. method:: SodaDatabase.openCollection(name) Opens an existing collection with the given name and returns a new :ref:`SODA collection object `. If a collection with that name does not exist, None is returned. .. _sodacoll: SodaCollection Objects ====================== .. note:: The SODA Collection object is an extension the DB API. It is used to represent SODA collections and is created by methods :meth:`SodaDatabase.createCollection()` and :meth:`SodaDatabase.openCollection()`. SodaCollection Methods ---------------------- .. method:: SodaCollection.createIndex(spec) Creates an index on a SODA collection. The spec is expected to be a dictionary or a JSON-encoded string. See this `overview `__ for information on indexes in SODA. .. note:: A commit should be performed before attempting to create an index. .. method:: SodaCollection.drop() Drops the collection from the database, if it exists. Note that if the collection was created with mapMode set to True the underlying table will not be dropped. A boolean value is returned indicating if the collection was actually dropped. .. method:: SodaCollection.dropIndex(name, force=False) Drops the index with the specified name, if it exists. The ``force`` parameter, if set to True, can be used to force the dropping of an index that the underlying Oracle Database domain index doesn't normally permit. This is only applicable to spatial and JSON search indexes. See `here `__ for more information. A boolean value is returned indicating if the index was actually dropped. .. method:: SodaCollection.find() This method is used to begin an operation that will act upon documents in the collection. It creates and returns a :ref:`SodaOperation object ` which is used to specify the criteria and the operation that will be performed on the documents that match that criteria. .. method:: SodaCollection.getDataGuide() Returns a :ref:`SODA document object ` containing property names, data types and lengths inferred from the JSON documents in the collection. It can be useful for exploring the schema of a collection. Note that this method is only supported for JSON-only collections where a JSON search index has been created with the 'dataguide' option enabled. If there are no documents in the collection, None is returned. .. method:: SodaCollection.insertMany(docs) Inserts a list of documents into the collection at one time. Each of the input documents can be a dictionary or list or an existing :ref:`SODA document object `. .. note:: This method requires Oracle Client 18.5 and higher and is available only as a preview. .. method:: SodaCollection.insertManyAndGet(docs, hint=None) Similarly to :meth:`~SodaCollection.insertMany()` this method inserts a list of documents into the collection at one time. The only difference is that it returns a list of :ref:`SODA Document objects `. Note that for performance reasons the returned documents do not contain the content. The ``hint`` parameter, if specified, supplies a hint to the database when processing the SODA operation. This is expected to be a string in the same format as a SQL hint but without any comment characters, for example ``hint="MONITOR"``. Pass only the hint ``"MONITOR"`` (turn on monitoring) or ``"NO_MONITOR"`` (turn off monitoring). See the Oracle Database SQL Tuning Guide documentation `MONITOR and NO_MONITOR Hints `__ and `Monitoring Database Operations `__ for more information. .. note:: - This method requires Oracle Client 18.5 and higher. - Use of the ``hint`` parameter requires Oracle Client 21.3 or higher (or Oracle Client 19 from 19.11). .. method:: SodaCollection.insertOne(doc) Inserts a given document into the collection. The input document can be a dictionary or list or an existing :ref:`SODA document object `. .. method:: SodaCollection.insertOneAndGet(doc, hint=None) Similarly to :meth:`~SodaCollection.insertOne()` this method inserts a given document into the collection. The only difference is that it returns a :ref:`SODA Document object `. Note that for performance reasons the returned document does not contain the content. The ``hint`` parameter, if specified, supplies a hint to the database when processing the SODA operation. This is expected to be a string in the same format as a SQL hint but without any comment characters, for example ``hint="MONITOR"``. Pass only the hint ``"MONITOR"`` (turn on monitoring) or ``"NO_MONITOR"`` (turn off monitoring). See the Oracle Database SQL Tuning Guide documentation `MONITOR and NO_MONITOR Hints `__ and `Monitoring Database Operations `__ for more information. .. note:: Use of the ``hint`` parameter requires Oracle Client 21.3 or higher (or Oracle Client 19 from 19.11). .. method:: SodaCollection.save(doc) Saves a document into the collection. This method is equivalent to :meth:`~SodaCollection.insertOne()` except that if client-assigned keys are used, and the document with the specified key already exists in the collection, it will be replaced with the input document. This method requires Oracle Client 19.9 or higher in addition to the usual SODA requirements. .. method:: SodaCollection.saveAndGet(doc, hint=None) Saves a document into the collection. This method is equivalent to :meth:`~SodaCollection.insertOneAndGet()` except that if client-assigned keys are used, and the document with the specified key already exists in the collection, it will be replaced with the input document. The ``hint`` parameter, if specified, supplies a hint to the database when processing the SODA operation. This is expected to be a string in the same format as a SQL hint but without any comment characters, for example ``hint="MONITOR"``. Pass only the hint ``"MONITOR"`` (turn on monitoring) or ``"NO_MONITOR"`` (turn off monitoring). See the Oracle Database SQL Tuning Guide documentation `MONITOR and NO_MONITOR Hints `__ and `Monitoring Database Operations `__ for more information. This method requires Oracle Client 19.9 or higher in addition to the usual SODA requirements. .. note:: Use of the ``hint`` parameter requires Oracle Client 21.3 or higher (or Oracle Client 19 from 19.11). .. method:: SodaCollection.truncate() Removes all of the documents in the collection, similarly to what is done for rows in a table by the TRUNCATE TABLE statement. SodaCollection Attributes ------------------------- .. attribute:: SodaCollection.metadata This read-only attribute returns a dictionary containing the metadata that was used to create the collection. See this `collection metadata reference `__ for more information. .. attribute:: SodaCollection.name This read-only attribute returns the name of the collection. .. _sodadoc: SodaDoc Objects =============== .. note:: The SODA Document object is an extension the DB API. It is returned by the methods :meth:`SodaDatabase.createDocument()`, :meth:`SodaOperation.getDocuments()` and :meth:`SodaOperation.getOne()` as well as by iterating over :ref:`SODA document cursors `. SodaDoc Methods --------------- .. method:: SodaDoc.getContent() Returns the content of the document as a dictionary or list. This method assumes that the content is application/json and will raise an exception if this is not the case. If there is no content, however, None will be returned. .. method:: SodaDoc.getContentAsBytes() Returns the content of the document as a bytes object. If there is no content, however, None will be returned. .. method:: SodaDoc.getContentAsString() Returns the content of the document as a string. If the document encoding is not known, UTF-8 will be used. If there is no content, however, None will be returned. SodaDoc Attributes ------------------ .. attribute:: SodaDoc.createdOn This read-only attribute returns the creation time of the document in `ISO 8601 `__ format. Documents created by :meth:`SodaDatabase.createDocument()` or fetched from collections where this attribute is not stored will return None. .. attribute:: SodaDoc.key This read-only attribute returns the unique key assigned to this document. Documents created by :meth:`SodaDatabase.createDocument()` may not have a value assigned to them and return None. .. attribute:: SodaDoc.lastModified This read-only attribute returns the last modified time of the document in `ISO 8601 `__ format. Documents created by :meth:`SodaDatabase.createDocument()` or fetched from collections where this attribute is not stored will return None. .. attribute:: SodaDoc.mediaType This read-only attribute returns the media type assigned to the document. By convention this is expected to be a MIME type but no checks are performed on this value. If a value is not specified when calling :meth:`SodaDatabase.createDocument()` or the document is fetched from a collection where this component is not stored, the string "application/json" is returned. .. attribute:: SodaDoc.version This read-only attribute returns the version assigned to this document. Documents created by :meth:`SodaDatabase.createDocument()` or fetched from collections where this attribute is not stored will return None. .. _sodadoccur: SodaDocCursor Objects ===================== .. note:: The SODA Document Cursor object is an extension the DB API. It is returned by the method :meth:`SodaOperation.getCursor()` and implements the iterator protocol. Each iteration will return a :ref:`SODA document object `. SodaDocCursor Methods --------------------- .. method:: SodaDocCursor.close() Closes the cursor now, rather than whenever __del__ is called. The cursor will be unusable from this point forward; an Error exception will be raised if any operation is attempted with the cursor. .. _sodaop: SodaOperation Objects ===================== .. note:: The SODA Operation Object is an extension to the DB API. It represents an operation that will be performed on all or some of the documents in a SODA collection. It is created by the method :meth:`SodaCollection.find()`. SodaOperation Methods --------------------- .. method:: SodaOperation.count() Returns a count of the number of documents in the collection that match the criteria. If :meth:`~SodaOperation.skip()` or :meth:`~SodaOperation.limit()` were called on this object, an exception is raised. .. method:: SodaOperation.fetchArraySize(value) This is a tuning method to specify the number of documents that are internally fetched in batches by calls to :meth:`~SodaOperation.getCursor()` and :meth:`~SodaOperation.getDocuments()`. It does not affect how many documents are returned to the application. A value of 0 will use the default value (100). This method is only available in Oracle Client 19.5 and higher. As a convenience, the SodaOperation object is returned so that further criteria can be specified by chaining methods together. .. method:: SodaOperation.filter(value) Sets a filter specification for complex document queries and ordering of JSON documents. Filter specifications must be provided as a dictionary or JSON-encoded string and can include comparisons, regular expressions, logical and spatial operators, among others. See the `overview of SODA filter specifications `__ for more information. As a convenience, the SodaOperation object is returned so that further criteria can be specified by chaining methods together. .. method:: SodaOperation.getCursor() Returns a :ref:`SODA Document Cursor object ` that can be used to iterate over the documents that match the criteria. .. method:: SodaOperation.getDocuments() Returns a list of :ref:`SODA Document objects ` that match the criteria. .. method:: SodaOperation.getOne() Returns a single :ref:`SODA Document object ` that matches the criteria. Note that if multiple documents match the criteria only the first one is returned. .. method:: SodaOperation.hint(value) Specifies a hint that will be provided to the SODA operation when it is performed. This is expected to be a string in the same format as a SQL hint but without any comment characters, for example ``hint("MONITOR")``. Pass only the hint ``"MONITOR"`` (turn on monitoring) or ``"NO_MONITOR"`` (turn off monitoring). See the Oracle Database SQL Tuning Guide documentation `MONITOR and NO_MONITOR Hints `__ and `Monitoring Database Operations `__ for more information. As a convenience, the SodaOperation object is returned so that further criteria can be specified by chaining methods together. Use of this method requires Oracle Client 21.3 or higher (or Oracle Client 19 from 19.11). .. method:: SodaOperation.key(value) Specifies that the document with the specified key should be returned. This causes any previous calls made to this method and :meth:`~SodaOperation.keys()` to be ignored. As a convenience, the SodaOperation object is returned so that further criteria can be specified by chaining methods together. .. method:: SodaOperation.keys(seq) Specifies that documents that match the keys found in the supplied sequence should be returned. This causes any previous calls made to this method and :meth:`~SodaOperation.key()` to be ignored. As a convenience, the SodaOperation object is returned so that further criteria can be specified by chaining methods together. .. method:: SodaOperation.limit(value) Specifies that only the specified number of documents should be returned. This method is only usable for read operations such as :meth:`~SodaOperation.getCursor()` and :meth:`~SodaOperation.getDocuments()`. For write operations, any value set using this method is ignored. As a convenience, the SodaOperation object is returned so that further criteria can be specified by chaining methods together. .. method:: SodaOperation.remove() Removes all of the documents in the collection that match the criteria. The number of documents that have been removed is returned. .. method:: SodaOperation.replaceOne(doc) Replaces a single document in the collection with the specified document. The input document can be a dictionary or list or an existing :ref:`SODA document object `. A boolean indicating if a document was replaced or not is returned. Currently the method :meth:`~SodaOperation.key()` must be called before this method can be called. .. method:: SodaOperation.replaceOneAndGet(doc) Similarly to :meth:`~SodaOperation.replaceOne()`, this method replaces a single document in the collection with the specified document. The only difference is that it returns a :ref:`SODA document object `. Note that for performance reasons the returned document does not contain the content. .. method:: SodaOperation.skip(value) Specifies the number of documents that match the other criteria that will be skipped. This method is only usable for read operations such as :meth:`~SodaOperation.getCursor()` and :meth:`~SodaOperation.getDocuments()`. For write operations, any value set using this method is ignored. As a convenience, the SodaOperation object is returned so that further criteria can be specified by chaining methods together. .. method:: SodaOperation.version(value) Specifies that documents with the specified version should be returned. Typically this is used with :meth:`~SodaOperation.key()` to implement optimistic locking, so that the write operation called later does not affect a document that someone else has modified. As a convenience, the SodaOperation object is returned so that further criteria can be specified by chaining methods together. python-oracledb-1.2.1/doc/src/api_manual/subscription.rst000066400000000000000000000202061434177474600235460ustar00rootroot00000000000000.. _subscrobj: ************************* API: Subscription Objects ************************* .. note:: This object is an extension the DB API. Subscription Methods ==================== .. method:: Subscription.registerquery(statement, [args]) Registers the query for subsequent notification when tables referenced by the query are changed. This behaves similarly to :meth:`Cursor.execute()` but only queries are permitted and the ``args`` parameter must be a sequence or dictionary. If the ``qos`` parameter included the flag :data:`oracledb.SUBSCR_QOS_QUERY` when the subscription was created, then the ID for the registered query is returned; otherwise, None is returned. Subscription Attributes ======================= .. attribute:: Subscription.callback This read-only attribute returns the callback that was registered when the subscription was created. .. attribute:: Subscription.connection This read-only attribute returns the connection that was used to register the subscription when it was created. .. attribute:: Subscription.id This read-only attribute returns the value of ``REGID`` found in the database view ``USER_CHANGE_NOTIFICATION_REGS`` or the value of ``REG_ID`` found in the database view ``USER_SUBSCR_REGISTRATIONS``. For AQ subscriptions, the value is 0. .. attribute:: Subscription.ip_address This read-only attribute returns the IP address used for callback notifications from the database server. If not set during construction, this value is None. For consistency and compliance with the PEP 8 naming style, the attribute `ipAddress` was renamed to `ip_address`. The old name will continue to work for a period of time. .. attribute:: Subscription.name This read-only attribute returns the name used to register the subscription when it was created. .. attribute:: Subscription.namespace This read-only attribute returns the namespace used to register the subscription when it was created. .. attribute:: Subscription.operations This read-only attribute returns the operations that will send notifications for each table or query that is registered using this subscription. .. attribute:: Subscription.port This read-only attribute returns the port used for callback notifications from the database server. If not set during construction, this value is zero. .. attribute:: Subscription.protocol This read-only attribute returns the protocol used to register the subscription when it was created. .. attribute:: Subscription.qos This read-only attribute returns the quality of service flags used to register the subscription when it was created. .. attribute:: Subscription.timeout This read-only attribute returns the timeout (in seconds) that was specified when the subscription was created. A value of 0 indicates that there is no timeout. .. _msgobjects: Message Objects --------------- .. note:: This object is created internally when notification is received and passed to the callback procedure specified when a subscription is created. .. attribute:: Message.consumer_name This read-only attribute returns the name of the consumer which generated the notification. It will be populated if the subscription was created with the namespace :data:`oracledb.SUBSCR_NAMESPACE_AQ` and the queue is a multiple consumer queue. For consistency and compliance with the PEP 8 naming style, the attribute `consumerName` was renamed to `consumer_name`. The old name will continue to work for a period of time. .. attribute:: Message.dbname This read-only attribute returns the name of the database that generated the notification. .. attribute:: Message.msgid This read-only attribute returns the message id of the AQ message which generated the notification. It will only be populated if the subscription was created with the namespace :data:`oracledb.SUBSCR_NAMESPACE_AQ`. .. attribute:: Message.queries This read-only attribute returns a list of message query objects that give information about query result sets changed for this notification. This attribute will be None if the ``qos`` parameter did not include the flag :data:`~oracledb.SUBSCR_QOS_QUERY` when the subscription was created. .. attribute:: Message.queue_name This read-only attribute returns the name of the queue which generated the notification. It will only be populated if the subscription was created with the namespace :data:`oracledb.SUBSCR_NAMESPACE_AQ`. For consistency and compliance with the PEP 8 naming style, the attribute `queueName` was renamed to `queue_name`. The old name will continue to work for a period of time. .. attribute:: Message.registered This read-only attribute returns whether the subscription which generated this notification is still registered with the database. The subscription is automatically deregistered with the database when the subscription timeout value is reached or when the first notification is sent (when the quality of service flag :data:`oracledb.SUBSCR_QOS_DEREG_NFY` is used). .. attribute:: Message.subscription This read-only attribute returns the subscription object for which this notification was generated. .. attribute:: Message.tables This read-only attribute returns a list of message table objects that give information about the tables changed for this notification. This attribute will be None if the ``qos`` parameter included the flag :data:`~oracledb.SUBSCR_QOS_QUERY` when the subscription was created. .. attribute:: Message.txid This read-only attribute returns the id of the transaction that generated the notification. .. attribute:: Message.type This read-only attribute returns the type of message that has been sent. See the constants section on event types for additional information. MessageTable Objects -------------------- .. note:: This object is created internally for each table changed when notification is received and is found in the tables attribute of message objects, and the tables attribute of message query objects. .. attribute:: MessageTable.name This read-only attribute returns the name of the table that was changed. .. attribute:: MessageTable.operation This read-only attribute returns the operation that took place on the table that was changed. .. attribute:: MessageTable.rows This read-only attribute returns a list of message row objects that give information about the rows changed on the table. This value is only filled in if the ``qos`` parameter to the :meth:`Connection.subscribe()` method included the flag :data:`~oracledb.SUBSCR_QOS_ROWIDS`. MessageRow Objects ------------------ .. note:: This object is created internally for each row changed on a table when notification is received and is found in the rows attribute of message table objects. .. attribute:: MessageRow.operation This read-only attribute returns the operation that took place on the row that was changed. .. attribute:: MessageRow.rowid This read-only attribute returns the rowid of the row that was changed. MessageQuery Objects -------------------- .. note:: This object is created internally for each query result set changed when notification is received and is found in the queries attribute of message objects. .. attribute:: MessageQuery.id This read-only attribute returns the query id of the query for which the result set changed. The value will match the value returned by Subscription.registerquery when the related query was registered. .. attribute:: MessageQuery.operation This read-only attribute returns the operation that took place on the query result set that was changed. Valid values for this attribute are :data:`~oracledb.EVENT_DEREG` and :data:`~oracledb.EVENT_QUERYCHANGE`. .. attribute:: MessageQuery.tables This read-only attribute returns a list of message table objects that give information about the table changes that caused the query result set to change for this notification. python-oracledb-1.2.1/doc/src/api_manual/variable.rst000066400000000000000000000072531434177474600226160ustar00rootroot00000000000000.. _varobj: ********************* API: Variable Objects ********************* .. note:: The DB API definition does not define this object. Variable Methods ================= .. method:: Variable.getvalue([pos=0]) Returns the value at the given position in the variable. For variables created using the method :func:`Cursor.arrayvar()` the value returned will be a list of each of the values in the PL/SQL index-by table. For variables bound to DML returning statements, the value returned will also be a list corresponding to the returned data for the given execution of the statement (as identified by the ``pos`` parameter). .. method:: Variable.setvalue(pos, value) Set the value at the given position in the variable. Variable Attributes =================== .. attribute:: Variable.actual_elements This read-only attribute returns the actual number of elements in the variable. This corresponds to the number of elements in a PL/SQL index-by table for variables that are created using the method :func:`Cursor.arrayvar()`. For all other variables this value will be identical to the attribute :attr:`~Variable.numElements`. For consistency and compliance with the PEP 8 naming style, the attribute `actualElements` was renamed to `actual_elements`. The old name will continue to work for a period of time. .. attribute:: Variable.buffer_size This read-only attribute returns the size of the buffer allocated for each element in bytes. For consistency and compliance with the PEP 8 naming style, the attribute `bufferSize` was renamed to `buffer_size`. The old name will continue to work for a period of time. .. attribute:: Variable.inconverter This read-write attribute specifies the method used to convert data from Python to the Oracle database. The method signature is converter(value) and the expected return value is the value to bind to the database. If this attribute is None, the value is bound directly without any conversion. .. attribute:: Variable.num_elements This read-only attribute returns the number of elements allocated in an array, or the number of scalar items that can be fetched into the variable or bound to the variable. For consistency and compliance with the PEP 8 naming style, the attribute `numElements` was renamed to `num_elements`. The old name will continue to work for a period of time. .. attribute:: Variable.outconverter This read-write attribute specifies the method used to convert data from the Oracle database to Python. The method signature is converter(value) and the expected return value is the value to return to Python. If this attribute is None, the value is returned directly without any conversion. .. attribute:: Variable.size This read-only attribute returns the size of the variable. For strings this value is the size in characters. For all others, this is same value as the attribute bufferSize. .. attribute:: Variable.type This read-only attribute returns the type of the variable. This will be an :ref:`Oracle Object Type ` if the variable binds Oracle objects; otherwise, it will be one of the :ref:`database type constants `. Database type constants are now used when the variable is not used for binding Oracle objects. .. attribute:: Variable.values This read-only attribute returns a copy of the value of all actual positions in the variable as a list. This is the equivalent of calling :meth:`~Variable.getvalue()` for each valid position and the length will correspond to the value of the :attr:`~Variable.actualElements` attribute. python-oracledb-1.2.1/doc/src/conf.py000066400000000000000000000114301434177474600174600ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # python-oracledb documentation build configuration file # # This file is execfile()d with the current directory set to its containing dir. # # The contents of this file are pickled, so don't put values in the namespace # that aren't pickleable (module imports are okay, they're removed automatically). # # All configuration values have a default value; values that are commented out # serve to show the default value. import os import sys # If your extensions are in another directory, add it here. sys.path.append(os.path.abspath("_ext")) # General configuration # --------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ["table_with_summary", "oracle_deprecated"] # Add any paths that contain templates here, relative to this directory. templates_path = ['.templates'] # The suffix of source filenames. source_suffix = '.rst' # The root toctree document. root_doc = master_doc = 'index' # General substitutions. project = 'python-oracledb' copyright = u'2016, 2022, Oracle and/or its affiliates. All rights reserved. Portions Copyright © 2007-2015, Anthony Tuininga. All rights reserved. Portions Copyright © 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, Canada. All rights reserved' author = 'Oracle' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. # # The values are acquired from the __version__ constant defined in the module # itself in order to avoid duplicate values (and therefore one being different # from the other) # # The short X.Y version. global_vars = {} local_vars = {} version_file_name = os.path.join("..", "..", "src", "oracledb", "version.py") with open(version_file_name) as f: exec(f.read(), global_vars, local_vars) version = ".".join(local_vars["__version__"].split(".")[:2]) # The full version, including alpha/beta/rc tags. release = local_vars["__version__"] # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # Options for HTML output # ----------------------- # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. # html_style = 'default.css' # The theme to use for readthedocs. html_theme = 'sphinx_rtd_theme' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['.static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Content template for the index page. #html_index = '' # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If true, the reST sources are included in the HTML build as _sources/. html_copy_source = False # Output file base name for HTML help builder. htmlhelp_basename = 'oracledbdoc' numfig = True # Display tables with no horizontal scrollbar def setup(app): app.add_css_file('custom.css') # Options for LaTeX output # ------------------------ # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class [howto/manual]). #latex_documents = [] # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True python-oracledb-1.2.1/doc/src/images/000077500000000000000000000000001434177474600174275ustar00rootroot00000000000000python-oracledb-1.2.1/doc/src/images/python-oracledb-thick-arch.png000066400000000000000000004214011434177474600252440ustar00rootroot00000000000000PNG  IHDR/xʅ&zTXtRaw profile type exifxڭi7sl8XsHUKTm6,Y̌oquß}tj+ϟs_z|~}y'>㿏(L{w|f7ragRG*O~:?B/?Y!7y]a1OƸbzTc+ROvj)OJ)%$߁KcfOϽ˳F!h1 }8F!h2W-ܷ&"ho~~n1-[%@:_nE-,<LH,k5b#@ǔ$,nsJ4g޵}JX*ȟ94,Y6b՚uRjQ͍jVK^GK-7kZoǞ(Izc̑ݰW1L3Oefc>+/[eV_cǝv޶ˮ Tr';N?k7|[oQ_~)]WFwkՍ-p3"s U b[9*r M !'Dgoͱ;s C;OqEԶp}Pk5#6c~kh7AQ#ALszg `\~ow.}fk]w07BB;;}xJL'Pqm©] iGکett wQpN" ,pwv^,})ij 8)-P>V&W7{פm?0}Kx:uJp.1e֣t"r8@99QC?+S܌l "z(b fk*=&~M[<3a׹g2̿:[-_ePQC[\UmI~ecieL=qf۞o$Γ_5}w+=t|׺[վFyk-DQgD&70d:%xsu-۩lcԥc fvEF0e]8^%=i+RږꭽiCSk-mP"cj1*m_oԕ@}3K.:0后Vл~\p/m>4:sDP̽727Ƽ(QJ8@u$B{ [m3hGijVZ`2U@Cj=Sr7{Z\k Ŵ19m{nY% ui4Ro܈g>k+T3Ynvҟ="wɢ9zD8:lƬt!Nx-cqlҐX]/>:PҽnJY$+A.ܘy ] &(:8lSJ#D026 2de E*J4C!"n6B2)CzS&鶶tPYO9aIlvM\ :$42~ FzVդ荺p3y`̛Ȍ E#+&:Ct i2 eei#!>0VY$^d\ZTvJ2n|@7w.`)ev=n5*d`#NEd*/|Hag<<Cؾb PZ)1<`Vӻ+WxDKRNGEV^QvBaȨB <[kGv'UnP?Xi|#8`f:pSq#3XS", S5 6D $I0wv8 Z)mZ\) pؑ u(Z dӚRݘεH!V5nj2hLA]Ų.Q,X|nhX9k-݈KBA + ̅TXf$u N9@̢_t*c x=L'C6bR׻h2BF茁Q|$p޺m wwifdJ&A=K@I /jBh؂V$'e [@()ZM@?o/ l@x$x rH=RƈU\հ  1/ɃJ$xh󰮦Ro$^D2Jm/H}$v oρcq&р8Fp,KNXTVݛ;n!A2RN~X( G Ԣ=OG`FHa(VƪE  p`m<3Ia%uqHQZT!2ӄ7A !c*R!_ݐ醼gvu6L$q T߯Wz@=U63$A󇯮t \0E}Š"E?RXfSd&IS/NIVjtTs2F{剦,D8א&آn0!99H;T0?GAUJFBqjwF -vTpu9<PnnN ΢ʹrNA*)Gv:򘥥$ !Pb桩e(ywmA p"V7D}8%=JQؿYDŒt[2f<}&ǒ e - 1p^I7J Dp/ԝp3DY% $g1tvP,b1/V ؠNXM C.EG`3 Gj#]1@(||5*ٴ$EXPsE|Q_K΢ uFaO7j.(Lb՘?V*aO6% H:4yUjX(( d @@%vBqڡC8 3&B߁H XU<#qa]uĊz)X j'XZi.e`C Q.QGu"h)ZfvR9.&iJaP](:>8'PbTئkGwn eCʼnE(yjG ̌MJBʒ<#qo< ',;$7oDtQ-'K1KV2NQ&QN)I7fm3Gr3H`jjFOfkDMAkR[  w&Zc TƎ4 `!p̒pkb|{zlO!@@B1U@5yW j@(|g"ܑ'%r?@nCCJR 0҈y#*RGX(I55}'\ ckQ#35uޛt$ɸc(0զA6w*G EMT#;uG](LZ|2nD" n*N3)qUi) jV;wj2@-z[S)#Fˈ>l(!k;;i [v4/c@\3Y WBe#l0DY+jϏ1oo! ס9Q|x0妍:#@ox 0ֺ_7;q8V>ɲsݏSbwϠ"d߈Oja$gmxARHs   F{a?[uKd<@w#tàk +~}ƕ+\I8tKǝrurlnJôQ05"'E${C-K&"ٓ6{;W@ԫ +"Sm1sH5 1Ps`eo6|Z Y(*Oe4q-`ܡI-2×Y,6hZutJL_h\!̹vH[0rN= <h"Ѻ-9| JmO>rN&h9u$ dTUKH}}4yh?v|yBsFM'B5)iY#ǠM(ovpSwPo_Crh́ ۨCHx}MQxPҏ_'9T)'ZQBb:Zૼu~_ݙݡW#OXJlrdF %vP]pUԱq4'iCCPICC profilex}=H@_SKEZ ␡:Yu*BZu0 4$-.kŪ "%/)=B4k JLvU "0ƨ,cN_.Ƴ9jbO$eY% ڬ#(#&]o  <3bSbJOGUM|!yV= C9}e4" AJ(:)R8~\ J`X@dnR(^lcͺmv?Wz_i3Z.ۚ\Olʎ)}SzZ8}U88 =rĂPLTEC|B{'6g}-Yg7h~Bz7i@w=r9lAxC{:n8j?v>t6h~u;o;p@{:m8k0,*4f|F~(We$Udr9lBz8j=zƸaNR;y8x2e|I׸.d{򙲾UŇߔ\׀4ӽ M_Nz >l} 9 <34ΐ4?!y/S\AEm:ȐFZz}Iϑ]%܃z}胱US')7us~W 5r޴tUw~Qja?""4s\$;(zk•7枊gM$"_Ş:]D߰zM,^v9uAU3郋Ÿ;{ v3Au왮qAQ== 4{Y8v:|e]'a.b&Ʊ?q a?^I\;]5l"6|P >>J;_XE׬*+įAT.|NtaPu"R|J!|u%430?2 | ped.oAT\xyL&No2?M&Wyf M A)s 7;$t$dK^7?]Wsgȟ)vȋ^NN>K(t䦫j ;h| n'd~G˗?kf]% {J)(0w9Le3p1Qo&}een djdEW+>%\:{ú@Lw#K(}W] I*9%R]T~zEe'7 T ?pʥ+jP_Q?R 'ƒf fٕ)_v2r{+7=j>S ˹[7ʿ--%+R|ҕ߂P'LD@/d_P~/~佔&+Nߒ ߂SUe}݉:Aw>wI~HP<=TuxЙ{2S}B@ SK*'(i @ cX`Y:oEkB::9tu-QWַo& B;Ϡ5@B@B:B:: tC@B[_З>x0 t1e'k(}SU'UhoA9 }*FGsnQP tNF cGE[+7o|s~JKtLsU+C4ܻu>з>v53 t!X޳u;U{%iMm:귭-+1C:95hHG:t B:Wp 0%޵ !t@}Tvtvؼ q# vmr:@Bz^ojhػ3r=Z!t@}Y sH"gssB/BW+:^,lSK)???@w г#Nv;zȑ !}%xcɃ{۾ae0O\oˏ\o uNDoyHU~wVW9TI,?!nK9ջuЫwl6?Hc.v4-vo6n{PO dJ@s=:}[퇚8wnUd<,nk28u;mzUmt۞zL}#dOn ?ٸ=}=/}[5ÝiwmkIldnG_ۺm+쵁*>~7w^˚=`}62 ЫFs)Ohքzki_iuj&ۅMoxFi\*^k/xsgێB/޾kj&&Z?_v>xur/﵎kq2-бexj)zM:ѺF|=.@}j8?^3j޼z"x :oo[Fn?9~%X׶gF>_u۾fg{^)t"u5DZõ7Zn8cn~xsXf 5:۸zrE[gÞm ԛ[56lغv-ޓ+Aw, ]N*8)śSꖕ}Wعc*|=8dh%ОzXO(!򾸭Բ,_cX^eܳ=$nԓvLLzSI;q&J^]Rpq; uQe?wBo;DZHzC/޾)L7VGI^?x jSUK쵺"n:~2yؙÚ#ڣBo{Wn1 sN{x*t*b^tBnQSx3Uz9&͜mR }{{L ? ,D[*8mti7پS?B7OIb +h6;gܮ[%n=9v|#~2г k8V{XLocBIOAd}[Դs67rTkBoF2jL%CԱtܸ(6ӕfGZIvFZI.} Im=>A[Qϔܯm[OO |`C:%ZvF5yp>S 8Qn7=̕ KgRe7^lcD_}-k; J|D"=oAo_gv}J몍=O\upMPzO e >"umk쭣@LoߝZ{=ܾYp''Q*L }#j~=uLkyH߂]+Yu\o&ަhNub[} !t} uߡC?z\H6]_CB}\B'eG׬cGnh[K7u"F &5h6K$=k O@!wAZC=!$:p`"?'?ej<_pŎmt{Y @Nχ[0wNӡ\NK誤}򤸴NVFB%;"K[#O$kǎ &z}YҶpnh(9qU; tx+6k äBW.[([Bz}mI!tŲ* w-[[_*?}V'wNd]oF0cBG;A7h8v,֡g?BoOMjձRm#U]I gMC7G 9>o lCxLKxcg84DpA=Y@4טR}_SGu4_H&!zX t9G)κא^dѽ$G|Bo"i;+v-}C'ʦlK>J^#kd5ַL9о>O[TTw|zS=Ow-W77cŷGO/oAJ/hod5K/bgPIVj[YRt_f+"tSi[=&CkVn'Ejw֐Wo;7U/^̺-SHZA~ρZBz_ɴ-'MW|mH tSqغ5H~\zBۧvuEn\*ٽGJqׯnP]zӔ6mek:Yةwp* ;]xUc t!UM ?i4U5G @ cJ @ t tsS畣Q̺gȏFWoAU3TkoA9 } 4g:QX%t: t t t@ 2U3 @_خ9m:Ȑpys]s~E+ԗ57^&)B0TAw((} 4g:F|^Z,%򣥥P:At;Y34uMBRqŲ5 z/Dz5n[BGa@B@B:B:: tp.SwH4g/ }lH4g:Șpsɵ%h@dyMB3YS1:tRз@%wnEo:A4Us"i͡UL)MBRwQׅekι ^aau єy B;A:]w_?qW?y&aN<`y.p )uB+h%_|O=/^Vlo~˶_ۗ>]_?]EطС:y&nCB}FzS^;7ۯ!Ɵo|z#n5}cc:A_%/.+  .?9fo FalǵG0KY=wY :kK{a}.N闏OCB&tu֕4k"s=dt/:S%HFv[&tsKn/O>1o$]EFO!t V(h0he!;JТkP ؼ:+u=fuo InU^_CL_8vM|l&B?{(ɗ5l3BZT7Ӹ=HA gz5lut,>OCt; `] r1zFBBҲc\E}>oy}2|QngfOpԨ̕[ Ȏì30-M8m,DwٍeO7E-wބ24g:LSgstyjAPg0֡dS76zQw{O#~'_g1- Ywkʲo>82c:c\7 ,#yx,.W2B>Ut9G7kav7fCފoL$IqBviPKҋCt*%.ƷnB#^ ïXDntZ0Q tSX\W't3u2+Θ'pxq_}Gddž-8nvb oE;x_"yNxcBx+͹xۓ H=aBbb0~@(>'E$wF>ĝ8"X1]/ 7Jy_fmã#Bxn)2eYi֍0.zUvuBRMojp|΀ gbPNtVn:B{ ? tW>t2JL4E^,K 8gv?.MNg6oR EmA ^ljҘs-XbLx ~V~zq>x0 t1U'6%7&seΖ<Ǫ3:=4OX8.zFx NU:cBpi5q+lXc /C'UmN D( )>:Nݝ%O&:j Idd˿!uV?uLNT[ IDAT>M.veSxcKB5q̪?&L7d[ȿ:k{igN[]yX>aΊzЭX& ](DRw_%9uaXYwb%%,ȶќ@ BBKvMӆH܍[.:I˵&:aIG2w`esUӥbev,U, i _GctIFGM-єBo [K^ \Xэ/tڦ_L謣0Β~ΓwLvc,61zP]D7d[іus:Ȕ'^ݔRLgټj{yҩ18=.:&:~2օioKgw\ս=.-ߕ!|e@v/6q2QBR1!ek Ik:ANnKݨ}VnZBB<CՅ(s< &j֚߆AekBzzey'I C~=ќ˭^&)~aBϭZ+w$g{vk' %(Jڸr\^Ytn! Z?+ t!th},i ua,. uhY] h -/ t0@Δkqt] =/=b~G31-UB~GP=@蠯 =,wyyN'•F!ta7GIcA"qE=9]{H6B!tdttס/-s;zF[!\nF6\Jc}IVզc(N^B:֡tץU)N!t!ʵ9=!toBi4vwc|Nn%(,tw9=!bcz@ B+EQ)@ sҪIcK(pFB^,zm/Q>kg%eI\ƪ(]]fsQ@.K(ݛXB)#t,e}1xPvky4˸[N@B}c ]lRPsBBO5lƊq;eQ}R٠!Hp/=mZR9:ȺУ ǎU:Ȑ3gn\bшB%YZ4^ilF=&W:w ebL}ӛ6RȲeg؈7 @ C]t4vjt*z^zBóp˚|Y89'ͬ Ŗnk( .9@7.tڞ֮%]\s-K!:?ϯn %a{Hf!t BGa&#~{7ЩnDYS s޽mtc1.&ǂ{'wCB:B9(fH=צ!sK+).fY#9\,.CK$;F'v t0._mRqm6[m0Z&%hk%tLZCcIH^FkΊL.Ol Jϳ@B:B;eN}7^:-gjm|=6vqgGimnGTeb@B}VH"nJ n჏Y7A?xV3n؋z*.7~et!O d[h t-]K}N>jWԮWU SIү B7E[h t u{|TTZwϿVGLY8;:kB/t.HG-wP#B*̼?3jFrs#yOB,t4g:EE="t1#i<[ڟށsvnD殍Em:AB8:uzBay4` ,>m[l!^񇯮 t!^~3Y!S4qƝ<3[Lpĝv<.-mId_s.!3+,3Yi}ؘ*Ì6?KVzn5k9YzևFfUXy8i1DV!tBGa>"tz歗1mT||mS.ylKd]VlѸF5&+-.[_@^z_|yGA(b/@B:8W*JvQs-n&b]XZhNy,}M tZus7AB:Bt[gp_F^-VXu[K,A_cº&xN7$/YJqB:BuvSƼjgWGԿZm^jМF<^*vfpU͛#]܍ "k HxB:A?z%ׇj'tMs)Gzs6:rw.{֡]lr1A/)@. t>̔9_u*e~ -;ݯݧ-wX4MwmzpS-P ٌwU{t51+_U ؑl t![ʚ¦:Dg7n-67UξkGoZTmxzǟ*8רkwKaXBt[DsO[S1M᪲x2&Zד` c,N/~fnz|6~uY[e.~ߊy{j%y ]{] ыٵ:b@2s+)tR t};AGzgfBw֚=}\y't_m܅ϲNwNůZTBz,ٰ>1͵)tO B:AHO3B=g tW{md>D[cߥ u:IN9.ԮOY. y lKy:ȾzKB1،lImcLWd_Z|/?F!qgʛ|]boRK0&\O>B Bo81n}=( pB:YPQNg.c3۽#Yb](Mݠ[5k`]ؖ(E =7qL*Ϙq'OgN>3f^  @t.{[T4dO$tV=&]:w0=?K_CtވqkB7 j7a:V g~߉/'' QMateP-:}$rѼ+E˿yY;BUM*e&tVU~{ BLْhBXrQ&?~w'߇u:k0P۩)M~iۨjj.V*Z`-C>OBK{1[BʕC'.Y?tO)B:;BB`* B*fڥo0K܈{zB:H5c]lZ:W{(!ɟ\?d CB '\>6cVYПҼA\erPͫE^P/*onEt=>P(7\p7ȧ/0B:ȔhYcIy)S[`> sSC}7ǫ(֐r(ƾwG%%DT/=+^$CXw!ˌ?;?c>yF/P^X@ P4?ߔu5P^[6.DJxc[)ʍ߸Ukj{7HqBU¸{B tS~~)t&>SN?'C~Sx Ɩt> r{>?h th%, /,{jtb BT>>;Ǵ'YC99hmց&t7F}Þ|/9~,xZJח57^ @ CXu*s=FH댂-}Yi>mILCYc4s:Ȑtס_}BF= p6w9KOLC4 :tl =%Y;_ ֦^}TΟ*`=BGalFfsF+_zV%ت}t'Rz"KʞL:EfҦ_k4B:8f r'o%zz[t 6)6_ݬ`x"%Oo})d'pWq[e2 B1B?}B !tjq ˥JqPO,.\OÏPﳫs #gFVd>"6s8kRXcFk%uΈOنB&B?It@G1wZ=AנN)7uJ'1oWOP{\.Ҥ+(I.GB!'h:A.]&ksi~Oـ%PVSuI.gu5joKη FR:'c!!tNW~)wȺЯ.2)ly݅ۧj6 2u8hH&҈߭~a8>( ڡx[pC@;C*B?ËHgH}ZB$,tYsv9npiޫ7j4兪0nl]Vŕ=-K99rz?q> tp/ cj+epk =`6kNm;὿p??̒AytrMFO/BYٹ"F}EDv\$ ]q۹MB"4VFNnӕɮr\$' mi ]F]@лB5s-C:Ȕ Nn Vy)XKөrzBImgB.Їc:lb^{0XY7~_RWqFҌu.V^/  t,, ݭV)Bwi 5'B_44i'Ϧ(1㕿B"*b^u6]`б@ BU =`S[O6:[mɅ߸֬s;}B%pAA+R]k tD:zH)$5^BXҢhM7!ɮreG,f5wZ!N Y}.A/yR CB@"ot$ڣ еR5^oub)ڬЋJK(`B)F{;D H9x,wyqBח|f=B:pBotB|,I@[y$_:[ azȸ,tyzM!Yo4&%!EYO> ]9NGs7A$ tsKt=ϪX W18z邛r*g?](>7!zZF<8zsK$8eC,{' {̊uF,]ő~#~_I9;:A?z&Bw)t:ͦ%tV%nu:so$r}.4z\d λn_L:+WigPFthlKetvoCekB*M:kbzhНqWt2s,^0tV"u x 'uͺ]0OCCvw\yйޕ{BvnjWqm=>כ!tgփBBX.Q:ppB[m$px<5sَ$#Nݦ{s\NvI>BXB.tEaXͼ~LcMz=>1A-wiYV& ozWH;xϥB_5i>]O&GXQ/L-[aj|>W]Kd8/_>̀S'}Gr~ZB}Kq1&BIe۴+i'o^k΄~ӭAW~B ]jV/UC/'#XOo? M*GkGC=Gս. خ-Ui7=2=Jc1ٗͻ㯪ud葀!ԗarBOvEO= ?G. t!vM06,wY=7Z\1nxcqdBޛ. gQy:#&+ؾ_˄{n-GS:KzB_AOnI[CzY}yB:} :ȐL֡U-Emvu2K!D!1wp?QϑzC1={ $]hU!t{%}`}z?ߋBuBg]>IZ̨ʑg ݯUS{es=0< _g`GA^$V}bvNW<96!z E,.*Yȋ+عzO|"Y= B':Azn4)S[u&5Zk$Oznj]$8v',;M7uq~ Aj4֞ .Ѝ~5Ect?"~Ðl<}F; tpօ.+Q4f%C\g|x]qmUޢ.}nyOإLX& \A?^:B:8B匱x<ר\Όg37E[T~3ŕ=Dx)F/"״Śdչ$t?D\0,#Atζc UE[hYB5_cS e7RGRy6Ϸ}Jcz0#ч0Ǎ TIIu[0y^*OvYoSABg[R <:mU6^Ƥ-  ?x֜![YEJmKk7'Mԃ%/#U`͞Tu6 j$s3?Di̢!:A;<)22ovމAժt[ ЋN2O=`Jo wAwuҲFK)S ]X*&_nk"j=ˮxX>E)J9K̮ #.霻O9-WəGMWyDU~=yl2~#BU"{ͣ:Bg̹F *?={B3:t;IԻcw8#p~J$t/He|T?( IDATӜy>1FusyWz]B<9yc /Κ'՘;jmB zhxnՅ#_vl-Y$NYxnD*\e })h,KB[&E7^6_~wK; ߟ EE:8Ks$:M9 }ҵ cvgM mVn+,ib͸eOhDuX?W!t.l;EMG*= t1%cg9\Z螀By(]RX4 }eO |yPׅvҹ\.?#+vfFZ5P;n5 ?zšf,J .YpBBWg&t}93'c:Ȑt*2֕ ^e {qCs+x 2.uSB~nErm|Xg}"5snۅBra)֪ԭ:|t6z}BLuB#t^1H6Mya5NBMk%g6q-fmFҿ?y]OOPIW +68;?-f_D\Hkz?'S{ON,A)AB}Dn:.ek]n|`{Rov[?ye|=c_nxɺBmR =l1]i<+>C #tBTMs2'g跟'zIt@oJUp)XzԻEp3,gȓ^{/ %_g7H-o?v# 7`ZU][5"tӒ*B;Yq |QB:B競+SYI^S]ܯXV)|ʟQKֻe4U]Bo_/sxkjl}BלfN7W?Ʉ% tN}HJINkq ӓGWsD=ٕ cg,zѠT-W6N2"ta>BOMe5'g)و?<_oL*tE"lmlhdQ{ i_Q§wˇ*1\MfEC=F9rteCiZEM6nY7lđEC?2}1 tpsUJ^˴kj ݮ>Ku͔]˨ǼGg^U9ov+w4jBЏn_X׶qCJ/uda>*/NrWA;̄>v53 t!Eӣ|SB)X"M%t-n__K ?y=%c1[Kh>v({iƜ[ab\ylĥK|O&_ sJnnHs }ᆽ{׾(};^wȰ _ȯ0(BG-w _ f5招^( o|A/u|co,,4^Y8ycg6 vY߂A%N֓-EB/ɿ-+ w&H^h9hUk'N!A?@BgJ#6#ʘ=阻j,X8fAg/R$lq8~͓e#ޛD PXtiN Yϵ #*wl4>C9C>J@ ;6%M2qEktYQ\S4 n`׳&o"@C::QjN18C*+,s#lwk.nZY!t6s:yи`X=>s S_O9}e׶mtu[eFn֍GNKsyޡ_}׃zEX tm|󵴄5I*~%_~vYZV_^^9{l{p0Otռ]K贳%xl~`%ȷ`_yaa[;٦/fFou1ylxmd% :,ei OOV{WD鈻ߜЍ~NKL#DMf`m[Y~+Oy+`K=1nINɂx~^KAZqF l$tBagx{va#taݻvD}?[۵S:>YڵVڵ; 㟾6:A_:/; ][EB~p2p/}TKX ɬڐ ců9]Cs/za;ݥN9v~16L0yu_s~rC9wryþ> w>嗠|řA t#t2`ח;юЗIu͠?S=߸ ։9$BW; ݘk6ǏCCz^N+s2Vӱ^bW+ mG\8otNNn., ܿ ә-;)g]|ι;3UCBCuZ|N*^Ԥaby,3w;+>b'e;{}_YTl[4 s/B/*2eǮE9aNme\ܽxNП}w# rr֯X )$D )W_FOy|P68,w@ sΜs)ujOUCP-t]τ>dn{;ѹo$VlF-i:/»}g͛ w_}EJ~H(t9"{vڵ{Vvt4>Ͱ(ZnR!t1'FNN!5> }˄+=а|Y:iFJ gVXF_6~̉m:ȐL ѴNUL܉QB:F2]VDϿ;z抵b6ί56cijgcU7k'Tz # NkӛlsG3UnPXXXNiQ ׵Poca{εC2->ϚϬkԱJ6dLڵIzjnVVSnM*tt{_blHqDlG_Va wֶl:JuA B_h܊ [ 6n{kީubi]BYϭ}SƝmm9qOCqkE> z={ΆGkvBB@蛒 Ҙ{I[ĥsW>Fx+]XR]QKx֜ℭMWߟsn}:- !_$Mq'=ͿYg8]& iw#,drWpBO/3USJ;k-:Eˣq#V8g+ʼn]hX3ca표t`X!0?B?}23$}=:i{j7>т& pAY#\uzkYF[31ПCBW9='cm9_3*gBo74%z.&?$ig!1H*e5@Wg$t}U\ekB =uϢ} e*t~[=M[u'B_WqzP XI=vz%#qc.=QmZ\ʚ9i)8Nb!v =Q^tdm£,>vtB'_XQ 4 IIl胾ȶV{V{P=!8Zoy]3?1".-dr. GXSz޹-CK.~%i/K?9\.6ֱs7Ӷj }yoDE&wdZ6bB: BD'N Bׇk z𯞾>NFZK.W6~-tj#~5NCq}AI;6=P# HeNJٷh=F7~jѣ;m߰{l=dQ39BO<O Bg,t4g:S\4r|UBr|MB~םܓw Ni29_w~LWCUo\"^k<&N+pxt c5R'H*)Rzؑ,oAji* $#LG苦14>j*}r.S|WYrMYyg ^}־)q'{̢%CX@[.1}`X xB7556oA M;=1%]țq؝NWL3B,NSD(P֢! Z1Z(t[0^=g~XlwgM{%t.HG-wPн!t+M~ 7O&}l6ڱf8M$toнs]w3Uŋf|DfQ6Ո;d맯tz4}:MZ96O'ʐ~^Tjϼf|zBR_R>nkBweЇ<ּ;8̣Yt:+^"cr :g#8(&˷BkLpХv+]VA΅9#/SoЌ"u-wAw(=nk# }W%o*/ l\1.fnbgZN~;M{jvk='tb}8&~.De2ə|J74W5 t!v]8al:Yd0Wt ߍԘNn |q! FWwVht%t$VE~zQ{[МB`\S *6lHvк/~hHjtO2ӓBgP^!jMzzLc՚ʃCB\t0ڭY\cB׊Нb1:N+=04ٍX$ ѩ}aT LjBϓ;2};V Em6K".jl[Hx IZI|?᧏?ۏEO0B:ȔƵQIBg+.)b гu%ep4=jMwB;M A鈥(#'5#Hz\Szfq{LC\Gۿ^Z9dU\,Gs5LJ7 /(-k|ŷM }8/ۄc$G,|U!eXq}cB?#p_}[Btr:rϿoV1yY%O\Gƻə=/Э ;nf G*fD;TBhq s0B[6zMh t-kīebDB]zf^9{f8M5k^I-tì.^'.քGtg4/ ݣxV;C2}%}*Aw7ו4Яϳ1w[.*B:^o_sCnYw\[4N%Ō]Qގ>fTF6Ac1>PUBC!S,%u͍s:ȐFvM:2B6JM\RwE-&OeިKGz7+  IDAT=t}KgIB5ޚ6Jr'VX1u}Ѭa7]iiDZ7l]8@WjL4>} ՗zNY|l92$u\NCIWB׹(M^ȯ.=y n/UJ/7|ܩL>7ׂemYVnxafɋqCÇot/Y.,QE25JW)t?Xԝ!? B:t y5:/?KI%t!fB_PHCfo=G9d-z{0*iPQQ3Bqåt6Mp)m"/EQ邷f,BTZL+ĬYZF-gR-}t2:1лMۂj}놨G@6ÍL`SYt WQY@6|\cN@p ]7pz-A?mډWWyynƊ|@=T8yN 5}G|0w_Rqlj:t@7_]7:o#<ͣX_jW]C jcz s؈5h7[_>Y◗8F@?-D@/r@Ŷ,CF@9ROI4tw-$m풐jB?K'~] qikA8ϊթԮkw_io^|dr ?/[#sry-h+AʀtIja:j*N tw[nßfMp4Y5-t -n<;̒"ܿrs?wد??~1xax*zr~iyBWc#e@7x\'w' j3wT}lNg21 Y@t{_;E ]uqjڭסekb_8m3M>!`0o% ϭAz"MOcYt ǿ_2o'' 羸wAm']-C'%ڨKOmvTѹ=%OFI%?ٽ~1|7c;V_&&j8AzvZ2>oCO6L;6r狟/zEj@tP}3ZC.N. wԿj;l=)}a>^R `@Ƒz+ITȠ i w ` 4?굕l~"߯3l @5qy.f3x-te!(m~Wr|ʽ'Ͽx+}JrbЃ"Ѝ @GoMel/?h|{45e@tЍz$[]\`Mx6mxʀo<ʾ3{E=h~#ݟh\5Bʨ+ }tJ5鏞'Ix/:@7Kk ƕ-YE+c=Vt`~vWa `['=؜Rj M4INLmܫ;0gyL59o@A7vvDcN-\Vib^cc',![s_?3xd2=מ & vŏ<Պy U `R{Y ]1p6nf+0@A84 ȬqxMcO:uff}I0_ف ?pꠧFUϯ[K2`u.|u~vo( ~=hۗxoujIe?zY51,sfd8| .@ݔ@|]Ώ̳QJnj,tl:}̟sj;=tR.ֿtR܆_\0m<e[=Wg\d GE*_WAtrA=O7t:G4!\M,z?Rq*Ό46Lg B_y##$aCDv?[2Hql}{' @_}~ÂɈm J"]BxĊ]A//u8Q9pN@|:qp@O΄4n3td} Nj[ g)cu[xHNJ[Wv\sp3Ӭ&݉6rY\87'FA~b^l.Μ]%HgI: gAtlFae_a aY n㰷ysթ.F>Gd{/Pnt@ݜ[sD\SioNt-;|MAҒ<zsśq=pW|!ЫŒ:S t/X~Cp@tPz/{dW@1HDvzֲ[cYyW[i!8rWrk(T,-E) ;i* 6:h>eS}\8Q :MZmrm&=e,k%봞[_S'%^7qVz|{b>NtФ}swA[l,f\hI+)8FN,%YZ\' k Ѓ0%G\^ 2Tp4~كw::ˀI?m'iSDx=OTR*Z:£9SB7Z/N tY1Rx6?X :薷ЇI!81K!Jd+WߎtW_ݼ^{*|X0t͛_^7ub_|{gyȘK :nm }8)6z49\qz&yk%D>ymA+:tCZU۸QUut@z\wmݭR'_ = miiuw_7<-SY-9rzƐGM@At V} = p7,ڮ>^t3A#͞}(Wgp~\a8 ^ybѢ{[C.Us{֬z3d.US+Q$fUm[5.*6бݎkדgC/w42e惤+!zV%HO.b7V\]]9${{J";Z2]5 C@, :h<8Z`fs #D'mj-)2rjӳ t c϶ q͞է_*5ڷ;U8_wI :D6ozto Y™eqH$JU48p6rV =^&3m=>cpp_=v5GOGg6Ѝn@M:w=7Tu0DLYLDHGciZjVPg.YJ?U'[]Fo*b#ZΒntd/[:t4@i vC̒"=z$Ώ/U̴y ozvWWrنF @]Cc4@̹t=k![ tb .&3-O ^0-t&Ip :n},BedkF3Jnu+zN=ֱ%@wh7 eL~{LVF  :6zZר>1t]U{G/ J["@e q:JgEϓ6:ج :-]-0.jSMPi9OR:o[Gs=+…3LVEjAttǘCglk:]MP5:׳I yZteWCԂqwq`M(t@ݴcs[垴4[%bgeMe{µ{"MtseQ|5I<. ׍@qGzeKϓ@;A|(Ԧ89Z^FrIsd5NTfT.dsRsi4% ;)) bo_+(OGQK8O,rRȆDI =3D蠶9ef=ImӲ5Hge-2m.OT7N@ŹC:k |w#{@t})p@tx5sLҕ B!dqodm Gol]gV>B|ŹN`0toRqw:::&yk;Zi ݕhHczVp_Iv/%qO'!_| :&=<7{[õh֚tnܨ3lڰNWAt]Ѕ8[:o7(ы Ɲ:4:hҁo}]p@tP闏?i?eና2TPvbH 2Ej*K^i t'5U t5-/O@M:Чyp7(ԦZC7rMsK)j&K%#=3%:C1^SF7-[ ڰt=+bOYb7nz:zAt$ˇ6bStyZ:9Վ j%A)|u*rpNp>3@WE@/> :ՁTfD#K' ~s U;Ec1+)ʁN9sOȤy@-'œUi6=@A쎷0e@O:ninhlW"wf7>%3ё囡[6|9b2Zi,2MM/P WMua <\2 :w~j(KhgL PD 1K4:%OF!4՜vk:&ppWCj=pɀ :fԓ}O4$7Neq`;8?nQ- \sn{03Wf@S Rh NZeZչM6~ߗ?~NjSs즍e\*丕/Abz>3n4& r6΍8F1V2dX:k5{H{~.d@ kD蠶R/ [2xd+HץqFAr1,gˀezWrLI,^荰ǝo7ʽlCW4@~j45l2#ݩȼlhL**Tc -̙Q,n-e5ztר,=Of{ nR2>u=Zѫeu[IRfȈ* ,Qj O}Iqq({/Ð7;g\,=M2ː|T4;t+/] )ƿʈNc3~\9:IVSԱ=t;˵GM^w! 63]鍫 CS -\JFt҇-l`%)t^œ[6)`zƌRn$I 㚵f'wv.dr'Uuӧ>OP-QN" щ p ʋcŎTĞ=jh#_rB-x0D)  hu :hR>f8e%8P6b jZ6rkF@7)W8WָI@A:Чh+o T͔\vU',Ō>w7gb{; Z؀V=ݑNEI{v͝}.@@(\иqۜ.IGq87_"ERFUiMk#'yCLT>=,&E.V\. :>}gѫ!˴\˓.oBũ4x+z@ E=jƳѻ~[jAtm]ֹ*#:vw x>)bOzbEN`^zhk5. f{ܣ =tGv<  nO߻#δZ;Oil!\cwTa؍i)awК'@7a|Utq]75{?{oj4,6nFy &0&ZO1 1%] K ;9&($xv#ۺZM:wy@|n_ʜp@tP3Oo[VnaE^ȉ3UJGwdEjz*ZmaMmN>MM:ujXP q`] l2=%^Gk"aut$ tԩ>3a[B[@:çjq];h#Sxb/ gAYSC'd)eaVv9wNeXZz%z\G:s8kh%+W hS>l&Z/Ͻgeqnf欑@:MtT~MZɕYVx9;c;3lsFtJe:z=LW]?}r!\& ҷ/~9Y9ܳk:+Ԧ'~Ч}2˪d6*e]lF•sW@U׭@'D@PR H=}[ΥO41ꜾNz_C}ʜE_9U[ek-g׻:ph IDAT;.g[)NEHv fu \ؚyl5@-|#+|];;2G~G@S-y Jl @]G%/}#s[ 7U&ګ/lrO2/5g O1gOS{6?*wl޳UOy{j8Sg@&Szq(4Aӿ]a]hE[Mj$O>8Z_ܳ^ӻ ˾wP_)?~AC=$e3}O3*M-ӣ}лf6߅嗃??fL=o~ԇ.4z<tt}9ƈ_5z/%tt+zk:t-2vGyg=EAE~/Y&Ԧ={&DAE3g~`q  %N @ݑ`pn-Xg/ @=s& $c=hܛ@tP}3f@t}s3f@:ЧA X@A :@ :@ :@ :?a8 tsw@Amkף>pnzGM :}Ml)pWAQ@AK@A :3q=4{Cn\`桃vO rnE_~+poAm A P @A @A @A @A :E\MfAtФ'Xt.7S DG޵9b&Զ:9xjGTh5޳~AQxi>}7[ӛ,_g@ov5pAtu_:rp?ӧ'J}rfۧhwі`C-'=wdG[O_:8D޽S :h24kb7Iwb#;5zc.GT NYV 6՘ UW!U@f21z~r{A ?7{j}Pܖ@tPz^>:scЪ+Mc2CDQ7j!Еlw+e1 sj-OF/-xX_@o_^}>Y,) HA/,cy':k`k^tDƪh}Q rD\ߠ H?\&Q;>c\([A7?;Kd>'z5ò6czE{TjApV h VJ'~`P@w<}/AtЍwʚ-JZa,sgYk*t'CQ[@>Y䕜hf;ߕ-JMfuttȪ"[lf;DW|ռn5v(mD7h@ףzd+f{ [Adtԩ n,g_ǃ62ݭN8M[Qh:@C\K-bgv.y݇@ݼjm8ώ)I!:vV TY&,QD[j.\LXjn3"б!z汅<+qt5kч1U\@,QuDD}TQێr DcT~)@!v߿|+@?ٱ [WocFcKr̂kv.LTŸiE5ɮޝ=O>qg- ϵlEו䞉-;Hk @'fx;iR˦YuTO0<ք =!3aB^@PQ䝸6G%Q@C5ѹST[Qobm44zϼcgcFMb\YJ@ǵG/p-?-%H*5|˨Qg-l_hukx<@e4,۠}e&, Q!*%1q qtBWyAJY QGX!to$|xʪ#o)5BTh pWu/hۆgjI\w#AdK6ys@8pdg&DEl'O|h*t4QeERBu<{}߄8h@;n\stTe4t]a{%+{jYײ:="+_ K@aZ|Ng>"e#W$G*xTBٞT)YT͕WS 珽^$jMq-$/' ⁎}x9`GMnpj+U~21 UZڃV9#ju_ǫKo/)kدN_?-0 a&$K4@:zؙU B+Gi="ɨ'eUv4=طzmG@<>Q8mOiz-xM9ge5רz OlvC%&:Z|YD~Msd +V/^3ҟ8JŤˀ]I;U%a.RxTg9͖_!{x .}]s7~N+ f'ÅP.I%=AoxCٮAPsc QK.?&;N܈B)QKCi :!EgpnUݗjrq҉zEYiO}r& Vmrz?Xd 3Eset:.%%^k&&z [R8 U%!<]ehXN6|.dzoTc%.P24 zD . `yQ :Ltb}+wJәnϑQU5|rrvS+pyBAuE(@r' #맑-7>R, VΞx:@Miȅ[\hpDP0a:~CS{6o%^sBUJrNCCVCSlÕX5ώ@OLIPusb/FM\"e$y!:⒃ B/L YD/  k쇟Ҭ:!YoK/?~:"!YaEYI(I"VVϦ8;Yp?Yϵ|(#\vxI,UㆫkpoY]TCbq5+^űzDu 1X᧶zS˰D="xtR,pDi[\M',c3\{L+iÓx m[eQㇳ t!WnD)t!Lo[12[q&d4p7ǑE8:Col2T*V 賖&Ao eATmn#㬸h7JNR4/;+%T^C+\7nFHi &y~Lm~Rq$oIt껵RQXFԭ۠eܸN'ǩC; tNjRYx`8iEש.4+?T1kzT@tku 0߹4"T% &<"W$9?_i^gUӝD 6WO6At,!6u^ 0JZ+@g<&8$g2I-V& Še+:7`Nj]|CRXUp*BT)ѽH:>U-9@w 8M0x=8ܘBkaw|Ŏ>R&3ڻ\2|kfA7zXL=peuyRW!ЕF@"Z X©[CygES1-MN%(+MQUP%ZEZzbӥ㍁nWf"Uu+b HTVų)TezϽ kvbvJj-~,3 ܗ#C":-Z:5t?$7᪸p0٨^&2@J5g8ϿpǏ~'?߶ $z:d%@M Zzr{ մS9i!\AtФ}ї?x8QD/"\vV)ʙQ->e tm kЌpP628L".~!^#Yhd,>=x ПYۼm$#_/osfK.phA+Jx> t! kdϳO|)if̴Sҝw2\Qg&iII\lMoX&$2T)QkY%ޘvIaerM6Qcp:O,%o*8PPJGutJASN]Ӆ|#TD{=t*DOD zʦ ;֞^VyMyTkIӱfh{;w>!^1w`{%{?"U+K)C#q> +"5~PiwvY c #r?F\h.*$&3gvu҉V }U2AQUx^;Y:]'@"x.c*o ܳņ2P~-Br?b v\d$BcZ>޾8]gӪH`f%1ƴZA|% a$c=7CzAtCgqT[i3Q%ơ%"Mv.^Qi21")!za6rT-!EIREgp˝|S:SꐓIW@F2.NAV]|xZlHMNش 2s{E#8 -.>@'Kec^AГR 7@Я1LW,YTI^ɀֲiR0رF>N0=d&󖀎oqA45ޭVɖ踅cD ek5W1|R;ȠkoJ+rNJ zݻUXc|]^B pKVa0/9Bw IDATe 6 NXXL*M97N:~],/ZBUBŅrT) -#4C t뇝t6Gѐ /͐~.TĪU-{S-U%kMϵzEiw` G9pwI#UiYd9xqrZ]$&N5}0JҨ S n9 ɍP$lN`-*B8{۝eTݪA%@ǣHvBc9ZqUD˺*Ζ-yoSOt6a5:nJScc`_%\v|T]D)n֑veD;FJMӷE[~;Jq9G 8ĺI)?Ů!.#H Hc#q5ȺuBAtFON0uJpiP@.74RyCJ&U1va2;?nh:!d4)bm T6g7 @(HO;3gμV3כo}K[d|_1{P2|YgMʔAQAu SYN\0"0+Kfئ֞>К=D5Q@gBW[zh7ϊr1wHFOsrMr'~:J2FdTϾnOCy IKb77 mYUvURg2Z.FxAtLDz1a@OBG 3O1kkIL*ƍꥴIw,]"HhtC&1tltn H rnJ=(0vܙTɳ,~twߢz6_IR'oM|MזBOh t ]t|G/&,iz"1 O!;4Kq=mS$JfӮŷhؓktGz`NL%ŧD8Y#̪U:ﯟ>A@9d^?p˜= P*zPa8K]>mk1x~鲶/UG*lc V4r29НƝx!6AؘΆm-$%mIo;axjRIA~]YRjβ#G t~nRb4:$L^QA|,fuYAO6zfԖ|m93fg–-M<~=q?UQL@M2ЧYϽ'Jѐ横i0dƌ0mM: T'MNG#6QtWt#ָqD&v'ONMEMymyYQR ԡV>mXza]Q}Ų֊n;M锿Ō6o]RrП L @s׬o@:F@]tve%LR!:߂Gs%9c^-X)XPst@{@On;5YI)е.wXFAtԦuO9x^%]0|p[x-~-65e)G h΋ D[2aWJ< ]*cN@j׀*ݤ ;HLd+xЪB*5F:eu[es?bWKg8W74S\90qsWӂ]l ׮?pRo{,ކwLHiа烖jIRzKTVZ2^?eWopG-ƍ*&gQtʩNC8>mW!*u= k]U)TtEdxǽ]Йik3ˀ^:C`|$xʖa6 - ?F@Ǘ7$H zP RZ'_TF͚tIo}Gf}[AtUaiWQ%'nWh*TLaW}N%TYUz4o?Wg` ב/Aɀ^քR{ Amd!82džY=2voӔ)5b+,tĖ-"e3q!?IA˧eUΑ\ Ƥ}yY֬ MtUzSPkP`ec3rf]B\̒~ Jm-1rJO C4{mi#8Ӄr$KϢz,vVKWI[Ѓ/z8ga)B_v͡4m311;˯pd]@.Hk㩬֔t&zL 8 U U,v1ljq>ҝ#VRE$=7 r~˙q_+Q`0c_l7S)?^ QJ!uvQER #QAO:v3C g l"٭g8bu<i_qcڹ\z oURu_f??4@֞z\4ft?79>YT)N2,eCi%e'tff$K*jT D4AU'н5Z- Lct5-L! t3Bܹ0Pi'F99̷;JVKnUk!V;Ž>RQ#rJtjQҷq4 FFs.^jpp``߾>%i?OQ!>3x3#Teꚹ|@([Q1SaNoK'+GN 3/-yG?c>/~X4ja: +ɸNҭ:rϫ/?V[* K=5*SNq fvb^?CQgш8@7.)#$E h&Y@t_pZWgPJ7WWp=9sϾ}ӯoyaǎWm48Q˽^MD)ϏdWw74F;W[f/;,Q0ME R¨9Y@Y @^w7@BBBl.#.ݾjS+/_Kؽif?;y[N>8dM}5+臨vK7|)N)I'C%3HlJҥ 7jٞUA֓藂\>ONO?&0.ɾ"JkV~}@HO tEk@/Y}>[,%]M9@w@G[SVL>pуoso$]0~HH4b9n](d`9:޵wA;Yr2{nFco{f0~T/A!g b m!~0^RFW{*mA7 !8Q-=@Z*m/G=q`ZsG8z{%ݬu*[P PUgMy@'2i?pD l 57b:ltФ}=_|c/‰j U) Xl9/YcwUR U:* Q;eHq#FWRW>e˾nM7gީf!&[Rؚ'*p^ Ş-4Ѓo85N Ϡuxw-^®(16:P|%1Haq |ynԡtU~. *mS}u߿ݓyȝ׬~bu{LPېf8D'es= Zݿm98N骕,E'?h5پ;vFV{ >ňx\R׆LV;j=с|%ޜL!>܍ L)HСL@Pe@Suw]ڕ:=&vKtxќUuP(嫮͙Vk+Vk3'epih^<|d )Pk ab{mSX tF2*@e.RIvd<߭֬1 k+VYpPΡ=GZէ=}HۑQ=V&(=ލ5s;74ЁyOEI}#jF=K)kJJbj>>nj|ȧKQfMIw`!jbƅ>Q]!˫ڴfKw-!-Б\xxܷ}iƱ3,YyŋJQ,fd`p٫W>|S?,}|/=ٕ\\= wسv۱uϼqlϗ.( H5|e⽞tVҧ.l4#Z{Yb=' hɨsӪ9**'=dPʑz+a۬leR VvZ2JI;ՒQ tp/qtr4홉z~+־wyKo֪-ڑH1cd$bbso냟yda%.j%PdCo Qcg)+Ё|+tH5# OC3N&! QbUT'-j=Ymt5Zs[}]m=WtQz l.W:wz> }+@*Y:h~z/']tZ,SaKSLeQ#PC@淶l<..UUT6 9%O[pI2*ߘ{(Is:Q2Ϧ^ppJ>ՑtfޛMv=w<л={Kxաm>neڵoB1{~?zM9c^u:tO_0{C]p@nk.n9༳g'[޾x.=t$-غ,bRYXdDZ1ҥTêiDA 4pfK%mKUģ%هS *}ZY}z'6ٛzC"іirV_4@,PukyZ3JqY9NfjJBN=V2/|xksFUɣnnaNmBk?8 ;.Α!'ɾ@V#Pё?\K{K1C<_Dv.Q^3#]nx| R',.NM:/St=^.Sƻtj0L&b㛮ANxކsp:p@IcnjhSqH5Eo|6ЃQYF{ Kl!39mh. !cв^1jܧ~̗@֒=< -%z ;;N, y8j \3HOQnA-.p`֩'5GL*6/oБ.LHXfWc ۻ~ցˢ%RO&66Gj,$M0ma;^ ٩8l !߄W>\V-ji;Sht8.78M_=c}B:%ȀnkpCdU`jF =oYIq/~^ia8 :zr2*-$@G MJ.&F61X5i _2Z: @M>>ν}O_;Z":7]qJ4j:5vЉGjz?{8$D5A'h`mb t*;SX 豾1XWNftǙ z@HI}>5B}݀C//ػ~A@m!!S[tEt\YA77вtHϐKT7phSX\Q\tO_~Ϧg`ןz?%Ч~~spa8םƕl tb{eQ H(l^;M/i`[ϝ+^589zt>9Gʮ@]B~@G;Ū[#<̓#EjBhI@+B5"zY |gԕlၮ@W0rP%5䀎HAi*^mdR6btФy^8:tQ#8i{H#ٱ|)-6TqPj@'&GS 4)YT tɗJyx}C罹}Yl1qm@܄# auy\~!z]wj^s; SD'mǝ^ˑwб\.,RFv}\s駢BրY*Ak'?xݛ"cTҧ Чuv/^VjI tIfZrg:TܾoMǻ/&QBtNne.Xڷsl񮮰ִΎyƖ%swsI:2cVj5~מNTgGg6> ~Bm Fmu?٭ܳ,t6hX*=;*N@ȮW&2r9:9t&%sVFk4. ~m≯6lj]Za~{k/ÉaBH !L4M)cp-gu=*&0\]GZ{sb1S?bI<ˠA19i`ߔ^xv? ;De}G⛽:)q8!N r'Ϋn5aZaʲp{=-2־UktE:hG]Y DkBz@9n)L?Ϯl/rKg^bk~Ö9糦CPY/ƎOHZ4zL!y:Q@/^r{\~Im6IqBU+}ǻJtj>3ù5r$OԉUS9k @'*>9=[o#_ 1N;a1zIq[z -Gz \!YBD'H,%;}X{l۟TGV"8Ǖvn"40<PU,>ß||DU#{G_ ?s J= wt[VY]w`XTNx:z̗/N?GK%5MKDɹym5JZFwj*V$) dr4ӀmOe"1bAu)* ^\ xAP~qjYhOZ5?w~~; ѐwW[u'x]/OS M&$oČ(1d<@D5|.ޱ(4T[#=;U $-eL)YtA3bUV UxObbC\TV#U)G*[3':@]Q)\qG\NQ$^ONdЅ r)a9XedB:U㉷iѿ=2@*윇 m!a2\qG@I^Sǧ!덎tV_%86:rGg{x,k)З]&x VGYz?u}8fXЏdM޲+SrԫFhYTٴJ_>ٔJo^ޮPΥM3zHttTW].W \4+E\}d2(yaxYf1xgsoaqH~M)@W [N"X> n3HL@7j#3ZNic Ăx;&Hc]r_v!,8#+Z9z4袢8e`kb@ʢo, zv; UJr˗m*>~d=DoӕZ}!qqɖ@z>Yndƀ4PY7.twrram{b 8f%6%kuKc&E,MѾeol{1(ac&o1k]#k΀<^lYDNB At@L1aifZ6e8hp6!YA^KՁk+FYrqD?>d1r`VeؔƬe^rQOI-kn{X92kWInnNNR)ꗕ)| )_٥K'N|=#~D۹*Уh7Tw{:~zK=;j 7VoY'OlڻwocǶnzX?ޯz򷿭R<>vi~#)*,j s[Es3{Qe`[f*Jڳ=l,l]{#z)ٮR#@ojw 46Hūp{j'6q(Qm^Gk"/ُC6@E Cq6@Mʼa G} k#|rZ U6* )=NMt-^S˚:33;P5RY 5[ˮɃ]J/(]IPxMgBY+#r(bjk|Ok@D:ʺlԼ9y810A*쇍,Z52F%Φб~ta}翄8%Hc.%b@V3a2 :` OO$:%Uq8 A\tGk?a<6kߵ trH T%滷V,A>)y''YXg?jmPj(fL}ۧrpϮ:E1bTϢY촙 Clwܐ:^ } uL3K-Qp-U]&"/th3YLL{Rzr+4بҐcnKڴ9M p\(? ^MT֮92 j;݌&G,c& ѧOcC*nH<ʬs8iAbGky;?!%."(ht4ȝmi/:\[*tFSqN_$*0aM=E&wDY?%_I[Ub> Jqw:3J6ma>Wp:w>,w#HЃbSE%_4QY4i!N<9uHߺ> *G.VSZBټiahaqB8vBZJ5X >8n{y/4cڂ Q$%x Ư%o.ICsm03k ,V[&x@ȑ:o5з]= =4:::hѝ >r={!|J?|gz#4ym~8'wPy@~Cls/?lKa=I(g+wVTnڻ5$'r*Q N!CBP*N:Hi4Qᅿ$1֎z3IKpF|UqBg.q"?-('K }~旒@o73BQc&%T  E^W20)իT9ZUB?2%N`cRguq(ow .=ߜ9󨻻`(Z}#-'jPhH *Q6݃/NA@}L& eOldoImk2A6q M0=6_Fz΅KJJ*VcK'?wlwtw` %?$~XRR舧vC[diUAeP%:p)'`/-D]K ڛQu5I5D](dnځ\ 9l-q37C/iO$+*V\Sb&ЏoḸNC9X`X z@农 /*]1uf(6njtrEt&HX  ^cALo!a ԯ0+k=ta;CX! tJ'dfFd@&mD1K4Xrҋ$X @*o\ $K!yJ΍2 =Wm^"sKM7_ ǕO$j w+ws K{d͘4hrN 3j 3t𧮅t`Vp*z|jHV΂&&ᒁAO=jIC@N6|},4DQ]b HvˆL]:Xz@wV8UT5HAL+dKT%Oc*'QcL@/hshF^`=].]= 50+k3Q0WXhPRվ;M*q=#cO@S%sBE=vXl4|&\wT0\)!;\QGIܯ$ʹ9P:'05H/nl號,̎}EL˸>t;T^Dtga NX6xUha j5u Sr;كCH>[߻@MٲM>)nð9 #ՕndS?]#}N;=y 3aU1=k2/U}NHZ3OZX'ft.Fyz)nsT8hzJIʂh:@rI`7nb螞7C}k+<@.Ј 4^;2Hr|HL\^ \ϵѮz*1;ݑTN(zsayn-rr7 iR5и@%)B·q*X'_\IMXo2r+A| W7x 7Rgn=9i6;5'm0T[, A i[KG1*8QFoӚ`G~||Yː6o9 {%o&U{]ٞ{uŋmKʿb耬ŋK]Co93:d/Nb+xۊk6(<<{sz_czZ +_d{n X{>K¼~MЄ>b Qbd O׍n2wW:_zn2e׼۟^ms(־=xn>p9`nj7ZgyT}yO wS/}k'|WtSL= KΟf>K)d^w9Z}$Lw y" -u&WPǪhM )`SsbçY^ߨ:#Y*pV{/줁e-/rr1wbau[VrZޥx7L[v`5F/ȺuJ0$fGϡ}q*z`mNѕ}Ln9ٹtsߵߣOۦ+RَG: G|VM@:\yPx'|1aGRur@%:ymZ2Y7%V U:U~=S<ce 0Af=X;xɮ:ςenv~j(\-~_ᇮS" =v,9FMWi,V[2\& ngGD*"l:bq&RG_4۰x; gq8(*:0` HEE%'>[tc:gAe9Gڑ'씎4Zfz\=R|pRh}';hh[;Щu?w#W޿?M}<z5gO8^DߢytoMU=AuK/^ Q6Mf{xA٧_ >us7t"6gku-FfNY_ }`+.5%_qtsnjԲQ0rrR R~,%c@~#:1|+ӮJMtbnBHNz3AyӓDx{] ;N?ۡ546 My(Z K6x |Ʌ t xmvddP=o*І8@6Seq@tK8/nZ4aDU#*(8eK L%N'Yb!MoЏ}@#6~^K9KOzx񣲾i1A(y1:+i _;bGgv3{N(8GKoBYu&9Wԛ j(֧uRs2zm}W1Q7@<gfVL(agn 3llE^Ut_ Aa:2It8\t.M tQF&W$_*iB-ȩGn"qnju|;zz`fB^R7i`Y=Z]<6K%|fYT,*:yI>쮕 Σi&g1i@Fl4D;`E彎D4A#eR(4o Я^;vɮvyiǎʺG<$n@}jv-<@v'+G@l„V ~ x C ædhnOwnb yTn,pa٣+Os@ vN/ OQXf  #w?w{ ׯX1:ydRJ*8УV TkDXЈb&:;n3l4ľwml:ʭy*fɣUr-+v"l zԝ/PЃǃLX}[fDŽڇ91#%Mt`cO܁B +WM,qM,\]P`[?b)JyԔOρ\CYC?( H&Y ް]6v-3@o/8]]M4гzOD`jU*Oe L/ ԓo?G}:ׯvc"c>,hb0]8Kv2j"UֈJ l,z>.4 X[H:>%t3yCߗgqY3,g:d8萨3=Tj88y Э:d]-:#;){0P褮\6 'HX8#LYr90}z tؼm@_wT)sBBCLlG[o=̢G~Bh@~tG[ӧ!)ߛ_^<%dz1 *p}]G*\!7#@ W2EH@@ $#=^#5Et~jqt7*Kυܑ%;dQk%،3 "oEoz ]ώd@ǮGXrfD܅ tB[e4QhICԎj _Wg&eӃkzX;xGc:Vy꿷 ̓&,Y1%SpW._zR[Yu0A,,(@b7Ճ-zְ'j\]@5 38Bt1ЁRJ5Y)7j$VPK㔗+mSIڂmrdLA!5@l.;h0g:`NS~]tI??'BC/ZXyS O@? ]@QJ÷qܴ5Z{荞ֳ @&LJ@x]+D%7kh_4fjӍړ>;Y4EoaW$:,dykZ+BXyH.V1Zz,Uޭrs ܹt۵Ǜoٚ Ҭv̈́JIs4j)i_?\/Z uOdHTһ#k/Oxfʵ/Яo|Qƽʞځ\^0HggOU!aL\~Ô{>\WmnyL6+Q;乗3A~ǿ Zu"$=itȦs-t ȧy]S: sK֟džBF+<{+>uZ6|s%<',C諏W> Chߟ_.l<QD,:({%C PpVrktoEy6H0V\>@/k#9Gy0=V(ٸFF)*jp`iȀoL>*ly5ŕ[&$'rcP.tF].k$mQ܅x_f}{J2"j: ¤ꔨ"}ogvn]2T޴4,BP!AWz~qn CCj.}P^PqC\mç<3}e2yh!S ޿z:]'';44Y0>{١3cw`6PN/^hLh3 {(=)r?4hÙZzE8G cO~ݻvBeCȡ5A;: __F4f~z7B 1;|}lښ4JnvNAw;T@_ z휂~'{ZLj33ԛ\*,gbV(:(VȚ@/ g\?8`8K8(ƈG:uG9Xp)PƨV9gڎejkgjBN-maeɎNo_*v}t_KƯp{yԃNr;tםeqeuz oE B7{uBEx=_z'ЁI8SΠQ ]iy %-H]4SFIe&tPԦ*$u|f\uBnd Zjc͢~ aq m[`"J=ܷU[벭]5a{ݸTf (,KgSH-5I8v+ƵF6Nre'h^ȂsS}n צBۮqch/X <C@%eN˿{@`&aaүt!lFy͛}^?zo>x=t5l墛}g[{ơpht5DSZα~]p}7DZw=yrFw7Gnx6WAs ztO=78ԫprϏOSzNXt0n l?מI"L@YFxf 4R@W,b ;sk`Ybj_l<ɛ{CSxv<;"X@T5+zl?zw0]<<Yt5@ϩ+Z `)lg)ll{\<{׏񨢃Byx}TEQQ  t}%D,J/W( IDATvup߿NiŁJJ@i`j w(t@,>7,;At_[‿yMӭ`yNmDС GCĴx{;YR빧=tAYWK2]QjV0 S@άUp:[-n>&sF@g mصp3ueM&n|FKR)*zn }@#&Bt]tAIٳg-&NӘR8 4 9a0@6 N%1 JbC%FmvXЙKej <>Йmb ʘU6@ҭщxR#C1 ɇ+GTk`*0^N &^֣/ CiT & u6wC"ߑ9^ '3Ӌry";lxQ3MPl )Ab4-2i0Y=0X'Qz[,lMnZ~@2JT' ]ĉ^A ab rޛtMXqg@a tDHZV~rcRj93cT*yy:@W`r0JstڧśPZ!qVns˗T~K< MƑTQ$;/}pKLqn4:V,ig;/NOʍ}v@g-x@i8Vx)-u &6@M%36Y)As X]ԼK}kv ?؆"@GDU$t'zr ]uG#%G7#]gFܗ}|9x 74Ef 8aH(%l]( zK&:U(<ՕLO>2?ZiHW|bhNg^*>ȑ8Q+3kתrvܛN:OB:\6;l Dpg3W'YI0n~Ϻɔ:i~`㵖2! 37 عv7|46.!<f 8ts/ DIUЋ2E]PP#{ c+r;tUl,@G3Gh'+lIs0yb.4~_`[|V!KI~E=BvAQRړVb&.:O,\@Q_I%.x?aYSm)a?>aL3n&xEZg4(|@~K[Zұq'ssUdatNVŋ/1MNrP=G="?ZG] "2B35qtyоKH&|$5u6Mc4&FQ%m]>A]dܩފMFgm͘B 3B:A47pcrWN\=mYB/p<-PRd#k6ߺ^ &}C}E\w@d[#@X7y/3eY:t&adVM:^JsOlU=p XO$zݲݠ' EAn]pԣ*Џ,otK8MHSYr`ܜƂrgYTxc?/ۓ@YKicpY] 3=/dzc-{ ]1JYr5{mݚk F;';w.]:qx߱c{7MWllK.^OįY|;&t~ʫ茍sAyZcx =5LaGZUdU~M{s)-oՔLQ[JZ%c+bB:NVݝ-% ި6~1}DgZ@wG>W@W(}@'t55Ft&ȢеhgM{-lQ[j5kWNnYex͚kʛxSX E6PR At=43&q %:v: ֻ%_b٪)fG4R=_?j_ ŵѧ#O[\K=zv3p/t ; Ljbj&)6Vv&u>l6z e"hH*t+ ȎI1`.9Ͽ69;h svVoJ]n&BQY4"X*j_ &(.tѝk+ /t[cD%@15 ͙̚loAe5kJ2}G`Qu̚SiǗ7חv0GBWeΌ4_Hzvu1Dj 4MpSN@#@_ևn¤*.HŤbv&m  :PW#@xoǑ5!ЙZm~ǹV\ZF:Zy(>ޖ,urgya{iJ3)@@cgtHHM63io{Ǐ3o\MYLZT*Fp=HLs=zΥ>i U~P"]! 0U3'nhH!0ȳFNU?yC“=4:: :8=Nl< <^)=;\3wWD1i4~ S% t/YrgF]Iʉms~5{vOnښzFp&3'y5K3j8ɷ9NJ5|o͖o_g/oI]5uV;XkkA{OEo5IYkW5ybGI~%^\XcXz@ϰp@R!8Wi} f\ ܭ ~G?om=}:gQϩӭGGd߼rtkGlGG;y[@٧ϝ:硻֍^9uy^_rxQLqG:G8 r][pG㉉qͮ劉x˒ێOOܱ[4ra.s@_kCGí=m: XҿC/o]fk>8ryM6Wj/xQy͗7;GsPkr?:$OrXп'ġz;;J$QjHjѦsIk :xVNwncaoqSpw\+궾W PF>Vٹ3ɾ-_ѧ+Yɗ3{aRTWk5F4 qw=\3'.ݹlڔxzp@pG R y5; a4% Gn%5VTVk՚ʔ$`R(֨܍ K>xF?D'VMr2-w:Kjz=ɽn؛_GFA$I6Rb@כhpr(/}xpvȩФzRDX:)&zs'{nC[='P^@hWznܢw] CNR/ tC_~FOYas[V:*tI@svnƜ  kw%7/.kYY2GX390UE7$եfF8 :0գ8ɫ=-a;nb^іcfiv-u@|(%วct@?&[*jxߎrC (cNDsa# г?yudS3`CFU?iFAG8ߜ4=(j~P+toq)v2t>5ZzWNRbERLΦyµ>Y>џsۆNN==V:Ɵ zD&$U*qNzQ^lNnIUVT]V`WC *O(eԪb p fC:#%ną͝j #gz/'}=7dM7jrԉi)_[m?z, "_!N-VZK\:7Ei1"@@Wܤ|@y9 ԭχd :MZH9਷3C_wJ&9*#@O"@].\ )TZj1CŪhi)Q'ƀQcm@7rμ1AWs K0dd= y0)i6m׃0p4pevWu7e]8#@'_4[0Qy`AWEoEZkZr.s?tW]2M09m=з(tβ:}r5yys*,?A9t:6cBz#SeթB R[Ҩ`nwz-ɐx =mUjnmQqpe߮yBTdm`9R֭io#.p%ެDQsOy*C=Nn1g t)݄>9 N=vveKndȃdzX[{K|O霳`:*)oB`{:;B)TNBNg#O0#i^9|Dp3ޕ;5bU8@@oMo81n&+4Y2.[1;9 iR$hLGe\+w #@'!=$-w)kUK v.nR-\t8"}]vdз24<)gmCw}Q->yGtFYРƌG)o^ 8:=J'$+@j|@X,f;"%GHQLmVruUCۡhK&,1ll[Id3+nUƲ&ٱ*e~kv1茰 L o}"TifTLSL_b?p5 @J<SCScspdgCwPvAk5X2pOtwNUL:QZMxthUjTM9& Sx px Ȉ;@>xS:HvcJ|J)hIʙZG8.])[[lm6 T\Uʹ 5YZf_e* 7%*Kq+Ц t85:@w~͗:*]ZׯA9t&n$4GM| Yin!Hta>W /@/ޫ#`@:f^9g tFZsg  vV1.헣du);;Lٳiqg}-% vӑM KS%^~9.#$ k~l=Aއ{eT9ss (fJB&D:4}iy/EILveG;q.\,cy^^Av"NuZ~7/ASd29`ӴMC~bp(^{1~ ;:&KV(=crf+4 \\g tQˌ3Wn4A)!K#}#yECŒQцj[;%=o=2~U~p;TGex@g4 *"53:}eʀ z:P=AF豅@'An3sͨD=e'wVQ8:H5Y9tqcVJqm DcrTF}Gh}~:/i;uilxW'w:$2=b5 t Q,o@b f>?<2KuiomKlZ)27RO4&nhlJ+{+շױN&u/?xdi}g]U22K&2"@%h'2ay񫲾+IXLͨ+BүKh!ڙ>vH&_ dww|v#@!A7:J4rpңR733^<]3zQˮļ@bN45qffQMpU*\/V@􄵵a/[ϖiV}ΓCO="era ( ϒMMy+ݓɆύ߾hrV0Y8G}Cd7}QYuCwmަG'gtT=NF5*H3:|w~VstpNA5fpWQހ.?i䎋ђ5D-!EUm7 x ԯU'(OTHEsrF$oo~a{>S t(G,r$;:CV:'k}ts7=oC}WBm^}ЁVYSikf|GtˮDz@Ƒ9:aELJA ^ lb`@y"zPXGu˷F URQwV1xBZD.KyjM[CUW'J#ݵNvT$.z?qh@J5} X;߿uZDҷno tCǽk=z!ATM)nu;Z1bݣ`Z 4Te=xћnB>6|nzEtg݈=p#@ǀ^AK Q*+*.8p7sJ-C =C$УJKQu)TfTp.Q:sN`@\% *~Wʌٵ/OCCHa&6ɣh,DLA9cU@4׈y5S~ .k406( IDATnOCe;o߻sɵ{wO^9yދmݼuk<9j=W\7Mwo^ |7>s؊<@}?ÿ :&CۄU>M'f=\3=yC FV. +Izߞ\&;:R:#n[{](JqLd(Qz 6/d"fg}ֲ^ؾ\{c@KZe `._t8Q+]衭7g^%.-M;,j_r{~EdA3;xtq8t-oP%. vor]@LvtSۺly[tFɃ{PB:6bCu ݼ7X4U&wtr?(ȁ@BvK؊ M˝*Xh t4RRLXqn_zq=h53C- 5hV `x+@蔓r_:P{Mf8M-VmU>Ѩ6Jy4a#\cܴ_.\]IoDUmNO:'*w yt:V6FɝƭJ[*6?ncUN5=*.:g_!gaƚG!R[:GՊ? ǛV<||#zr0cD4.pׯ tDCI4R ?|| Cύ9znns: rJJr 5 DI(ݣ"fl 硣<U[ˆ1G@Ϙt@/wđ].{tPK**JI5x-g@&"v@hC7 ۆ .xĜG{y\!6{|w=P[4I䄪8N硣Ftdcg?_CD$==AB_ lk15@l}'] N5$z@Y]{Ct8GTa`9J(FhWN.}f}/~_d] a:3BB:fGI*VMH{; J)ϴ5 is')> il2d$W~݅26EYo_;p1$|E>.ɬ@ 2yM ||H֑MԐC3@5MLs-$oYEOϿGA=|\!ot'^=qqˮ-gN򭬍i2vyͽٓ[.y@*V4xZLpV둂n/z@u3#d3JFZ!M[Vf +7)Ejv>d^w-M~R@]<7ɮ@ȡC8x.`%|%+tt㥎x~\~^/!*j `ik8GJ*BTR546ШP:EY@). q\{ZQCP4F <wB X(D3&SKc;^[^Œ+ @A1Upye|@-|i6DYþ_R@y.oJJ ;ɯ>ҁjS'&UrәO<)!&w zdT.Yܙ br+mTCϷn[9ÁtX t t ~56 e8K>0̶zG\B3*,I) @MFrYtVuA?t+jJQC0z@J gy>?&Џ,rl~UkwRUS)5]Me#_BWE g@׈9 xQ$r~\mkp[z@4>V]S'_ :t%g@=*/J*_C VNSiFJOL"fX5C硿ql'Z٭tjպCj >rwx,2?B.2WAY K ꊖƃ @vS@7|CFƺ$ХǜaGQsIc@(^>(ZYޙOQI_6*|iQjJDGgb@8R4#t&V\qF/8=CU8.~n.<Ыnvx`{YCWZ _?3yMa?ih*$Y`jO')8!*~nol}C@|B-R$Х4O߃dEGETxT1@`yc-8a+655D CbshqMJn* 9k0L(ZhR@.\,Vr:ٷG}6x; yJ9WeU]n9@јC|ºgX[oΞ]E3wv;מ($)Lt(C4mqA<nЛKScUN$1s[ݰOǃM?mj,9 9qMe@a0.vh#& E+^M[{Gj5 ߀9R,Э՘?7^n9r#ϴ󊓃=Y;L=lʕ{mE~(t.\}(%(Ojq'TH |=`HKY@4t4^u]& TR;&=0w':Na+gqKfl6 By*XLBʏS@WQt(LkC[PP:4#Z"[H=cCpԢk3;F8`°i[ +[Q-m x&ͱhy'[ۇP诹I isZQ皛ʍ羙:[.±+I!nQ)8aHK?ȯXϯtP]],+*mꕗTxd{^}3 s_\)VIչh'<Ŵg9~9r?_i!oml[@ ۬/ t#h3D@wx܏,`|CTvDs܁r:X!KB%Vu%yOQG f;!YvW/= W55_ZX&fZKI2̓f1TUUAtJt/BCuzZb# 蜇>o2 |k|{ƌwXz=C_ .C;Lgan*bDUX7Qe>޼yM`Zd&s"&Pz>E  NT Z1HW8bjMK]bz L ==t[%-}z@ςu ?C?+%8SA@~<|x<:r3gj-8U@L7C@[fD}]]zB2- THZ6kJR <:] Ʉ $a3rEv-vz)`ޟJk!zc>hNx-{y!-nx8FY/rni㸥,&aDtQ*/ݡ>3h# Y&$9OPgxBڮ6`Q?y IK| +K{GF N=Ns"]wm'<7V8@<_]z@7+tӛYcE((ЉtE;MшpyT[4:@@0@ }05`s}?8;s4MeVY7IгZW ?{胩6@gKb<}o3b;TcoB>)r|Nz}Fqn4qzP fjU#*fa=ީjv80&saNf|N]uh9袡h@k459zԨɉkik^1arѳz :)-E3Ó=؀+PD x䮿ӇE=DZU*dX*lFa{xv7c &e>]ZBw!Џ5KLF۾|YIZWu=w/}Q9]uƞ "YUѓ^q/a}H=54T t$aϽ<֐cD^„ӬbdEMZJ,e8}sR@RF~^$8ۑ B^N_vz`-Rz IDATL^ZU]]5qоw>f Pʭ nX  T۫" ;@X#:nY"i!S~  G|D6Б):9s=@GW&PJ:^n%~a] Yn-1;yH *p)R {(-#fb\ f4$zO::jr]*ӄk## Q{`Xj-=T;b/U tl#Qf~=q@OC]AUvc&TG= j$?߭7:"(Q4%ur/3!n)@W j{ LKx:QGi9Ѷ~@vs^1h>{DzXF 7t/{_@˗"jSY5Z.LG;ט&\>`ҏLBd/;)xZf\I缇nyP$.}xap>r?[W x/)DI:z@ՠ T$ܱ w' upo5nW]Z=@wGzɠ ,Rr8 xNhSt^ccKzol@3cL{<\:F4JsJx]T O>,u0PATM~ C_D_Mo=}akK~}{tQ4&EANz ~@Q*.Ik=K2o6ɇ:w%Fq|]z~[Z`TnAYrC@GF ֣U K.DU|j )%X+<|q;CĄkP[CLlnH$bMek{4x>?z5Ly~+OC%=7H1;{}r|_p0[uJZ.ӀC'>Q rzK9rj5'bWC/6 titprwz@CJ]A/XeY$ qnF4FUAa/;cܑP%uO3B= ]Z#00@!(H*]W|C?cE^g|nG/o8 'iAfָ:''üI‰ v cCɜ 3ྫྷK|6nouRf&WD%8j`HK@7o/ݾ2Z3ʿtO2j (⢇Hsy3%9Vlͥ 1X֣,3@jB3$Cl8Psб~ ,aݹֽ1%UqE8!ŅLJ-6ZYu qg{LQmё@v Cy޽IB12[\nDfp*l:Fym7hua[8z#q$]:ܭl|wQ7 t[D2wrŁ? krpL*cdžH3.4 Ɛ:6]-~AzJ/eIs'qC.ĨdSBotID9(秹j+Yn+O >}]HVbD3P:Z},Q2 R8}~`n-Ł^Q^pCwP&iMBϮKl' .} )LNJ8 \2@P[ ti t,5;Dyeh,Ymy{LA䥻(GD2հ[O2A0Slsںx }#*k28 d@ c(ۥXg.EU09Eq\<) BQ2Unz&uMEYi̱a_ 3a08ng_;@;ڡD ,N@@/ڇ v;i5q@4Uᄮ|>WeYP憍3xTih6  t,W3~F*emMQt~4K3iv`;*X9@y*tOSVgտ‎<;"@w?E/薇N\pC%FΧFY~BÏc76^ZM舠)Qcx AP[B]ڭ \n /`phoLpߛ/}*c^;ücsg2? #p< $zeV Ct]浟Q;v9"iɴ`bT@C@IY@>PE:l1w3p7:C/ۄE&30>#ף\[Cѥ~gAGWН9E}f@  nEn?cFYQС,_i>M5[D!EIUs^Pt?nWLY{]q\t ㏖҈`K:_cj5vŦaMN"H@= x|?T ']Z7`#s.:7 #A@$$ؚ:D|yo~s{5DХL; * ӹ@v+FݚL;n!yo~[ę@%v)}K_kF+n :5b/\C*8u\,@o ';C]@7| ڧ:^C 3! V)Ѵ{׶{(AcXT=Opf5>pY#1'|NmԐ@@/mǘ tJf fr%ײ#lZŰI iCsй PhckJޛo1A-&>2R ~oS0nЦ;@GĻV g:$w =t+D3mdbM+ꡣј=viMe gA~,oeXBǟ;R(bE%H.+.޻e$O:3JܟGxb/=E ;W;[t;;:xo-Eu+u,B L `JZAVٲzIzdLCC/ثBn)˽+o;{]δ5TUEKD7ydoZA5<2A ti޽zɳDN!qbJ:w'I:p،=ewb5#Ja#b_X|9*d\U%ShzqUXY)0b[Zr=:ðF.(Cn= dRѐ&~ҥR`n(I5y󻥥߀z}g,[EOBXQ$Хu>Kk0¢<P/:!- zA?5qGP׮&2-;m+yb um. FK\5$ЭZGzllf۠s:@5Ex rSe{ jO@/4t"B w} K+.kx} wW,xӲ8x Wdxz e? FN,#(tc ti7P.gxvlkhKb[2ynjǦ $+_TF`H#]* tO<Ǒhv硧. @!j:=+R}b:J{<Ы\m{y@Ds5M2> Lh*{S. $Xvaڂp PǷc85ltޜGI105T%Хuon:J(wզ 45P#}'(`ش3,'.ƭV^>OPM'/8jjkV5칃T6A 9U}!X},*\n]a:7DMt.uc`ھez z<#JM\=9Ϭ`g,`pAșKa9@΢ϥvs (C= xrza#WYa>tv1DM"@BSU&0)i'Э `f|\{J9H&<Ⱥ2%ХuC9@MPHxHZ{+"'6N6/'1"5IG'\z|uTTOFnZܲ/^l=6i!O(ʂ2>JY@W/ush(%/Xez|j4ahfg*x d{?Zטd>}hغZܫ}9r?>vjL# E 44466^:xv{vʩ*&gF7Lguw0 ` KByF}Rz8WrDLS'EK.MPF@?p̬c=HF)Ўiͷu8{&/t\ ^{j ;!]5zPV #^ @o 4_t" ('iXbo+$ХIw1'A&()8wH%Ӹ&RJ y UiU_>ư/O\~cK۸rŒi>r7W\f1{󖌞mLs-E(,tTee'<]`|4:v?t-YC g{!u@o$et~,~qw;S=EdH$3m I`DR hD:== 1>nh !%ZFr1zly2r_W yw ti] tغdDKH>w[_Zpr)ƭן{ Q>ީ@XCek[c.@3 ]vs g@Lj9NMX&:@l[2B$QG_=!f&^0@A]Z]j@w TWƎf1Rz:.UU (rv;r3z[4q|>Ʃsg5RCt+^nP.:1@En{0IOCcI-wU5 )jRISz1@,`` r]gvrck@7@]Y$9@ >\L:2qɻ!b L CT[Eh-йy8nDe!="$oН:Z6C*@UDU=BAl6x!ޚ=sNN:vS^|[?jWWEKM>o={F766԰Cd1;k~|6 }ݔkLN"] BҞoHN'L_Q$С^ddO~-}΢VB7M;xNf0 ̥T9CX^V d YO(c3@AYA:}[55' ꅉd !Ӑjǘ#Q$cFxЩ{[`FcاΦz",\&AE ̓EP֊߭H$nZ:tGS G8@j쌲P4G➵D& 2tJ6N!)w(z*F+zY@L^#$" _:iq,P$ '{Z1źa dmnqSt=vB]z97c" -*Iz`YH 7 tK gS'P4'AN ¿=:u*X8¶c@ q㏌Q903I]d5c \Bxr/ipoQl(NTq[%ґ;YZich~U簏ނšmPiP.Jg~<5y/.y@ϐ%ƥgqV\r@:0.gu LViH[ Ty2qϨ//L;בD5ɂ6n:P4* cQJ잘1IhO3 ]\S5O Tl >mQ~TIrv}7nxKi^߫1=o<7$PG eA>q&ER(t$|p3$"3)V<&Wi{o~~pz}[/V663h-1=#7llT'(<ݨIpaQˈ/( >Ȟ5i]Z>T4R2\0܀(7ۈW|5壵Hf7*(Ia?-gT]~5y)buiBqz%\Mx@" SD/Xɰ ¾?墏;c84$\%`;"3=sT tN0, >DЈ:F8D:AxJJG~D@ǵㅄˬ55VG'TUQ(@MrHXLŜrzki̻;BQGGϟw=O4~S"s+'PH:$#Gn*s\y**BCP;љsQ^=@~P૯@6~m7.G^cz^CmwMl ,F i))tcxěse]ZGٽ&,^)*gI҅6gȷ/@UyP KGQ zHJ2RtT$Lr)X!p#' Ӕf ED|Vrn%: $c9YZ֊ݳT]@ݫ+jFɉJF faj1&N#, tbCtQ/dOZZ%UiL(^.}. 2tk0RN1{0* ;q(^o4E[t5 ǸRCT2J@ }~2\`!+@;&Du(=%-F OYUR\ 0~,~ɧi[q("Z(dKPtQQEǏx# 6+Эp>JAA3bp\fީrdQOZ%ql.e|t">q5ȩS x0W{75߻}o6"@ǗRW|۩#^3F8I82!"nNj8^d؜.Xv|۟I6oMPq]*e JD*%s(ԺF%]Ǫ7p@77 ?Ek A>LԆӋ}雈qX3O,lBXoÔH<$NB`Fb?";TB#4'|hjUq7 SAMcVD˺-TŁ˙p=em!7-͵Vk^ +4'GG,%/45JEc1PzDt(ޛz Qf~(Klhm[1(ПCGEMx2þ`|0GKQXW*~]!;rysP#ǎI0_/޽XPZ=EF}>^ػp:hK+{ ##>T/q~"yۣv4imww:Dk9gGO~‚A1:*uZ4AC uN\D@GIsaVv Պk.d쪢@s0[p;zI_H  Usj_c:kfſ!ГE!W5њ`u9 uJO[ZaݣI֡o_zJ#c奜1  Qmj Vb(Kve8/J@'J TS FyI1H XUzw6s-hxT 7@Aw넟*ƗJ/G#sQNJY@jWYcG@uWzzo?i:mi]Vw%[!ⴙk1FUN8P.Ha S} $:;E ):k$68b>$ X{q%^\Q?@1F܄W4- l7( 9*kgR\#.or"f{ԥIHVe'ehr41qu `A@*%k:U BD@G ?6'nrL{u棚SQ]xnTF[cY8GGjNގC?]0:H;o >O2yC['G#+4i>RuC(TњU6|#jQyHF=@y pgЯZCO-\y~a[sE%>d[:5p0*&fQQ]qsz,! BxnЃm7pp '8q;84itY2[m:ҵTu.2Jȸ9]K`"tSV  \`e Ԕ@;$+rmD x QGGam}?FTA1< s0A43yۇ, ti$omn mrmUJ/S]tďFiz)$: t U9Z!(lSFL\:ۉ5Ȗ :j\cQ+8c8}N]AG%ϑ9S)Mڭe<(HL&E[LܣDh*A1|!Zsa)x'8=]&]гgx.zEՄwV' ֝&H_0Ca=]@ =w̟~H"M;R/pKD1HG0( 2tt@8;% `7xDl-֔@)KAZ>n#5dS; ơ\=}Hx)8)ɗ: ŃBD covd0,Ҥ2χ *(cTÞe6V9lvϠ*JiQ}d|ȗ: Wjy >PNWTL9q*Íw%cG[ @-2x4k* <r)GY;GF#ƈ'7/Ѷvl[&Jeۚ{&H'd?!pF-J♧6"GK^EqMupl4{6b3S^<ىu65кJ åd뜳ۘnP'lt1Љ2mkvm'\@%!9eC&q3Uh1 |۾p[#i,.:桧.@oӉ! ܋~fBqTy򺋗/gq`v,Bb;:M'*x;tn QQV̷iމN]4 \rc*7@qEtoP]`G}ju%=t tUn:l'@/Ay UJS,Zw1Yd"ne,jQP(WIHWSS0C)`SK EA IDATҏՐk,3ω>ytdc?>wM =9_O:j[[Cw4i?Y7Qw$IRoCjچW0I<ѰL z(VE}+@&m >BhԆ?o[ygne|M=QSוLh0¦mFFFI/mYF>p*`J݋, ~\iԫgp>M|;@&Mm}>1 Uqbb=F~gcQ5Cz6^vUXҤuG,Dؽɼ%+צNgyw}6MoO%ХR֧yDݣPc*@mNuFpׇt>gSOј]P"yI[mӿ۟.uʯin= zz7vW;1fdiFqz:ٱ !QQO&s\v %9uإyeĞkDZGl~g[@Tvrɝޫk@wG_<G>;r>oT?.Mo;:|By vw|꠷dxx&pZ>6fn@m$tO x: G Az!"iy_۱Z^%:Ț-;K ԩ頓^3^j:jy_> @upAGdY_#8u㣺J&7oW9wnvԙO=?;凶np곫"nO}ؤ1YEӽ!S fјzUas@r;= GT#Bٙo=!/iu=ymw-tO'T_#)h̩C|q׎|}v+y}3[.hQ@>rYV>}k< gf.s]~IJW@?ݵWT =n+%i:_}9Kh/ JTO#VyPB.p_΄Mi_Fe|~? CvuEGwW3?{j&~;З'_8tԶm~2ro{}&>pu Wm10$OgʄBbpOYvYՀ;=QWXO]92T, @M*(sIFPzÚ z`0uahN$qy{ ё\IV6n:ߕ~… @6 *@<>ٕզ]>t mf;Жoa*LX53O?Ao즶vW%/Noo{,o!/8w^{MKL3 |ɮqO<#/zKehTxbL~Nqy2g<}RGZ# eIo%;sI/)pE۸+yY@p諬1f[FQM/}W?_^o'vZm+: W^kv{Yiѧa[S96rI,**"˧5i?rvsFBw!jfC@UqQ !sx/CxO*[CZ0N:yHtnyFγTEyCtCۻs 6o޵tJ,ݪ~'hl&F mnWۄwܮ-ŕJN1{nt^L3v_'(@4u'b)/a^!$:zB8N$ё^s\ 7-޸V:wu\:Ϫ *[;b_@fV6޴[з%r?|.Wl7B`uP@eeRSWϻRXt7C߼UTw(,g*sEWGt#+F2uܐ|XNs@ 34s0 up 0B>e_m4L.XjV,6׻\=.>Џx>~|nP!kD@6{jg_z?T:UnkW?4OWݏ:&N;{#:aZey{F!##N s;:szHf * {(#GUէ0bZꚑFJ?R^.:r8K'[MκB+CzHXV*Gzٔ._eḽj˾e=Vme> W>BZ.M[̎,tnoZH83))izyp aR@ t@t<'RPӎ0Iqm֣e5E@{o&Y5ԉVԄϖ}ӷ[EWi:-_z[$P+0=[m'z6*n)_l2~5~LdT-z:*C=Ix[CO~RPk&K4Npdax NĢ)]OD@izzP <8{LXSQ~\WDwTU|B82MWNz~<.wQ0zUzrߪTW,.k[yoG_Z󨮐D"B __3~|qsssᐊ#,[zܮ+ [Q?et;s{9}~ Jp0RXi1GH wVE=+Q&ED?e]Zӷ.:*[dp V1%sǗ{w>z~ԇ`ZE6>}U~r\Y7t#nC,v_^K5pZ^Zm [nFݴ ƭ]Ч m.߰ë:e 4O6j^+}nfE}QZz(j|7$_ZJ .e:l;_[aJ}xd{9[eׅޭ+Ɲ;dA[z@<<[޽zh.@vt t;@ [C;߾`kW6}/z- u_Zɬ#V(2*5h鈐~[:l$BtGo}V~}gpɬly Zm/zHFCĀHD)W@:8ae5(A2%Р 3F$5} /i*=IWZQ{է۷{[>5 Oo fW2ؕʊkm~͟n~aOt<]y,^uȪTzuӨPNNUǷ%T^ebCe$o PT"WPG1+@W"XTq+('NN>GyMh0wΘ5')`|ޜԼdIw78>$L.u|U?6,WoQ-`]8w7uQ<#Ai ,RR me.rFJ>$kxSPdQi}y'Q1ܑYs[Ms^Kl_@&.V0 ti<vݣ8cSQ:^8]Q*g٩w-:W.z>&]ߡgrx titY.M]uX?Jc: ;/FDcTJ7NTc"I'*s)WF*B]Zg|Ժ&.V}ZS:D; ACPS۱<%x/xNa܎J|t-y@^2:onuUH@~+k8 t@:ݓF:|+r*ցhspҺ* W@@YIK"z<bi ԇ[jiJ:A!wq>! ti$Хlȸv1&:H=pCxx$̀G̕Là -jm( E#@ֵ@wyn)njmzk$ХqvO{t<4!@܁n3/r3Fz!yV.MIKt~7^%,.MtjS5}@/稨b8n> r@]0@5@2dӽ]$kkS]Z:1n1lәa:2%A:Tܡdhc4 w2m'rሻxJ]ZO:D 7BtlFYE=1evg]3W'VUh4?IK@iO_|ͫ[üBXzz$=Z{+ t; %gSPk~53!ɌR)NO G+++뛾Zmnz"@ tVuzn{>tНRG.(tCw߽/8K-wi?56ܼ-oK]O"JMhՒrLTrt";7r^L0o{HWp0Q= B_]Z.Y @Z]> tW5xqWy55/^<7sѫGYȚ^kǺ}?kPa\`{S"ЛF&w>2r[a6J#gw7tLT#g;򬲩Ȇ MވG3JU{ )riɎjXxVhLdب41-B{Zĝ9PAWh84 gGf}=*4<)|ý|FG@dž'I/q=yV<ԋt~}Le4xۺ"qS 5sm˦UO،[D,˳2[T,<[;Ή1tW܆pa|5t|M 'PA>ޅ?{kDWkmE׉kEW5 c7AW1eA:t+?d=_q3l<塦ztٮںڊ3O@MFѯQn n1A.Doi9x1}3@q(/*Z8s&n4|n^?%q iqJux4+?gύBnߑztP<*@07rK ~Sc6Ǹ@)RS_@簕\YgQ[_w=RINLo:ɑ]KrKtJMw]}Sr@G^O;O/1FxoQ?]]1g+iڟ ЈwSD!\ J(ЏQ /׆0Jw9t.ݻr:A*ͼ]Y.i9,zc<rM֯hT]sssB04z<72}pA„E3 IDATJ-{j;^_/y39 9Թ ˎͥOUr*/-+tZ$UGt1v /qFMt qT;DbgH)}2nAy:;>J?=b(籘]*s Y:>^{aQQ\ q ŗ@o;-;6(!<1*8fstP{?ۋ/ۑFk8yYy7P&gGVXMyHcy z}'i[KA g]@o ] Z@&@o?8z]|b J PX z)O|<tJ5EE=T̸ʸǣ"dWNGAV=Afv3.B1glN$\@'ŗܔ#vK8sы4Тd30ڙq/ ۧzw}~[5N*f=c~İ;/m~[te))C OU&j2n1KyGʌ{TzG$Wih{<cNe)osA:x_>mkkpמ_BE j SO mkatPiup*24퓶-Էq<'v}̡͆Im~lryz0>m0LVTl:3T@W gQCg t=R*+HH+g7I4| ^qhN yd2r]pa~66|I fɳh`J+$~QC<0"^8߉||!ԝ6y>9~fhF??ɭ69 @] @h ~]m̍GZlP-nU)R!ztJ|yH-ss3i鱑q <L;}@'w3}j~i_rc}1:(*^uO5gŠl w|ge%(Xz|%~/)ΑOxA5 l$Z@g;ʬ@+~ߒA]UjT1-5뿹{zRk!vC p݉C˭57w!Ov c7v uUZ;$Z;8o>Ԁo"ᢡVt֡Űʫ΢mm#jwt5Dٌ8·Ubo-jϿ/[.:F,9S@W_b.GqJTye#CS1Cj76;Qsت0/T]3U&Yeȱz h,=79T]T.ܭlұtf^ CG:˺|S@w8YOg玴(dc6<.5[3}o~g t%}W@p,q0et,P8?8BG[AweN@w/ޘa=t]f"eй-SwttUUسBgN|BvW{5-l'CnAt4#t0_hZY2Lk~wi5:>t@mSgtT#ro z k m8Rٻw~ʅ$ut I&J @AtМz!G3WŃ/r1ܿJ~M.ï8ӈS77՗K/ǣ&_wyO+ H7F,i^ ::u~Y6ikzSFs^);R@@vݕ5+\,q-s6r zRWѥXN&йG8 eA ]=MAUМ,).a8>7ʇ*'un{EWHS S=MfC,r3)}[ ($! ~-zEeDZ@K@4gSv>ĦmuSքv},Љ;T63tP֯ :Jc/.:Е@wTQdQe}zY>uKlt@] ϛW<,:*:btkNeQ"tIȞP]znkN }<@tP--ڜn1Kk슻^ݪ 9rh"tRJ(oGG=oxB7Yzo|P :(G%KVg].j]$Ih}a~9)tݭ|c^qN =Gyj?:U/yAtPʡ]6;)/e@re;wz4TVoKg0^Ӗypetxk̪5: AtЕ:Zԛ+=$./ 64dXOoFDŽ7tAc AttmIw ;e@'p,_t\)2B܁^{8J6B& @魴ݨ0 :hHA t)-@!*mY[XovAo2!Y!;&@rsgikE}inv@2KVpUᖟY6C 0XܻX#ӷj}s_}Ad(_$@̝ "#n:/hiݚ0Ѓ&f#+W9~ʲ>eTŦ9T"`n!lhsv%I:n_sAttDYriGBNt;5W Iql^U7Oڸ7>_'oqw&,6@]QUXl^Y9BWֲ85Xtj&7P}NSG肱eo[: ۘ@ٝtyq; V674&A9+;/̵4]> U1E!4~Eˤ<_7r\* 1it][F?@8Bg#},BwEXu[m}>k~Jb= ^ݺ:%ڗ~IΩ2)]:OS :UvY6jLTqXVN afR=ͿGP[RDy8'g{KK }}yoȡ:gNOnAW 0@DE%M-.Bg#mGtZ:XN]_P] ; 9Q9>) ]`wd@g{2]gYw@赋m* @ҿT`*5niO}IMZ&?±RL_mRLLs:­]a<8vO \;~֮Gt  ׿.Y Ycc@Vo%:ųz˞WwXz<]ލE6H,(WpVt^ )٠)}@AWk+gt=ɸQߩn㹹q{!lInH/$f3ŝn.ZHB~K|l2Cҝ||(,) @һߘu 褗+nWȱ7Spn7h¯q]Κ]Gje[6&x~le7w--yj)EA'χ5;sCRq'q˒6~.xg=8p2.I%)G|i tt͇̾ ۭ^xO~_($]xHY@]!C:@ׇ(š,drՑVwjdbKmO+ߕyAtNDZ%?}Ym :j7f۶f';⭊wSl~z7xV tϊ.W$s5y{z ts@tY@x#@䝿:ҜM͏{ּV ç:93mz5еm{5uW] /B//n8Kѕy+ wH3|P :(we;#/"Z4l:lG25\+[ߦ@7[@ws е9UC###CM>O =vmF^,.h*[/wrn")ju6gyStMoƩl"sZNNwKok)f= 83|L?]CX{Ppe trAMIu:X/gZo"TVn>toT]#ܯ0/Gvrc|1dt\zљ.Fĉ,RV?64dAXOxS xN ON_P<,Qra6@M^~Nq:gk&QO UEqW҇.sv[·~XsźSD6LsT-s赽fPk{{kw:?ph{6@]裷yOpǎQOv<<ۛ?;q~+ WLwQ>/kr/{PO1bL9;Ў,sk M]oqeNqF٨M{9M;4@g}&)+`֚.9XtT{.[k@=dV9Nn|7Ԣ#*HZf{KP'a;xY&YI tUs5 I'0̅dzba6a9F @ AƸū'D2=bnY)5%.o^ bޓYu+tvm"jɁU 3(k銦:6GFz#JzFJ%Mj9 }Ս=mm=%Y#\S,t˿^9\]__{-?V f t>8Hk]%Nq߷Ʃ{)Tu{VywPWɀsjf& f)O1 >V3=tHP349>=5~.) wC Ч{B=>Kxpgg'}bTgWCwW2pí{ϡ5GuVsb7@@z=-vpe)u~8J;\l^MeH#˜T$Mr\@90GGb1̥r9sz,>,=0 j,#n8q' =vL= : @ B[*M:'*"U\.y+w@7Sye/d`ԸӄQM$_@o:0526qQ׵3'9(#u tO vBiNۇ[3LWd1MzT7zO ]ܛwv0ݭ% ÇP Ç.\ :*U5MF3Q[XDV`j%ץ1v4P}-?rh^:.I ab&M IDATBiP}x|vjjjaZ+1L_YtZƩZ?+jG5.0_0{]qk]"TxO473d5Y@Ay ֮^}S kCJҌX][L˛6\/M"z#Mu JίΑB+0Q|m,r*6qv6ͽh^QלVL5*yR'|iL)^tߐ܋Zzrւ_ ;qYmk@}hˎe 8[6._Oˆ7F%+6kk Fɂm@u Ӝ?>EӝrOwq@.@?e=wtPA9!}i8E\P͝T-.߄JRG<sL`k\>ށ֯ :h&t4"r_¿uߖIگW]:e'{O>~qŵd>w'˟ܥ4N1(zw׋7&ќQ#`PEXǺU<ɼJW3ݓd/ÌwWML7w;@GsRԹǵ*}f[aRBj-ֽuǷnәB@[㦹r;H3Ir;;~ɓu`Π }L!V9[.F[3G熙q1¥[;_@蠙iQV %:p쎇i]ٿ݃ɷ( t|gg׉EqyĄd"Z"-x!11$5g/40} t 4hubkCa:JZKQ}u]R-Kg&/nuvoD t]>W^!K7Q+yj4}@;0G#%)~*M :(GضVyq5ӟ躭Βo^W &ޑ^tD>'>}HlrG29G:iNMHO4#N^@la#nf_Do@tt\6&CP@׭vn:>7H}:v -Q S\~"#u\dݾD9V AhG6[:c>Ҋf+Qc:@]qPSV3eq*tn^YO}u)t^Iuȫt ږ,l@՞smF%b%rr8m 7 *x9[T8%̧I:nAy:θKר,Gto)c'\--Cm,"k +6.cV?dF t 1:ЧǸ7340r"H,C}9i-|c : gftNy9" }u:27 6iøJ+Vo>%vq mn2ׁE05Ɵ3$@O3V&i{t|zd8KM3|k( 2~zUOXN50g~Q}"kǹתW@]@ǎrGv<(s:qm1.IvTx$ Zʱzw0Λ-W t:@O\@e'O=)Dl&C?;뭎h@'h?VZ;]q9jգdl[szF7SPuh|XoM*m}#J  Xޤ7Ed@{6)Ʊmq.j{t󩁮 C@7Ԝ؇vcZ^"{Rv}L5k:':{8/zNE)!Շ>xc'T@AZǚ rCGUltlmnq̹CAu#Ͼ;V÷tN\(oo=[<81ӓ0]!s$nOTwvt<_ONutMPD|9?8x ;rKjki 5^pDY ~7ܑ;2y*PSĥGK&PzdFݚ>q#XVٽT~hۥ>&֏%cfXLU,62ZQ'CC=ɲ0BB"9:2Z]2W]4TC/@.j*M+Q5=4\@KُO zU^7o|@קk@ͳ^G%֭ 5indOb _f>#A=4^}ee躐W]@a@_YW[0UQošFV[zG-Zq e{ߺ%J{"tEp玽|5J-8 @1@bO7ZQwϿ1˶5ԡq[ScTLPҶ@+g /*Ce<1>܁}:.4^럟u0z{/]@>tWc k\ I{6t3^sw'@١@At5 tԡ iݖ/:q\$ĥ?[m[p,%_8Ygܓ 6gz5m>k8GLqmj1/w?|L1ϥǏsW0"o@ײr+{ dޚ={񻱶&Y{;̥[. :jzHˉ=3wb]h:-j:@~EF>ӈ=?L|޾@,GZ6~ꆷy7KnQh> X#ӸK㽽82`l~z{_+:#>?;WF,Y}DqUi N䈝 Ff t# @9 ɳcO# !nk5KC;?=?ȢwLRftDH_iF̥@9s\DckzutC8tP}@_{KA9(ٜSώ* GgtMypw@wrC\U@Wx* )K; zȱcC5j6@/XٜiB΂;fqhz[@YȈ /mAj"X"@ B 0"@V=jm)w&"U t6&;MZn~2ܤ:Dށ., :p(n:|nf\)SU@7jX#޸ծw37htzDǯsOt1>ULIX#cx)mN&uZLx6lT^.m2C/**OUͥ=wft4"@E*U@BqsO5$wS RMe~!ЇΟ?fddiӱޮ֓燚 N:bh,=w|PTC#M9gOj/?c>ŭtմ8l=>8>:FzF7SZ @tP*Ƭ I<݂#j=jkazuz+>Df !L zי h}dL݉C p`XD?r3sB+ajßS]NJBCµmkx @ ,e$HrUۚ}ݮj FY!Ě] _G tuXݘ'ѿ K G=L)t: m =\G?#Ct0A Eq:)2cˌF#)bVt@8k'@o=T_?0c(`ޑfL{T}1>1Z__2>lc}당Ctt }_xן|^~rQ#~й}ѺeV^!.GT)w tT@UFEneGOTL SuC:\EItg5.V@^˞h[t ι!<9E.L+;R<йHu_nhB>7N"}GϔI!Vz3KFNrinr)[Rݖ (N5 ӛ {gK{.cF)'@O&Ps:G*o۔lAtP?vtv}@Ʊx@2w7C=h4}v4z=:zz~znIb:_|nB=>bPbSbឰ׽3}'F+r??~} :(W/.)N3iZ3u}l&wp$DzXM@$[f::fTp{{t@gd:6JζzG)UW~BrVx޼{TU5G_}=ȁ.r&B[:is65c(Bo}jFjj*kHz.B_`5J4]&=M>~-蠙hVYt&tɳ3_o[^ u[|i&d:/)jCF x|uNɀqԵNrEkʋ{Z< .j/wjVe腷OY%U[ĿƦ^qInu￿>?Stih굀no{&1 \p}:{,~,/ SOgY[HAަlЍ&,t/7mf>z_.}n4#to}3i"t<8N=vz d' :}l1 Jgz*.~7VgJ\g5Ƕ)f+g#ߺnH+MSڪ hRycs>40R"VN2e8С@]v?:l6l..q}r=0BL tr|j*%dߏ4qt:VVW?0 =tݣ i DvHEt]radO}tb:[em蠫Vbw=۰}BVr;oñ0BO t^ ttKT$V 'Ν;MiQ}Y;12\z:ԅ] 3343'.;0N]ftk,qYxД- _ڍr`WG3iݯ͡zܢNZEk a I4@7%kM^ :z5.B)6|#-0_4И&zPtkTS-k}(1秹Mg{#yǚO s/)?Ӏ^1 rjOՆ V4w`pUuW`RE"+i4oXW&ʹ_=5k D p?dtK{,X5hhq6ϔg7Y|d{:}D@G{i#t^QU/<@+|eZn? {) @,\ӨV謑];B?]# ԆaKp-؞ѿU5.ox]'26oSa8 B_͜;߼CI 0зFR,,~G_H&B'4T@:@@w4:(@, :(@w;t;N"e9HGЍ>{ёXӐȎ7Q0CCE$@JcHf_ :jU{CGf]gPYY /^fbٖl=>.h3f=lW@hF*,*- @ltKs w:|&ⴳˮK/Mc Q} n KP聴U}4;B7e :*/K^M. 8rsJcC/Nm>b=t"tRpA9M_ކ!SU?2|ѻ rUmiϝea,5gif3: 3 {j. MN*gO%o˟&@(w xONꝷAyzAs=v IDATrV6֯(B8>;8طuH.+4NOB xncV݂Rg ,t;9- @ (Bgz܃YkmQy#?{Ť@wk; yFP!;JH@ -tV/yn:JAtLT exfN:SS9znm=Te6B< (?:B6ht f%EYEހC0F!𚣑/ <@G>`l䣼Enz+G%@@'%^O+A9{kEV:k3*#'3"l 9It{Tsښircm%`* Us>oϫ_=KA9*>t.vuSϹgZ2[S=.*n\Srӷh@ty leyttCAy:Q59f t)h$j:t+}(B<`%:%#\QUF`k Bg~:>K*M-@7rʷd;M:I_$*r?GZ@;R2ۅ8j8˲8+:z0b5msa :hGN!LmaL:JGq=.^jqd@s}tw6JVMt yPRw0@At\:ٖFM`Wg t!OcZzԮ]o!雱Sq^:.d@'975 :h} ѣ|F2]ȹKvĥ@ mw7 v}Qe@'w*+t%>rWUٜXXʰ!g/ϨN.Gx[oɽA]!G=ߦ@'~RCTՐ{n: y} Q Dq.5*r?+| _^Q}'ӏ.^a]:K twSٸ++e1@tPHj8K!iNo6S6@ } 6C-@W,~Ơ^LBͣxŭ+F)@TYmo3a8 d t(Z@_7|g/Cj.a{x/MytʋjW$Wft+3s4oО^5[ȃN~:xs(:?ܨFx%3o%WrT]kWߔ v=&DgV&붾O.=a ?mMNzP)Qw:(@W?rTo̦m $AnYM^a3<3o]&v~ޙuI'pw y؈CUgn'χ5;гC/v5Ú@+?VK4z5i  /FO$t:ˀ<AƗ?dXU]gFFg{zs;fF"oi1zThRGE t:U݇G]%` ::?w()3YE5tiЬrz9W<` t#um@7@E5FvP@At5tuYlUAfsf tKT +l)Ti\Q}F(*ِts7*g;T@At\AfT'/Mz@ܪ joT<Qnݨ̸ۣ҉x @AW)гrwH#8͗5`@"_u=DfUg͊Pn TɷMQC8N͊7ɺUtЕ:TZEz ͅQV)9~ kF!k +hX tbRʸhaͯY4C@뵷}@+맛VrTqesmFmvqNb@_ķx0e՜y!tFZiYG"ѢNBCdjw:(@/XY\S @솳ldv [RcwN վ)Sr>q*?9쐯z/ozeXFQ{ @;a8 #й]? $ym425hR*TsR^4j"k.B:NkXt:NO-&B,tz~OąvpxZh.H-|ɫ5 ѝu uuS0@U7[sV:&2ZQ2xc\nL@߬*-;#G_C!г2!::)3?q4reY[7 }50t  *xWs;E׾9͇{fBN=. :@w1%dn.xos ѹyNC/BvH.޼NWiV{ι:-At4^h!yOM|HU>Ñ .rk/)COh.½ƲY=kfњf>R) 7k 8t@q p+c=%>tP#DSXMx:a6MJ$*J}Ǐa'lG@At َO=n=5 [)5' @wf ts!"@TǼy23<:+trA3Տ>ӊlUD٭wq,zWk tǝ[WtW|ә.sxY)dRx&/>;ЋzѵA\4'KVetG3Q&OܨL<$U1w\MmYGXN \9%́8VЙ:(@/(*.I6Or֢y :GtlcP _{D䑺1ZT5r tW.գAV{V $f+zSJg˾ttzy)蠙(iklDF__rW- fu (6oˬ ֣&E+N+F6K tEY@tL%Vb8w<ѽDXdHo1Q۬@GSV2t%#QM?5AGvI8[B>\Y\U]kKf1mELj5;n#J $*-^-SF4v Qs]ԑ,N$Yӓ #wy7AUVg# (>jn_%Kf7K 5kYUtb*E2HMoV=!Xml:wlmw@Їf N[&k&9j{|6CgcFkZե$ʁ@WRۻ>8t>6aNAt4ׁN9-[> =*2/wQйU"K.5s] tT.[Uv\uCԼnVw :#wU}KtGh ];jgCGlK7 e7s%t :ZQI&5.:I9lTG|9GjhvXN\%p&~զ2-':xYvtWZ%usgLwT YIwY)%7*w&5l4c{@b*,=Z 7۫qt@qJ8DΥ^3<[\a,6./F7*< t5I<6o%4׫k04 :*Gx஬xGa%>2ƠTK2E/ű0ҩhjDL]7Rxq˪ Ӈ?>@tP]\{ƃYȞNIKd']Oe7zCYfQ6%1+қZU&H? ڢr@tPZNDאYhC!hrKZ΍!-p!:g՛ݣ9C]tO'="TVz:)cnt¯ (@t0~A3Q6YUl DP'@bv[~k#7EK1DyEQ|֯h*Io߅'7b٥;a8 I!!fNכ](f hm힨uӬxQ[h>86q9KC}&zdm'oT4%;35|m\M SV7[5K H&~m2OE|fz@t;?\պ2;f@tPz藻<yj| [nߘn iN{rZ0KҞϜw( teFb&TIs J?- @, zX/FF>az~M5oFҀDtuAkĔ bx#MkJdr M7OPށ} :(@]1:-hw{ͪu1rkw%Ѝ)N8 &y&fVD7i=0Utz~z ߂P^PW8YU‚TMuy6sP=#=D7I\)g=rWME7{)f)Эr9|*!Ho@[]lgb`^p7ZegFV{M_0I|,q 8TѹVV F@WFOw٥s]$<?++t0$A3 ѳB6>=J%Y@tFC4$7fBXIɭeЭF@& I 蠙nqu,x0ʱs#`T)j6b)3uRFtZ![]F@w:*Ot䉷&O 貨 ^{o"@tPO,Yb:KR~>.sXRvI6^WsE(G!@{iLSt׀LxIPɈs<_"]%QY<*G6i];5zX-&-F%9y=RRn@ILlWI~OV,r@ } :z?3*/C&S1iEt2\s:tWw˝Z]ѦF^ݬ:;zzseR]70Z:^Oѥs$r1$V7ˁNb=QQgʽ^Y&hF{@i/}Rtx7𗞕te4YY;T'TFV+&`t];ߘF+39YΫUEtYT\rC+ :ƿ|w)Dj'kw*BYHN}OQWۿ# >S mȴʈ"`e-:ϱւ@G{>*q32@wϿ1gst&n=kwwJL،joo].݀p~Ò>+ :(g-޺h^A-*!U? Ѥu0U/_܄tS/tYG{2uJ.c+W.,LG~HL'iLgXpī}h/K.yeF+1hA+Vޞ;A˩#pfP.5]\ ]-*ZQ\;@tPZkͫ_-M :(G.?mk (O@/yb|h[A3 t:ˀ :@At@At@At@A3+~_rU-R EAETp4A|]M1<f.;@tt]*1n%-N&JP]5;%O/жftC.C:@t@@t@@t@@t@ :|:tU}|:Yū$X} @.]UH, :hF/Px :@Aץ± (th@:UOVPjA_{-M :(G>"BT__[).*Ҽh"ԺWsǬ+APV0+vʠPH8&  /p.;%N.wW-Yi\٠y|2ŧǙg'3|~lvOn_t~yW;/ 𛗾oTr?kg7Z$. °8y# &kbfwfK7{y]XXK,h%O6O` d~F} ֓w`Ab?5/,IeALXۼmg$‚ڂ݃<)cHPZzdYPĂJyl`RL;k?$ڃ_˼2*I u=XРAO2oXYPYnpA.R؂f2X[P^E˼zw?o TXţeݓ8_h;ACǟ4iX,MR,}6p ʳ}_s*b~RACOՕ*|qP Q>J䣱}yE*Kͫ2Vn JB j-AQ_F J$h IQRe4'tQug4V.he3c\R^l+}ضAej Qh3ѽe-T2vL[ 2$^̟4#+,I}\(4hoY(LF_w E 2*9eT%ɂ]λ3ot62tw=ZZ-f^Kҏl gv{y_?&::4t@C4t@Chh  ::@C4t@C4t@Chh  ::@C4tT[ IENDB`python-oracledb-1.2.1/doc/src/images/python-oracledb-thin-arch.png000066400000000000000000003305141434177474600251100ustar00rootroot00000000000000PNG  IHDR v57&E-фMJl-*+<_/JHҬZ=_uޯϏjzyG~B P~8!}?~q7ߍNH}{{kv#W& NV)=U m~~ =Dn}Cۅn8ab9xqgW;RO;YiœUKs,oq84.8{gBb?cT1'Qpu+|~/MTӎ~k7|˭]Ϫ?ZxW->qjۚ.\"gjFbT:fBQS|tE 6;BXnݷqkwuR9*I?Ub_ݨ.ԚDq̈ˏv#UB_~:MsIqUoZus|Ow)_]3׹' yeN(Hj{[_Xq,o3ojkY\FqPw} yks3bn;sZwj&4X2Ø3+h gahֳv/5'a2QSSlBsmN%y:)y3 ar3\BW>|oEYy34|"g%4+whͲ[˔HaM$ŰHPkAXB1eKm5dYVjZ9̱m9Z!jaN{dRt|: [c9}3Ї,CXvhJ کL\NxHxE©hbiJpUλH]9_`~YPtqf17օOjiu??ȥ:wkfAZ⍆G(%$U 'q#Cc`VX>6OOlv+“74ESAܹ|ϐJFJni:7 QbNwf-~۬w% PB Wrb K-H!ō;†qPn`׋P> `31՛ $̡7e.B'JN1\&2D H@Gl7'W2v0GX_TTD=Nsh!xĠ(3dZՆ=z$lH/PMאEt6niA{ /ݽ=ME0\ ^޸\P"YxS *B:JngFEГ!QSC =I!B6jNDau--[dz$_Cƀ](t?]{,ý7° 6x$}!vX!wd 2e*hOJZ$ h1pQe V*O%Rpm$P9'xJp/$BDD (pY]Z@"^!hҰ4dLă&YnE~C_e& J6qSNJ]`!a9=_y,Eh*x-FO(=wXpf.FHjD2D\E\<$QXMOKmߴ }8q 1z:JV@R>Iyqa&&<N&ʇެ)hJؽV <0L+p٣DGiR72YLU8C-pDy|jz.~k$ƒ~weh G$(#i% iK؇!=Va^q(ȴ0Q!7I F^”Ee=0ujϤ+~"<0u33%*ȎKmdv JvtCRCtl,gUuD(;L2n{)s;̠+-o}y~(2^u  {](@5sO<җ?7HqÆX%A94b2 \9 +kF }`r1;7C3coz[! )ealIh @qg#`Ѕ̴fymיІ’ƼP,Rox>| |^-Zy&O>T{ڧզ>Վ AYkb n@5c\!aðF Τի=j룄}M#t1cCqSZOVZ$-W[ GhPAL-I.p{'082Hٲ19t`3V_[yihm qX(Bp kFfZ~" 1 &R%"CE+0 "G3Ĵ^\d|X ^s׋h;gLlP`9{%aR "  ^(QpY= ed`1ZҌ|c!A~czЩBP+ѩƅУ^#T C$H0lkZʹgb,,-Y H]}vkFd (~ R"zp*@s,ʔ:j: <)-t>u=rB|zv8j:nH샢pܴ+Xg񚔍?uQ/d{d3f|XTc6i巳+)&yq pHYs%%IR$tIME 8V IDATx{tus͍!ͭiJ=M[vੵ@R"m K\D[tEne"+.ʣ{Pwգs?~g23\6Жkه-4|JG[@B t@: t@: t@: tB:BB@B t@: t@(ਫ,(X,kjBٮ.4*~L؉92a'™։/G(`PS//;P'9L|/}uA,ٝtN7X#~Ţ#p2Unmqҭ^:m,9ZSoPr+ {"iR|bBM⭬|k8[Q|W֥+t=Bqr!F|%2Ɨ3dqnLMյN)MMMmZRSӿ W4 VG&g01Ut9Btt%tԪ 0D0s ~`_sx~ٮ_!x]ٮnp G31;Jnog^x / 3;&z̙ zEr=no%ogmjp_J71*U;xZ 똝tZL%K-52o+16t slt{7 Y*n2؛Jk-?BP|+VF0[ nik~#,T]A_rɭ%O-8"Ƀ/9mK~[ə?Ǐe:3e&qWTw/`EZ/t13daa h odHs#dVwist~f z#o]BnBIta!s]y1K&V3UЃwi<&f']= o0Hc<}u8=]ke,EAdҭd<<-03V¦i7Xğ}$55 [yc! .lg5;n- yӶ3w+ ̵҅3=&new ne7Y+ʞ&dR#tB߭KJK/9{+n%_7y!tĄ>t/5Na~YRBy.43 UZ ;GhV<S]@r&)*J5(ީHLaoߩ9ZG"Noe ]^y[zaZ%>BJ5)؃U[T8?Rxy͒/N~ɋ%Jry-o~7 NKΤk+ twt*0(uuea:H0xX$@%[Ï(B:HҮ%A0((,)Fw7L76 [:H Bgǡ/$B8t@B@B:B:: tpak *cAͥ$KOsiA0P&D@-w( t\LO@RfJ/ tW63:@xҫz;v.M o @yɸq#});-l)˓n֌RKFcdCXmyAcFII!B  A#\ #]pt: *;N#F꾤}O: A*E`ik';i: A,KWleCt!BW<.7;fVa8>@Bz.c}mn^BfJز6X vu65 62 轊١9UXn) W}0:>Q8hPosC9;Ko Oobʬ*m䮐 n&| tK%OltoGǎ Eю_[W8vl˭m=oZt'wQBcIn]mnG 9@sZnhOv$;owܾصg{:;wY,%8L#ӻ+}`q[iI.]jcŠ >4#LB MЛzIFz䡩4\:a z`\l]Խ6nFN蹹wmIΜo֣c;Xv4NZv{?&d|dhk9-w.BJ$ &gIЛv>ݜ96w0Uzdzֻ[֯E.n\mG7+7-OvaW;.:;lnéDdЛ9C>dro V+N8~z7=:]7w[v[Mzzvw9 :>^ռffμ;9 +^v7lzg#"NM`Qvзh6gT }gOz0ذkgBt*w_;A7{_-]̣@Qf% Y1Wq!h?{-CC.5 +:Bݜ^Pph'ݩltnf5[o]V8ܭ970]K-`wokz@ щ{s]k/'|,B?vk/ цƪ f$뮵H&1n&qUKNWsT1 e?G?Pj tg&N1z s}t.'9M>[v3d.M?nUyn%l%Dm@wt}=43l鮢BYw' 䍇Kh[d!CAvc GSK7T )S@fO=Г Z>aƽK^clɸfJBO3wlee]ՑlI׵0>Z'Σ۶IAo?$B&CЏ3I62?v| w=-KNn܋MܳpIFskst/?B8t}vvaa]#ڃ.FݍBK$Bod3k[٦CD軉u=r^F8om gFwѵ=Klq[8:$%U'H{'؃&C:k7FZ@Bz]q\*6nK>hvjfΥ'jmzUwt+mN&۪M"N =5?Ɔ~[g6:>~̥;@bӏFz8B.k=BF{ŏ }7<.~MQm'! ,֞CU!t} vJ*RvJ+B?̬>)5JzO>Rs27$-/%C J9PhnXxྵ}UCl{.| t} }|}w=Bk#)iD#GU]lqjC'CYqF觹,FЏO;,г[alyUci~$oeZCBn&gIS[!o_hו0kOEй&\n[C4=F+BqsqTl^2=C::J*WU']wsƍi=]T%{:CvByY9BO-:nܾU{\:N2)׼/:W4=(VRd2BO &+:NB_.n'ҹ?q|.YB| tfsӬT3]A2 :P׸2";vdnR6wo͞ 36mEU4-kSUr!tKeE "(hӶMAEGsS; նsӌF+WgAʼonydt>̫8%a[waS}EژZ3l|4J^Ϭ;Ƭ=ɹifaM.5в{ܝyG[1`4 tg_so }~m,gj-*\ZU$ڋ;}Ī ii \EW-qzozH;Eg6-GZ @z% up[v[{W)9_c#% ArƢNtwpt͵)/A߅q`7;hR'M'B`:>M @: ڞ@RσB:A2޳u!͈!t$SlqM="t@Bɤi󁓻^+Mv6xzΝ;bL8 tиm[Kz 駩jNOOo 2@rz okALtVA 2}EK#~0x,B Z " t@B@BCIq g e:1l A@@B:B:: t tBB:Bk@ s7G tѳ3 ࠪ15;*0$J0@W@2&0Xa>t(kho., ڻǯ/A8iq dq tHq &c: t t@@BB:#$B 19 AU`PP-A";~:.c&^Ɠx,=u|Se`CFx.] q(p|WF t@x^2zܸ/,w]0uN`obRALLwm t@ޯq?`(K_ܪ%ezֳַ6~mG0ЌXF8t-Ƣ9+gY^>@ㅯ#+^__6}E7@?Cٻ=@hM <`2ϲuU: /O=vJOV*UJ[iD:ݣR;H$n (Cpn =: d?ngVXT1&`@(h=b"@`P=оBBt20F'iDPv["L>KmI@鏮 uG_5Dv{_Wkl.FֱQUY0xVG-r; *ٓH(+]C[L;o 0l @ 4__%mI =yVkl͔EOdn紉PެIuNt?irwf{Q։.CtW&O2|Se܈G{k.,L@ !Rϋd>Wif0#TnșJq`%xfIul8Ʉm 5dR]|^u/VF;z붰7sqPItP.6 7[m|聀&i؝֋4?j#jj|QP7:zmBz5=Wxej3?]!?k[3T<4>BvߢzF&7!V"zF|S@)-%KB XPgzCi&j!@jA+B"o]Tz9,aIrFYYX]^Ӑ&i<'槧/‚`nAKWû9I瘴 ͔𛆰g[+D:I* ]M{T~3 / Eci5*i߮$]lr:;})P^kHTd-o H19 AzzFW6eFL{Hdh:ޜup{cutӑrP+#tI> (ܦR8iC9H7 %u`UL?B,ҼSZ4n vݬ4=+.t:®&e1RM[e=6ra.WBh_ t\5u5:;e&OqE,wפlsZjJ#u5GqiG 扇|7*2NJ3OaVz(_ɼ406^҂ wG] t.s3uȌsF51NkzBgssi,|.۽KUk8\}ߦg tlc:ht%̩+,çK Ylt^9B8riE-|8)2=9V@BE* UHU•KniQ4V 9pj&H70O×FJ3m3!t Ӂal:MnЭ^. IL^UͶvCxq5gJs#*LB:p:-jUХrK*NM*6έ< ^j7V/Ӧ@B:B6sZFnI]6qzP?vKl>#L&խ t08iq}:ĔK'{Qk# ]Ȉ4Jy8@ {Rs߄K1O3 BHBNN Ҧ!8 _M7o!ߘ#hi2BIM]p 㘜E&t#_կ\w2|\&t3C݆P':?*]<,d:3vbB9O tL t8qr ]15kҟ-5>K nr(&XԶLAIn$YU1K/A)k]i'k6Sxa =Tis,q9; #fwFj`0fP, BAbr8ttF:3.D"C،Vi'v{n[_gH׮YdfwG-wlc:JSa'tRXV8}4S(&Q)X3*mjYM:UjYE#؞i?-yX: y>Iưz8Z=&oRf3{:UFQB8tVkktb5Qa֪.tnElv\4GNV].E 9n[J7CB:BgGm c<,jF(WVn]6*[j^hyL4ܱX&.nat!3Lv@BT*-OPno>?Hγ oM6|\UK 94ڝ@` UhжPR+7=n~#o. N3seFh0l @BT}|}{s_;DX?=ax_0KF[35Q)$[_q{7 @ *!_1dds6.t~:BW4>+)UC\6gjZE.@rJwa(iƛ+6_AqBC@B:LL йj9;2aH++wy̳Vx}6\c>t(˸') caǂdձ.}N#_|t r%Yjv9  )@ ؚ߿L75>5RVFnϭ3,P~ns{uNX#d~#dڨ8O d "uGh4* VB31w,+eSh=wmwT ?EW&%)殳ftΩH3Ch52B:&YwH&:U+rLͨ9ةbMF';w*Ԁty6ϢG:N׺!t mH.-ۊh\;D6 OFXTbӸh=FfDoBRerr:gL0B'QUT:ӊs>u}CB:B7й>pS$kKy|5wTr=D"H(I 6vXNݙçC},@BX㯹#]kY]{,:;xHQmkqjݳ`-VSWeg<ܕϮ*KI;)tnHkhUMZ7lF BIzc 7 @  7-B}dMn-׽Qlc U|R YֻB,D7^K43O6cLC BG-w"tƣ^ƪZ 6\?j_=ۤ>[Ixm jIByF`.t#H7z :-uxϑ;BgO\{փt9{>WIAr t]|zhay2kуtBI$FYaIK`2γ5 N-bՊ"g=4B=Gsz$(&2Չ؇ iXW,Wbp$U躦uQB}'=xJ567խ5W5qrg3z_>.ϸ,O,~.Vq.<#y [=Ǩ(.khtkfcrdqaAz_j[6?kt[CzrS٬i{GlVK˸SS6O<)`3lOSLx慔/+Ǝή9uܹ"a}a6{؋.tC:Bg SLH:Zfoc{o̙:E/;,yNHs6 V{a4 ]9M» ˄153o9w:Y_~S3f  i9fɤvYw(B'v۔kfWN Uz3;E5k'BdPyoBCrZ1UдjL#~ؼԅ_r(3aqԨgܩz@`8 CtTBt籾ϒͭ",փ)6wP;GG ,/]I*/CÇ.ݨOΌ [ O@)B:TBW]΍"ٔP]H۠N=uK5 <.q*8OF*1)*3=>ySjQ\*_ךj}ؒ}ዳgr~ t08i&"ybK }rNy]^n E$g=euzrYZ„kM >Lq3'4FƲƢ> rXN+-l1M7,&kKҍ7)@~0!E$_'6~Bw}9uSAq] >$D\T*:MA5ZV*tJ0CÂSߍJ/Zr:|BVyÒ4/P ٳun  t0kr#]cBW46}1>@?˯?_ů_N%_蘜@ 1ʚZb϶?ІUb}8$ssy_>ѽ!zLfd4B %,Cs =dt8KBBg.3&3?7D>b#@ =q|8 BW&!a؟i~͌q7=tcA҅nQKWz;WlرJǎ{= $2,&tGƉFCA=OLtesk)C㣛%|0SF^O_̙3/o93l_VDʌЇ }~_ =#aSI,wM0X*j]kMaqpS°xPQWNke]rV;,FJY.+ fP$|cO}rB:B: 4Og3Mrqk}Wj跸MʳNw;:!_P5BOoj(M tPO-gc\may@Q=lLO+>V=Ehlf|JM&_3q }ԙ~=w˘ -zj|FB ,/sB/)%]访ɄN hZY!=,*K@(gƱ/3z\BW˩!ae~廳Q@"]*Ӽ7ݨmOpgU.`+԰U`3^AߚLnDRK'cCYrsج B{͙|k}BB: lܚDyFIrNr_:cأW  yBM6a0fCBAүDNy18ݭUFN̞qŹ=.ذaUcsZvk?=#* =O<˫Vkn10z59B٨@B-FtҴӺRsSf/ʰKCA=]}޷N7,ݧ Fw:Z~&>3\tO Cvh_>2o _y:TBgGϞfe}qXΜҨLQqh爢l7l`D[gL94^,lnS֜i=ЇѣsKT!tJI堅ry(,Bdj S8~|RWd2TMM{ "?ܸ[Ϻ rdeDBW`' ѹ >uɅakB F]K2 &T*3Mar`_3^ь熡oZ@V7vk]m+_0΍w䇷Ys$)wХ(ŕCCc6O?1B/4 "[KЍJF60}xN lB7 xþ|S3/ZBפuwO_b&M>?_1B-w"e:&YT׈0{R MO)-]N~իɿax1O1a;[g U‘ #Dg ! ?O_YF `仟}i t08.hvlVx3ZT]RyH1S\Zf-iKa 9+yXɮW]m=ц =T_&Ǣ tmNzqߝ=3j`aF> B:/[,OijY vS(dWtE>E{yf_ 7|j I{v#ko*96~RVЍJ US\! %8ǙQ.,.t3zLeuI+rr'ѮcS)Dohp?21u7 L ~lz>bw^[YUSy|ԬtwZBMy{ݘ!3IB1>Z AB+ v9ZBW+c|>cSîpKd`ڛ紦i=(S7,~F-=/HսQZێY'! t !2*D:B8t t|2TQ$81B7(ysݣsOa3m"JL\.r߲vvV`1O^$D7ycN}$rq$CBAsjhQ!W:Ԫ(B{!f|U֔^`w2S6 ۍ\1BF ,;%/zn tm6>4әQ.v>ᅣ jivQD 1b.5Agkr/O4[{|iϳ//n >/Zho@Z SGH4D{ACgGntCrZ,.0Y_"!גiSn;TYbr &so5{ Q^=˨ M'Bc rWcf }HpQ.A΄!t! P jEQj;B2{>?^F9K&ܸm5 #O_t_kmHƺL´l|6A_$t#*t>Q"BgB:A8i'PjJs+CW:?KJʌŝWNxtSܖgԗqW} .cG5m>i'tR82УG#G~=/ tP}9f)#C4m bX).M"~~2zV s czƑ#ݓ)$B,oibhÅ.$L<;_ة\׳l>6 t;R,{妮INLQ'F2BĘ_$8k ]tVƑ\31B'+寙e}ΆP IDATN_.FO>j-CtI:.^KYCa軋Eb&-BBݙE]ӫ)tr:HЅel:mvE՜jռMV$6sS^MqqM 0+Df^QxB8hf=b)^M~}s>y:u LuDB:TWU!%C%mnΐ:+z AgS(Ztu /[Tld,ݗ7>%<|>>ꓟO|YEMЫteeyu};K© +*kӭWxeJ-x3X_^K3C/s$^kZV/$7#Y)ؕn#k,,y7]O.8ǡya-BQt 7OU,Fai{3 GJsiVm߭Ϭ:* Ї2c/|.QS|md:ơ\,[r:ctf]e.D6Y!VwxLv]/ef]4sve>M.`ӐGa6JM B>}h2\QߝKIhBщ k壵I!P`ؤ ⫹9$׹UjUR4GBOA|{%̝|{fCB'ﴆj}SYbߍ&!t!"tI6f4 +ElYMUi)՞Ír73y_:^KNR|_fUZX)j{ȅNS5!4ww s$v>.#B:Bg$%)E*|2>ΗpUkl._Ѩ\E^_. s 5lvEU2MBřmЛN'>QsԶ';9Ĝ.х= t0$fK ,fu[3M9Y>rr0ms+ِ_푦ىL*N k\^B?| u掖eo{0BAN^sy}?BBF_w]wsBgpϳ{,Q#tR6WJz;\Tl]Sg 6p/iFF9*ԗ_ۢDHFnn(wx}uyW}ntB:C:HPUq~BJIfM8eڕ"tR2k훤MT=uyæ.ZBN֛WӞ) j4pӒuiM[6e8:w;>rzjT:H'05~ۘw$iR.l#Ӑo N|cV~+CVofm`Sn"7L3)(B EBzG]Az6A6Bl{~A BG-w!tIRUs1ƈi'[_=}՛/OE]}o_[i6ofKfyU/q]>xY a!m9MA׹SOO1}; tD&3::W\fgrBw+F{'3}+z-oq9O%ṫ t eMG\oRVdSňU*q< T4++POBg}.侗=8+Ka:UidO4{oƍYk=~tm%;ce eL~!R"x歖u) --MBжm'&\rgۉFuRZOovbD /_X}*H?;rd҅qȑF2=<˰5#[uTvѕ#t: =+oZj,Mshk(iUƘi|^tԚSW4OXԶ<;u-xyYe%Gܹv[ecg-[wxnZ[36\=;5hݲ%doIqL}B?[kƦ-WMoG^cAz_4ib3:BNg3WQHK^>V{20 pe^ &79 ߑOn⊻"-\- OGViZ$?mw61[1?gﲷz;Ev3{GO>I6|"%Fo>;eL?s@`hr&# ]y+ʿ-XzT-2AXkM)(ϛJJݩUɼlG?YܡV9D%ͻ;3b8EO<߾kcrg{{,-ㄮݮrώ;1qJ^W?N|~ûߎ  svY ]2ZRIcM¶|Ldyԧ/[9+Rkr DЙ+FLm*t{'R+ݥׯ/XmGb4;{|k rهjOjg^W<{ܣ]u)5-?Z>\3ђO?ߍŷ|0@B_Fy)mqWsUJBE3JVzj%ҙF|"aKD*?%/R:)d+}Bd'zSN 'q:.dw퇘h.b]TF^ˆڮ\=~9Oǯ?蛫!ta"t5He_]*17h,k*$+:i7զRU>K:vQG\zrN?HBWiò.57uf$;%_?Vܝޝ":onS(׶ $lS ]4=X~WR2z2+): t/t]6@M($tq]TSEܷiK+ЩSV^ .M*̦ӯ'n|isϋ+ΟP&ɬoZ7ʃJB' veVuݻuMz;&'yRy}jJ JMhٔORQ 4]<%%9B)O?vdrga@ Q">{JpjqF ~WVVFbszuW*XˣSs;U."`cϪ\1g"tRN^ tu]<'yL. 0a:](,SC~`:Dt[nk]{rvN[uY_&SS,G6Eფ߳;A҅ζ2J$BWbn|pSvs޶+`Uo3x!JPXW߱.%`M<:&#䰳zS| "SD'?ݒ2|CB8'ga zBi9{ FU$ϫNtv tDiSoѷsiR[=^߻a7 }-Ѧ[ctݑI:&g:HZZB7*B5ב&=͑*I Y+Ҭ0*^xv\ tI<Ҫ?q<Й(brB/ٻVњKg҉ ۃ~ՏIȯ'O %%te:3'7xfyEZ恹alхЗzBu7Mvg+E-@/}h^-5wхr.ڸcRR>Of>'݆akB DqC4ijfpZc9ۦ}+ZkR5@GGzXئAZ:8]f}i'vo)"eG`•1;Tׯg//DɊZܿ6> -t/d jhƥBcx>.U҆8"{C!Z'/l8=%eL1[LWwۏnc+*E)]{MM;r@?G r!%F^.t@`0N4S>Bd;*}qE{Ez4i; tupf zb'Q{rK[:ӷ1o^~~Kjg~f?]: :;k^ش^S#t)ө0=smx@fr~~1>kHOBg4$- cZ6so`"tDzN)-n5VL.zg]uL2⪯)-: :+rr3TGk }):sx>1YTfWsO.5Z\žA!;/og>}IulĝPٲv>tmYv|wGCl:}4IO^sHF?+~  Qgx%%'^|EWX˼|dzE'˿w,o?s/I~??끧d4"=xY =2ma#Fj^ҥ"Da2?[\&N߰[C5ԣ ]*M^]X {y˗^[q,'t%?X%{X56]ezvcu-t [:H]QsizcB&\I$V>m)t.`Vl<];#Tƽ,iT'iq!K R.{-ۖhzW~K$4ֵzsiAQc ~!Z$B]y_rʯvMǡKNWͼ܅{n^.?:v[MMue:{x^2nqg z[uCE8Baf= {|B/1H7M0a-ōXnTXw<ܚUS rSWM!N>tqFf7@!Bwzj/mНB#46sO\tyWqr*EIqNijHRF$(dIR7G]E%5\MS Ke6z o >+ǽUv7*]C?7e%{-lGc_alCzzccאй2 BZbRнa5$yތqC觾8s{>j@KXޏ+ߐ r>߈$H -bJ˃oUc7sF72Zo1;Ә%ǭ3k C&iV@ЦY.4/^=ßIv4FauŋC+W` FKЇ6cOtqWs]z>9\H*5@8ŒrsBֲsk6RkT{E2$5ʕ#S~#S㒔*ڝzdJQcAH7^Qɀ#wE=s*Fyα*$4t~ E,n~ wSVU.݂>3B_lY眝cT|WOhB|CSG$eK008($BC-k%$ר/EEEL#bKJ#aw*TSI9)K_/Md/F&.t9^MNgSd|^+DJSŊ>AR#.&~zu$$2*'0-LG,t:GG>/_LO}e_y'ʌ = :T&]B0B7YT_]&j#'̪Ԅ>f?dt^w Z Ta3m4`( ]<%4i OH.Y)?j}UNү*o)2 qlذaΜDwf=15BS>(O,|lg}3<Ƶ9+4#)jSRiaB'2i),mQ.te8􂍫R)/akq볒ٜ=D$Q.N &[$;xIȖK* [SVoGzhcv#iGsj9*t\GjzdEx%΍Dv\yyf{%OowH&MБE~]c" B񫳌sBlr[#Uwgv2Xog[ ]#oU| O }nJLf457Ei^ѪJ{d^gʯݧTR*t'q6[rdcM7uY5}zن-#r3.;˝ž#M-vܼ7ze8JZݣONlλSYko3 u~]dQ,X_-% ƍ(&~4\X&|^u6OI|b_B'(HرaS܇ |#/)B5 y >MCT^RTnEc-9"Q~u?w; _y!GwCa9M鮤F'?a@$bNBicx-%"|tcj *q&BI;Ѝn3q<讵N*Om IDAT =Ұ5%Ѝ~UÚW- G!υEJ :g. w_w9W3f)eF:C;7Jl߇FР3n2reZ͍q);U xn9$Wa#I`T5й%v،T%?M_BFE/?z =X)7MZ_ix_MU9д8Ohg[N=y?$D>2_y/B{*iq[\7¬/q"Ӵ>1ӄ.(4b9aF'N1z\\ָoRNlҏ WSo׹ {0t;1Lm1mtںXV-K*%oT8]aGa5P#6CZ-\ Mi M3.֚5HTսXxg=}OvH$ \(SrXBS/ۡ\t(Q@S_|i[#Vm&GC*`co\w&(klݕѼ1ٺ |hZfE{~&cO rQ]]AР~(o ">?h1Vp>f~4P|>t}˕UoXʍ Kcp8>H>i@j uR:hJmS;}:j!cBgՙ*m,ݴ%[0A{^A6ç% O;&HM77W)JEn(J&1e,A+'TxiF ),R!2zA]DV{].~|EK .G2Fx,6muI$^q>f # tBcadZ݉ƫdݸ*t:4:ɱlGcy=qt?#IS1bRl޿t.Ы~(\FD&-=X nT%@qF "CVא<@ڨ?Bt@N%%.DsFi&{h^BZ t0%"tNYYSWyH ŗrBLpIj &SXw0ʧ2(t'vt&E!@vMnXwcKa N޸[&ΉJ|F aѰ>̽ޘ-WJ +.Aԏ>6]"j@-Oކ:zD39Щ&4|!m[sD?}8Է D1 HHX3Y!,*(F &P4H8 ]!),_ʑX=]HO/S b.% 6s2a`#DXY%r.chICTAܰR-%'s>t@׏Ts[[^WՉMvɚ mpΛǎ] ?_?]лLv):T,=k]l9kM|klR3p׏ D[:h9 ͝H#(R'嘩 kiYE Pn-wÐ*qAo:\⨉Q]) ':Y(IQ`_zQ*K,r:vmOq5bH2~3ҥI.v„qm+qO$ԡ+uA;Bd( }ޠUL L!NOLQ tV.B?7?ʉ>k2yPVXHᨀ++<ut{PhA%܁-ߣf )N  hQ$&+z(r3emP05`^NHaTdF5YSϥ"ix%:}'m g"<|ST {UoXa66Fe2d+M %rb tj/N@rSUVBwxN=y|,BOtnF+H qRxC@K@Msɂc9iA8LBf(ȥBHn, )ٕg ECZF h2dx}]dW@Cl Uuqhi3TUԝ*\<=?/e{,@1w4x[MŽy sc1ZuOE./H=lyuc*?~ϧU_旟p&I# l.Frm6!](.@W # 1Xv ̠6Syir/4zӢt u*aq$%D0S͙Z룥'b:<’芘f@GC20jF?wfl gyw",X1 N8^5. +WY1A=o~:)4^<=vO򮃠G9~&BBSvZC*lvUP7ݿk*~r8 USxE*0S c}~ǪF~r4LNQ~^}jhNy3&*]uXeA%e50ˀ@RX?о _6N/ZP%鷆:} t);E]0:dnͺ`+,v6^ә5R^;;qsts}i0i=B`}qch)-4z $ڣ޸fq?V@E^l~K3z>)k@%$YWJ%tM!wnimwz>`?,iE#.9 RRA0ޮ%ǝ4k|/?| .K%T,eFߩو"Il]I O4a`<Gb.nl4g ǖz گ]ݻ1;!WtR m@1 y8@-@}R?+wF5v8Ζ! R)ʫڷC|v-rэagv @CBW c:r*e,H0?G@+A!e;(墜Vw|[hf )n@/@OR!UW 7zLhoZ]l1Y!(iśbPxQ~Y}y{w@'}\ure`(mA=POek J$mm : :Z8 Oi M{_mPA=zݽ,qܳfBC< '^р^?޼ѻu9Ux:yx?5蕆sSwvڹ#/a_٬0!x -XrԴP鉷9@_{s W<;@2%1$Rb~t*di8:n>10ݕ UsgGoNjoNzF tKC`w pVO?5!^;)@k:Q 5rѪzPfC2 *żn ezJ->ܸQthf7Q4\9)ww t7ړ]/& %qęԿmR4t@vV+Z WLK‰:)yJ%PѕjMm#͢*j|B=A:t"y *M IpV`?|N4UG2!To8v^wzw`Ș'9@;{#;\9CDS@/و';o_-T|].Te  (;tz!@]F芑 u0Ӯen`G}@v!띟U^3z}JDlG.{6oho`EI#jQMՙ{͕֨f%\y/9z}G. t#xۢO:@Hۙу*?ae=C@7W+BLb8h+QA;tzbSGݶ mb-r $%&zBHpK?|ejx}@Gg#@,]g sK<)d|]OmU< #U\pv'[cLr~L (;@#Kĉ9ˑkV:n0eoz.h:u 0>gkw8@E=x@Pv~yB:F\wO_m~=[:5T:LySIWae8͂ &HBTD tJ)yz{84ϣA|,a>#]Hqݶ;| "3g766^DښkZ]uM"#t-΋Aʽq5n@׶zmy 8mxχapoe"pCςT|s|9zv[4GO= SC4zR@ׯC3dz,i%n\?gϻ|?WJ[?o*yn_i[{0TP6!n`^nK8u`PeCkEތvDT!Շp.ro5d y CG$ݏ^—_VR\( KзCLl0k;Bw; L*տ3zz&]T;p^!p4Y>T)"*4LNsjֻjX+r@WNR@ (2/7GX@TPo8~kHJTT@w%R~D,,hJٌb|"JG<]|>Y#u}˥sASt*uCk蒁#—?yx A~?O%KNN#qd1u7].p :03]Oc=Dl89DCzf t5 R,*d5oN!!:WpuڔۤA@J")RtZAޔ29Pgs,ےf^cz'L2I|a=fKRV4!DkUhT9sfaaar֬;;7^M.NWWWOOVlmm~b4wj]BIc#Fr>R/,\* ) ^<}V6)$Bư"_/ IDAT݈7/wĈ]CAC$=5vˋ*kNf)Ae.WY]gW+-Gޯ._*&B܄x t4֘cL;.9M@ʧBJ0|TsDr,}x>Az]]]RL\L-zZ(IH%e!~0k񒒷=eYTRKN]r-. oN\xpb!QoE$"n$@l9|2.3F [BFNʩYNuw9ڂg} \m4$*}6zgP2h]r,wt?dY(&4pa d$|BXxeaytt{nsscrfWPfA4"w:baSK4 9#( ʔӅm8i/ye23`2~ q|ٽçZZ[M zx% a|OцAH8:-Z7im& hVni_n%F[8Yygӈ$uqjg0M]G8NV:'z@٫OYw oӶU\KlasuF ު02xUTYYn-d`8́Rɠc>p5j?Z}ܽm-w8ZOޫFAeGMlZi{E!M^IQ ҏzn `1Ch!o>uy}>(٥Q=[~ŵGBc]!^.ђů8,dIcu]+BdQvt}G.b#cMҦ4N[[ŋW6,kZ{licn^4pQh m5%{ljheycw.ܸ8}G{8c瑒%cwoi{YҊJ!z=ɨ^+azիɵ4%(z(_]%,5Г.\o@zO&y sHS%rtPNNwWک}#s3]>QTn:ZuWW+4i}SSc-7}Y޸dEQɐ )M+ʁȤS7ɎmAFΤ'F ~& kr⎾fzy0O=1Seb/^Qp?k`_hO{@D bV|-9@mkUغc~jKl4h#Ӄl?tNab޶wO5fyr}HyVu6HTV,&7oogD'=Y:a`|pQw{2Q8XX)6Xy굵S}qVu;}Hy&X}-$nXAArZ(7{P3T" pM68ݷ|<43:;ɲ8Ιf nV7<&;4Ο;jNpmt:,!Џm=Wo/SN]Yvt^E"`,㦛7FN-($HenVHr5)=!̨tg=d?OC4@DtUK|[x);DGb8 "ݪP^ In%tgJYHtmZllz6@Ug] 3qR]>@=Dc0/*%#t^CB 'c6?ڇf ~~־\+j,Za^ YY莗;tMlRdb;F7BI280JǮcnل)} @"[:ZqF 疡GEhNf\VsP~.cmD-& tno8{>% hDciu]ndfn5_Yr+ꩻy^ӗ罗A kC tg9@eHf0D뫇: }Nŧ?YSwXaH6~ЯXLRՇ)!zz12;!B=`k@6Uӧ7 y:c'#IRbL[TQ8 TF'q-r_gRhFmz.҃(7@ 25-Ɔ٭qdJ*82D9,( :J|îo*g_Om]L²yd:A~~+!'6m̤5N:F ]UW&t!:uX F )u o|+?eN]?`؋$m5GV-v3'xŹI5ņȈ 2Y`8Ҽw{©JCJ4nɾ\*mC{ =g̒/qF/mPAlIyI@:-4 R%Nn~/{宝=_OǗOśTc3玱Hx;,{1cΟS_v3o{̡-l*!'4Jxrt;aO=k֎PA:|Ѓ3e# /ءqyd{Jar@ yRxz ֯@VIJn`BBixtm:2AI3H{V)4p4t[B];9Q/+rəeT;}W{㧧mYɤ.JjH i/ \/gw;%pƌ@7RMxK#`ƩQ#@Ƽt?(y 5gKLl[YNhin_I(i+Kh\~i @  K|fάۚ Бƛy%~d4 )ȉF=M  E;K4emu8j&KK:\6Lbrs>HVAN6ªKn$ݕa.RAe@bʝ:0ӏ:i2(*\7%A5g)pUP#a$hr?Bu@qJ K ;VNL[S =0c\E^d5Ho3sqtj\GBt>Z|%07@OϰBdGy@w-Ï8@GAm- \C7Zwc`̺]9B,Rzؤq6C  GL0vp{|#\HS£FfKem{N!.9tPg2OaJge, ]{E|f!S?jz/@*YHB@Rym[@G 3b+JgDS@w T%4Y{b2 99W.г-`u 'ln(Z$+t4ʇҏ+*F~1"ȃ ".qXN\+l_Y [).t.hkE .rD~g@7YX=@^XkH  H/;!h2ItBʎ=LIywkǏ7'Ȃ8~}By6 tpiZ(Kgh_}GjmxDԈs.zEakAݝBU/*LV"Кd"+7\&WM$ps*H :wuۚz9D/ug 5!Wwl>57bMs|) t\i@*C@WCߟ tpzkԝ?wze4E\n=5ҳ[x ڽ" hLS;򼣕ƦgO1,q(f{0Uh^趀XlTܐӈ]~pk Yk*A!D2Ӄ{5 SFȯ<Ocwh,^)e?>mDܩc֥jLh($ H hz"( F+M7Ә,[@oxVRfjqIJj0Fb*nl՛dhhx:9_=W<;;n&:SΤ#ѹug?g5ߝGy/G:Lb@<ڠ ۦYN~2P4RĤp/ AН}/}dR1tE\fk@>ӗw._No΋1+ޱ=qX_?KM#}75lq{4hcmyٶ~5l@o-^Ybak@=IM[TLIG29~#1oz^)5oT|cAyDTng8{x98JH"!Ȑ9~VtqŸ1йmkdN=ciFZ`M=kXY п'?}55r̢h Iz s龡7zzL נT&(#IYZo-_eqf Dzm&;N%"mGE~׀0YBWxx('p00pv߸n\_y3y˃EdSv?+2Z9e՝ %NՔd IDAT#x bM4 %гř8NT*[:qZ.f܉+3hrНЁqk^)? ۖLwKޙ1Yw.];@ t.й@  L6yi~ye;$c* y 1;\uOX zj6,4Mtxd8oe4$ЉGtb rQ\P ;Rԝtv([ aܨ/r 7W/m.cLWU]2Vmȁ3}sZ--5Ue@0Hzr@w;X#<@Tc\vk{\κknlGTҽtx}mF'2ѻ\Í!g-ЉB;H%" s{'HD"b5.7g**r S%`i1l,&pfOO:@wZX~"k0R%"tk|Fƺ++UY_k:|q5es=,Ìy}sl5՟'6(۫ROKm [V򦴪S JQѕ*}O^jzm\d+==F`IP~ze.\G5:nw:Fp,8)n%=`&u饞p*hq-@׼cԈ\EBŒ5jr䬿 kv]7pn%YܿF!Gpq1Q;spau~-F#6k\7KtTCX$r/x:ΐ,|Xf\!rRRLG(tC7_Cgwߵ.SiWV8оOq}{qKGIͶzmI qx[PfеQIwa QMP,K]JE O3_m7L;X YO֦w]wGtOki T" *B:7X}gT߁}ửٷx/61":4]o)NǴ]C!el&lm@B̢>fls[1w N_I) UݽO15@$mYUT}@q?}.t ۃ~Vq7 1GW)W Ajnԝg)1om<,3c WǴ0f0h`w!.пf|Wߕ^x7M3$ Q:EH)AF^ ͯ=;y݅};qj;uz7z|B)A"BauPpyܮ+辿8M˳~C9"=a.M'uu=ެ.Ѯq?>'z{P+G-&D9cƜ=aL-l0UxB ߀5P]QHz@ +}|C}ztHJq˰4^T8Q U~2$v͙,t SBڄ^ӗє>;@~;S~}W=Hi@ʾ3XrF]?X701s{;Vwɜڏ7ǕIDP`"Jԉ2Pb_Ħ*?t9!j8˝]6ƟRtS:TZ0@{mg&Oٷ &?k>pfm/$5 jwm[Vxrvn[Z\1`;`aJ[;&XvN]OY8}q4{ dž]j (Fyk@=8 @Z/ei( /P⦯(w D+9ZP><^S҃С1,~gN@+)wX qyZҖ-a4uf{{{.^\zγ++33533<ٶ΍c f6lW78ԞV EGZzV<1$kcAd+Tk<6z&8cMR{l~Wa{ǹ-AY< zUgzF菽}F3_z6B+V ҩF 󴜊RD0ViS@W 8@SLnPkYs䬇 g_z~G®-nY_upRڲhРQz' \W$_@)0+-* dNLsѲ8B? m22AVp#v 9`&p;7ƋpGp,bʧMKoN"v:כNțhb0>E7j[{YBٶgnJky3CLy= ]+@La\LGhON>2Ry7j|efdtƪx8'݁;=k,=f tFL2p:\V݃Ur"x6XDd БZ\'[:#t]E0|pﱘ%5qA/ĕzX7F8ϼbdo4l#\b{x 5ݑh:1p W8w#PB ؒe-YRp-.?1 xi&>[S0 wH\]h2XA* pۇxפnn[{lޕo5>!9yT((dU=lz<RYq2](Qo@~o`V7AtF~ e q:R sXCg@2 :(@kySYwAqí.K@oSڨs!7:eSVlZޛgҏSm8.ܭ&П܌5܃q2J[C;iN(9r"&[S!@'NDgBIl龃#g+ב9 (j2BW\|q\,)=+u(I,HHeD<5olo[zNLKɅapoY}~ YΛO_&ЃRcTUI4ڎpTk׎[&G?<]Z?~M75(kW-l}+8MY(tDg.9{Ƌ*7h0~ő;έ_[/e_<iH: :63Ŋ4bxj}7GݕF%uo?Aw@0}fb JȜ`{5\ %Ń0:0uC|0]ɩ g0nި΀o4ZnI/<|l$á :Uj@Ģe0E^$:ЭoF`>gHx@'|eumt/^0$ܝabum:F:jx qywnB[pG ng $Rw Bj@[5zƪNobiw^暇Aھr,0:Ox]\; VOG0Zv=TN YtZkG`vJ&7/Œ G $ko ,bW^*=@BB 5nC#v%ׂdRJl^fG?h9`"Ź@#lhcQh_(B\Iu [܄_ 7iKeѧsX;.tZН@zE Hu =GCLpT`DT+WKJ&)+pP^8"t%aƸ:;x;,&LL: |.X}%yDczꍬ!=ƇKvGPl GFDO:_I~{TJ5aP.-Ϸcv׾'m;6.su")B5[_?QsbLҸ6uLb$~^4P%q vp3uKMENGRaQ=X‰=FU# T]}@<o_]?q :5% w'=e@6ئg_ ϛe 8m/o8I%;kG_Voϔ~/vq-D06JyhQ3"t"@G6֍i1RRGKQ:obn7v ,Бe!q8Xy md TzMXj2x^u dьW 5 u_ 0qa|ٶذ/tz`RvqWbZܯ并W57&M%~94 ˜R /\{+ [RAulc4r>Ќow$/~ /xYvU|~WJ#ϺoʈС=g!.a"\9X*@J#6^@ю0N-l}iG Mf=Xe=j,6 )!%Ľw𘷆f-VY<-gO\1)DuT1O< t,̜c.2Cqմ ),Ыs~Dڑu*Ͽn}7#o#=܉<I #gboD0x7ApPLJAև\6[# ,˚R`d&,$K&By^eL3՞Gʗrk;>{Odo^mw'K]r~2o/ZvQolRs:!:܅ҝ1.B>ASbt.3@'0:N#~5>|vd//|ҳYv=П3x~py:fDJVz\)FO[@JJ L{M@e:,ǸYcԀΟΙ\M]ݻ#hI~^XCR[<}|Tٍ~<4v2@wv7Љ~5Kt}[$w =}bD=!.(:18}B>/QC1wWP] ==T|M5] fjVw[K[  Eiw&N/eDަ%i@Svr 8Li%6'.#?^峻XFpN}@'dEƈ%M]{Sb)a~{ e `!o}{I0bcW>TeW,^/g&z/!W6.kR6fP nGX{\(SCW] t_3n#0}ֽ7Z2~mA-cZtdq|%+_ԗg9kGSr nCGjdy/eIhUF.YU x& RV@H] &x@#Ƥ2B>Љ-ACܪ=]NqtkA2V=Vd=DŀN9׍<ݖ Qr?ØC6U(*Z*v/. t}_xԝ#"^K;?Z\~A]Gؗ CS__t㬝^__I]tԯЉVSسF#㭟~'_v>odaw{_OUU:a30u 1h[@m,QNkJ_* |lt/]Z'wYyߒK:O*J7~`,9~-k J*xݮ[ g0u+bbC57wyJգ t;YɲsJn?1]{5+d-ZsY: tƒz~ ٱ$>yݦ tKeQ@'v%G :X13Ab #c-S&;B6⛛m% :D .-":4<'ӝr}2exY _\_`:dq_)bv$Nỳq6@jnpW{mytm$pv8G5XY^8.;Dv(4շN[kÓZKbCPX1B KwZ螻%nz|Н?G IDAT,:>ݪ @7$f"eΗ;Q΅r55/w<tE<gV/]ae2_F " -dg,O8X֪QWUqv6>{}L3%t+۷;?k,ۯGoW٭@n뿷?Z:ѢƎ]&8C+tTW X07b$Q tyD27-ذH;f| iX%"ozU@`b+jnbs u>7 S9oUzEuV@cD M&sxj0l=:)~"_ /Ɗsԁ!$j<*ckFtK5yן-7P4ţd(zޥ&l;0j0@`F"5ΚQYWʃQzӗ?~ٿ?^~a9㔆cpNzE^kRR 3hJf.b8@7Q]q2"lfg15BgnA7{zh⮨>[W$s LoW4v|HKP)M_6<85gN_jφM0k;k*i b:5A;;@w֜Yfr `U zrt]Hp*9 AKFS2L@W7)7\:POqi'I[CrZ_:,y܄T_R`ssbu}\[B֨%ZH!=h:c"z:@ m?ZE,!!tYr9zRbuCǎlxc!1X/_T1,T?D{o7U;.Fs\Et-oݸPlBh [޾.qcrd;{_i~8@ 5谀,Lwщ|ٍ'7(:=vng= ژuZxxT£k'g z캧gumWچ~#b ej޻@Cmy8nX#BozF"tcj_tYrt ItԭpaMmy$l׸wzsP9uH4n@m)UyOϾw-jn ްp*,g}ێj-c1ָ[ hQ͵;cL0ٙ3SK7i#bDZcIwkҲ,6ij$Ion4q;@w#]VoZi7f]2Z=}F>3m ^ _7חn8}ddz_m=*4dG:y!n#Fl/jk|`֩jʩ6ف3-Y:U e{ݮ9jlҲ(ި6Glkh||S_6%t>пo=Krc}[?goP;q;δ{LfƉ9DB`0k/5p(r#ctnr8h9u9keSs֧tWUrYe֥}T9U'oy= lx~[D~>Gݮd`y ǖfk=k?pfgmƚ憆&mtZfc;yl[hX+tжfO^uf.4?W܍g߮W;k/uTK}}͂Э3%6{2@s(o]o,3ϴ{,,M_xjttl[[ו+ǎ~gkN][^:|fzImmum?rۑPL3 v;^ЭhiAy+˭RY1i+} X?9+w=]wCBZ/#Qa¾:&_iw6>-˶{Xm>|ixŶ[ 즎5@8**=Dwhf7,[Ek-eƩu+;o  )wdkQ9 ', &z>D)<F0N Kֻ~G]]ܝ鶹L ?k׭Zxknn(cuŋk]?x '=D kYl2:1@E^\|ҫܠsjhԈڽtld'g>ySk>vok]m$j:>:2j4ώNt>,*<\ht2359d}_gFΜ;[3SOگffjIkmV, N5P*J nH0/5PG~nעh9jjZ,d ^Ҟm>2 c5Kk757]1:޻ [Fa%X^%{%bY:TȕܙddY'..LFm*Н]ҏmjМ¯6Ͻyۚܖ\!H6%G|ƀTX#@6:a2!÷^9|neJeN4hgxOMW880pp?;|`2IF@BWk(Ј}{-ܼvrC..^=&LVnp4^1:z3=B>PB0;lYx~>Tc!Ezv׶o@ۉw.[cWU5~q"amwQs1JN o_tH_BdLި TDt^oڗtt0?0a[h|ig@v|z&קN !yFO2]ԢDaƀ&(Rqd,{`WMHmfl]c ȅ!2U@@c/ۿT-?xLb'؈;9M$Cd9UH$`)k'jSSS泤Ue2s}ೲDf<.Y )yޮ meN!l[ۀ ϗ]e>f)#.O?&l?}%)D+d46[/3> ĵ]0%l&ngsxUUg );^4Mr'㑰Sdճ|7,͕9duK%חr\=}u&W|6Aמy}y?1VOl;_1O/7]r//y&lkg?[ؓ{z(G C8IphSڇ]H)E;,Bq8KQ HMfHrα3t׬5guVbPHR̚FfZin_sS(|g=" AItm37:Й}fb[27tf[d9#K1>1!䰌VqFS1DIڃA2mż|#';o}X-ҿÍn.!JݪCݾ{-@*g Mb@aHx m v=y_EǻLq'M[tӝ;?4%'ös55ԏ[h?UL5#W0x~DG]m~rn:Ko- YJ  VZ?NPYGCpp'KР0Ae ؿа/o 7~cu F9s=2\vҵpҼ"vX5"3=/۫zݕ3.-g_2쩦2): qRi}mkgۃE{̷?h%gLKk| w'ԏIx83wB\-GJ#Hz۱$$7:~)bꑀX&ʱi$HqXZ몰-_'&џ3;,qa"jEdtR\hceGpf @r!¼@~~։6w7N@L4{ezϙF"j1vbRDjxppnn?LtQCRSW}F'&ί;CjJX_~׿|?A T&C Dʴ#D!OMEYϖ-Q>DNJ<;-4Ѓ!$-@u,b*5jUє8(Va[aG}7P{WQJ+x6luR RF\9a}gd47 >w@Ebptg]ƚ>]X>ro,3|v_) tR*xR@_ȏ?t|vGN/<=2nuս5"z蕶: !GleŴĭ tә {)XzT ĸwBQV ے<#}"b7τTNpU:O~1By& 0`>+Pr{XNV7H[uc-BMLS%^@3ue'RGoq,} y~.#IJSbe#Rb%J|Αr+A%#ATTt=8Х eB y M ;>t@74^}x#{ *[@t+O2Q. Z=»˚wZD݅mE&ݳ~E;/}^YuJ| t Ѩ2z_@ϻaźy쾋S4?Fzmg3J9Ay;oȸ7қX^edN}S@NQ:W.BSs*# /itbR¨L4Ԯ8MRI=Qwa[a?43t@E(m>h@)҂đA "fdp]Ǖ/8UX3v?tP֖=g7:s K\Q9hT+A?y _=f1z x}MOr`:ռ/SSn) ;*Az3rhRLm l;4]yN/z ;3[ԊUg0*:XȘ>:dHA= sֽ4D!j[ק@zvg|=3צJ~n;H|*.,h1A"CǙƽW/j=2 ?) ? A /ې誡-S+*)en}s a7jʉt* LzA4+ :Ͳ`IrEo-@{DoA KkcS-E vAM0N.dFS#˳6s #<ԀC C{'t3Wa8Dߠ]EXQ,۷:̙;GH; Prq|QhU}@,¶菊z:hOc6uo IDAT GA-q w]C]~v/yRawkk]M@f GANE˦A+ (fwMWo-#.5T!,ЍrEq(Sa\QE*OL9 z T5_8{Yᇎ  n_>jsUzaGAz@:*tYHQpwпR1zbgbcC_׼T-r2v2 +wGyOjhQ]w.e,]9=h dYa JcB}KcXs"cI UhE]?-bJ}{fxXEzګ:)JQɾ31]Փt:/j<tԮ9, @f1<'*[4#,S@OvS?*f&{)`?RL:LA.B frn纀[6Xx1;AֳnD-0Os[܅m3Э,Q{2wrg'jzoًs :$iys Q{-̡@-//'Y-#x:CWt`f^}Ovd>Ǧ;Q[ѽ>& '.7C$dk] $}@FG誺wBF,kOrF0*$lKps|'-:X6zUc_˖q~߹dἷۉxzM @]e@}|dpGP7z,3tzdYYÌk_GyaGv3&P{%ÑhD3dr;^WBl"ne٣5M#ⷛ '0-~^Iso=Zp#Wtu|8MTS U^H$¬i5].WR[t֡d& DPyz=: 1w;/ݰ6W+:J!/S r12x"zTÀ]wZ:sKi!;;|7}n@ ;/ywS5ts ?`"n#L#߰Na(k3xC^DO>& EdVPd>荮ͻ-qXA.mIj {VSDHr7i( )yJ aCa/ ma{0zL#: t cCSw T]]"/]:8Nx]6(+Z=,A~M^@G3Z =|ew%@%&yC!flok* t5wk a}';/_iב /|K8R!ZЍ tp"E"&U8E%wG-u_[_  Z?;Vr_@S]oN\SR n:TA:Tj{xΌnyc&Gr=`!5@\W:Qmq=> t) 8<>{Bg,]BiZآy1/'TdC)s*-c9- wب^bѝ>JPPZȥm+$a mzٶ5?jst~QM9@߯73-풴MC*^j(]Z`jgz@d[ VǢKn]=4@԰$X۽nyUo=%u&> t(5%[- ,K17LNB$HfZ$@@SO_lwϭ>ЋYRҙV=k̿޶ڛ91ar]F@6ɨLС>:=0:ڙ@Z)~'@Ipb*V{ ٷO+=ƛj?!ÛEt&zNO[s d=f}h"y$0jw =ד f?Xy7޾x^Pr@?[_朧=UUOOuh)6 6Tuf2OԽt,&gn+~3YB߻n/H{ IIÀa .Ə;t$mId80Pб;園3I%pW!yD !M] ~ C0WP=Pl]ѭ?{|TW5fӾ#{̌y}HR݅gT|ݫ(. kW Aι Eu=tBcУv>tQ?S6 tg~z@OC~.4g؝ҦCzH e92w.KX+$~:ta _οg˻(@g:.-Ӧ:^?3s왅S76?77d\Sؾ}WΎ^\8~`f~|L#ƃmSlt(EzH|=D6iCLyR)ur:tDxYUY$YVC'2>H9c$УХj;獿[ϾT A36d^zy@Gw'X"Ui3`ՌjۊBJRЅ=P?ֳ8ʻ̪:>te)];5tezoY/lkll"kl쫭ްqRj'$6w@[[ ЁGԇ8 XFܪ`0o /`3g'=(#{4a}?=efE@1 *'A)<')^z$N ZhS̫F:n̵mOI$ПOYw}q`@OE SCxȅcGj?as7^V18a / tPN*QX;C]J$δX5V GC78BNGIc\qMvt}'5_;nI[tW宗rGt2Kn <9^ 'ZQYqTT=FIt>tN]sмDQ` nحf??{oy1>@V 5=p||:Zg_~?5dj2@tN+V`nЪ t{Jtu$4]S?rň3Q+vk[sk\_S3+@/x[nTҞ:޵p2N=mvNJ>|tPAf7zsWl #T"g]tF/[ r %ta٫Rf`RJ Q)]_{74ܡK "L蜺4״znٵSK# Ycكg.=vjPgap/1@fz:5j_@\T.@9gM)eCx\ɌVVV̷ GB%:dQ4׊=*wKe@˰\ F<13; ]O @ϡ?Byo ifR@0sD@oGݽ(>u.:,{-oe:07>dIhr;r: Ie}UkIpq z*'jsj;KXNZp:渜ygፍsAKAI]ض䝝݇ *7VDChİ3J3xMÀ51ܬ;:oTt l6dU.h.r3YW͎^u]WNpnEQ`2ЗP(2@Wە +h/0`.5yjKT X 1@oBXcn$ն$)ڍDc.aP5g`(eq-cq\Vh;m7?w4-].*Ŝ5xT͇Dh̡d'9FAWҸ.$9lV܅Q88<=k.Ѕm3пmސ g!%81a-4r'ݰ)F[1ä$",=tO4GF|}_%>rb#O>Бv];Z؝%xZLTC?XU菼nG"XK ٬#+7 $SBLe Dw?1A}(`$B=Rr.ta t,U /y5@UwlBZ4=ari]'rydMPzV=t8@"e*v$0  [Ts\w:Tǁ"x@j^'M&0r!iZZ&EGCAA9e?A3CE {V~(N_tذ 1yѬ+_=k#@G!"[Yc)'™k)@bWuOn320#{t9 ,B2()?1j*LC :T i@:|wv}n%|!sjI˦z/k?zzt~$h'xh}dIKOv^> > ^[71,Rݙb:eyЃЛ"tYV-ЍnC*tm` ޸a@GcIc'W! tKT{ޗVBi/Wth` fQͅ=6P)U;/Z-]Ү/6l4ߏ3s_;O>t5TEUj /H%ϩ&:J+A J~~4 KCw*Kgn=Ɯ;yo9ldET/vAݼyo~1/yQ/似/ɗ~%YAL hT+3F\S<Y ByޒcE@97c{ %e]TN/2@Awl~ܽsu֊HTr !,#j]ҽneceGQ:ciH4)th\WwEHqi0т-v[A{a>wn.6 aц־#}ztnAq;?"O{riۺ);]y摍_I4^O~}#|O*Ѕ=|WӽVCPN yb;J Ftx>8=*ݪd)[PVCw; Rqkw. `[ο|δ<=$1+Lry[{^U;u(^PuGr֖ZH׬- 5Ѽnkjο) ޚd_@,l<5X0zXk٠5..A[xmIo೵ e=tY BJ\J]'?LwP"1^zͼhٷu@geLؘ{J&RNcz<  `5)B 1Ыj<smkKz$,eP8-ӷ rXy(#nejC@%¶>:kv2F'!ZC?O8&Rҡ;"Q$kA]ΗݭeV8j ZJWgA" ƙ[ :\zpqLCln|_l 9|=t@z痆+K_P,쏽vps_D)z Cq GX}yG{gsvMa>2gz꾀 %`NzХ~\7:ܗM4@^gtXc1Zk @$'<:8~nM9.z(zAp?ed<58rO.:gQ-8<ɍR &.+/O/qGO0gX.,]oXy4ޘ†@Q-{lJeRJgW;?H`e +c [=gzc ȔN?V]{A3j>rU@kUkHDS?*C;`ò@$ )|y;o"LV*ӎpR]= [X۷I6 t*t{,;e1Ǚ ps@m߱ P,al5Г|Q;&AẑIC71Ω@ںPZ2$Rz*J4}y# 5Z Og.N=E޸8ioAyOQ=I])hC^!N( @@GjwgDֺ12ѹ2DY}XaGa0]: Wk5 +!]K~[4 ϫ[ tzUA$E&H\]CbKd'( t 5}k?}ߴ5Y2dZ(-e<'ГUXpd>.% ] [?\IkBa7NLqzJЭB/@@?<+Nt[pQ/tͲ٥rU1Z+J_)ǐ%07njkH}\غ}x?AarbBp:wz1= T*ړJYõ]rWųϼW] Ah k3'0{yTxѾZ>CID8GM81t[ GyNA!1,.ag9,N tv "w+b2)kYkL35q*m`79tvqhv{x !.ZG?B[ ojŞ@;hfr=tGp2,㰐qp,rBi=jy=֡Y-3V-౽*:1Fc)s4^i6E9w {H@徻.ga Mpo/ˌ-L'We@3(NtfsFxV"hb}-vXEqۤ2B~nt>#y蟵$lefЅm7} g]@W-E؊ZнL˕gFuNLL L 'T ‡ɔZ?:tY> mSH6͍gE|IЃzf]*N@ ݮ\.P=81w D>~yN 5ܗRU~|iJs{Z%ݺin,5(USl#T 桛'4uЅm7_}㍧8QH- 9D֖sp/iZ6r|ǏYZWӅ>:˗J2:9mvɵ| )J-'2o^9{;{=FtpJs2-:HXm 5} m'ԠJAR@?  nt4c MߨCO>?xR;Ş!ևƀny,M_2HY$_'Et9T~=t{H9{ta=-րn $|NVpdRyg5d.uMy&=靏]ڷ:R)HֈF*nhL4XA-yinu_gw;>{C.Y}Pm]In9 &x T9]57T2.F#VMs$&醁xANUڵxRKPW$]'ؼ.2* SB,НY*pVp=EB- e29{gn7X:ؕ]=y eҼ#waik&M~Gѽt8dqpyݧ| 6Ķ:\ܶzƁB w,_m&+  ;x|D_??ۚO${`NxzЅ ?D;W肭tE\Muq Sxss[~,^Ld:IACwHl뢖<e>\FダלD8iJ`z= K3@H50t~ۆ˅ N޹]}k')®}کS>(|ŵQq{yx5G- uݕNe0=ܮN{;K'Z.][Y8k޸ܝWn^k{ sp'v0҄rB_x$tk4 1mMt:O'u= ']s.q$9%qbzʭa1 t=J5`syv=ԣi[HGt,Л'~6?g_dL=i3Uݷp[#.5C]ӏ5NZME:rHJ.ڙp22ٿ|m8ko=:!*kyQD|nBm]x =BdAYpۓZikty,K,$$U.ЃЫOܺ;RsogY^\ZxjFN)18*L?NT#)J87~TϓMkll;n=KM㍖{ISӵs@O^~7=ۉ>Ms% 5."XwRrl!)t QZ4 [WMHA07eΩKCCsλ~ؾ+Wں\ٷollwy &:ȸ3Uk=gátQÅ"q@HS R銝O])'Fm;-B+u] txH0z@ozPә]mpږ`+4 3!|xJn}xzd_'Ls辁n ,ai_S?;Lm{)+p6(cNJ6pËPa^ u:TsЇy`"9˴DF," U6 ttD)8p;sz16G4Ӳ|W2IUj* f5wv60Yo+D:~Oc uRz@ٜv]]؃΂i1;ũ8VFZ PbNDLW"eN;SsXβ,=(}]A(z#LL+7YBo:2[BRCDP'Joo/uIdxgta ?Qq [H DM 3z9u+L NL$B4fwxE'M)ɒRal)H45y)3Q @ERUہsO<7C\@ ЛwvZ7e ׁ?f?x9@@'_E)tc౫=N=s@ǧ%0DLu](8ͭ_;1E\FZj͏O$>`<ij~@7&'emfgndB,6!zoGJWR/ 'm <˘jTMN6I? ?|d'F:`F]ض]@縝&rZBHe>УA~!w5È;7J)!= 7Ct2؏̟#e M5$M5 `FNm0 I ႇRSw ,0]GPLҢ R`rdS|3^gm@dmrTy'S/ =\tG{@]=*nt-@o t "4-9 íS Wt:: *|@&%aRxDgn:)dž OO$6P,֊EW hB+Dʦ+iJ.G(!/v1@B$PQ@?LI,}@$ IDAT)󃋛8oΚ;r61%=fs dT)iߕ6Amnta7Zە^?,HIFT,!6LۭT䖿GB dt+ʞ" pSg2v(YbbzKiR}0ڥa, Wd-ݦo,n=riJL *ׯׄ..LA0H&NjdRiC*R!uVf3Bi Ѓ=t5=:-v"B(|=΂Im;x8\GY;8Cōp,bDN6HÂ=Nk[B]`׸C:t:j_sBxQR.JV((X tmh[Á~A )̄>!A@*k]ɮ7 :Qݪs"PB;ΉZr\^)`;^! *n >%g*( jD8 3^'^Rf a26)!plޤc:0@)eebz\7Ij` :J ta a|:x@ j:-U r|:|!Wb`#[q F |0!}c*RzHL0hY~S t`&b@ 'S tk[ KLqǐ( NWTo&)1茊sԥaȿC[@ǀPQj}UjUta +PBDžI,2:0cR123 Ťnp|]qyE^q$z'8hwǰXdSpHH20dBsߘ&XLt0U0}Jp]ejRaȆ~vʎ\Hp)x/tpIԂܒ=1@W f, k\-H)\Hh@c^JqZ@ sRJW PtȊ[z\i=da gx,˗ARȅmL@YG> II8jLD);;= Ҽ[Rzr|ҝAOiEqxe$)Ѳ~('} v>t.lж?K`HdpAzР%Llߓ>!> d̡El=>-1g] &񳥥x재փ{GxSV wMz R?u ++)B0wJ%pk7p٘v#^(p"(ίFǃC-p8jy .Lp!C;[!M4ctJazaq!1SbaБWȏ~i@w#FxM pG<w Q\#F 犇J^a,.ߥ:Ő5@{@u1E[^qWAAc6 (͌N`*)+Y 0m ź h0őt>M_[uN|YdIwcAOXp€\]œo;֢f嘼}&x~$8K ?S5ϊ8:W,:[4t!&\$ݺЅ ?@_ `nG D_pN4mW tk kV@(ayZk6:S̮q+衋YyP_:=>'NwˁAbrU)tʜg7]ܫabz*5bؾtcY4c&z"ug{H|d]) NO`]0i7O 4(-foB vױ̺ƪUS=*Q2۩KK3@ Ձo'sA&\gShd&JU qw#mb|;Lcd3Ω pC73)7Anu@ ypF7Gi~S؍EM^Ա/&D`?b@/rMJxy]N|pw{X:Ĩ80Xb' }=ganjhkRli#+y:[Tf@oЛ:r~iA'%/>eL ?0Jt+m <~b_a:>O>pCaCy߶5,T(ΉAZ$ DY<"g(sN &yXtamk0dFh< ):kS jO;ym"@*b] gHc]^prܜV@bb*#4FOUEAn dݎ^;8 >4񇊣1h'vfw ç{o:I:j"@W8=ÂF2.ICa=/GЅm7_}㍧8QK@*GUK)Hed t})4"p.W4vs$!J~^+AT,1@6&K`NO<)d̝t>:H{JDz{Gv=S,9:0sPr0{^ʰE\W3#< 5_}i\vδ_Dk_m7t?&^UJJs!H+'Q,@K4@} qtx@Gu: ЁO=6#LJĢ~0 ; (qZ4G Patn闣/c[:lJ;,2Aw[㮿gjǂ(s9qέ:WRyj7 j(IXoơ{(lIKB@?_?ij???r]hknNץB!bi.:B*JhHcօTAs852SgN FO^#^Y;H+B3+2'Q3'8 p/Wn }3c>^O Yk^C n5G URnLgĉGr|0}G3/:ۢ&!x?a5\7yeay%pl<`pYdb\'s[-tU2oh#r]$Ы}oE@ K2oz՞3ku G3;@{0Tn+z/jG~γU ݼ@J<#H 1Kd@mTef5)$Tel 5v! L.n=}NUܲ#Sh+d"rGw M䥅q|7y@0@\;B=Jb Ju)yE]Cw7wbS@A(eei&g1K:Жvs9]bĎ}!ZZkjiyi}7%1%u;+.1 Ĝbn"gܶ xcyStH#Wirk(űի_rxl%I{ o][]t8ρ3 tFo*W_g'QV@7ڎ<aI8-xIiC'}倾 @oe21Z$^X)] R\~9Y}uEeR:@uV@'2$NR%Myp4-i[+U=YݷjZe*ZUUI,S,GV-ﻶ% SR_FЦ97|* DG_o߃mP]qdA/:/l-Ur#+7"ҙr&[cEQTjGṢN54LtHuҕen. wp:M^P sBL7 DX"!WJ߰I0\Pk\̩|nƵ^W*#{CI@/+d$M,^ S٫>~3*QFY w?إ%ðQ6)Yrn B-L,N'Md6)fi7gv/4El=YZ\ IHcr mmd'wœID"C6Tシ)J@ O[JjItmM(\vTfdlUU-iZ| cTٛDo+J<"ձ_,L U.2h mgxJL,h[ *4NO0٤KEx](.5Ѕz5^(0+NeΌe,RM5P?$ܚ~7^;p:+z`7~LϠN t8vlK= 7u3< MO O/Lϥ8n`ttt=tv-'fħhљzpzÃtHЏ |p`HηvWXTOSvbx{`<=tKdlfc 5ޥљ}{g?:p$K %}24s!$e۱J8VGE(>8 ru!B,}nAh.PvHn.7ZitҧYn|R%&;NrЅL6ȁ^^XRQԷHUyj\)+*\V@'st.~=b~r^Giyd29 q'ާ{ώgQiEL>vˀ^vQcҹi /qX)D ǍÖLsOFɆfr4Qy`m妼nckVK.MZg.Qt\ϴgPKRcF ;&l or=: @u!$K9X0鮬TEӫ "P_rE%/Xi۾]o.r"({9|j*\#z]$+O2eZdSQUx^XH z h)u,ɣL5$7OG|$٤Lbe,Icn請~nFuzo-)jHm#>_KBdE+lc@ [LffKhG.狀pzY3<j&iȾEtN : '@A y4;XTMN =??]|0"ph[zFDOjKژItI[iE$Os\_ls~ߙL\:DP3oeW.!Yո"QÝN]X-rEۡU?Km[=[xjuHhO0#rU=gMLR+5vs/RVs.rig8W X1b; ~._e$|@˻ Bt oƪn?GNsNsE^;2gBz#:Z[s^4ZAt N p}KEG<;2M$(Qza8AKq!A #pfRC[$ Ifq6="1t\oOY.}>C [@ SLf"Ѓ3l; >k9 h`ڀDc%oDqHGJɻQF;\:9Tji['ˮb9$A':~ž_ 3i"LvաJWWi{^qowoO(qn)¿ɰMT;PaWs1:/Ь{(͔3OINk@Ȗ67xy?ƵN !m`!q3zFsӼYĸ'vOLnCߣ^9b>( U>X€U,nG boO΀޴q}Bn{#g>;/C_`F:4g7HdR=YWNE5Z!7M\#]d땫Ft\[lri@wɀ.1A,T1A}{~"&Z…|FfJxr%ЉJ[&?g;]?^@ piDj IDAT8EzQiYYCV)L:ӬU[$=ioG`[1k}N8gEq=Aͱ#_Z=$y~q}c]#MCheESDD4OsR'_ּh1vjk̖߳bIEE0/|Hd(hAЋ@FԨ˗wٵ69sYdybiwɷA(|?{5Xp(BZh>@Y@EF1u{4Yfg Uͧ؋4@਽9AJ`wW@_Hp mh.qGhfz<%ўW3}1!R7Ttj}i7?)BʩDe~s eJi_H?Y!Y֕RǤSbܲ.Vā))ZQڥdtE?t~Sh~^Zs^v!'כJ?|5HWഩ,UNLMx_SS'/EJ2G^/qA=w8/ qG^X&0O<-:su[#aqz0.w/w` #86d`nFxKSt`;T;@/Ah$^t ͆d]X|gyPsKU2%U)Euv5v;U@/R]^-֖S$ $VU/X=v~O帚fCiwހ@>pzx2i=⣗ ғ%ZW%T1s*V%m[doRzhJkv6K)x˷/q~C\v{/-7–?|>Xȇ"JF @>dlqG=F0mrh4W]+p`#? YI{>~[v⊊SYh0S Y{>VHE-A 7@Vz -?zG9Rڹ1֌Fe }e\&@Z-:|}q{Vg{72|.Hݭ?V0E싵 rH= (7dz|n^D=/ny^>ik~"9Pu=#?gy&״|M~?}Ϳ۷ ~?|$U^JQc@'E0%=♴}™}[}-oߓ<ʀf̲@ge31 bQd8 qLg>@or3@PcDK}`}l 蝈 >$a5KWo$NCzr'BmnjW\5HQ<Ϩw+?VR{JWp3~HYnT1^$RaV<b|H?0a==βe~KtW5uCPBHxlm+ 5&b\z{ "H q Md@9#d{8<5Ѓ\4#/ $H܂ tvʖiڵSgG zuIʚ?/+|Fg8oqa_M(vNt ¬z)gM  aWu*ƍH`M$8Քfjke"d zjЅ9nQ R1l)'+- @A輧ۤW RSK|Թ$ nVh褍=_*,.|JSV:5jlBHڣ(7Aۆ~d:?`75t^"мլN3߆I{{@o$^m@oa At5:"2TuU]3X^U>~~m /pU!HO_E`w>aB t7k}s;W3x)i|?6)ı tC|b ֜\/?Z|v,/"]$ICGHRE"0M@\ :.kR v@g5W=[ vêH,*ҊN{_zٻX.}F4{L0e+N Vǖ(MOzá[p׮,o#;; hD6F!ܳڻ&-t=о0Յ{#tS^#v( #ãx MP}wIMK5ڊ?8:0gyǝI\^ A 2R>6>[¶y- .s55Y]2̴J%(N@FF0Cn}/Y6!{/S td{{g=mX2>gǿ$@Act/w-p݉V2?{S^H t&m5\iAܾuypktDM˽[!@ Aۛ:Y)/8/4UPvK_Kl/z9}WXM#+ ھ×^ì7b%X:vd) @OdioLHop袰c$L#:}xh"}yz_y@2\mqgZYazrs tL+e!@' #,˽ ejh?ATWk ْ<_G֥kidtm ?ɣ]Z|潷~ʸKvbY!n}~>Ryn)kM 'eL%3cAy;#?ow$3Gxo" &o/\FLf/ B:jAs*05F ќdDz3陑Zaw"NdF݆Ho$5 M3c+A:Zx7zYZh.MAz[_Atf:dyUiQ5{3SlI~  ]_3tbDW# o{ӿj'9hE~cicjI[Jr{w-Gk wQ3$H7V$Sc( kYׁdɂD}d͡B| :hη:\Zˉ]i 9􂃏thR_woz:<.Iܫ5Oע !]e 9d7xG6J`j7,19J5z606;ʗ~[H&y_ w68utx7RU]Mg[@[AJ9h=4ty.hv!cs;o5ϴ5\PMݪz*ˊbg|R}]y=bO7v?ۻmr=# :mؒ@d9f~m[G/|+]HMM~[X\\?Ȕm-9+Cwi^˝onek*lYNK>=鑽4L]OV$Iz MH4'Զ[渾9na Яpzt4=<5{huz:î;'زgZMՁǿ۴ܺ v$hbS(K){`8 ph㺷йy>:FEe>;j@/0W˥;Ց=<+>C~K  Sc9s[%/@wmrO*WZ}tjX$Zl R}:3 U˰9W`_# _}.mC%m4¡B)D<&~yu-ں@/*ž finu ?ּ];c,`EAn;sB5 HeCDM̂{D:N,Eo&#`Rݾa uZO%A[CZmgSgTW@狢(9GB'eze]:F'lEUlM,w;mRkjN2y=ur_1kv睿,BzZl~[Hs\tO#O  @Wl[X辶\Lfde!@/\ %)!2#B0}cl<%JđBY 4e&,4L^"Et܌&+}Vi~@llmaG9nVb474 Mw8yۂKcq禆Lf,=0ԛY*;8ƺz>ӊĺS _oH7ڰ;wf+o@o:e9nLd=9=55Tt1Gby/dcvFZZZYYh]Jo^no'=/bOF/Aʳ9KV;~u-tjJ]WV^YYYZU;%o\B}A,MthβZE_?=:`5lcK~m{֑ťL8npree1f`L gx_I{oOL:N,0Owf7Bd3ik?}AUv8}UHxԶ{9-Tm{/;5xSH*$yT$ lh΢ Y0Q8l(DFiS56o8nIiA|:.33g*!Z9͑O/ a/y2ܠwX赣\4#qa&HPATLR/ 7V|OOGZդr3ZmG]洗+9l4*}xsן@oILufgѯn21:a.LeTP8,8500Z`@'tݛH@_X̤{{ҙ vڝǣ1abM6@)ѰaAD뙖jd[}Z]g '%޹h=D|d],y` }||k盇]&|#,\AqĽ!QbLciQBNnwb9_W ZMڙ2_x7W'8A &/.|ͫ/CuX70-'V랣@O48 lmv!K]OȀAOmr~ -q>4~}d%eeoS)St{K cZXe! -g +|w98Ձ^MՌDkӬun8GToCQ.ӕ«F(У ѮlnMm [݁pvz ia`F[4h}9Gku= NpӾ9tFD'ݜ\щT'x7Aq9-&t}RU]b"/TԎ=k[4:<X@ϧhM&W? Ѥ:#6C| ϴ#@{h'/w#b3!Kws=tkCiC8> tТKɵv|^ߋ\q&H"bmG=o XR~TXeKuvIkb07TO Wa|ݹ:M1йD6q(;Q:v{ظ^y=8}!!f4WƅY]M>D>OÆILh_kk+X77z+'!I` 9Qhe|zU}F pnt6 R@'uebJ[uexI|E]E@W[褘;[- .Go᧟i܋G; mǭЬ@mb4bnt\8}Dv}qyORt&fQjA<9V3sYgOr2 Ma7vdDTHt~(6p/nB<ﲂ&g|.T3Ӿ+]Bv@k~C\\hF(dG9evhް!`_L'Bj@G˻}p}Ǐ"R'[B]RNm4EVzbJU@.ȱ gr\tFxE a-̢N½55 6U;鎹D5J#ߒ9,t֯>@.o7Ey*S,S'쫟AF{n Y O!m:ɟqܢ1=:+軰|@WI5>hwYktCCraooF&`B h @d#mzV@׮^Pꔭx?u@SS)tᝊKjVU,tZ,tl'VQ%Z3WbwGhOm\Z~N Af IDATit0ooǽ8꺕8cX™Y辥VDl{b-$LmMxpzўᑩQ|9#z纗f%^z=Z3M-N,@{%ɮ(|S7womY>ItYPڨ tN{h %}5![n05Q56$l-gi7[Y|K'z9͵h]37 tu@o_D@JEf{}lz̀nh .0IdSB ti07DoXү6̠@'Q 'Y^ja@װ]s7]~,tϦbTGс.usݳ,}kahE7R4o|p.KmDSnxr}HNWtC紀 yXdx>#C VEzn [o|5t z^W8]9NhX$ ݩ8mf_@6 o 79Ee/ {ܛ"#kDt|Qڑys^5q,#J0.!|O-{k|yOe5߀@7ޥLfl:+tiz2#ؘLM{>&/m\M1YdI+ID E+-B7BuYM}\/ sz %(z'zy}?,e_q!l?8CL&2TR RS)ĥ7I_,eS%qRTS;4]d7^PwgRQ>/;Ýq|7嘾kT@'e\tWS&F-tC#&Ҵ,t~ݫ{f6hc>q7loZ><㝕2cP'pg K"w(Yz4\\X*"̇trK"~v(qV79жiK-f Ym<=2o󍐹tb tVt[k0]):@]?kZ^HrwHn}+?jW:i~HzŤY tv ٌܬsgv MIj2dSM(A&ˋeYP^dڪϯ ::O[ia[2_؛C"@ ou$oWAqcʐ=aA@:Of2ӍEϰM%3ɚ1Ivdbdp`}Fs@XT`pg<>dtxf.2Atzhtk9%T}E\NUh% t:O蟙,YZ4+K 1nRmMQ-PlkFBT65-x#md 6J{o3mܢ2uJ!,V㕲"ESբ6Hm)-TVYO|3es@۬ueжd8z3g746:o l\^Y m @ J<·rѭ tsf1> G'rڅe :~4^9G&ݏN5c,k~( gܙOWT*Fr]nՉ}OEE fI '>:?bվnecḏ_.ʕ@7]@mc'|.لu 5Z|,N3ܜrb1Jˬ@ s$Sz.uN4]JZAМ*ihji(c{&7am3:GGi; Dwݬ3c}NB!i?w5/еì>}Z&6H R4O[EsD=L6*}oI31:hӫ-^K# >.M~T?,Nn7iNϦJ\f8M'ӳJzr8ӊh;%IXyh6vP?^,֙*^sw0O[5^uģ!@7jDeR]iE-K-5$UMIY?T,lg!&ި:]; 3A萶v?71ןK zs=JB:F.HaT;kTTj2ǰj^ϣXXownu\B'uۥ1q$ E\@קΝ{G^;DZΝ:~ȫNroEvQ9ˈI_蕹" V .΅[RFW9EBPڞ=߿#(=_C[{OO=={uΜ:f꺬SEAЫk6mfk$kU[\7e׾@hvjlw1Be.D"|/s_ܻ4962 HϮ-,ml1)%B##S)+k 4=9һT@m.H_5#w"NM[{.QW@7zl]~^-w8nC"o6Ы,1ɻN:,EJoEc A X6w|t"R;z@:3269q`&3q] lG{ቑޑt7i@)p#c#8Xw%Цz~cxnlldbg?(h0 Bt[#meJ+2w[eiS]z^pм$ ݤ\t=JEqPW>/6_8\3dGd[{;v/,Nxd8 0pP7=g +! K}N3$xp2oH_DxtOst1g2.ռ8c>uݟ`f}e^ Ɯ@UZj+%[,E)ؓnR|=1:hl<"}DƹI]ZQ}r}8B\!deƶ?p ..O>/>q˗kj##ZT9\gмvU[$]*-.:"mwJ@\K270 :r 57!+Zbئ@WOpci,v$4,Ǎ:hgx:WؚіO g_$@/8)z5ηJwj=BkV , 3:.:Lt\ oQ[9n)D` =+z[2z{n5w st@C-m z(`h+a:q1߃ @}|n*s㊀~v+KBļ{+"bMߌEҪoE(y!%iL36G~\t_pOkb2 ׹Hj]7wXpx*Ԏ70AՏK5A,m{弢܍2|6^ +>D6N~ޱ[>:Td/MOVIegS\lZ$u|UriЫ_]e:h{}B]ug4_8S#G1N iORknƩe>o< 9>]5:HЉKU_ ϧ A5<_ā'N|=+7?mW:7Ypv:.: 7@m4!}>GqYĸB_H\9L_n_J =͏2GnxGhHhkk}Mt\$o`f}`x푃Oy+^?}嗿~ubUt@{*I?$֋sH :K9sٓ)udjP2];oQG9n+~IuaFe.# 0Ȳн8`X&B`!FMO$޳$*xtЏv>afy\O@+rg?2'>:YsNjyAIIi.:qWw^\oQOa/ܽ\kZ7H87bkcuzGZ' +H#:G:*յ,78ټa9rl~xھ6|U~r(zj:RUn{% :.ƭ4Q.]BeG)Г]\=^QWY23ar2v5帚Y:hGÑ&.]@>ھOlZBF%ќ,#-s&ZqR@zܱwoILJKdS31+ol`QQkMkD{Qqu>Їe@7>~? @mB?$[O;s]t>ܰWx~rF:v9a]u- ]:!Z&d#c0B+*|]YIPE˸; o tFu=>J1nƴw<B~d H:};P ߳C 3zG*͍ 7#c 5vԓ6_ Xx'"Q~ @[oui/ma+ۚz.d$weW3[(_e@vjzHo-d2x7n6`TE }Q.6:7+ΆX[zz6<2ǖb+źy_OLB ٩Q\Gd*Y K~w@g[IBJ[@ʏt?s]Bw* 5cYg42?^*p2ț=FOj7B蠍zÅ*@X|gi{jh]Ѻh0bs^cУN>7Ig(\l8*>/o 1hF=үp֟Mey'Us#үl{u ݣX.(wMM zZJ]RP&S@Y"ЩZ+h=_, G[NG-r.FJOQIv00r倡Wky$Oebl[W t}DOJiUUq{wݭ t*@ׅJm*-Z;Ⅲ1Mwo)+{*V'At~eJ|xjj"?64fi[;;5֋j$2C?oŇL'0rav67꽳sJ㾆^ij{[AzA9A'8%z~-}:o{3, -*W_nijQ8 i#-KLq7RO'sH" @j uw;lw!P߰)ݚn/eo`Cg`##."ANZR? ,tY"'FM3^f@vIJbʳ6*Ww낂jIRhi/ g{?ǯ_jikb@94U ?r̋鱅2X tX'Łu=ߴ_S(;#2އnBG>ul۽?}_=Th&[ru*Vu5re@2" ~ :ktZ8,PjAzXOg 'UHЏ?+9YW:]2Z/ Kl3e@꼫CȇZ :@/p2eU-tZjr'KRF-wwU_kC-ʢ.佱.-2TQF:ntK !CtS~~yXU}xrwU@9T]٪ rs5NՓ=#@A7Y'Q!p;)"_?˵bE*(+7H9+3f=a˨ c :!nXH+/v8vJ}@~ZMq*Ҡ Rl{emh@AtFYh6FVT#N+T+^|\ǘ:iʀ-KAtY~w9fpgWVI{uCo6:LLy4" |ZẖD BFXq@m<ˍp@tЕw])R,$n9&l5}&$<P :h=ʫEtYcEVG@2;ZՁnwhVj-U ?*cR]ٞt#t AtІ],Re5*:芫@:ifӒlfeaՁlSG ƨ|JeEjuZd@At;wtni!Հ;Bth5#Vzܷ+<,p]~Q} gWJ3'k@OH3/[<+r)O͎:;u0Nܡ;dD2a(Od'}~@7Ӑ8bۈG:hcJ"8\(tyM7ߚO_2Ea J|{o~^sJ}m"?\{DͭMsy""axt{ͷBZo_d1hXH/0&?B9ɶ' 9HEFYH?kuA p8~;\(UFԠrYM$n1YJJVӂ >IRܛXKU^S6Mceh2db~:bkT}u9Z @m8С;@GDgap6g:GS^dYʔ̕ZM eg]݆le.#mʄs8:R$EK:6kO66 ;"yܻT 7MJFkBǖj:^GI8?lbf-\>0tFudCyT#kK"CcVSwH9G *cNt5c3_@[K>撆#cShO&C :h[|JȺ.q m#Nw$!tr[&ЋMܔ>Y5:z:&YSuqнwAZ~ܩ]όt]ޙY(s\9$Ǝo?+t&CכT>|bӻOv,@[@Y(wt3}GN ثZ122;l&@늈wۦpT5JJ5jcB$EݍB&d.)݊At}Xҕv&ikLJ^ʖT.':]ɖU-zR6|$_t)ʺW Unh;@A[oTq{V) 4+2lrUwrstn1kE՗a u [rOIF"7y NM@mmD\HDIcVJ_&0i%unnqj6yS^_ eeV&6j;@A[;4\jۦ1ť-d2v)ʺZ.4^։M trKUps= WAt}!a/fjUEZ#!9ijI>3(S,$S,.+^lo!g%c]fVtu7 6n/m -OK$/.[j8$y钇Y ymJ lVUzE _G} W}{/ :uZ dmI|Lo,v!^.5u謶*5߉,f#5ixJ `@gg/"_-NtnބtІ}m%P,@H߹sG^z^&ևc hw-fhJJ;Y!pJFotb !nHtjWZAU 蒏3{:A?{@߹h+< nn,F)-8n,Tny}VVw@3*.]U;!DN{Vm<С;@PDHn󰬶jNW+;-+$MlGHvb]Rˁ.)cß8?Kdԑ=w_ :  wGe)8GcԪQUE<$yL^5=ZyK:NQi^D[EV!Ǐ@[UZ@wC:rj_;n;wAޢQv;euz"{le6.-"+^Ћ̲:5쫋sU;~j@ٝwZC'zBt Ψκbih~Ls]8+VuZv{9) +K3~Lty<$k~L&Uqy]t%G :h}GDƺl$-o2V]hm*_D/+䵳*'uj@'m`[18@|;ȝUntt̀cGOd5^eَ2]dtR.W3TUm_[S7pa69!e62󕊬t{߷n~M(MGp7a!]FK;=18՚@T,,s%~PfIzLG UP/"Z2@SyW t?;\OPX<:~Ƀ}e^baC5K>PnD6<-KkpD@J?@~ޛ'0ttPBCCێ“ @G|h54j^ ;5Z}Zc^?ٙ%nn5SP@W n yS.yC3w U- UTg}~@O>w(:ڱ @tzx{/QhS~24gAzA  At : : : :@At:@A[?()}RdA~6+@t@@tu@+)_OF (*wD:<5h @tz-t:@At@At@At@t 6o_\@Cgw4“ @]6:;M>x0u! ~At9 iY@t:kF6d'<@tЕ7zmyG!m @ByGn߱cxbbǎI"(!/n+ۚы,}a_YRKGhܱ(,I'::ޖE ҃nC#hf"a\Obd>Irl s_=fs˾mh>}lIzi]ʬɮkmQcK)|RI_Wv4k^QǢo[Ϯ+KٌNB/o[Sy)d%7`R m+$.e}#{6|sFHGEe7Rq￲ew y :h@COfi2z?I^;(7*=/hgtZ/yq_ ?:?Sa$݉i =Iby_Dwl/Os se̱G~O5.eRމY#4sɥQmg}"ߋcK>;D~II.e3?Cx}ȋ%/eg/'o^eRs ^Ev)I/_J/%h,u֥|}FvFH&2|~ &OJ/7y&ţKIor{uG?}tGy}wًlӝCEyh*~IPe op;:BGKwo?3He :;^4\G҃E#kF5!N=;(~zG| گ/~%oݶt-)хs_>i'D't E߶ѫy)z^tRr)wF֓w>}At /e=:hm\u=]ε}[U:UB߶]W.e/q+zξRхε\k%r_$qUi?{ ֥F׋ݢws.eF.im79͗׼ɇ’KM_`__. The python-oracledb driver is an open source module that enables Python programs to access Oracle Database. It conforms to the `Python Database API v2.0 Specification `__ with a considerable number of additions and a couple of exclusions. This module is currently tested with Python 3.6, 3.7, 3.8, 3.9, 3.10 and 3.11 against Oracle Database 21c, 19c, 18c, 12c, and 11.2. **python-oracledb** is distributed under an open-source :ref:`license `. Changes in python-oracledb releases can be found in the :ref:`release notes `. You can use assistive technology products, such as screen readers, while you work with the python-oracledb documentation. You can also use the keyboard instead of the mouse. User Guide ========== .. toctree:: :numbered: :maxdepth: 3 user_guide/introduction.rst user_guide/installation.rst user_guide/initialization.rst user_guide/connection_handling.rst user_guide/sql_execution.rst user_guide/plsql_execution.rst user_guide/bind.rst user_guide/batch_statement.rst user_guide/txn_management.rst user_guide/tuning.rst user_guide/lob_data.rst user_guide/json_data_type.rst user_guide/xml_data_type.rst user_guide/soda.rst user_guide/aq.rst user_guide/cqn.rst user_guide/two_phase_commit.rst user_guide/startup.rst user_guide/ha.rst user_guide/tracing.rst user_guide/globalization.rst user_guide/exception_handling.rst user_guide/appendix_a.rst user_guide/appendix_b.rst user_guide/appendix_c.rst API Manual ========== .. toctree:: :numbered: :maxdepth: 3 api_manual/module.rst api_manual/defaults.rst api_manual/connection.rst api_manual/connect_param.rst api_manual/connection_pool.rst api_manual/pool_params.rst api_manual/cursor.rst api_manual/variable.rst api_manual/subscription.rst api_manual/lob.rst api_manual/dbobject_type.rst api_manual/aq.rst api_manual/soda.rst api_manual/deprecations.rst .. toctree:: :maxdepth: 1 release_notes.rst license.rst Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` python-oracledb-1.2.1/doc/src/license.rst000066400000000000000000000306421434177474600203430ustar00rootroot00000000000000:orphan: .. _license: ******* License ******* .. include:: .. centered:: **LICENSE AGREEMENT FOR python-oracledb** Copyright |copy| 2016, 2022 Oracle and/or its affiliates. This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. If you elect to accept the software under the Apache License, Version 2.0, the following applies: Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. The Universal Permissive License (UPL), Version 1.0 =================================================== Subject to the condition set forth below, permission is hereby granted to any person obtaining a copy of this software, associated documentation and/or data (collectively the "Software"), free of charge and under any and all copyright rights in the Software, and any and all patent rights owned or freely licensable by each licensor hereunder covering either (i) the unmodified Software as contributed to or provided by such licensor, or (ii) the Larger Works (as defined below), to deal in both (a) the Software, and (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if one is included with the Software (each a "Larger Work" to which the Software is contributed by such licensors), without restriction, including without limitation the rights to copy, create derivative works of, display, perform, and distribute the Software and make, use, sell, offer for sale, import, export, have made, and have sold the Software and the Larger Work(s), and to sublicense the foregoing rights on either these or other terms. This license is subject to the following condition: The above copyright notice and either this complete permission notice or at a minimum a reference to the UPL must be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Apache License ============== Version 2.0, January 2004 TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. **Definitions**. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. **Grant of Copyright License.** Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. **Grant of Patent License.** Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. **Redistribution.** You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and 2. You must cause any modified files to carry prominent notices stating that You changed the files; and 3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. **Submission of Contributions.** Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. **Trademarks.** This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. **Disclaimer of Warranty.** Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. **Limitation of Liability.** In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. **Accepting Warranty or Additional Liability.** While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS python-oracledb-1.2.1/doc/src/release_notes.rst000066400000000000000000003653061434177474600215610ustar00rootroot00000000000000:orphan: .. _releasenotes: python-oracledb Release Notes ============================= For deprecations, see :ref:`Deprecations `. oracledb 1.2.1 (December 2022) ------------------------------ Thin Mode Changes +++++++++++++++++ #) Fixed bug determining RETURNING binds in a SQL statement when RETURNING and INTO keywords are not separated by spaces, but are separated by other whitespace characters (`issue 104 `__). #) Fixed bug determining bind variables when found between two comment blocks (`issue 105 `__). Thick Mode Changes ++++++++++++++++++ #) Fixed bug creating a homogeneous connection pool with a proxy user (`issue 101 `__). #) Fixed bug closing a SODA document cursor explicitly (instead of simply allowing it to be closed automatically when it goes out of scope). #) Fixed bug when calling :meth:`Subscription.registerquery()` with bind values. #) Fixed bug that caused :data:`Message.dbname` to always be the value `None`. Common Changes ++++++++++++++ #) Corrected ``__repr__()`` of connections to include the actual class name instead of a hard-coded ``oracledb``. oracledb 1.2.0 (November 2022) ------------------------------ Thin Mode Changes +++++++++++++++++ #) Added support for binding and fetching data of type :data:`~oracledb.DB_TYPE_OBJECT`. Note that some of the error codes and messages have changed as a result: DPY errors are raised instead of ones specific to ODPI-C and OCI (`issue 43 `__). #) Added support for fetching SYS.XMLTYPE data as strings. Note that unlike in Thick mode, fetching longer values does not require using ``XMLTYPE.GETCLOBVAL()``. #) Added support for using a wallet for one-way TLS connections, rather than requiring OS recognition of certificates (`issue 65 `__). #) Added support for connecting to CMAN using ``(SOURCE_ROUTE=YES)`` in the connect string (`issue 81 `__). #) Fixed bug when fetching nested cursors with more columns than the parent cursor. #) Fixed bug preventing a cursor from being reused after it was bound as a REF CURSOR to a PL/SQL block that closes it. #) Fixed bug preventing binding OUT data of type :data:`~oracledb.DB_TYPE_UROWID` that exceeds 3950 bytes in length. #) Fixed bug preventing correct parsing of connect descriptors with both ``ADDRESS`` and ``ADDRESS_LIST`` components at the same level. #) The complete connect string is now sent to the server instead of just the actual components being used. This is important for some configurations. #) Fixed bug resulting in an internal protocol error when handling database responses. #) Fixed bug when calling :meth:`Cursor.executemany()` with the `batcherrors` parameter set to `True` multiple times with each call resulting in at least one batch error. Thick Mode Changes ++++++++++++++++++ #) Connections acquired from a homogeneous pool now show the username and dsn to which they are connected in their repr(). Common Changes ++++++++++++++ #) Added support for Python 3.11. #) Added attribute :attr:`DbObjectType.package_name` which contains the name of the package if the type is a PL/SQL type (otherwise, it will be `None`). #) Added sample for loading data from a CSV file. #) Improved test suite and documentation. oracledb 1.1.1 (September 2022) ------------------------------- Thin Mode Changes +++++++++++++++++ #) Fixed bug that prevented binding data of types :data:`~oracledb.DB_TYPE_ROWID` and :data:`~oracledb.DB_TYPE_UROWID`. #) Fixed bug that caused :meth:`Connection.is_healthy()` to return `True` after a connection has been killed. #) Internally, before a connection is returned from a pool, perform additional checks in order to avoid returning a dead connection from the pool. Thick Mode Changes ++++++++++++++++++ #) Fixed bug returning metadata of SODA documents inserted into a collection using :meth:`SodaCollection.saveAndGet()`. Common Changes ++++++++++++++ #) Fixed type checking errors (`issue 52 `__). #) Enhanced type checking (`issue 54 `__), (`issue 60 `__). #) The mode of python-oracledb is now fixed only after a call to :meth:`oracledb.init_oracle_client()`, :meth:`oracledb.connect()` or :meth:`oracledb.create_pool()` has completed successfully (`issue 44 `__). #) Improved test suite and documentation. oracledb 1.1.0 (September 2022) ------------------------------- Thin Mode Changes +++++++++++++++++ #) Added support for getting the LOB chunk size (`issue 14 `__). #) The error `DPY-2030: LOB offset must be greater than zero` is now raised when the offset parameter to :func:`LOB.read()` is zero or negative (`issue 13 `__). #) Internally, before a connection is returned from a pool, check for control packets from the server (which may inform the client that the connection needs to be closed and a new one established). #) Internally make use of the `TCP_NODELAY` socket option to remove delays in socket reads. #) Fixed bug when calling :func:`Cursor.parse()` multiple times with the same SQL statement. #) Fixed bug that prevented connecting to Oracle Database 12.1.0.1. #) Fixed bug that prevented the database error message from being returned when connecting to a database that the listener configuration file states exists but actually doesn't (`issue 51 `__). #) The error `DPY-3016: python-oracledb thin mode cannot be used because the cryptography package is not installed` is now raised when the cryptography package is not installed, instead of an ImportError. This allows platforms that are not capable of building the cryptography package to still use Thick mode. #) Fixed bug that prevented the `full_code` attribute from being populated on the errors returned by :func:`Cursor.getbatcherrors()`. Thick Mode Changes ++++++++++++++++++ #) Added support for getting the message id of the AQ message which generated a notification. #) Added support for enqueuing and dequeing AQ messages as JSON. #) Added the ability to use `externalauth` as a connection parameter for standalone connections in addition to creating pools. For standalone connections, this parameter is optional. Common Changes ++++++++++++++ #) Added support for Azure Active Directory OAuth 2.0 and Oracle Cloud Infrastructure Identity and Access Management (IAM) token authentication via the new parameter `access_token` to :func:`oracledb.connect()` and :func:`oracledb.create_pool()`. #) Added method :func:`oracledb.is_thin_mode()` to support determining whether the driver is using Thin mode or not (`issue 16 `__). #) Improved samples and documentation. oracledb 1.0.3 (August 2022) ---------------------------- Thin Mode Changes +++++++++++++++++ #) The error `DPY-3015: password verifier type is not supported by python-oracledb in thin mode` is now raised when the database sends a password challenge with a verifier type that is not recognized, instead of `ORA-01017: invalid username/password` (`issue 26 `__). #) Fixed bug with handling of redirect data returned by some SCAN listeners (`issue 39 `__). #) Fixed bug with re-execution of SQL that requires a define, such as occurs when setting `oracledb.defaults.fetch_lobs` to the value `False` (`issue 41 `__). #) Fixed bug that prevented cursors from implicit results sets from being closed. Common Changes ++++++++++++++ #) Fixed bug with the deferral of type assignment when creating variables for :func:`Cursor.executemany()` (`issue 35 `__). oracledb 1.0.2 (July 2022) -------------------------- Thin Mode Changes +++++++++++++++++ #) Connecting to a database with national character set `UTF8` is now supported; an error is now raised only when the first attempt to use NCHAR, NVARCHAR2 or NCLOB data is made (`issue 16 `__). #) Fixed a bug when calling `cursor.executemany()` with a PL/SQL statement and a single row of data (`issue 30 `__). #) When using the connection parameter `https_proxy` while using protocol `tcp`, a more meaningful exception is now raised: `DPY-2029: https_proxy requires use of the tcps protocol`. #) Fixed a bug that caused TLS renegotiation to be skipped in some configurations, thereby causing the connection to fail to be established (https://github.com/oracle/python-oracledb/discussions/34). Thick Mode Changes ++++++++++++++++++ #) Fixed the ability to use external authentication with connection pools. Common Changes ++++++++++++++ #) The compiler flag ``-arch x86_64`` no longer needs to be explicitly specified when building from source code on macOS (Intel x86) without Universal Python binaries. #) Binary packages have been added for the Linux ARM 64-bit platform. #) Improved samples and documentation. oracledb 1.0.1 (June 2022) -------------------------- Thin Mode Changes +++++++++++++++++ #) Added support for multiple aliases in one entry in tnsnames.ora (`issue 3 `__). #) Fixed connection retry count handling to work in cases where the database listener is running but the service is down (`issue 3 `__). #) Return the same value for TIMESTAMP WITH TIME ZONE columns as thick mode (`issue 7 `__). #) Fixed order in which bind data is sent to the server when LONG and non-LONG column data is interspersed (`issue 12 `__). #) If an error occurs during the creation of a connection to the database, the error is wrapped by DPY-6005 (so that it can be caught with an exception handler on class oracledb.DatabaseError). #) Ensured that errors occurring during fetch are detected consistently. #) Fixed issue when fetching null values in implicit results. #) Small performance optimization when sending column metadata. Thick Mode Changes ++++++++++++++++++ #) Fixed the ability to create bequeath connections to a local database. #) Fixed issue fetching NCLOB columns with `oracledb.defaults.fetch_lobs = False`. Common Changes ++++++++++++++ #) Fixed issue where unconstrained numbers containing integer values would be fetched as floats when `oracledb.defaults.fetch_lobs = False`. (`issue 15 `__). #) Ensured connection error messages contain the function name instead of ``wrapped()``. #) Improved samples, including adding a Dockerfile that starts a container with a running database and the samples. #) A binary package has been added for Python 3.7 on macOS (Intel x86). #) Improved documentation. oracledb 1.0.0 (May 2022) ------------------------- #) Renamed cx_Oracle to python-oracledb. See :ref:`upgradecomparison`. #) Python-oracledb is a 'Thin' driver by default that connects directly to Oracle Database. Optional use of Oracle Client libraries enables a :ref:`'Thick' mode ` with some additional functionality. Both modes support the Python Database API v2.0 Specification. #) Added a :attr:`Connection.thin` attribute which shows whether the connection was established in the python-oracledb Thin mode or Thick mode. #) Creating connections or connection pools now requires :ref:`keyword parameters ` be passed. This brings python-oracledb into compliance with the Python Database API specification PEP 249. #) Threaded mode is now always enabled for standalone connections (Thick mode). #) The function :func:`oracledb.init_oracle_client()` must now always be called to load Oracle Client libraries, which enables Thick mode. #) Allow :meth:`oracledb.init_oracle_client` to be called multiple times in each process as long as the same parameters are used each time. #) Improved some :ref:`connection and binding error messages ` (Thin mode only). #) Added :ref:`oracledb.defaults ` containing attributes that can be used to adjust the default behavior of the python-oracledb driver. In particular ``oracledb.defaults.fetch_lobs`` obsoletes the need for a :ref:`LOB type handler ` . #) Added a :ref:`ConnectParams Class ` which provides the ability to define connection parameters in one place. #) Added a :ref:`PoolParams Class ` which provides the ability to define pool parameters in one place. #) Added a :ref:`ConnectionPool Class ` which is equivalent to the SessionPool class previously used in cx_Oracle. The new :func:`oracledb.create_pool()` function is now the preferred method for creating connection pools. #) Changed the default :func:`oracledb.create_pool()` ``getmode`` parameter value to :data:`~oracledb.POOL_GETMODE_WAIT` to remove potential transient errors when calling :meth:`ConnectionPool.acquire()` during pool growth. #) Connection pools in python-oracledb Thin mode support all :ref:`connection mode privileges `. #) Added new :ref:`Two-phase commit ` functionality. #) Added :meth:`Connection.is_healthy()` to do a local check of a connection's health. #) Added a boolean parameter ``cache_statement`` to :meth:`Cursor.prepare()`, giving applications control over statement caching. #) Made improvements to statement cache invalidation (Thin mode only) #) Added a :attr:`~Messageproperties.recipient` attribute to support recipient lists in :ref:`Oracle Advanced Queuing `. #) Added a :attr:`~oracledb._Error.full_code` attribute to the Error object giving the top-level error prefix and the error number. #) Added a :data:`~oracledb.DB_TYPE_LONG_NVARCHAR` constant. cx_Oracle 8.3 (November 2021) ----------------------------- #) Updated embedded ODPI-C to `version 4.3.0 `__. #) Added official support for Python 3.10. #) Support for dequeuing messages from Oracle Transactional Event Queue (TEQ) queues was restored. #) Corrected calculation of attribute :data:`MessageProperties.msgid`. Note that the attribute is now also read only. #) Binary integer variables now explicitly convert values to integers (since implicit conversion to integer has become an error in Python 3.10) and values that are not `int`, `float` or `decimal.Decimal` are explicitly rejected. #) Improved samples and test suite. cx_Oracle 8.2.1 (June 2021) --------------------------- #) Updated embedded ODPI-C to `version 4.2.1 `__. #) Added support for caching the database version in pooled connections with Oracle Client 19 and earlier (later Oracle Clients handle this caching internally). This optimization eliminates a round-trip previously often required when reusing a pooled connection. #) Fixed a regression with error messages when creating a connection fails. #) Fixed crash when using the deprecated parameter name `keywordParameters` with :meth:`Cursor.callproc()`. #) Improved documentation and the test suite. cx_Oracle 8.2 (May 2021) ------------------------ #) Updated embedded ODPI-C to `version 4.2.0 `__. #) Threaded mode is now always enabled when creating connection pools with :meth:`cx_Oracle.SessionPool()`. Any `threaded` parameter value is ignored. #) Added :meth:`SessionPool.reconfigure()` to support pool reconfiguration. This method provides the ability to change properties such as the size of existing pools instead of having to restart the application or create a new pool. #) Added parameter `max_sessions_per_shard` to :meth:`cx_Oracle.SessionPool()` to allow configuration of the maximum number of sessions per shard in the pool. In addition, the attribute :data:`SessionPool.max_sessions_per_shard` was added in order to permit making adjustments after the pool has been created. They are usable when using Oracle Client version 18.3 and higher. #) Added parameter `stmtcachesize` to :meth:`cx_Oracle.connect()` and :meth:`cx_Oracle.SessionPool()` in order to permit specifying the size of the statement cache during the creation of pools and standalone connections. #) Added parameter `ping_interval` to :meth:`cx_Oracle.SessionPool()` to specify the ping interval when acquiring pooled connections. In addition, the attribute :data:`SessionPool.ping_interval` was added in order to permit making adjustments after the pool has been created. In previous cx_Oracle releases a fixed ping interval of 60 seconds was used. #) Added parameter `soda_metadata_cache` to :meth:`cx_Oracle.SessionPool()` for :ref:`SODA metadata cache ` support. In addition, the attribute :data:`SessionPool.soda_metadata_cache` was added in order to permit making adjustments after the pool has been created. This feature significantly improves the performance of methods :meth:`SodaDatabase.createCollection()` (when not specifying a value for the metadata parameter) and :meth:`SodaDatabase.openCollection()`. Caching is available when using Oracle Client version 21.3 and higher (or Oracle Client 19 from 19.11). #) Added support for supplying hints to SODA operations. A new non-terminal method :meth:`~SodaOperation.hint()` was added and a `hint` parameter was added to the methods :meth:`SodaCollection.insertOneAndGet()`, :meth:`SodaCollection.insertManyAndGet()` and :meth:`SodaCollection.saveAndGet()`. All of these require Oracle Client 21.3 or higher (or Oracle Client 19 from 19.11). #) Added parameter `bypass_decode` to :meth:`Cursor.var()` in order to allow the `decode` step to be bypassed when converting data from Oracle Database into Python strings (`issue 385 `__). Initial work was done in `PR 549 `__. #) Enhanced dead connection detection. If an Oracle Database error indicates that a connection is no longer usable, the error `DPI-1080: connection was closed by ORA-%d` is now returned. The `%d` will be the Oracle error causing the connection to be closed. Using the connection after this will give `DPI-1010: not connected`. This behavior also applies for :data:`Connection.call_timeout` errors that result in an unusable connection. #) Eliminated a memory leak when calling :meth:`SodaOperation.filter()` with a dictionary. #) The distributed transaction handle assosciated with the connection is now cleared on commit or rollback (`issue 530 `__). #) Added a check to ensure that when setting variables or object attributes, the type of the temporary LOB must match the expected type. #) A small number of parameter, method, and attribute names were updated to follow the PEP 8 style guide. This brings better consistency to the cx_Oracle API. The old names are still usable but may be removed in a future release of cx_Oracle. See :ref:`_deprecations_8_2` for details. #) Improved the test suite. cx_Oracle 8.1 (December 2020) ----------------------------- #) Updated embedded ODPI-C to `version 4.1.0 `__. #) Added support for new JSON data type available in Oracle Client and Database 21 and higher. #) Dropped support for Python 3.5. Added support for Python 3.9. #) Added internal methods for getting/setting OCI attributes that are otherwise not supported by cx_Oracle. These methods should only be used as directed by Oracle. #) Minor code improvement supplied by Alex Henrie (`PR 472 `__). #) Builds are now done with setuptools and most metadata has moved from `setup.py` to `setup.cfg` in order to take advantage of Python packaging improvements. #) The ability to pickle/unpickle Database and API types has been restored. #) Tests can now be run with tox in order to automate testing of the different environments that are supported. #) The value of prefetchrows for REF CURSOR variables is now honored. #) Improved documentation, samples and test suite. cx_Oracle 8.0.1 (August 2020) ----------------------------- #) Updated embedded ODPI-C to `version 4.0.2 `__. This includes the fix for binding and fetching numbers with 39 or 40 decimal digits (`issue 459 `__). #) Added build metadata specifying that Python 3.5 and higher is required in order to avoid downloading and failing to install with Python 2. The exception message when running ``setup.py`` directly was updated to inform those using Python 2 to use version 7.3 instead. #) Documentation improvements. cx_Oracle 8.0 (June 2020) ------------------------- #) Dropped support for Python 2. #) Updated embedded ODPI-C to `version 4.0.1 `__. #) Reworked type management to clarify and simplify code - Added :ref:`constants ` for all database types. The database types :data:`cx_Oracle.DB_TYPE_BINARY_FLOAT`, :data:`cx_Oracle.DB_TYPE_INTERVAL_YM`, :data:`cx_Oracle.DB_TYPE_TIMESTAMP_LTZ` and :data:`cx_Oracle.DB_TYPE_TIMESTAMP_TZ` are completely new. The other types were found in earlier releases under a different name. These types will be found in :data:`Cursor.description` and passed as the defaultType parameter to the :data:`Connection.outputtypehandler` and :data:`Cursor.outputtypehandler` functions. - Added :ref:`synonyms ` from the old type names to the new type names for backwards compatibility. They are deprecated and will be removed in a future version of cx_Oracle. - The DB API :ref:`constants ` are now a specialized constant that matches to the corresponding database types, as recommended by the DB API. - The variable attribute :data:`~Variable.type` now refers to one of the new database type constants if the variable does not contain objects (previously it was None in that case). - The attribute :data:`~LOB.type` was added to LOB values. - The attribute `type `_ was added to attributes of object types. - The attribute `element_type `_ was added to object types. - `Object types `_ now compare equal if they were created by the same connection or session pool and their schemas and names match. - All variables are now instances of the same class (previously each type was an instance of a separate variable type). The attribute :data:`~Variable.type` can be examined to determine the database type it is associated with. - The string representation of variables has changed to include the type in addition to the value. #) Added function :meth:`cx_Oracle.init_oracle_client()` in order to enable programmatic control of the initialization of the Oracle Client library. #) The default encoding for all character data is now UTF-8 and any character set specified in the environment variable ``NLS_LANG`` is ignored. #) Added functions :meth:`SodaCollection.save()`, :meth:`SodaCollection.saveAndGet()` and :meth:`SodaCollection.truncate()` available in Oracle Client 20 and higher. #) Added function :meth:`SodaOperation.fetchArraySize()` available in Oracle Client 19.5 and higher. #) Added attribute :attr:`Cursor.prefetchrows` to control the number of rows that the Oracle Client library fetches into internal buffers when a query is executed. #) Internally make use of new mode available in Oracle Client 20 and higher in order to avoid a round-trip when accessing :attr:`Connection.version` for the first time. #) Added support for starting up a database using a parameter file (PFILE), as requested (`issue 295 `__). #) Fixed overflow issue when calling :meth:`Cursor.getbatcherrors()` with row offsets exceeding 65536. #) Eliminated spurious error when accessing :attr:`Cursor.lastrowid` after executing an INSERT ALL statement. #) Miscellaneous improvements supplied by Alex Henrie (pull requests `419 `__, `420 `__, `421 `__, `422 `__, `423 `__, `437 `__ and `438 `__). #) Python objects bound to boolean variables are now converted to True or False based on whether they would be considered True or False in a Python if statement. Previously, only True was treated as True and all other Python values (including 1, 1.0, and "foo") were treated as False (pull request `435 `__). #) Documentation, samples and test suite improvements. cx_Oracle 7.3 (December 2019) ----------------------------- #) Added support for Python 3.8. #) Updated embedded ODPI-C to `version 3.3 `__. #) Added support for CQN and other subscription client initiated connections to the database (as opposed to the default server initiated connections) created by calling :meth:`Connection.subscribe()`. #) Added :attr:`support ` for returning the rowid of the last row modified by an operation on a cursor (or None if no row was modified). #) Added support for setting the maxSessionsPerShard attribute when :meth:`creating session pools `. #) Added check to ensure sharding key is specified when a super sharding key is specified. #) Improved error message when the Oracle Client library is loaded successfully but the attempt to detect the version of that library fails, either due to the fact that the library is too old or the method could not be called for some reason (`node-oracledb issue 1168 `__). #) Adjusted support for creating a connection using an existing OCI service context handle. In order to avoid potential memory corruption and unsupported behaviors, the connection will now use the same encoding as the existing OCI service context handle when it was created. #) Added ``ORA-3156: OCI call timed out`` to the list of error messages that result in error DPI-1067. #) Adjusted samples and the test suite so that they can be run against Oracle Cloud databases. #) Fixed bug when attempting to create a scrollable cursor on big endian platforms like AIX on PPC. #) Eliminated reference leak and ensure that memory is properly initialized in case of error when using sharding keys. #) Eliminated reference leak when splitting the password and DSN components out of a full connect string. #) Corrected processing of DATE sharding keys (sharding requires a slightly different format to be passed to the server). #) Eliminated reference leak when :meth:`creating message property objects `. #) Attempting to use proxy authentication with a homogeneous pool will now raise a ``DatabaseError`` exception with the message ``DPI-1012: proxy authentication is not possible with homogeneous pools`` instead of a ``ProgrammingError`` exception with the message ``pool is homogeneous. Proxy authentication is not possible.`` since this check is done by ODPI-C. An empty string (or None) for the user name will no longer generate an exception. #) Exception ``InterfaceError: not connected`` is now always raised when an operation is attempted with a closed connection. Previously, a number of different exceptions were raised depending on the operation. #) Added ``ORA-40479: internal JSON serializer error`` to the list of exceptions that result in ``cx_Oracle.IntegrityError``. #) Improved documentation. cx_Oracle 7.2.3 (October 2019) ------------------------------ #) Updated embedded ODPI-C to `version 3.2.2 `__. #) Restored support for setting numeric bind variables with boolean values. #) Ensured that sharding keys are dedicated to the connection that is acquired using them in order to avoid possible hangs, crashes or unusual errors. #) Corrected support for PLS_INTEGER and BINARY_INTEGER types when used in PL/SQL records (`ODPI-C issue 112 `__). #) Improved documentation. cx_Oracle 7.2.2 (August 2019) ----------------------------- #) Updated embedded ODPI-C to `version 3.2.1 `__. #) A more meaningful error is now returned when calling :meth:`SodaCollection.insertMany()` with an empty list. #) A more meaningful error is now returned when calling :meth:`Subscription.registerquery()` with SQL that is not a SELECT statement. #) Eliminated segfault when a connection is closed after being created by a call to :meth:`cx_Oracle.connect()` with the parameter ``cclass`` set to a non-empty string. #) Added user guide documentation. #) Updated default connect strings to use 19c and XE 18c defaults. cx_Oracle 7.2.1 (July 2019) --------------------------- #) Resolved ``MemoryError`` exception on Windows when using an output type handler (`issue 330 `__). #) Improved test suite and samples. #) Improved documentation. cx_Oracle 7.2 (July 2019) ------------------------- #) Updated embedded ODPI-C to `version 3.2 `__. #) Improved AQ support - added support for enqueue and dequeue of RAW payloads - added support for bulk enqueue and dequeue of messages - added new method :meth:`Connection.queue()` which creates a new :ref:`queue object ` in order to simplify AQ usage - enhanced method :meth:`Connection.msgproperties()` to allow the writable properties of the newly created object to be initialized. - the original methods for enqueuing and dequeuing (Connection.deq(), Connection.deqoptions(), Connection.enq() and Connection.enqoptions()) are now deprecated and will be removed in a future version. #) Removed preview status from existing SODA functionality. See `this tracking issue `__ for known issues with SODA. #) Added support for a preview of SODA bulk insert, available in Oracle Client 18.5 and higher. #) Added support for setting LOB object attributes, as requested (`issue 299 `__). #) Added mode :data:`cx_Oracle.DEFAULT_AUTH` as requested (`issue 293 `__). #) Added support for using the LOB prefetch length indicator in order to reduce the number of round trips when fetching LOBs and then subsequently calling :meth:`LOB.size()`, :meth:`LOB.getchunksize()` or :meth:`LOB.read()`. This is always enabled. #) Added support for types BINARY_INTEGER, PLS_INTEGER, ROWID, LONG and LONG RAW when used in PL/SQL. #) Eliminated deprecation of attribute :attr:`Subscription.id`. It is now populated with the value of ``REGID`` found in the database view ``USER_CHANGE_NOTIFICATION_REGS`` or the value of ``REG_ID`` found in the database view ``USER_SUBSCR_REGISTRATIONS``. For AQ subscriptions, the value is 0. #) Enabled PY_SSIZE_T_CLEAN, as required by Python 3.8 (`issue 317 `__). #) Eliminated memory leak when fetching objects that are atomically null (`issue 298 `__). #) Eliminated bug when processing the string representation of numbers like 1e-08 and 1e-09 (`issue 300 `__). #) Improved error message when the parent cursor is closed before a fetch is attempted from an implicit result cursor. #) Improved test suite and samples. #) Improved documentation. cx_Oracle 7.1.3 (April 2019) ---------------------------- #) Updated to `ODPI-C 3.1.4 `__. #) Added support for getting the row count for PL/SQL statements (`issue 285 `__). #) Corrected parsing of connect string so that the last @ symbol is searched for instead of the first @ symbol; otherwise, passwords containing an @ symbol will result in the incorrect DSN being extracted (`issue 290 `__). #) Adjusted return value of cursor.callproc() to follow documentation (only positional arguments are returned since the order of keyword parameters cannot be guaranteed in any case) (`PR 287 `__). #) Corrected code getting sample and test parameters by user input when using Python 2.7. cx_Oracle 7.1.2 (March 2019) ---------------------------- #) Updated to `ODPI-C 3.1.3 `__. #) Ensured that the strings "-0" and "-0.0" are correctly handled as zero values (`issue 274 `__). #) Eliminated error when startup and shutdown events are generated (`ODPI-C issue 102 `__). #) Enabled the types specified in :meth:`Cursor.setinputsizes()` and :meth:`Cursor.callfunc()` to be an object type in addition to a Python type, just like in :meth:`Cursor.var()`. #) Reverted changes to return decimal numbers when the numeric precision was too great to be returned accurately as a floating point number. This change had too great an impact on existing functionality and an output type handler can be used to return decimal numbers where that is desirable (`issue 279 `__). #) Eliminated discrepancies in character sets between an external connection handle and the newly created connection handle that references the external connection handle (`issue 273 `__). #) Eliminated memory leak when receiving messages received from subscriptions. #) Improved test suite and documentation. cx_Oracle 7.1.1 (February 2019) ------------------------------- #) Updated to `ODPI-C 3.1.2 `__. #) Corrected code for freeing CQN message objects when multiple queries are registered (`ODPI-C issue 96 `__). #) Improved error messages and installation documentation. cx_Oracle 7.1 (February 2019) ----------------------------- #) Updated to `ODPI-C 3.1 `__. #) Improved support for session tagging in session pools by allowing a session callback to be specified when creating a pool via :meth:`cx_Oracle.SessionPool()`. Callbacks can be written in Python or in PL/SQL and can be used to improve performance by decreasing round trips to the database needed to set session state. Callbacks written in Python will be invoked for brand new connections (that have never been acquired from the pool before) or when the tag assigned to the connection doesn't match the one that was requested. Callbacks written in PL/SQL will only be invoked when the tag assigned to the connection doesn't match the one that was requested. #) Added attribute :attr:`Connection.tag` to provide access to the actual tag assigned to the connection. Setting this attribute will cause the connection to be retagged when it is released back to the pool. #) Added support for fetching SYS.XMLTYPE values as strings, as requested (`issue 14 `__). Note that this support is limited to the size of VARCHAR2 columns in the database (either 4000 or 32767 bytes). #) Added support for allowing the typename parameter in method :meth:`Cursor.var()` to be None or a valid object type created by the method :meth:`Connection.gettype()`, as requested (`issue 231 `__). #) Added support for getting and setting attributes of type RAW on Oracle objects, as requested (`ODPI-C issue 72 `__). #) Added support for performing external authentication with proxy for standalone connections. #) Added support for mixing integers, floating point and decimal values in data passed to :meth:`Cursor.executemany()` (`issue 241 `__). The error message raised when a value cannot be converted to an Oracle number was also improved. #) Adjusted fetching of numeric values so that no precision is lost. If an Oracle number cannot be represented by a Python floating point number a decimal value is automatically returned instead. #) Corrected handling of multiple calls to method :meth:`Cursor.executemany()` where all of the values in one of the columns passed to the first call are all None and a subsequent call has a value other than None in the same column (`issue 236 `__). #) Added additional check for calling :meth:`Cursor.setinputsizes()` with an empty dictionary in order to avoid the error "cx_Oracle.ProgrammingError: positional and named binds cannot be intermixed" (`issue 199 `__). #) Corrected handling of values that exceed the maximum value of a plain integer object on Python 2 on Windows (`issue 257 `__). #) Added error message when attempting external authentication with proxy without placing the user name in [] (proxy authentication was previously silently ignored). #) Exempted additional error messages from forcing a statement to be dropped from the cache (`ODPI-C issue 76 `__). #) Improved dead session detection when using session pools for Oracle Client 12.2 and higher. #) Ensured that the connection returned from a pool after a failed ping (such as due to a killed session) is not itself marked as needing to be dropped from the pool. #) Eliminated memory leak under certain circumstances when pooled connections are released back to the pool. #) Eliminated memory leak when connections are dropped from the pool. #) Eliminated memory leak when calling :meth:`Connection.close()` after fetching collections from the database. #) Adjusted order in which memory is freed when the last references to SODA collections, documents, document cursors and collection cursors are released, in order to prevent a segfault under certain circumstances. #) Improved code preventing a statement from binding itself, in order to avoid a potential segfault under certain circumstances. #) Worked around OCI bug when attempting to free objects that are PL/SQL records, in order to avoid a potential segfault. #) Improved test suite and samples. Note that default passwords are no longer supplied. New environment variables can be set to specify passwords if desired, or the tests and samples will prompt for the passwords when needed. In addition, a Python script is now available to create and drop the schemas used for the tests and samples. #) Improved documentation. cx_Oracle 7.0 (September 2018) ------------------------------ #) Update to `ODPI-C 3.0 `__. #) Added support for Oracle Client 18 libraries. #) Added support for SODA (as preview). See :ref:`SODA Database `, :ref:`SODA Collection ` and :ref:`SODA Document ` for more information. #) Added support for call timeouts available in Oracle Client 18.1 and higher. See :attr:`Connection.call_timeout`. #) Added support for getting the contents of a SQL collection object as a dictionary, where the keys are the indices of the collection and the values are the elements of the collection. See function :meth:`Object.asdict()`. #) Added support for closing a session pool via the function :meth:`SessionPool.close()`. Once closed, further attempts to use any connection that was acquired from the pool will result in the error "DPI-1010: not connected". #) Added support for setting a LOB attribute of an object with a string or bytes (instead of requiring a temporary LOB to be created). #) Added support for the packed decimal type used by object attributes with historical types DECIMAL and NUMERIC (`issue 212 `__). #) On Windows, first attempt to load oci.dll from the same directory as the cx_Oracle module. #) SQL objects that are created or fetched from the database are now tracked and marked unusable when a connection is closed. This was done in order to avoid a segfault under certain circumstances. #) Re-enabled dead session detection functionality when using pools for Oracle Client 12.2 and higher in order to handle classes of connection errors such as resource profile limits. #) Improved error messages when the Oracle Client or Oracle Database need to be at a minimum version in order to support a particular feature. #) When a connection is used as a context manager, the connection is now closed when the block ends. Attempts to set ``cx_Oracle.__future__.ctx_mgr_close`` are now ignored. #) When a DML returning statement is executed, variables bound to it will return an array when calling :meth:`Variable.getvalue()`. Attempts to set ``cx_Oracle.__future__.dml_ret_array_val`` are now ignored. #) Support for Python 3.4 has been dropped. #) Added additional test cases. #) Improved documentation. cx_Oracle 6.4.1 (July 2018) --------------------------- #) Update to `ODPI-C 2.4.2 `__. - Avoid buffer overrun due to improper calculation of length byte when converting some negative 39 digit numbers from string to the internal Oracle number format (`ODPI-C issue 67 `__). #) Prevent error "cx_Oracle.ProgrammingError: positional and named binds cannot be intermixed" when calling cursor.setinputsizes() without any parameters and then calling cursor.execute() with named bind parameters (`issue 199 `__). cx_Oracle 6.4 (July 2018) ------------------------- #) Update to `ODPI-C 2.4.1 `__. - Added support for grouping subscriptions. See parameters groupingClass, groupingValue and groupingType to function :meth:`Connection.subscribe()`. - Added support for specifying the IP address a subscription should use instead of having the Oracle Client library determine the IP address on its own. See parameter ipAddress to function :meth:`Connection.subscribe()`. - Added support for subscribing to notifications when messages are available to dequeue in an AQ queue. The new constant :data:`cx_Oracle.SUBSCR_NAMESPACE_AQ` should be passed to the namespace parameter of function :meth:`Connection.subscribe()` in order to get this functionality. Attributes :attr:`Message.queueName` and :attr:`Message.consumerName` will be populated in notification messages that are received when this namespace is used. - Added attribute :attr:`Message.registered` to let the notification callback know when the subscription that generated the notification is no longer registered with the database. - Added support for timed waits when acquiring a session from a session pool. Use the new constant :data:`cx_Oracle.SPOOL_ATTRVAL_TIMEDWAIT` in the parameter getmode to function :meth:`cx_Oracle.SessionPool` along with the new parameter waitTimeout. - Added support for specifying the timeout and maximum lifetime session for session pools when they are created using function :meth:`cx_Oracle.SessionPool`. Previously the pool had to be created before these values could be changed. - Avoid memory leak when dequeuing from an empty queue. - Ensure that the row count for queries is reset to zero when the statement is executed (`issue 193 `__). - If the statement should be deleted from the statement cache, first check to see that there is a statement cache currently being used; otherwise, the error "ORA-24300: bad value for mode" will be raised under certain conditions. #) Added support for using the cursor as a context manager (`issue 190 `__). #) Added parameter "encodingErrors" to function :meth:`Cursor.var()` in order to add support for specifying the "errors" parameter to the decode() that takes place internally when fetching strings from the database (`issue 162 `__). #) Added support for specifying an integer for the parameters argument to :meth:`Cursor.executemany()`. This allows for batch execution when no parameters are required or when parameters have previously been bound. This replaces Cursor.executemanyprepared() (which is now deprecated and will be removed in cx_Oracle 7). #) Adjusted the binding of booleans so that outside of PL/SQL they are bound as integers (`issue 181 `__). #) Added support for binding decimal.Decimal values to cx_Oracle.NATIVE_FLOAT as requested (`issue 184 `__). #) Added checks on passing invalid type parameters to methods :meth:`Cursor.arrayvar()`, :meth:`Cursor.callfunc()` and :meth:`Cursor.setinputsizes()`. #) Corrected handling of cursors and rowids in DML Returning statements. #) Added sample from David Lapp demonstrating the use of GeoPandas with SDO_GEOMETRY and a sample for demonstrating the use of REF cursors. #) Adjusted samples and documentation for clarity. #) Added additional test cases. cx_Oracle 6.3.1 (May 2018) -------------------------- #) Update to `ODPI-C 2.3.2 `__. - Ensure that a call to unregister a subscription only occurs if the subscription is still registered. - Ensure that before a statement is executed any buffers used for DML returning statements are reset. #) Ensure that behavior with cx_Oracle.__future__.dml_ret_array_val not set or False is the same as the behavior in cx_Oracle 6.2 (`issue 176 `__). cx_Oracle 6.3 (April 2018) -------------------------- #) Update to `ODPI-C 2.3.1 `__. - Fixed binding of LONG data (values exceeding 32KB) when using the function :meth:`Cursor.executemany()`. - Added code to verify that a CQN subscription is open before permitting it to be used. Error "DPI-1060: subscription was already closed" will now be raised if an attempt is made to use a subscription that was closed earlier. - Stopped attempting to unregister a CQN subscription before it was completely registered. This prevents errors encountered during registration from being masked by an error stating that the subscription has not been registered! - Added error "DPI-1061: edition is not supported when a new password is specified" to clarify the fact that specifying an edition and a new password at the same time is not supported when creating a connection. Previously the edition value was simply ignored. - Improved error message when older OCI client libraries are being used that don't have the method OCIClientVersion(). - Fixed the handling of ANSI types REAL and DOUBLE PRECISION as implemented by Oracle. These types are just subtypes of NUMBER and are different from BINARY_FLOAT and BINARY_DOUBLE (`issue 163 `__). - Fixed support for true heterogeneous session pools that use different user/password combinations for each session acquired from the pool. - Added error message indicating that setting either of the parameters arraydmlrowcounts and batcherrors to True in :meth:`Cursor.executemany()` is only supported with insert, update, delete and merge statements. #) Fixed support for getting the OUT values of bind variables bound to a DML Returning statement when calling the function :meth:`Cursor.executemany()`. Note that the attribute dml_ret_array_val in :attr:`cx_Oracle.__future__` must be set to True first. #) Added support for binding integers and floats as cx_Oracle.NATIVE_FLOAT. #) A :attr:`cx_Oracle._Error` object is now the value of all cx_Oracle exceptions raised by cx_Oracle. (`issue 51 `__). #) Added support for building cx_Oracle with a pre-compiled version of ODPI-C, as requested (`issue 103 `__). #) Default values are now provided for all parameters to :meth:`cx_Oracle.SessionPool`. #) Improved error message when an unsupported Oracle type is encountered. #) The Python GIL is now prevented from being held while performing a round trip for the call to get the attribute :attr:`Connection.version` (`issue 158 `__). #) Added check for the validity of the year for Python 2.x since it doesn't do that itself like Python 3.x does (`issue 166 `__). #) Adjusted documentation to provide additional information on the use of :meth:`Cursor.executemany()` as requested (`issue 153 `__). #) Adjusted documentation to state that batch errors and array DML row counts can only be used with insert, update, delete and merge statements (`issue 31 `__). #) Updated tutorial to import common connection information from files in order to make setup a bit more generic. cx_Oracle 6.2.1 (March 2018) ---------------------------- #) Make sure cxoModule.h is included in the source archive (`issue 155 `__). cx_Oracle 6.2 (March 2018) -------------------------- #) Update to `ODPI-C 2.2.1 `__. - eliminate error "DPI-1054: connection cannot be closed when open statements or LOBs exist" (`issue 138 `__). - avoid a round trip to the database when a connection is released back to the pool by preventing a rollback from being called when no transaction is in progress. - improve error message when the use of bind variables is attempted with DLL statements, which is not supported by Oracle. - if an Oracle object is retrieved from an attribute of another Oracle object or a collection, prevent the "owner" from being destroyed until the object that was retrieved has itself been destroyed. - correct handling of boundary numbers 1e126 and -1e126 - eliminate memory leak when calling :meth:`Connection.enq()` and :meth:`Connection.deq()` - eliminate memory leak when setting NCHAR and NVARCHAR attributes of objects. - eliminate memory leak when fetching collection objects from the database. #) Added support for creating a temporary CLOB, BLOB or NCLOB via the method :meth:`Connection.createlob()`. #) Added support for binding a LOB value directly to a cursor. #) Added support for closing the connection when reaching the end of a ``with`` code block controlled by the connection as a context manager, but in a backwards compatible way (`issue 113 `__). See :data:`cx_Oracle.__future__` for more information. #) Reorganized code to simplify continued maintenance and consolidate transformations to/from Python objects. #) Ensure that the number of elements in the array is not lost when the buffer size is increased to accommodate larger strings. #) Corrected support in Python 3.x for cursor.parse() by permitting a string to be passed, instead of incorrectly requiring a bytes object. #) Eliminate reference leak with LOBs acquired from attributes of objects or elements of collections. #) Eliminate reference leak when extending an Oracle collection. #) Documentation improvements. #) Added test cases to the test suite. cx_Oracle 6.1 (December 2017) ----------------------------- #) Update to `ODPI-C 2.1 `__. - Support was added for accessing sharded databases via sharding keys (new in Oracle 12.2). NOTE: the underlying OCI library has a bug when using standalone connections. There is a small memory leak proportional to the number of connections created/dropped. There is no memory leak when using session pools, which is recommended. - Added options for authentication with SYSBACKUP, SYSDG, SYSKM and SYSRAC, as requested (`issue 101 `__). - Attempts to release statements or free LOBs after the connection has been closed (by, for example, killing the session) are now prevented. - An error message was added when specifying an edition and a connection class since this combination is not supported. - Attempts to close the session for connections created with an external handle are now prevented. - Attempting to ping a database earlier than 10g results in ORA-1010: invalid OCI operation, but that implies a response from the database and therefore a successful ping, so treat it that way! (see ``__ for more information). - Support was added for converting numeric values in an object type attribute to integer and text, as requested (`ODPI-C issue 35 `__). - Setting attributes :attr:`DeqOptions.msgId` and :attr:`MessageProperties.msgId` now works as expected. - The overflow check when using double values (Python floats) as input to float attributes of objects or elements of collections was removed as it didn't work anyway and is a well-known issue that cannot be prevented without removing desired functionality. The developer should ensure that the source value falls within the limits of floats, understand the consequent precision loss or use a different data type. - Variables of string/raw types are restricted to 2 bytes less than 1 GB (1,073,741,822 bytes), since OCI cannot handle more than that currently. - Support was added for identifying the id of the transaction which spawned a CQN subscription message, as requested (`ODPI-C issue 32 `__). - Corrected use of subscription port number (`issue 115 `__). - Problems reported with the usage of FormatMessage() on Windows were addressed (`ODPI-C issue 47 `__). - On Windows, if oci.dll cannot be loaded because it is the wrong architecture (32-bit vs 64-bit), attempt to find the offending DLL and include the full path of the DLL in the message, as suggested. (`ODPI-C issue 49 `__). - Force OCI prefetch to always use the value 2; the OCI default is 1 but setting the ODPI-C default to 2 ensures that single row fetches don't require an extra round trip to determine if there are more rows to fetch; this change also reduces the potential memory consumption when fetchArraySize was set to a large value and also avoids performance issues discovered with larger values of prefetch. #) Fix build with PyPy 5.9.0-alpha0 in libpython mode (`PR 54 `__). #) Ensure that the edition is passed through to the database when a session pool is created. #) Corrected handling of Python object references when an invalid keyword parameter is passed to :meth:`cx_Oracle.SessionPool`. #) Corrected handling of :attr:`Connection.handle` and the handle parameter to :meth:`cx_Oracle.connect` on Windows. #) Documentation improvements. #) Added test cases to the test suite. cx_Oracle 6.0.3 (November 2017) ------------------------------- #) Update to `ODPI-C 2.0.3 `__. - Prevent use of uninitialized data in certain cases (`issue 77 `__). - Attempting to ping a database earlier than 10g results in error "ORA-1010: invalid OCI operation", but that implies a response from the database and therefore a successful ping, so treat it that way! - Correct handling of conversion of some numbers to NATIVE_FLOAT. - Prevent use of NaN with Oracle numbers since it produces corrupt data (`issue 91 `__). - Verify that Oracle objects bound to cursors, fetched from cursors, set in object attributes or appended to collection objects are of the correct type. - Correct handling of NVARCHAR2 when used as attributes of Oracle objects or as elements of collections. #) Ensure that a call to setinputsizes() with an invalid type prior to a call to executemany() does not result in a type error, but instead gracefully ignores the call to setinputsizes() as required by the DB API (`issue 75 `__). #) Check variable array size when setting variable values and raise IndexError, as is already done for getting variable values. cx_Oracle 6.0.2 (August 2017) ----------------------------- #) Update to `ODPI-C 2.0.2 `__. - Don't prevent connection from being explicitly closed when a fatal error has taken place (`issue 67 `__). - Correct handling of objects when dynamic binding is performed. - Process deregistration events without an error. - Eliminate memory leak when creating objects. #) Added missing type check to prevent coercion of decimal to float (`issue 68 `__). #) On Windows, sizeof(long) = 4, not 8, which meant that integers between 10 and 18 digits were not converted to Python correctly (`issue 70 `__). #) Eliminate memory leak when repeatedly executing the same query. #) Eliminate segfault when attempting to reuse a REF cursor that has been closed. #) Updated documentation. cx_Oracle 6.0.1 (August 2017) ----------------------------- #) Update to `ODPI-C 2.0.1 `__. - Ensure that queries registered via :meth:`Subscription.registerquery()` do not prevent the associated connection from being closed (`ODPI-C issue 27 `__). - Deprecated attribute :attr:`Subscription.id` as it was never intended to be exposed (`ODPI-C issue 28 `__). It will be dropped in version 6.1. - Add support for DML Returning statements that require dynamically allocated variable data (such as CLOBs being returned as strings). #) Correct packaging of Python 2.7 UCS4 wheels on Linux (`issue 64 `__). #) Updated documentation. cx_Oracle 6.0 (August 2017) --------------------------- #) Update to `ODPI-C 2.0 `__. - Prevent closing the connection when there are any open statements or LOBs and add new error "DPI-1054: connection cannot be closed when open statements or LOBs exist" when this situation is detected; this is needed to prevent crashes under certain conditions when statements or LOBs are being acted upon while at the same time (in another thread) a connection is being closed; it also prevents leaks of statements and LOBs when a connection is returned to a session pool. - On platforms other than Windows, if the regular method for loading the Oracle Client libraries fails, try using $ORACLE_HOME/lib/libclntsh.so (`ODPI-C issue 20 `__). - Use the environment variable ``DPI_DEBUG_LEVEL`` at runtime, not compile time. - Added support for DPI_DEBUG_LEVEL_ERRORS (reports errors and has the value 8) and DPI_DEBUG_LEVEL_SQL (reports prepared SQL statement text and has the value 16) in order to further improve the ability to debug issues. - Correct processing of :meth:`Cursor.scroll()` in some circumstances. #) Delay initialization of the ODPI-C library until the first standalone connection or session pool is created so that manipulation of the environment variable ``NLS_LANG`` can be performed after the module has been imported; this also has the added benefit of reducing the number of errors that can take place when the module is imported. #) Prevent binding of null values from generating the exception "ORA-24816: Expanded non LONG bind data supplied after actual LONG or LOB column" in certain circumstances (`issue 50 `__). #) Added information on how to run the test suite (`issue 33 `__). #) Documentation improvements. cx_Oracle 6.0 rc 2 (July 2017) ------------------------------ #) Update to `ODPI-C rc 2 `__. - Provide improved error message when OCI environment cannot be created, such as when the oraaccess.xml file cannot be processed properly. - On Windows, convert system message to Unicode first, then to UTF-8; otherwise, the error message returned could be in a mix of encodings (`issue 40 `__). - Corrected support for binding decimal values in object attribute values and collection element values. - Corrected support for binding PL/SQL boolean values to PL/SQL procedures with Oracle client 11.2. #) Define exception classes on the connection object in addition to at module scope in order to simplify error handling in multi-connection environments, as specified in the Python DB API. #) Ensure the correct encoding is used for setting variable values. #) Corrected handling of CLOB/NCLOB when using different encodings. #) Corrected handling of TIMESTAMP WITH TIME ZONE attributes on objects. #) Ensure that the array position passed to var.getvalue() does not exceed the number of elements allocated in the array. #) Reworked test suite and samples so that they are independent of each other and so that the SQL scripts used to create/drop schemas are easily adjusted to use different schema names, if desired. #) Updated DB API test suite stub to support Python 3. #) Added additional test cases and samples. #) Documentation improvements. cx_Oracle 6.0 rc 1 (June 2017) ------------------------------ #) Update to `ODPI-C rc 1 `__. #) The method :meth:`Cursor.setoutputsize` no longer needs to do anything, since ODPI-C automatically manages buffer sizes of LONG and LONG RAW columns. #) Handle case when both precision and scale are zero, as occurs when retrieving numeric expressions (`issue 34 `__). #) OCI requires that both encoding and nencoding have values or that both encoding and encoding do not have values. These parameters are used in functions :meth:`cx_Oracle.connect` and :meth:`cx_Oracle.SessionPool`. The missing value is set to its default value if one of the values is set and the other is not (`issue 36 `__). #) Permit use of both string and unicode for Python 2.7 for creating session pools and for changing passwords (`issue 23 `__). #) Corrected handling of BFILE LOBs. #) Add script for dropping test schemas. #) Documentation improvements. cx_Oracle 6.0 beta 2 (May 2017) ------------------------------- #) Added support for getting/setting attributes of objects or element values in collections that contain LOBs, BINARY_FLOAT values, BINARY_DOUBLE values and NCHAR and NVARCHAR2 values. The error message for any types that are not supported has been improved as well. #) Enable temporary LOB caching in order to avoid disk I/O as `suggested `__. #) Added support for setting the debug level in ODPI-C, if desirable, by setting environment variable ``DPI_DEBUG_LEVEL`` prior to building cx_Oracle. #) Correct processing of strings in :meth:`Cursor.executemany` when a larger string is found after a shorter string in the list of data bound to the statement. #) Correct handling of long Python integers that cannot fit inside a 64-bit C integer (`issue 18 `__). #) Correct creation of pool using external authentication. #) Handle edge case when an odd number of zeroes trail the decimal point in a value that is effectively zero (`issue 22 `__). #) Prevent segfault under load when the attempt to create an error fails. #) Eliminate resource leak when a standalone connection or pool is freed. #) Correct `typo `__. #) Correct handling of REF cursors when the array size is manipulated. #) Prevent attempts from binding the cursor being executed to itself. #) Correct reference count handling of parameters when creating a cursor. #) Correct determination of the names of the bind variables in prepared SQL statements (which behaves a little differently from PL/SQL statements). cx_Oracle 6.0 beta 1 (April 2017) --------------------------------- #) Simplify building cx_Oracle considerably by use of `ODPI-C `__. This means that cx_Oracle can now be built without Oracle Client header files or libraries and that at runtime cx_Oracle can adapt to Oracle Client 11.2, 12.1 or 12.2 libraries without needing to be rebuilt. This also means that wheels can now be produced and installed via pip. #) Added attribute :attr:`SessionPool.stmtcachesize` to support getting and setting the default statement cache size for connections in the pool. #) Added attribute :attr:`Connection.dbop` to support setting the database operation that is to be monitored. #) Added attribute :attr:`Connection.handle` to facilitate testing the creation of a connection using a OCI service context handle. #) Added parameters tag and matchanytag to the :meth:`cx_Oracle.connect` and :meth:`SessionPool.acquire` methods and added parameters tag and retag to the :meth:`SessionPool.release` method in order to support session tagging. #) Added parameter edition to the :meth:`cx_Oracle.SessionPool` method. #) Added support for `universal rowids `__. #) Added support for `DML Returning of multiple rows `__. #) Added attributes :attr:`Variable.actualElements` and :attr:`Variable.values` to variables. #) Added parameters region, sharding_key and super_sharding_key to the :meth:`cx_Oracle.makedsn()` method to support connecting to a sharded database (new in Oracle Database 12.2). #) Added support for smallint and float data types in Oracle objects, as `requested `__. #) An exception is no longer raised when a collection is empty for methods :meth:`Object.first()` and :meth:`Object.last()`. Instead, the value None is returned to be consistent with the methods :meth:`Object.next()` and :meth:`Object.prev()`. #) If the environment variables NLS_LANG and NLS_NCHAR are being used, they must be set before the module is imported. Using the encoding and nencoding parameters to the :meth:`cx_Oracle.connect` and :meth:`cx_Oracle.SessionPool` methods is a simpler alternative to setting these environment variables. #) Removed restriction on fetching LOBs across round trips to the database (eliminates error "LOB variable no longer valid after subsequent fetch"). #) Removed requirement for specifying a maximum size when fetching LONG or LONG raw columns. This also allows CLOB, NCLOB, BLOB and BFILE columns to be fetched as strings or bytes without needing to specify a maximum size. #) Dropped deprecated parameter twophase from the :meth:`cx_Oracle.connect` method. Applications should set the :attr:`Connection.internal_name` and :attr:`Connection.external_name` attributes instead to a value appropriate to the application. #) Dropped deprecated parameters action, module and clientinfo from the :meth:`cx_Oracle.connect` method. The appcontext parameter should be used instead as shown in this `sample `__. #) Dropped deprecated attribute numbersAsString from :ref:`cursor objects `. Use an output type handler instead as shown in this `sample `__. #) Dropped deprecated attributes cqqos and rowids from :ref:`subscription objects `. Use the qos attribute instead as shown in this `sample `__. #) Dropped deprecated parameters cqqos and rowids from the :meth:`Connection.subscribe()` method. Use the qos parameter instead as shown in this `sample `__. cx_Oracle 5.3 (March 2017) -------------------------- #) Added support for Python 3.6. #) Dropped support for Python versions earlier than 2.6. #) Dropped support for Oracle clients earlier than 11.2. #) Added support for :meth:`fetching implicit results` (available in Oracle 12.1) #) Added support for :attr:`Transaction Guard ` (available in Oracle 12.1). #) Added support for setting the :attr:`maximum lifetime ` of pool connections (available in Oracle 12.1). #) Added support for large row counts (larger than 2 ** 32, available in Oracle 12.1) #) Added support for :meth:`advanced queuing `. #) Added support for :meth:`scrollable cursors `. #) Added support for :attr:`edition based redefinition `. #) Added support for :meth:`creating `, modifying and binding user defined types and collections. #) Added support for creating, modifying and binding PL/SQL records and collections (available in Oracle 12.1). #) Added support for binding :data:`native integers `. #) Enabled statement caching. #) Removed deprecated variable attributes maxlength and allocelems. #) Corrected support for setting the encoding and nencoding parameters when :meth:`creating a connection ` and added support for setting these when creating a session pool. These can now be used instead of setting the environment variables ``NLS_LANG`` and ``NLS_NCHAR``. #) Use None instead of 0 for items in the :attr:`Cursor.description` attribute that do not have any validity. #) Changed driver name to match informal driver name standard used by Oracle for other drivers. #) Add check for maximum of 10,000 parameters when calling a stored procedure or function in order to prevent a possible improper memory access from taking place. #) Removed -mno-cygwin compile flag since it is no longer used in newer versions of the gcc compiler for Cygwin. #) Simplified test suite by combining Python 2 and 3 scripts into one script and separated out 12.1 features into a single script. #) Updated samples to use code that works on both Python 2 and 3 #) Added support for pickling/unpickling error objects (`Issue #23 `__) #) Dropped support for callbacks on OCI functions. #) Removed deprecated types UNICODE, FIXED_UNICODE and LONG_UNICODE (use NCHAR, FIXED_NCHAR and LONG_NCHAR instead). #) Increased default array size to 100 (from 50) to match other drivers. #) Added support for setting the :attr:`~Connection.internal_name` and :attr:`~Connection.external_name` on the connection directly. The use of the twophase parameter is now deprecated. Applications should set the internal_name and external_name attributes directly to a value appropriate to the application. #) Added support for using application context when :meth:`creating a connection `. This should be used in preference to the module, action and clientinfo parameters which are now deprecated. #) Reworked database change notification and continuous query notification to more closely align with the PL/SQL implementation and prepare for sending notifications for AQ messages. The following changes were made: - added constant :data:`~cx_Oracle.SUBSCR_QOS_BEST_EFFORT` to replace deprecated constant SUBSCR_CQ_QOS_BEST_EFFORT - added constant :data:`~cx_Oracle.SUBSCR_QOS_QUERY` to replace deprecated constant SUBSCR_CQ_QOS_QUERY - added constant :data:`~cx_Oracle.SUBSCR_QOS_DEREG_NFY` to replace deprecated constant SUBSCR_QOS_PURGE_ON_NTFN - added constant :data:`~cx_Oracle.SUBSCR_QOS_ROWIDS` to replace parameter rowids for method :meth:`Connection.subscribe()` - deprecated parameter cqqos for method :meth:`Connection.subscribe()`. The qos parameter should be used instead. - dropped constants SUBSCR_CQ_QOS_CLQRYCACHE, SUBSCR_QOS_HAREG, SUBSCR_QOS_MULTICBK, SUBSCR_QOS_PAYLOAD, SUBSCR_QOS_REPLICATE, and SUBSCR_QOS_SECURE since they were never actually used #) Deprecated use of the numbersAsStrings attribute on cursors. An output type handler should be used instead. cx_Oracle 5.2.1 (January 2016) ------------------------------ #) Added support for Python 3.5. #) Removed password attribute from connection and session pool objects in order to promote best security practices (if stored in RAM in cleartext it can be read in process dumps, for example). For those who would like to retain this feature, a subclass of Connection could be used to store the password. #) Added optional parameter externalauth to SessionPool() which enables wallet based or other external authentication mechanisms to be used. #) Use the national character set encoding when required (when char set form is SQLCS_NCHAR); otherwise, the wrong encoding would be used if the environment variable ``NLS_NCHAR`` is set. #) Added support for binding boolean values to PL/SQL blocks and stored procedures (available in Oracle 12.1). cx_Oracle 5.2 (June 2015) ------------------------- #) Added support for strings up to 32k characters (new in Oracle 12c). #) Added support for getting array DML row counts (new in Oracle 12c). #) Added support for fetching batch errors. #) Added support for LOB values larger than 4 GB. #) Added support for connections as SYSASM. #) Added support for building without any configuration changes to the machine when using instant client RPMs on Linux. #) Added types NCHAR, FIXED_NCHAR and LONG_NCHAR to replace the types UNICODE, FIXED_UNICODE and LONG_UNICODE (which are now deprecated). These types are available in Python 3 as well so they can be used to specify the use of NCHAR type fields when binding or using setinputsizes(). #) Fixed binding of booleans in Python 3.x. #) Test suite now sets NLS_LANG if not already set. #) Enhanced documentation for connection.action attribute and added note on cursor.parse() method to make clear that DDL statements are executed when parsed. #) Removed remaining remnants of support Oracle 9i. #) Added __version__ attribute to conform with PEP 396. #) Ensure that sessions are released to the pool when calling connection.close() (`Issue #2 `__) #) Fixed handling of datetime intervals (`Issue #7 `__) cx_Oracle 5.1.3 (May 2014) -------------------------- #) Added support for Oracle 12c. #) Added support for Python 3.4. #) Added support for query result set change notification. Thanks to Glen Walker for the patch. #) Ensure that in Python 3.x that NCHAR and NVARCHAR2 and NCLOB columns are retrieved properly without conversion issues. Thanks to Joakim Andersson for pointing out the issue and the possible solution. #) Fix bug when an exception is caught and then another exception is raised while handling that exception in Python 3.x. Thanks to Boris Dzuba for pointing out the issue and providing a test case. #) Enhance performance returning integers between 10 and 18 digits on 64-bit platforms that support it. Thanks for Shai Berger for the initial patch. #) Fixed two memory leaks. #) Fix to stop current_schema from throwing a MemoryError on 64-bit platforms on occasion. Thanks to Andrew Horton for the fix. #) Class name of cursors changed to real name cx_Oracle.Cursor. cx_Oracle 5.1.2 (July 2012) --------------------------- #) Added support for LONG_UNICODE which is a type used to handle long unicode strings. These are not explicitly supported in Oracle but can be used to bind to NCLOB, for example, without getting the error "unimplemented or unreasonable conversion requested". #) Set the row number in a cursor when executing PL/SQL blocks as requested by Robert Ritchie. #) Added support for setting the module, action and client_info attributes during connection so that logon triggers will see the supplied values, as requested by Rodney Barnett. cx_Oracle 5.1.1 (October 2011) ------------------------------ #) Simplify management of threads for callbacks performed by database change notification and eliminate a crash that occurred under high load in certain situations. Thanks to Calvin S. for noting the issue and suggesting a solution and testing the patch. #) Force server detach on close so that the connection is completely closed and not just the session as before. #) Force use of OCI_UTF16ID for NCLOBs as using the default character set would result in ORA-03127 with Oracle 11.2.0.2 and UTF8 character set. #) Avoid attempting to clear temporary LOBs a second time when destroying the variable as in certain situations this results in spurious errors. #) Added additional parameter service_name to makedsn() which can be used to use the service_name rather than the SID in the DSN string that is generated. #) Fix cursor description in test suite to take into account the number of bytes per character. #) Added tests for NCLOBS to the test suite. #) Removed redundant code in setup.py for calculating the library path. cx_Oracle 5.1 (March 2011) -------------------------- #) Remove support for UNICODE mode and permit Unicode to be passed through in everywhere a string may be passed in. This means that strings will be passed through to Oracle using the value of the NLS_LANG environment variable in Python 3.x as well. Doing this eliminated a bunch of problems that were discovered by using UNICODE mode and also removed an unnecessary restriction in Python 2.x that Unicode could not be used in connect strings or SQL statements, for example. #) Added support for creating an empty object variable via a named type, the first step to adding full object support. #) Added support for Python 3.2. #) Account for lib64 used on x86_64 systems. Thanks to Alex Wood for supplying the patch. #) Clear up potential problems when calling cursor.close() ahead of the cursor being freed by going out of scope. #) Avoid compilation difficulties on AIX5 as OCIPing does not appear to be available on that platform under Oracle 10g Release 2. Thanks to Pierre-Yves Fontaniere for the patch. #) Free temporary LOBs prior to each fetch in order to avoid leaking them. Thanks to Uwe Hoffmann for the initial patch. cx_Oracle 5.0.4 (July 2010) --------------------------- #) Added support for Python 2.7. #) Added support for new parameter (port) for subscription() call which allows the client to specify the listening port for callback notifications from the database server. Thanks to Geoffrey Weber for the initial patch. #) Fixed compilation under Oracle 9i. #) Fixed a few error messages. cx_Oracle 5.0.3 (February 2010) ------------------------------- #) Added support for 64-bit Windows. #) Added support for Python 3.1 and dropped support for Python 3.0. #) Added support for keyword parameters in cursor.callproc() and cursor.callfunc(). #) Added documentation for the UNICODE and FIXED_UNICODE variable types. #) Added extra link arguments required for Mac OS X as suggested by Jason Woodward. #) Added additional error codes to the list of error codes that raise OperationalError rather than DatabaseError. #) Fixed calculation of display size for strings with national database character sets that are not the default AL16UTF16. #) Moved the resetting of the setinputsizes flag before the binding takes place so that if an error takes place and a new statement is prepared subsequently, spurious errors will not occur. #) Fixed compilation with Oracle 10g Release 1. #) Tweaked documentation based on feedback from a number of people. #) Added support for running the test suite using "python setup.py test" #) Added support for setting the CLIENT_IDENTIFIER value in the v$session table for connections. #) Added exception when attempting to call executemany() with arrays which is not supported by the OCI. #) Fixed bug when converting from decimal would result in OCI-22062 because the locale decimal point was not a period. Thanks to Amaury Forgeot d'Arc for the solution to this problem. cx_Oracle 5.0.2 (May 2009) -------------------------- #) Fix creation of temporary NCLOB values and the writing of NCLOB values in non Unicode mode. #) Re-enabled parsing of non select statements as requested by Roy Terrill. #) Implemented a parse error offset as requested by Catherine Devlin. #) Removed lib subdirectory when forcing RPATH now that the library directory is being calculated exactly in setup.py. #) Added an additional cast in order to support compiling by Microsoft Visual C++ 2008 as requested by Marco de Paoli. #) Added additional include directory to setup.py in order to support compiling by Microsoft Visual Studio was requested by Jason Coombs. #) Fixed a few documentation issues. cx_Oracle 5.0.1 (February 2009) ------------------------------- #) Added support for database change notification available in Oracle 10g Release 2 and higher. #) Fix bug where NCLOB data would be corrupted upon retrieval (non Unicode mode) or would generate exception ORA-24806 (LOB form mismatch). Oracle insists upon differentiating between CLOB and NCLOB no matter which character set is being used for retrieval. #) Add new attributes size, bufferSize and numElements to variable objects, deprecating allocelems (replaced by numElements) and maxlength (replaced by bufferSize) #) Avoid increasing memory allocation for strings when using variable width character sets and increasing the number of elements in a variable during executemany(). #) Tweaked code in order to ensure that cx_Oracle can compile with Python 3.0.1. cx_Oracle 5.0 (December 2008) ----------------------------- #) Added support for Python 3.0 with much help from Amaury Forgeot d'Arc. #) Removed support for Python 2.3 and Oracle 8i. #) Added support for full unicode mode in Python 2.x where all strings are passed in and returned as unicode (module must be built in this mode) rather than encoded strings #) nchar and nvarchar columns now return unicode instead of encoded strings #) Added support for an output type handler and/or an input type handler to be specified at the connection and cursor levels. #) Added support for specifying both input and output converters for variables #) Added support for specifying the array size of variables that are created using the cursor.var() method #) Added support for events mode and database resident connection pooling (DRCP) in Oracle 11g. #) Added support for changing the password during construction of a new connection object as well as after the connection object has been created #) Added support for the interval day to second data type in Oracle, represented as datetime.timedelta objects in Python. #) Added support for getting and setting the current_schema attribute for a session #) Added support for proxy authentication in session pools as requested by Michael Wegrzynek (and thanks for the initial patch as well). #) Modified connection.prepare() to return a boolean indicating if a transaction was actually prepared in order to avoid the error ORA-24756 (transaction does not exist). #) Raise a cx_Oracle.Error instance rather than a string for column truncation errors as requested by Helge Tesdal. #) Fixed handling of environment handles in session pools in order to allow session pools to fetch objects without exceptions taking place. cx_Oracle 4.4.1 (October 2008) ------------------------------ #) Make the bind variables and fetch variables accessible although they need to be treated carefully since they are used internally; support added for forward compatibility with version 5.x. #) Include the "cannot insert null value" in the list of errors that are treated as integrity errors as requested by Matt Boersma. #) Use a cx_Oracle.Error instance rather than a string to hold the error when truncation (ORA-1406) takes place as requested by Helge Tesdal. #) Added support for fixed char, old style varchar and timestamp attribute values in objects. #) Tweaked setup.py to check for the Oracle version up front rather than during the build in order to produce more meaningful errors and simplify the code. #) In setup.py added proper detection for the instant client on Mac OS X as recommended by Martijn Pieters. #) In setup.py, avoided resetting the extraLinkArgs on Mac OS X as doing so prevents simple modification where desired as expressed by Christian Zagrodnick. #) Added documentation on exception handling as requested by Andreas Mock, who also graciously provided an initial patch. #) Modified documentation indicating that the password attribute on connection objects can be written. #) Added documentation warning that parameters not passed in during subsequent executions of a statement will retain their original values as requested by Harald Armin Massa. #) Added comments indicating that an Oracle client is required since so many people find this surprising. #) Removed all references to Oracle 8i from the documentation and version 5.x will eliminate all vestiges of support for this version of the Oracle client. #) Added additional link arguments for Cygwin as requested by Rob Gillen. cx_Oracle 4.4 (June 2008) ------------------------- #) Fix setup.py to handle the Oracle instant client and Oracle XE on both Linux and Windows as pointed out by many. Thanks also to the many people who also provided patches. #) Set the default array size to 50 instead of 1 as the DB API suggests because the performance difference is so drastic and many people have recommended that the default be changed. #) Added Py_BEGIN_ALLOW_THREADS and Py_END_ALLOW_THREADS around each blocking call for LOBs as requested by Jason Conroy who also provided an initial patch and performed a number of tests that demonstrate the new code is much more responsive. #) Add support for acquiring cursor.description after a parse. #) Defer type assignment when performing executemany() until the last possible moment if the value being bound in is null as suggested by Dragos Dociu. #) When dropping a connection from the pool, ignore any errors that occur during the rollback; unfortunately, Oracle decides to commit data even when dropping a connection from the pool instead of rolling it back so the attempt still has to be made. #) Added support for setting CLIENT_DRIVER in V$SESSION_CONNECT_INFO in Oracle 11g and higher. #) Use cx_Oracle.InterfaceError rather than the builtin RuntimeError when unable to create the Oracle environment object as requested by Luke Mewburn since the error is specific to Oracle and someone attempting to catch any exception cannot simply use cx_Oracle.Error. #) Translated some error codes to OperationalError as requested by Matthew Harriger; translated if/elseif/else logic to switch statement to make it more readable and to allow for additional translation if desired. #) Transformed documentation to new format using restructured text. Thanks to Waldemar Osuch for contributing the initial draft of the new documentation. #) Allow the password to be overwritten by a new value as requested by Alex VanderWoude; this value is retained as a convenience to the user and not used by anything in the module; if changed externally it may be convenient to keep this copy up to date. #) Cygwin is on Windows so should be treated in the same way as noted by Matthew Cahn. #) Add support for using setuptools if so desired as requested by Shreya Bhatt. #) Specify that the version of Oracle 10 that is now primarily used is 10.2, not 10.1. cx_Oracle 4.3.3 (October 2007) ------------------------------ #) Added method ping() on connections which can be used to test whether or not a connection is still active (available in Oracle 10g R2). #) Added method cx_Oracle.clientversion() which returns a 5-tuple giving the version of the client that is in use (available in Oracle 10g R2). #) Added methods startup() and shutdown() on connections which can be used to startup and shutdown databases (available in Oracle 10g R2). #) Added support for Oracle 11g. #) Added samples directory which contains a handful of scripts containing sample code for more advanced techniques. More will follow in future releases. #) Prevent error "ORA-24333: zero iteration count" when calling executemany() with zero rows as requested by Andreas Mock. #) Added methods __enter__() and __exit__() on connections to support using connections as context managers in Python 2.5 and higher. The context managed is the transaction state. Upon exit the transaction is either rolled back or committed depending on whether an exception took place or not. #) Make the search for the lib32 and lib64 directories automatic for all platforms. #) Tweak the setup configuration script to include all of the metadata and allow for building the module within another setup configuration script #) Include the Oracle version in addition to the Python version in the build directories that are created and in the names of the binary packages that are created. #) Remove unnecessary dependency on win32api to build module on Windows. cx_Oracle 4.3.2 (August 2007) ----------------------------- #) Added methods open(), close(), isopen() and getchunksize() in order to improve performance of reading/writing LOB values in chunks. #) Fixed support for native doubles and floats in Oracle 10g; added new type NATIVE_FLOAT to allow specification of a variable of that specific type where desired. Thanks to D.R. Boxhoorn for pointing out the fact that this was not working properly when the arraysize was anything other than 1. #) When calling connection.begin(), only create a new transaction handle if one is not already associated with the connection. Thanks to Andreas Mock for discovering this and for Amaury Forgeot d'Arc for diagnosing the problem and pointing the way to a solution. #) Added attribute cursor.rowfactory which allows a method to be called for each row that is returned; this is about 20% faster than calling the method in Python using the idiom [method(\*r) for r in cursor]. #) Attempt to locate an Oracle installation by looking at the PATH if the environment variable ``ORACLE_HOME`` is not set; this is of primary use on Windows where this variable should not normally be set. #) Added support for autocommit mode as requested by Ian Kelly. #) Added support for connection.stmtcachesize which allows for both reading and writing the size of the statement cache size. This parameter can make a huge difference with the length of time taken to prepare statements. Added support for setting the statement tag when preparing a statement. Both of these were requested by Bjorn Sandberg who also provided an initial patch. #) When copying the value of a variable, copy the return code as well. cx_Oracle 4.3.1 (April 2007) ---------------------------- #) Ensure that if the client buffer size exceeds 4000 bytes that the server buffer size does not as strings may only contain 4000 bytes; this allows handling of multibyte character sets on the server as well as the client. #) Added support for using buffer objects to populate binary data and made the Binary() constructor the buffer type as requested by Ken Mason. #) Fix potential crash when using full optimization with some compilers. Thanks to Aris Motas for noticing this and providing the initial patch and to Amaury Forgeot d'Arc for providing an even simpler solution. #) Pass the correct charset form in to the write call in order to support writing to national character set LOB values properly. Thanks to Ian Kelly for noticing this discrepancy. cx_Oracle 4.3 (March 2007) -------------------------- #) Added preliminary support for fetching Oracle objects (SQL types) as requested by Kristof Beyls (who kindly provided an initial patch). Additional work needs to be done to support binding and updating objects but the basic structure is now in place. #) Added connection.maxBytesPerCharacter which indicates the maximum number of bytes each character can use; use this value to also determine the size of local buffers in order to handle discrepancies between the client character set and the server character set. Thanks to Andreas Mock for providing the initial patch and working with me to resolve this issue. #) Added support for querying native floats in Oracle 10g as requested by Danny Boxhoorn. #) Add support for temporary LOB variables created via PL/SQL instead of only directly by cx_Oracle; thanks to Henning von Bargen for discovering this problem. #) Added support for specifying variable types using the builtin types int, float, str and datetime.date which allows for finer control of what type of Python object is returned from cursor.callfunc() for example. #) Added support for passing booleans to callproc() and callfunc() as requested by Anana Aiyer. #) Fixed support for 64-bit environments in Python 2.5. #) Thanks to Filip Ballegeer and a number of his co-workers, an intermittent crash was tracked down; specifically, if a connection is closed, then the call to OCIStmtRelease() will free memory twice. Preventing the call when the connection is closed solves the problem. cx_Oracle 4.2.1 (September 2006) -------------------------------- #) Added additional type (NCLOB) to handle CLOBs that use the national character set as requested by Chris Dunscombe. #) Added support for returning cursors from functions as requested by Daniel Steinmann. #) Added support for getting/setting the "get" mode on session pools as requested by Anand Aiyer. #) Added support for binding subclassed cursors. #) Fixed binding of decimal objects with absolute values less than 0.1. cx_Oracle 4.2 (July 2006) ------------------------- #) Added support for parsing an Oracle statement as requested by Patrick Blackwill. #) Added support for BFILEs at the request of Matthew Cahn. #) Added support for binding decimal.Decimal objects to cursors. #) Added support for reading from NCLOBs as requested by Chris Dunscombe. #) Added connection attributes encoding and nencoding which return the IANA character set name for the character set and national character set in use by the client. #) Rework module initialization to use the techniques recommended by the Python documentation as one user was experiencing random segfaults due to the use of the module dictionary after the initialization was complete. #) Removed support for the OPT_Threading attribute. Use the threaded keyword when creating connections and session pools instead. #) Removed support for the OPT_NumbersAsStrings attribute. Use the numbersAsStrings attribute on cursors instead. #) Use type long rather than type int in order to support long integers on 64-bit machines as reported by Uwe Hoffmann. #) Add cursor attribute "bindarraysize" which is defaulted to 1 and is used to determine the size of the arrays created for bind variables. #) Added repr() methods to provide something a little more useful than the standard type name and memory address. #) Added keyword parameter support to the functions that imply such in the documentation as requested by Harald Armin Massa. #) Treat an empty dictionary passed through to cursor.execute() as keyword parameters the same as if no keyword parameters were specified at all, as requested by Fabien Grumelard. #) Fixed memory leak when a LOB read would fail. #) Set the LDFLAGS value in the environment rather than directly in the setup.py file in order to satisfy those who wish to enable the use of debugging symbols. #) Use __DATE__ and __TIME__ to determine the date and time of the build rather than passing it through directly. #) Use Oracle types and add casts to reduce warnings as requested by Amaury Forgeot d'Arc. #) Fixed typo in error message. cx_Oracle 4.1.2 (December 2005) ------------------------------- #) Restore support of Oracle 9i features when using the Oracle 10g client. cx_Oracle 4.1.1 (December 2005) ------------------------------- #) Add support for dropping a connection from a session pool. #) Add support for write only attributes "module", "action" and "clientinfo" which work only in Oracle 10g as requested by Egor Starostin. #) Add support for pickling database errors. #) Use the previously created bind variable as a template if available when creating a new variable of a larger size. Thanks to Ted Skolnick for the initial patch. #) Fixed tests to work properly in the Python 2.4 environment where dates and timestamps are different Python types. Thanks to Henning von Bargen for pointing this out. #) Added additional directories to search for include files and libraries in order to better support the Oracle 10g instant client. #) Set the internal fetch number to 0 in order to satisfy very picky source analysis tools as requested by Amaury Fogeot d'Arc. #) Improve the documentation for building and installing the module from source as some people are unaware of the standard methods for building Python modules using distutils. #) Added note in the documentation indicating that the arraysize attribute can drastically affect performance of queries since this seems to be a common misunderstanding of first time users of cx_Oracle. #) Add a comment indicating that on HP-UX Itanium with Oracle 10g the library ttsh10 must also be linked against. Thanks to Bernard Delmee for the information. cx_Oracle 4.1 (January 2005) ---------------------------- #) Fixed bug where subclasses of Cursor do not pass the connection in the constructor causing a segfault. #) DDL statements must be reparsed before execution as noted by Mihai Ibanescu. #) Add support for setting input sizes by position. #) Fixed problem with catching an exception during execute and then still attempting to perform a fetch afterwards as noted by Leith Parkin. #) Rename the types so that they can be pickled and unpickled. Thanks to Harri Pasanen for pointing out the problem. #) Handle invalid NLS_LANG setting properly (Oracle seems to like to provide a handle back even though it is invalid) and determine the number of bytes per character in order to allow for proper support in the future of multibyte and variable width character sets. #) Remove date checking from the native case since Python already checks that dates are valid; enhance error message when invalid dates are encountered so that additional processing can be done. #) Fix bug executing SQL using numeric parameter names with predefined variables (such as what takes place when calling stored procedures with out parameters). #) Add support for reading CLOB values using multibyte or variable length character sets. cx_Oracle 4.1 beta 1 (September 2004) ------------------------------------- #) Added support for Python 2.4. In Python 2.4, the datetime module is used for both binding and fetching of date and timestamp data. In Python 2.3, objects from the datetime module can be bound but the internal datetime objects will be returned from queries. #) Added pickling support for LOB and datetime data. #) Fully qualified the table name that was missing in an alter table statement in the setup test script as noted by Marc Gehling. #) Added a section allowing for the setting of the RPATH linker directive in setup.py as requested by Iustin Pop. #) Added code to raise a programming error exception when an attempt is made to access a LOB locator variable in a subsequent fetch. #) The username, password and dsn (tnsentry) are stored on the connection object when specified, regardless of whether or not a standard connection takes place. #) Added additional module level constant called "LOB" as requested by Joseph Canedo. #) Changed exception type to IntegrityError for constraint violations as requested by Joseph Canedo. #) If scale and precision are not specified, an attempt is made to return a long integer as requested by Joseph Canedo. #) Added workaround for Oracle bug which returns an invalid handle when the prepare call fails. Thanks to alantam@hsbc.com for providing the code that demonstrated the problem. #) The cursor method arrayvar() will now accept the actual list so that it is not necessary to call cursor.arrayvar() followed immediately by var.setvalue(). #) Fixed bug where attempts to execute the statement "None" with bind variables would cause a segmentation fault. #) Added support for binding by position (paramstyle = "numeric"). #) Removed memory leak created by calls to OCIParamGet() which were not mirrored by calls to OCIDescriptorFree(). Thanks to Mihai Ibanescu for pointing this out and providing a patch. #) Added support for calling cursor.executemany() with statement None implying that the previously prepared statement ought to be executed. Thanks to Mihai Ibanescu for providing a patch. #) Added support for rebinding variables when a subsequent call to cursor.executemany() uses a different number of rows. Thanks to Mihai Ibanescu for supplying a patch. #) The microseconds are now displayed in datetime variables when nonzero similar to method used in the datetime module. #) Added support for binary_float and binary_double columns in Oracle 10g. cx_Oracle 4.0.1 (February 2004) ------------------------------- #) Fixed bugs on 64-bit platforms that caused segmentation faults and bus errors in session pooling and determining the bind variables associated with a statement. #) Modified test suite so that 64-bit platforms are tested properly. #) Added missing commit statements in the test setup scripts. Thanks to Keith Lyon for pointing this out. #) Fix setup.py for Cygwin environments. Thanks to Doug Henderson for providing the necessary fix. #) Added support for compiling cx_Oracle without thread support. Thanks to Andre Reitz for pointing this out. #) Added support for a new keyword parameter called threaded on connections and session pools. This parameter defaults to False and indicates whether threaded mode ought to be used. It replaces the module level attribute OPT_Threading although examining the attribute will be retained until the next release at least. #) Added support for a new keyword parameter called twophase on connections. This parameter defaults to False and indicates whether support for two phase (distributed or global) transactions ought to be present. Note that support for distributed transactions is buggy when crossing major version boundaries (Oracle 8i to Oracle 9i for example). #) Ensure that the rowcount attribute is set properly when an exception is raised during execution. Thanks to Gary Aviv for pointing out this problem and its solution. cx_Oracle 4.0 (December 2003) ----------------------------- #) Added support for subclassing connections, cursors and session pools. The changes involved made it necessary to drop support for Python 2.1 and earlier although a branch exists in CVS to allow for support of Python 2.1 and earlier if needed. #) Connections and session pools can now be created with keyword parameters, not just sequential parameters. #) Queries now return integers whenever possible and long integers if the number will overflow a simple integer. Floats are only returned when it is known that the number is a floating point number or the integer conversion fails. #) Added initial support for user callbacks on OCI functions. See the documentation for more details. #) Add support for retrieving the bind variable names associated with a cursor with a new method bindnames(). #) Add support for temporary LOB variables. This means that setinputsizes() can be used with the values CLOB and BLOB to create these temporary LOB variables and allow for the equivalent of empty_clob() and empty_blob() since otherwise Oracle will treat empty strings as NULL values. #) Automatically switch to long strings when the data size exceeds the maximum string size that Oracle allows (4000 characters) and raise an error if an attempt is made to set a string variable to a size that it does not support. This avoids truncation errors as reported by Jon Franz. #) Add support for global (distributed) transactions and two phase commit. #) Force the NLS settings for the session so that test tables are populated correctly in all circumstances; problems were noted by Ralf Braun and Allan Poulsen. #) Display error messages using the environment handle when the error handle has not yet been created; this provides better error messages during this rather rare situation. #) Removed memory leak in callproc() that was reported by Todd Whiteman. #) Make consistent the calls to manipulate memory; otherwise segfaults can occur when the pymalloc option is used, as reported by Matt Hoskins. #) Force a rollback when a session is released back to the session pool. Apparently the connections are not as stateless as Oracle's documentation suggests and this makes the logic consistent with normal connections. #) Removed module method attach(). This can be replaced with a call to Connection(handle=) if needed. cx_Oracle 3.1 (August 2003) --------------------------- #) Added support for connecting with SYSDBA and SYSOPER access which is needed for connecting as sys in Oracle 9i. #) Only check the dictionary size if the variable is not NULL; otherwise, an error takes place which is not caught or cleared; this eliminates a spurious "Objects/dictobject.c:1258: bad argument to internal function" in Python 2.3. #) Add support for session pooling. This is only support for Oracle 9i but is amazingly fast -- about 100 times faster than connecting. #) Add support for statement caching when pooling sessions, this reduces the parse time considerably. Unfortunately, the Oracle OCI does not allow this to be easily turned on for normal sessions. #) Add method trim() on CLOB and BLOB variables for trimming the size. #) Add support for externally identified users; to use this feature leave the username and password fields empty when connecting. #) Add method cancel() on connection objects to cancel long running queries. Note that this only works on non-Windows platforms. #) Add method callfunc() on cursor objects to allow calling a function without using an anonymous PL/SQL block. #) Added documentation on objects that were not documented. At this point all objects, methods and constants in cx_Oracle have been documented. #) Added support for timestamp columns in Oracle 9i. #) Added module level method makedsn() which creates a data source name given the host, port and SID. #) Added constant "buildtime" which is the time when the module was built as an additional means of identifying the build that is in use. #) Binding a value that is incompatible to the previous value that was bound (data types do not match or array size is larger) will now result in a new bind taking place. This is more consistent with the DB API although it does imply a performance penalty when used. cx_Oracle 3.0a (June 2003) -------------------------- #) Fixed bug where zero length PL/SQL arrays were being mishandled #) Fixed support for the data type "float" in Oracle; added one to the display size to allow for the sign of the number, if necessary; changed the display size of unconstrained numbers to 127, which is the largest number that Oracle can handle #) Added support for retrieving the description of a bound cursor before fetching it #) Fixed a couple of build issues on Mac OS X, AIX and Solaris (64-bit) #) Modified documentation slightly based on comments from several people #) Included files in MANIFEST that are needed to generate the binaries #) Modified test suite to work within the test environment at Computronix as well as within the packages that are distributed cx_Oracle 3.0 (March 2003) -------------------------- #) Removed support for connection to Oracle7 databases; it is entirely possible that it will still work but I no longer have any way of testing and Oracle has dropped any meaningful support for Oracle7 anyway #) Fetching of strings is now done with predefined memory areas rather than dynamic memory areas; dynamic fetching of strings was causing problems with Oracle 9i in some instances and databases using a different character set other than US ASCII #) Fixed bug where segfault would occur if the '/' character preceded the '@' character in a connect string #) Added two new cursor methods var() and arrayvar() in order to eliminate the need for setinputsizes() when defining PL/SQL arrays and as a generic method of acquiring bind variables directly when needed #) Fixed support for binding cursors and added support for fetching cursors (these are known as ref cursors in PL/SQL). #) Eliminated discrepancy between the array size used internally and the array size specified by the interface user; this was done earlier to avoid bus errors on 64-bit platforms but another way has been found to get around that issue and a number of people were getting confused because of the discrepancy #) Added support for the attribute "connection" on cursors, an optional DB API extension #) Added support for passing a dictionary as the second parameter for the cursor.execute() method in order to comply with the DB API more closely; the method of passing parameters with keyword parameters is still supported and is in fact preferred #) Added support for the attribute "statement" on cursors which is a reference to the last SQL statement prepared or executed #) Added support for passing any sequence to callproc() rather than just lists as before #) Fixed bug where segfault would occur if the array size was changed after the cursor was executed but before it was fetched #) Ignore array size when performing executemany() and use the length of the list of parameters instead #) Rollback when connection is closed or destroyed to follow DB API rather than use the Oracle default (which is commit) #) Added check for array size too large causing an integer overflow #) Added support for iterators for Python 2.2 and above #) Added test suite based on PyUnitTest #) Added documentation in HTML format similar to the documentation for the core Python library cx_Oracle 2.5a (August 2002) ---------------------------- #) Fix problem with Oracle 9i and retrieving strings; it seems that Oracle 9i uses the correct method for dynamic callback but Oracle 8i will not work with that method so an #ifdef was added to check for the existence of an Oracle 9i feature; thanks to Paul Denize for discovering this problem cx_Oracle 2.5 (July 2002) ------------------------- #) Added flag OPT_NoOracle7 which, if set, assumes that connections are being made to Oracle8 or higher databases; this allows for eliminating the overhead in performing this check at connect time #) Added flag OPT_NumbersAsStrings which, if set, returns all numbers as strings rather than integers or floats; this flag is used when defined variables are created (during select statements only) #) Added flag OPT_Threading which, if set, uses OCI threading mode; there is a significant performance degradation in this mode (about 15-20%) but it does allow threads to share connections (threadsafety level 2 according to the Python Database API 2.0); note that in order to support this, Oracle 8i or higher is now required #) Added Py_BEGIN_ALLOW_THREADS and Py_END_ALLOW_THREADS pairs where applicable to support threading during blocking OCI calls #) Added global method attach() to cx_Oracle to support attaching to an existing database handle (as provided by PowerBuilder, for example) #) Eliminated the cursor method fetchbinds() which was used for returning the list of bind variables after execution to get the values of out variables; the cursor method setinputsizes() was modified to return the list of bind variables and the cursor method execute() was modified to return the list of defined variables in the case of a select statement being executed; these variables have three methods available to them: getvalue([]) to get the value of a variable, setvalue(, ) to set its value and copy(, , ) to copy the value from a variable in a more efficient manner than setvalue(getvalue()) #) Implemented cursor method executemany() which expects a list of dictionaries for the parameters #) Implemented cursor method callproc() #) Added cursor method prepare() which parses (prepares) the statement for execution; subsequent execute() or executemany() calls can pass None as the statement which will imply use of the previously prepared statement; used for high performance only #) Added cursor method fetchraw() which will perform a raw fetch of the cursor returning the number of rows thus fetched; this is used to avoid the overhead of generating result sets; used for high performance only #) Added cursor method executemanyprepared() which is identical to the method executemany() except that it takes a single parameter which is the number of times to execute a previously prepared statement and it assumes that the bind variables already have their values set; used for high performance only #) Added support for rowid being returned in a select statement #) Added support for comparing dates returned by cx_Oracle #) Integrated patch from Andre Reitz to set the null ok flag in the description attribute of the cursor #) Integrated patch from Andre Reitz to setup.py to support compilation with Python 1.5 #) Integrated patch from Benjamin Kearns to setup.py to support compilation on Cygwin cx_Oracle 2.4 (January 2002) ---------------------------- #) String variables can now be made any length (previously restricted to the 64K limit imposed by Oracle for default binding); use the type cx_Oracle.LONG_STRING as the parameter to setinputsizes() for binding in string values larger than 4000 bytes. #) Raw and long raw columns are now supported; use the types cx_Oracle.BINARY and cx_Oracle.LONG_BINARY as the parameter to setinputsizes() for binding in values of these types. #) Functions DateFromTicks(), TimeFromTicks() and TimestampFromTicks() are now implemented. #) Function cursor.setoutputsize() implemented #) Added the ability to bind arrays as out parameters to procedures; use the format [cx_Oracle., ] as the input to the function setinputsizes() for binding arrays #) Discovered from the Oracle 8.1.6 version of the documentation of the OCI libraries, that the size of the memory location required for the precision variable is larger than the printed documentation says; this was causing a problem with the code on the Sun platform. #) Now support building RPMs for Linux. cx_Oracle 2.3 (October 2001) ---------------------------- #) Incremental performance enhancements (dealing with reusing cursors and bind handles) #) Ensured that arrays of integers with a single float in them are all treated as floats, as suggested by Martin Koch. #) Fixed code dealing with scale and precision for both defining a numeric variable and for providing the cursor description; this eliminates the problem of an underflow error (OCI-22054) when retrieving data with non-zero scale. cx_Oracle 2.2 (July 2001) ------------------------- #) Upgraded thread safety to level 1 (according to the Python DB API 2.0) as an internal project required the ability to share the module between threads. #) Added ability to bind ref cursors to PL/SQL blocks as requested by Brad Powell. #) Added function write(Value, [Offset]) to LOB variables as requested by Matthias Kirst. #) Procedure execute() on Cursor objects now permits a value None for the statement which means that the previously prepared statement will be executed and any input sizes set earlier will be retained. This was done to improve the performance of scripts that execute one statement many times. #) Modified module global constants BINARY and DATETIME to point to the external representations of those types so that the expression type(var) == cx_Oracle.DATETIME will work as expected. #) Added global constant version to provide means of determining the current version of the module. #) Modified error checking routine to distinguish between an Oracle error and invalid handles. #) Added error checking to avoid setting the value of a bind variable to a value that it cannot support and raised an exception to indicate this fact. #) Added extra compile arguments for the AIX platform as suggested by Jehwan Ryu. #) Added section to the README to indicate the method for a binary installation as suggested by Steve Holden. #) Added simple usage example as requested by many people. #) Added HISTORY file to the distribution. python-oracledb-1.2.1/doc/src/user_guide/000077500000000000000000000000001434177474600203155ustar00rootroot00000000000000python-oracledb-1.2.1/doc/src/user_guide/appendix_a.rst000066400000000000000000000420611434177474600231620ustar00rootroot00000000000000.. _featuresummary: ***************************************************************** Appendix A: Oracle Database Features Supported by python-oracledb ***************************************************************** By default, python-oracledb runs in a 'Thin' mode which connects directly to Oracle Database. This mode does not need Oracle Client libraries. However, some additional functionality is available when python-oracledb uses them. Python-oracledb is said to be in 'Thick' mode when Oracle Client libraries are used. Both modes have comprehensive functionality supporting the Python Database API v2.0 Specification. See :ref:`initialization` for how to enable Thick mode. The following table summarizes the Oracle Database features supported by python-oracledb Thin and Thick modes, and by cx_Oracle 8.3. For more details see :ref:`driverdiff` and :ref:`compatibility`. .. list-table-with-summary:: Features Supported by python-oracledb and cx_Oracle 8.3 :header-rows: 1 :class: wy-table-responsive :align: center :summary: The first column displays the Oracle feature. The second column indicates whether the feature is supported in the python-oracledb Thin mode. The third column indicates whether the feature is supported in the python-oracledb Thick mode. The fourth column indicates if the feature is supported in cx_Oracle 8.3. * - Oracle Feature - python-oracledb Thin Mode - python-oracledb Thick Mode - cx_Oracle 8.3 * - Python Database API Support - Yes - a couple of features are not feasible. Many extensions. - Yes - a couple of features are not feasible. Many extensions. - Yes - a couple of features are not feasible. Many extensions. * - Oracle Client version - Not applicable - Release 11.2 and later - Release 11.2 and later * - Oracle Database version - Release 12.1 and later - Release 9.2 and later depending on Oracle Client library version - Release 9.2 and later depending on Oracle Client library version * - Standalone connections (see :ref:`standaloneconnection`) - Yes - must use keyword arguments - Yes - must use keyword arguments - Yes * - Connection Pooling - Heterogeneous and Homogeneous (see :ref:`Connection pooling `) - Homogeneous only - must use keyword arguments - Yes - must use keyword arguments - Yes * - Connection Pool Connection Load Balancing (CLB) - Yes - Yes - Yes * - Connection Pool Runtime Load Balancing (RLB) - No - Yes - Yes * - Connection Pool draining - Yes - Yes - Yes * - Connection Pool session state callback (see :ref:`sessioncallback`) - Yes - Python functions but not PL/SQL functions - Yes - Yes * - Connection pool session tagging (see :ref:`conntagging`) - No - Yes - Yes * - Password authentication - Yes - Yes - Yes * - External authentication (see :ref:`extauth`) - No - Yes - Yes * - Oracle Cloud Infrastructure (OCI) Identity and Access Management (IAM) Tokens (see :ref:`iamauth`) - Yes - Yes - Yes - in connection string with appropriate Oracle Client * - Open Authorization (OAuth 2.0) (see :ref:`oauth2`) - Yes - Yes - Yes - in connection string with appropriate Oracle Client * - Kerberos and Radius authentication - No - Yes - Yes * - Proxy connections (see :ref:`proxyauth`) - Yes - Yes - Yes * - Connection mode privileges (see :ref:`connection-authorization-modes`) - Yes - Yes - only :data:`~oracledb.AUTH_MODE_SYSDBA` is supported in Thick mode - Yes * - Preliminary connections - No - Yes - Yes * - Set the current schema using an attribute - Yes - Yes - Yes * - Oracle Cloud Database connectivity (see :ref:`autonomousdb`) - Yes - Yes - Yes * - Real Application Clusters (RAC) - Yes - Yes - Yes * - Oracle Sharded Databases (see :ref:`connsharding`) - No - Yes - No TIMESTAMP support - Yes - No TIMESTAMP support * - Oracle Database Native Network Encryption (see :ref:`nne`) - No - Yes - Yes * - Connection pinging API - Yes - Yes - Yes * - Oracle Net Services ``tnsnames.ora`` file (see :ref:`optnetfiles`) - Yes - Yes - Yes * - Oracle Net Services ``sqlnet.ora`` file (see :ref:`optnetfiles`) - No - many values can be set at connection time - Yes - Yes * - Oracle Client library configuration file ``oraaccess.xml`` (see :ref:`optclientfiles`) - Not applicable - Yes - Yes * - Easy Connect Plus connection strings - Yes - mostly supported. Unknown settings are ignored and not passed to Oracle Database. - Yes - Yes * - One-way TLS connections (see :ref:`onewaytls`) - Yes - Yes - Yes * - Mutual TLS (mTLS) connections (see :ref:`twowaytls`) - Yes - needs a PEM format wallet (see :ref:`createpem`) - Yes - Yes * - Oracle Database Dedicated Servers, Shared Servers and Database Resident Connection Pooling (DRCP) - Yes - Yes - Yes * - Multitenant Databases - Yes - Yes - Yes * - CMAN and CMAN-TDM connectivity - Yes - Yes - Yes * - Password changing (see :meth:`Connection.changepassword()`) - Yes - Yes - Yes * - Statement break/reset (see :meth:`Connection.cancel()`) - Yes - Yes - Yes * - Edition Based Redefinition (EBR) (see :ref:`ebr`) - No - not at connect time. ALTER SESSION can be used. - Yes - Yes * - SQL execution (see :ref:`sqlexecution`) - Yes - bind and fetch all types except BFILE and JSON - Yes - Yes * - PL/SQL execution (see :ref:`plsqlexecution`) - Yes for scalar types. Yes for collection types using array interface. - Yes - Yes * - Simple Oracle Document Access (SODA) API (:ref:`SODA `) - No - Yes - Yes * - Bind variables for data binding (see :ref:`bind`) - Yes - Yes - Yes * - Array DML binding for bulk DML and PL/SQL (see :ref:`batchstmnt`) - Yes - Yes - Yes * - SQL and PL/SQL type and collections (see :ref:`fetchobjects`) - Yes - Yes - Yes * - Query column metadata - Yes - Yes - Yes * - Client character set support (see :ref:`globalization`) - UTF-8 - UTF-8 - Yes - can use Python encodings. Default in 8.0 is UTF-8 * - Oracle Globalization support - No - All NLS environment variables are ignored. Use Python globalization support instead - Yes - NLS environment variables are respected except character set in NLS_LANG - Yes - NLS environment variables are respected except character set in NLS_LANG * - Row prefetching on first query execute.(see :attr:`prefetchrows`) - Yes - Yes - Yes * - Array fetching for queries (see :attr:`arraysize`) - Yes - Yes - Yes * - Statement caching (see :ref:`stmtcache`) - Yes - new driver also supports dropping from the cache - Yes - new driver also supports dropping from the cache - Yes * - Client Result Caching (CRC) (see :ref:`clientresultcache`) - No - Yes - Yes * - Continuous Query Notification (CQN) (see :ref:`cqn`) - No - Yes - Yes * - Advanced Queuing (AQ) (see :ref:`aqusermanual`) - No - Yes - must use new API introduced in cx_Oracle 7.2 - Yes * - Call timeouts (see :attr:`Connection.call_timeout`) - Yes - Yes - Yes * - Scrollable cursors (see :ref:`scrollablecursors`) - No - Yes - Yes * - Oracle Database startup and shutdown (see :ref:`startup`) - No - Yes - Yes * - Transaction management (see :ref:`txnmgmnt`) - Yes - Yes - Yes * - Events mode for notifications - No - Yes - Yes * - Fast Application Notification (FAN) (see :ref:`fan`) - No - Yes - Yes * - In-band notifications - Yes - Yes - Yes * - Transparent Application Failover (TAF) - No - Yes - no callback - Yes - no callback * - Transaction Guard (TG) (see :ref:`tg`) - No - Yes - Yes * - Data Guard (DG) and Active Data Guard (ADG) - Yes - Yes - Yes * - Application Continuity (AC) and Transparent Application Continuity (TAC) (see :ref:`appcont`) - No - Yes - Yes * - End-to-end monitoring and tracing attributes (see :ref:`tracingsql`) - Yes - Yes - Yes * - Automatic Diagnostic Repository (ADR) - No - Yes - Yes * - Java Debug Wire Protocol for debugging PL/SQL (see :ref:`jdwp`) - Yes - Yes - Yes * - Two-phase Commit (TPC) - No - Yes - improved support (see :ref:`tcp`) - Yes - limited support * - REF CURSORs and Nested Cursors - Yes - Yes - Yes * - Pipelined tables - Yes - Yes - Yes * - Implicit Result Sets - Yes - Yes - Yes * - Application Contexts - No - Yes - Yes * - Persistent and Temporary LOBs - Yes - Yes - Yes * - LOB prefetching - No - No - does have LOB length prefetch - No - does have LOB length prefetch * - LOB locator operations such as trim - Yes - Yes - Yes * - CHAR, VARCHAR2, NUMBER, FLOAT, DATE, and LONG data types - Yes - Yes - Yes * - BLOB and CLOB data types - Yes - Yes - Yes * - BINARY_DOUBLE and BINARY_FLOAT data types - Yes - Yes - Yes * - RAW and LONG RAW data types - Yes - Yes - Yes * - INTERVAL DAY TO SECOND data type (see :data:`~oracledb.DB_TYPE_INTERVAL_DS`) - Yes - Yes - Yes * - INTERVAL YEAR TO MONTH data type (see :data:`~oracledb.DB_TYPE_INTERVAL_YM`) - No - No - No * - Oracle 12c JSON - Yes - Yes - Yes * - Oracle 21c JSON data type (see :data:`~oracledb.DB_TYPE_JSON`) - No - can fetch with an output type handler, see :ref:`Fetching JSON Differences ` - Yes - Yes * - ROWID, UROWID data types - Yes - Yes - Yes * - TIMESTAMP, TIMESTAMP WITH TIME ZONE, TIMESTAMP WITH LOCAL TIME ZONE data types - Yes - Yes - Yes * - NCHAR, NVARCHAR2, NCLOB data types - Yes - Yes - Yes * - PL/SQL data types BOOLEAN, PLS_INTEGER and BINARY_INTEGER - Yes - Yes - Yes * - XMLType data type (see :ref:`xmldatatype`) - Yes - Yes - may need to fetch as CLOB - Yes - may need to fetch as CLOB * - BFILE data type (see :data:`~oracledb.DB_TYPE_BFILE`) - No - Yes - Yes .. _supporteddbtypes: Supported Oracle Database Data Types ==================================== The following table lists the Oracle Database types that are supported in the python-oracledb driver. See `Oracle Database Types `__ and `PL/SQL Types `__. The python-oracledb constant shown is the common one. In some python-oracledb APIs you may use other types, for example when binding numeric values. .. list-table-with-summary:: Oracle Database Data Types Supported :header-rows: 1 :class: wy-table-responsive :align: center :summary: The first column displays the database data type. The second column displays the python-oracledb constant Name. The third column indicates if the type is supported in python-oracledb. * - Oracle Database Type - python-oracledb Constant Name - Supported in python-oracledb - Supported Python Types * - VARCHAR2 - DB_TYPE_VARCHAR - Yes - bytes, str * - NVARCHAR2 - DB_TYPE_NVARCHAR - Yes - bytes, str * - NUMBER, FLOAT - DB_TYPE_NUMBER - Yes - bool, int, float, decimal.Decimal * - DATE - DB_TYPE_DATE - Yes - datetime.date, datetime.datetime * - BOOLEAN (PL/SQL) - DB_TYPE_BOOLEAN - Yes - ANY (converted to bool) * - BINARY_DOUBLE - DB_TYPE_BINARY_DOUBLE - Yes - bool, int, float, decimal.Decimal * - BINARY_FLOAT - DB_TYPE_BINARY_FLOAT - Yes - bool, int, float, decimal.Decimal * - TIMESTAMP - DB_TYPE_TIMESTAMP - Yes - datetime.date, datetime.datetime * - TIMESTAMP WITH TIME ZONE - DB_TYPE_TIMESTAMP_TZ - Yes - datetime.date, datetime.datetime * - TIMESTAMP WITH LOCAL TIME ZONE - DB_TYPE_TIMESTAMP_LTZ - Yes - datetime.date, datetime.datetime * - INTERVAL YEAR TO MONTH - DB_TYPE_INTERVAL_YM - Not supported in python-oracledb - cannot be set * - INTERVAL DAY TO SECOND - DB_TYPE_INTERVAL_DS - Yes - datetime.timedelta * - RAW - DB_TYPE_RAW - Yes - bytes, str * - LONG - DB_TYPE_LONG - Yes - bytes, str * - LONG RAW - DB_TYPE_LONG_RAW - Yes - bytes, str * - ROWID - DB_TYPE_ROWID - Yes - bytes, str * - UROWID - DB_TYPE_ROWID, DB_TYPE_UROWID (only supported in python-oracledb Thin mode) - Yes. May show DB_TYPE_UROWID in metadata. See :ref:`Query Metadata Differences `. - bytes, str * - CHAR - DB_TYPE_CHAR - Yes - bytes, str * - BLOB - DB_TYPE_BLOB - Yes - BLOB, bytes, str * - CLOB - DB_TYPE_CLOB - Yes - CLOB, bytes, str * - NCHAR - DB_TYPE_NCHAR - Yes - bytes, str * - NCLOB - DB_TYPE_NCLOB - Yes - NCLOB, bytes, str * - BFILE - DB_TYPE_BFILE - Not supported in python-oracledb Thin mode - cannot be set * - JSON - DB_TYPE_JSON - Yes. In python-oracledb Thin mode use an output type handler to fetch this Oracle Database 21c data type. See :ref:`jsondatatype`. - ANY (converted) * - REF CURSOR (PL/SQL OR nested cursor) - DB_TYPE_CURSOR - Yes - CURSOR * - PLS_INTEGER - DB_TYPE_BINARY_INTEGER - Yes - bool, int, float, decimal.Decimal * - BINARY_INTEGER - DB_TYPE_BINARY_INTEGER - Yes - bool, int, float, decimal.Decimal * - REF - n/a - Not supported in python-oracledb Thin mode - n/a * - XMLType - n/a - Not supported in python-oracledb. Use ``xmltype.getclobval()`` to fetch. - n/a * - User-defined types (object type, VARRAY, records, collections, SDO_*types) - DB_TYPE_OBJECT - Yes - OBJECT of specific type Binding of contiguous PL/SQL Index-by BINARY_INTEGER arrays of string, number, and date are supported in python-oracledb Thin and Thick modes. Use :meth:`Cursor.arrayvar()` to build these arrays. .. Python Types supported for each Oracle Database Type are shown below... list-table-with-summary:: Oracle Database Types Supported :header-rows: 1 :align: center :summary: The first column displays the Oracle Database type. The second column displays the Python types that are supported for each of the database types. * - Oracle Database Type - Python Types supported * - DB_TYPE_BFILE - cannot be set * - DB_TYPE_BINARY_DOUBLE - bool, int, float, decimal.Decimal * - DB_TYPE_BINARY_FLOAT - bool, int, float, decimal.Decimal * - DB_TYPE_BINARY_INTEGER - bool, int, float, decimal.Decimal * - DB_TYPE_BLOB - BLOB, bytes, str * - DB_TYPE_BOOLEAN - ANY (converted to bool) * - DB_TYPE_CHAR - bytes, str * - DB_TYPE_CLOB - CLOB, bytes, str * - DB_TYPE_CURSOR - CURSOR * - DB_TYPE_DATE - datetime.date, datetime.datetime * - DB_TYPE_INTERVAL_DS - datetime.timedelta * - DB_TYPE_INTERVAL_YM - cannot be set * - DB_TYPE_JSON - ANY (converted) * - DB_TYPE_LONG - bytes, str * - DB_TYPE_LONG_NVARCHAR - bytes, str * - DB_TYPE_LONG_RAW - bytes, str * - DB_TYPE_NCHAR - bytes, str * - DB_TYPE_NCLOB - NCLOB, bytes, str * - DB_TYPE_NUMBER - bool, int, float, decimal.Decimal * - DB_TYPE_NVARCHAR - bytes, str * - DB_TYPE_OBJECT - OBJECT of specific type * - DB_TYPE_RAW - bytes, str * - DB_TYPE_ROWID - bytes, str * - DB_TYPE_TIMESTAMP - datetime.date, datetime.datetime * - DB_TYPE_TIMESTAMP_LTZ - datetime.date, datetime.datetime * - DB_TYPE_TIMESTAMP_TZ - datetime.date, datetime.datetime * - DB_TYPE_UROWID - bytes, str * - DB_TYPE_VARCHAR - bytes, str python-oracledb-1.2.1/doc/src/user_guide/appendix_b.rst000066400000000000000000000357331434177474600231730ustar00rootroot00000000000000.. _driverdiff: ******************************************************************** Appendix B: Differences between python-oracledb Thin and Thick Modes ******************************************************************** By default, python-oracledb runs in a 'Thin' mode which connects directly to Oracle Database. This mode does not need Oracle Client libraries. However, some :ref:`additional functionality ` is available when python-oracledb uses them. Python-oracledb is said to be in 'Thick' mode when Oracle Client libraries are used. See :ref:`enablingthick`. Both modes have comprehensive functionality supporting the Python Database API v2.0 Specification. This section details the differences between the python-oracledb Thin and Thick modes. Also see the summary feature comparison table in :ref:`featuresummary`. Connection Handling Differences between Thin and Thick Modes ============================================================ Python-oracledb can create connections in either a Thin mode or a Thick mode. However, only one of these modes can be used in each Python process: - By default, python-oracledb runs in a Thin mode which connects directly to Oracle Database. - If :func:`oracledb.init_oracle_client()` loads Oracle Client libraries before any standalone connection or pool is created, then the python-oracledb mode becomes Thick. The client libraries handle communication with Oracle Database. See :ref:`enablingthick`. - If an application opens a connection or creates a pool and then calls :func:`oracledb.init_oracle_client()`, an error will occur. - Once a connection or pool has been opened, or :func:`~oracledb.init_oracle_client()` has been called, you cannot change the mode. .. note:: The parameters of connection and pool creation functions :func:`oracledb.connect()` and :func:`oracledb.create_pool()` are now keyword and not positional in both Thin and Thick modes. This change makes the python-oracledb driver compliant with the Python Database API specification PEP 249. The old usage will cause an error, see :ref:`connerrors`. Connections to a Local Database ------------------------------- In Thin mode, there is no concept of a local database. Bequeath connections cannot be made since no Oracle Client libraries are used. The Thin mode does not de-reference environment variables such as ``ORACLE_SID``, ``TWO_TASK``, or ``LOCAL`` (the latter is specific to Windows). A connection string, or equivalent, must always be used. .. _sqlnetclientconfig: Oracle Net Services and Client Configuration Files -------------------------------------------------- In the python-oracledb Thin mode: - The location of any ``tnsnames.ora`` files must explicitly be passed to the application. - Any ``sqlnet.ora`` file will not be read. Instead, pass an equivalent setting when connecting. - There is no support for ``oraaccess.xml`` since there are no Oracle Client libraries. See :ref:`optnetfiles` and :ref:`optclientfiles` for more information. .. _diffconnstr: Connection Strings ------------------ The python-oracledb Thin mode accepts connection strings in the same formats as the Oracle Client libraries used by Thick mode does, but not all Oracle Net keywords will be supported. The following table lists the parameters that are recognized in Thin mode either in Easy Connect Strings or in Full Connect Descriptor Strings that are either explicitly passed or referred to by a ``tnsnames.ora`` alias. All unrecognized parameters are ignored. The connection parameters shown can be used in :meth:`oracledb.connect()`, :meth:`oracledb.create_pool()`, :meth:`oracledb.ConnectParams()`, and :meth:`oracledb.PoolParams()`. .. list-table-with-summary:: Oracle Net Keywords Supported in the python-oracledb Thin Mode :header-rows: 1 :class: wy-table-responsive :align: center :summary: The first column displays the keyword. The second column displays the equivalent oracledb.connect(), oracledb.create_pool(), oracledb.ConnectParams(), or oracledb.PoolParams() parameters. The third column displays the notes. * - Oracle Net Keyword - Equivalent Connection Parameter - Notes * - SSL_SERVER_CERT_DN - ssl_server_cert_dn - If specified, this value is used for any verification. Otherwise, the hostname will be used. * - SSL_SERVER_DN_MATCH - ssl_server_dn_match - In Thin mode parsing the parameter supports case insensitive on/yes/true values similar to the Thick mode. Any other value is treated as disabling it. * - WALLET_LOCATION - wallet_location - Used in Easy Connect Strings. It is same as ``MY_WALLET_DIRECTORY`` in a connect descriptor. * - MY_WALLET_DIRECTORY - wallet_location - * - EXPIRE_TIME - expire_time - * - HTTPS_PROXY - https_proxy - * - HTTPS_PROXY_PORT - https_proxy_port - * - RETRY_COUNT - retry_count - * - RETRY_DELAY - retry_delay - * - TRANSPORT_CONNECT_TIMEOUT - tcp_connect_timeout - * - POOL_CONNECTION_CLASS - cclass - * - POOL_PURITY - purity - * - SERVICE_NAME - service_name - * - SID - sid - * - PORT - port - * - PROTOCOL - protocol - In python-oracledb Thin mode, using the ``POOL_CONNECTION_CLASS`` or ``POOL_PURITY`` parameters in a connection string is similar to setting the equivalent attributes when creating a connection or connection pool. In python-oracledb Thick mode, the ``POOL_CONNECTION_CLASS`` or ``POOL_PURITY`` values will only work when connected to Oracle Database 21c. Note if ``POOL_PURITY=SELF`` is used in a connect string, then python-oracledb Thick mode applications will ignore the action to drop the session when attempting to remove an unusable connections from a pool in some uncommon error cases. It is recommended to avoid using ``POOL_PURITY=SELF`` in a connect string with python-oracledb Thick mode. Instead, code the python-oracledb Thick mode application to explicitly specify the purity and connection class as attributes. The ``ENABLE=BROKEN`` connect descriptor option is not supported in python-oracledb Thin mode. Use ``expire_time`` instead. The ``Session Data Unit (SDU)`` connect descriptor option that is used to tune network transfers is not supported in python-oracledb Thin mode. The value is hard-coded as 8 KB. In python-oracledb Thick mode, the SDU connect descriptor option and equivalent ``sqlnet.ora`` setting are used. If a name is given as a connect string, then the python-oracledb Thin mode will consider it as a Net Service Name and not as the minimal Easy Connect string of a hostname. The given connect string will be looked up in a ``tnsnames.ora`` file. This is different from the python-oracledb Thick mode. If supporting a bare name as a hostname is important to you in the python-oracledb Thin mode, then you can alter the connection string to include a port number such as ``hostname:1521`` or a protocol such as ``tcp://hostname``. Token Based Authentication -------------------------- In the python-oracledb Thin mode: - When connecting to Oracle Cloud Database with mutual TLS (mTLS) using OAuth2 tokens, you need to explicitly set the ``config_dir``, ``wallet_location``, and ``wallet_password`` parameters of :func:`~oracledb.connect` or :func:`~oracledb.create_pool()`. See, :ref:`autonomousdb`. - :ref:`Open Authorization (OAuth 2.0) token based authentication connection strings ` and :ref:`Oracle Cloud Infrastructure (OCI) Identity and Access Management (IAM) token based authentication connection strings ` are not supported. Use ``access_token`` parameter of :func:`oracledb.ConnectParams()` instead. See :ref:`tokenauth`. Transport Layer Security (TLS) Support -------------------------------------- When connecting with mutual TLS (mTLS) also known as two-way TLS, for example to Oracle Autonomous Database in Oracle Cloud using a wallet, the certificate must be in the correct format. For the python-oracledb Thin mode, the certificate must be in a Privacy Enhanced Mail (PEM) ``ewallet.pem`` file. In python-oracledb Thick mode the certificate must be in a ``cwallet.sso`` file. See :ref:`autonomousdb` for more information. Native Network Encryption and Checksumming ------------------------------------------ The python-oracledb Thin mode does not support connections using Oracle Database native network encryption or checksumming. You can enable TLS instead of using native network encryption. If native network encryption or checksumming are required, then use python-oracledb in the Thick mode. See :ref:`enablingthick`. For example, if you use python-oracledb Thin mode and try to connect to the Oracle Cloud Infrastructure (OCI) Oracle Base Database where by default native network encryption is set to REQUIRED in the ``sqlnet.ora`` file of the OCI Oracle Base Database server, the connection will fail with the error:: DPY-6000: cannot connect to database. Listener refused connection. (Similar to ORA-12660) Connection Pooling Differences between Thin and Thick Modes =========================================================== Python-oracledb introduced the :ref:`ConnectionPool Object ` class to replace ``SessionPool``. A new :func:`oracledb.create_pool()` method is now the recommended way to create a connection pool. The use of the equivalent ``SessionPool()`` constructor is :ref:`deprecated `. The :func:`~oracledb.create_pool()` method in the python-oracledb Thin mode differs from the python-oracledb Thick mode in the following ways: * Not all the parameters of the :func:`oracledb.create_pool()` method are applicable to both python-oracledb modes. Each mode ignores unrecognized parameters. The parameters that are ignored in Thin mode include ``events``, ``tag``, ``matchanytag``, ``appcontext``, ``shardingkey``, ``supershardingkey``, and ``handle`` parameters. The parameters that are ignored in the Thick mode include ``wallet_password``, ``disable_oob``, ``config_dir``, and ``debug_jdwp`` parameters. * The python-oracledb Thin mode only suppports :ref:`homogeneous ` pools. * The python-oracledb Thin mode creates connections in a daemon thread and so :func:`oracledb.create_pool()` returns before any or all minimum number of connections are created. As soon as the pool is created, the :attr:`ConnectionPool.opened` attribute will not be equal to :attr:`ConnectionPool.min`. The :attr:`~ConnectionPool.opened` attribute will increase to the minimum value over a short time as the connections are established. Note that this behavior may also be true of recent versions of the Oracle Call Interface (OCI) Session Pool used in the Thin mode. If the new ``getmode`` default value of :data:`~oracledb.POOL_GETMODE_WAIT` is used, then this behavior will not be an issue. With this new default value, any immediate :meth:`ConnectionPool.acquire()` calls will wait for the connections to be created by the daemon thread. This improves the application start up time when compared to the python-oracledb Thick mode and cx_Oracle 8.3 driver, where :func:`oracledb.create_pool()` will not return control to the application until all ``pool.min`` connections have been created. If the old default value ``POOL_GETMODE_NOWAIT`` is required, then the application could check if :attr:`ConnectionPool.opened` has reached :attr:`ConnectionPool.min` and then continue with application start up. * In python-oracledb Thick mode, when you close a connection pool with the parameter ``force=True``, the underlying Oracle Client libraries wait for the current SQL executions to complete before closing the connections. All of the connections are then dropped from the pool and the pool is closed. Closing the pool in python-oracledb Thick mode could stop responding indefinitely, depending on the network and Oracle Net Services timeout parameters. This is also applicable to the cx_Oracle 8.3 driver. In python-oracledb Thin mode, the parameter ``force=True`` disconnects each connection's socket using a background thread, leaving the database to clean up its end of the connections. * In python-oracledb Thin mode, the ``cclass`` parameter value is not used to tag connections in the application connection pool. It is only used for :ref:`drcp`. * In python-oracledb Thin mode, the connection pool supports all the :ref:`connection mode privileges `. The python-oracledb Thick mode only supports the :data:`~oracledb.AUTH_MODE_SYSDBA` privilege. Supported Database Data Types in Thin and Thick Modes ===================================================== The python-oracledb Thin and Thick modes support different Oracle database data types. See :ref:`supporteddbtypes`. .. _querymetadatadiff: Query Metadata in Thin and Thick Modes ====================================== In python-oracledb Thin mode, :data:`Cursor.description` metadata can distinguish the ROWID and UROWID database types. The UROWID database type shows the new value ``DB_TYPE_UROWID`` and the database type ROWID uses the existing value ``DB_TYPE_ROWID``. In python-oracledb Thick mode, the value ``DB_TYPE_ROWID`` is shown for both ROWID and UROWID database types. In python-oracledb Thick and Thin modes, comparison with the type ``oracledb.ROWID`` (defined in the Python DB API) will match both ROWID and UROWID database types. .. _stmtcaching: Statement Caching in Thin and Thick Modes ========================================= The :ref:`statement cache ` implemented in the python-oracledb Thin mode is capable of determining when different database data types are used for the same bind variables when a statement is re-executed. This capability is not supported in the Oracle Client libraries that are used in python-oracledb Thick mode. Note changing the type of bind variables for the same SQL text is inappropriate and gives indeterminate results in both modes. .. _fetchJSON: Fetching JSON in Thin and Thick Modes ===================================== The python-oracledb Thin mode does not natively handle the Oracle Database 21c JSON data type but a type handler can be used when fetching the type, see :ref:`jsondatatype`. Error Handling in Thin and Thick Modes ====================================== The python-oracledb Thin and Thick modes handle some errors differently. See :ref:`errorhandling`. Globalization in Thin and Thick Modes ===================================== All NLS environment variables, and the ``ORA_SDTZ`` and ``ORA_TZFILE`` environment variables, are ignored by the python-oracledb Thin mode. Use Python's capabilities instead. The python-oracledb Thin mode can only use NCHAR, NVARCHAR2, and NCLOB data when Oracle Database's secondary character set is AL16UTF16. See :ref:`globalization`. Tracing in Thin and Thick Modes =============================== In the python-oracledb Thin mode, low level tracing is different because there are no Oracle Client libraries. See :ref:`tracingsql`. python-oracledb-1.2.1/doc/src/user_guide/appendix_c.rst000066400000000000000000000645571434177474600232020ustar00rootroot00000000000000.. _upgradecomparison: ***************************************************** Appendix C: The python-oracledb and cx_Oracle Drivers ***************************************************** The python-oracledb driver is the renamed, major version successor to `cx_Oracle 8.3 `__. As a major release, the python-oracledb driver has :ref:`new features ` and some :ref:`deprecations`. Also see :ref:`upgrading83`. .. _compatibility: Differences between the python-oracledb and cx_Oracle Drivers ============================================================= The differences between the cx_Oracle 8.3 and python-oracledb drivers are listed here. Mode differences from cx_Oracle ------------------------------- By default, python-oracledb runs in a 'Thin' mode which connects directly to Oracle Database. This mode does not need Oracle Client libraries. However, some :ref:`additional functionality ` is available when python-oracledb uses them. Python-oracledb is said to be in 'Thick' mode when Oracle Client libraries are used. See :ref:`enablingthick`. Both modes have comprehensive functionality supporting the Python Database API v2.0 Specification. cx_Oracle always runs in a Thick mode using Oracle Client libraries. The features in python-oracledb Thick mode and cx_Oracle 8.3 are the same, subject to the :ref:`new features `, some :ref:`deprecations `, and to other changes noted in this section. Oracle Client Library Loading Differences from cx_Oracle -------------------------------------------------------- Oracle Client libraries are now only loaded if :func:`oracledb.init_oracle_client()` is called in your application. This changes python-oracledb to Thick mode. The ``init_oracle_client()`` method must be called before any :ref:`standalone connection ` or :ref:`connection pool ` is created. If a connection or pool is created first in the default Thin mode, then Thick mode cannot be enabled. See :ref:`enablingthick` for more information. Calling the ``init_oracle_client()`` method immediately loads Oracle Client libraries. To emulate the cx_Oracle behavior of deferring library loading until the creation of the first connection (in the case when ``init_oracle_client()`` is not called), your application will need to defer calling ``init_oracle_client()`` as appropriate. In python-oracledb, ``init_oracle_client()`` can now be called multiple times in the one Python process as long as its arguments are the same each time. oracledb.clientversion() ++++++++++++++++++++++++ The :func:`oracledb.clientversion()` method shows the version of the Oracle Client libraries being used. There is no Oracle Client used in the python-oracledb Thin mode so this function can only be called in python-oracledb Thick mode. If this function is called before :func:`oracledb.init_oracle_client()`, an exception is thrown. Connection Differences from cx_Oracle ------------------------------------- .. _connectdiffs: oracledb.connect() Differences ++++++++++++++++++++++++++++++ The :func:`oracledb.connect()` function in the python-oracledb driver differs from cx_Oracle: - Keyword parameters **must** be used in calls to :func:`oracledb.connect()`. This change makes the driver compliant with the Python Database API specification PEP 249. See :ref:`Standalone Connections ` and :ref:`connerrors`. - New keyword arguments can be passed to :func:`~oracledb.connect()`. For example you can pass the hostname, port and servicename as separate parameters instead of using an Easy Connect connection string. In python-oracledb Thin mode, some of the new arguments replace ``sqlnet.ora`` settings. - A new optional parameter ``params`` of type :ref:`ConnectParams ` can be used to encapsulate connection properties. See :ref:`usingconnparams` for more information. - The following parameters are deprecated and ignored: - ``encoding`` and ``nencoding``: The encodings in use are always UTF-8. - ``threaded``: Threaded Oracle Call Interface (OCI) is now always enabled in Thick mode. This option is not relevant to the Thin mode. See :ref:`deprecations` for more information. The use of the class constructor method ``oracledb.Connection()`` to create connections is no longer recommended for creating connections. Use :func:`~oracledb.connect()` instead. Connection Object Differences +++++++++++++++++++++++++++++ The :ref:`Connection object ` differences between the python-oracledb and cx_Oracle drivers are: - The attribute :attr:`Connection.maxBytesPerCharacter` is deprecated. This will return a constant value of 4 since encodings are always UTF-8. - A new boolean attribute, :attr:`Connection.thin` is available. This attribute is True if the connection was established in the Thin mode. In Thick mode, the value of this attribute is False. See :ref:`connattrs` for more information. Pooling Differences from cx_Oracle ---------------------------------- It is recommended to use the new equivalent :ref:`ConnectionPool Object ` instead of the SessionPool object, which is deprecated. To create a connection pool, use :meth:`oracledb.create_pool()`, which is equivalent to calling `cx_Oracle.SessionPool() `__. For more information, see :ref:`connpooling`. oracledb.SessionPool() Differences ++++++++++++++++++++++++++++++++++ The python-oracledb ``oracledb.SessionPool()`` method (which is an alias of :func:`oracledb.create_pool()`) differs from `cx_Oracle.SessionPool() `_ as follows: - Keyword parameters **must** be used in calls. This change makes the driver compliant with the Python Database API specification PEP 249. See :ref:`Connection pooling ` and :ref:`connerrors`. - Passing a value to the ``dsn`` parameter that contains the user name and password is now supported in the same way as :func:`oracledb.connect()`. For example ``dsn="un/pw@cs"`` can be used. - New keyword arguments can be passed to :func:`~oracledb.create_pool()`. For example you can pass the hostname, port and servicename as separate parameters instead of using an Easy Connect connection string. In python-oracledb Thin mode, some of the new arguments replace ``sqlnet.ora`` settings. - The default mode is :data:`~oracledb.POOL_GETMODE_WAIT` instead of :data:`~oracledb.POOL_GETMODE_NOWAIT`. If the mode :data:`~oracledb.POOL_GETMODE_NOWAIT` is truly desired, modify any pool creation code to specify this value instead. Note the namespace of constant has been improved. Old names like ``SPOOL_ATTRVAL_NOWAIT`` can be used but are now deprecated. - A new optional parameter ``params`` of type :ref:`PoolParams ` can be used to encapsulate connection properties. See :ref:`usingconnparams` for more information. - The ``encoding`` and ``decoding`` parameters are deprecated and ignored. The encodings in use are always UTF-8. - New keyword arguments that are used internally to create a :ref:`PoolParams object ` before creating the connection. SessionPool Object Differences ++++++++++++++++++++++++++++++ The SessionPool object (which is an alias for the :ref:`ConnectionPool object `) differences between the python-oracledb and cx_Oracle drivers are: - A Python type() will show the class as ``oracledb.ConnectionPool`` instead of ``cx_Oracle.SessionPool``. - A new boolean attribute, ``SessionPool.thin`` (see :attr:`ConnectionPool.thin`) is available. This attribute is True if the connection was established in the Thin mode. In Thick mode, the value of this attribute is False. Cursor Object Differences from cx_Oracle ---------------------------------------- The differences between the :ref:`Cursor object ` in python-oracledb and cx_Oracle drivers are: - :meth:`Cursor.fetchmany()`: The name of the size argument of ``fetchmany()`` is ``size``. This change was done to comply with `PEP 249 `_. The previous keyword argument name, ``numRows`` is deprecated. - ``Cursor.fetchraw()``: This method was previously deprecated in cx_Oracle 8.2 and has been removed in python-oracledb. Instead, use one of the other fetch methods such as :meth:`Cursor.fetchmany()`. - ``Cursor.executemanyprepared()``: This method was previously deprecated in cx_Oracle 6.4 and has been removed in python-oracledb. Instead, use :meth:`Cursor.executemany()`, by passing None for the statement argument and an integer for the parameters argument. - ``Cursor.bindarraysize``: This attribute is deprecated and removed in python-oracledb. It is not needed in the application code. - :attr:`Cursor.rowcount`: After :meth:`Cursor.execute()` or :meth:`Cursor.executemany()` with PL/SQL statements, ``Cursor.rowcount`` will return 0. If the cursor or connection are not open, then the value -1 will be returned as required by the Python Database API. Advanced Queuing (AQ) Differences from cx_Oracle ------------------------------------------------ The old Advanced Queuing (AQ) API is not available in python-oracledb since it was deprecated in cx_Oracle 7.2. Use the :ref:`new Advanced Queuing (AQ) `. Note that AQ is only available in the Thick mode. Replace: - :meth:`Connection.deq()` with :meth:`Queue.deqone()` or :meth:`Queue.deqmany()` - :meth:`Connection.deqoptions()` with attribute :attr:`Queue.deqoptions` - :meth:`Connection.enq()` with :meth:`Queue.enqone()` or :meth:`Queue.enqmany()` - :meth:`Connection.deqoptions()` with attribute :attr:`Queue.deqoptions` The AQ feature in the python-oracledb driver differs from cx_Oracle as follows: - AQ messages can be enqueued and dequeued as a JSON payload type - Recipient lists can be enqueued and dequeued - Enqueue options, dequeue options, and message properties can be set See :ref:`Oracle Advanced Queuing (AQ) `. .. _errordiff: Error Handling Differences from cx_Oracle ----------------------------------------- In python-oracledb Thick mode, error messages generated by the Oracle Client libraries and the `ODPI-C `_ layer used by cx_Oracle and python-oracledb in Thick mode are mostly returned unchanged from cx_Oracle 8.3 with the exceptions shown below. Note that the python-oracledb driver error messages can vary between Thin and Thick modes. See :ref:`errorhandling`. ConnectionPool.acquire() Message Differences ++++++++++++++++++++++++++++++++++++++++++++ :meth:`ConnectionPool.acquire()` ORA errors will be mapped to DPY errors. For example:: DPY-4005: timed out waiting for the connection pool to return a connection replaces the cx_Oracle 8.3 error:: ORA-24459: OCISessionGet() timed out waiting for pool to create new connections Dead Connection Detection and Timeout Message Differences +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Application code which detects connection failures or statement execution timeouts will need to check for new errors, DPY-4011 and DPY-4024 respectively. The error DPY-1001 is returned if an already dead connection is attempted to be used. The new Error object attribute :attr:`~oracledb._Error.full_code` may be useful for checking the error code. Example error messages are: * Scenario 1: An already closed or dead connection was attempted to be used. python-oracledb Thin Error:: DPY-1001: not connected to database python-oracledb Thick Error:: DPY-1001: not connected to database cx_Oracle Error:: not connected * Scenario 2: The database side of the connection was terminated while the connection was being used. python-oracledb Thin Error:: DPY-4011: the database or network closed the connection python-oracledb Thick Error:: DPY-4011: the database or network closed the connection DPI-1080: connection was closed by ORA-%d cx_Oracle Error:: DPI-1080: connection was closed by ORA-%d * Scenario 3: Statement execution exceeded the :attr:`connection.call_timeout` value. python-oracledb Thin Error:: DPY-4024: call timeout of {timeout} ms exceeded python-oracledb Thick Error:: DPY-4024: call timeout of {timeout} ms exceeded DPI-1067: call timeout of %u ms exceeded with ORA-%d cx_Oracle Error:: DPI-1067: call timeout of %u ms exceeded with ORA-%d .. _upgrading83: Upgrading from cx_Oracle 8.3 to python-oracledb =============================================== This section provides the detailed steps needed to upgrade from cx_Oracle 8.3 to python-oracledb. Things to Know Before the Upgrade --------------------------------- Below is a list of some useful things to know before upgrading from cx_Oracle to python-oracledb: - You can have both cx_Oracle and python-oracledb installed, and can use both in the same application. - If you only want to use the python-oracledb driver in Thin mode, then you do not need Oracle Client libraries such as from Oracle Instant Client. You only need to :ref:`install ` the driver itself:: python -m pip install oracledb See :ref:`driverdiff`. - The python-oracledb Thin and Thick modes have the same level of support for the `Python Database API specification `_ and can be used to connect to on-premises databases and Oracle Cloud databases. However, the python-oracledb Thin mode does not support some of the advanced Oracle Database features such as Application Continuity (AC), Advanced Queuing (AQ), Continuous Query Notification (CQN), and Sharding. See :ref:`Features Supported ` for details. - python-oracledb can be used in SQLAlchemy, Django, Pandas, and other frameworks and Object-relational Mappers (ORMs). Until they add native support, you can override the use of cx_Oracle with a few lines of code. See :ref:`frameworks`. - python-oracledb connection and pool creation calls require keyword arguments to conform with the Python Database API specification. For example you must use: .. code-block:: python oracledb.connect(user="scott", password=pw, dsn="localhost/orclpdb") This no longer works: .. code-block:: python oracledb.connect("scott", pw, "localhost/orclpdb") - The python-oracledb Thin mode ignores all NLS environment variables. It also ignores ``ORA_SDTZ`` and ``ORA_TZFILE`` environment variables. Thick mode does use these variables. See :ref:`globalization` for alternatives. - To use a ``tnsnames.ora`` file in the python-oracledb Thin mode, you must explicitly set the environment variable ``TNS_ADMIN`` to the directory containing the file, or set :attr:`defaults.config_dir`, or set the ``config_dir`` parameter when connecting. Only python-oracledb Thick mode will read ``sqlnet.ora`` files. The Thin mode lets equivalent properties be set in the application when connecting. Configuration files in a "default" location such as the Instant Client ``network/admin/`` subdirectory, in ``$ORACLE_HOME/network/admin/``, or in ``$ORACLE_BASE/homes/XYZ/network/admin/`` (in a read-only Oracle Database home) is not automatically loaded in Thin mode. Default locations are automatically searched by Thick mode. - To use the python-oracledb Thin mode in an ORACLE_HOME database installation environment, you use an explicit connection string since the ``ORACLE_SID``, ``TWO_TASK`` and ``LOCAL`` environment variables are not used. They are used in Thick mode. - This is a major release so some previously deprecated features are no longer available. See :ref:`deprecations`. .. _commonupgrade: Steps to Upgrade to python-oracledb ----------------------------------- If you are creating new applications, follow :ref:`installation` and refer to other sections of the documentation for usage information. To upgrade existing code from cx_Oracle to python-oracledb, perform the following steps: 1. Install the new python-oracledb module:: python -m pip install oracledb See :ref:`installation` for more details. 2. Import the new interface module. This can be done in two ways. You can change: .. code-block:: python import cx_Oracle to: .. code-block:: python import oracledb as cx_Oracle Alternatively, you can replace all references to the module ``cx_Oracle`` with ``oracledb``. For example, change: .. code-block:: python import cx_Oracle c = cx_Oracle.connect(...) to: .. code-block:: python import oracledb c = oracledb.connect(...) Any new code being introduced during the upgrade should aim to use the latter syntax. 3. Use keyword parameters in calls to :func:`oracledb.connect()`, ``oracledb.Connection()``, and ``oracledb.SessionPool()``. You **must** replace positional parameters with keyword parameters, unless only one parameter is being passed. Python-oracledb uses keyword parameters exclusively unless a DSN containing the user, password, and connect string combined, for example ``un/pw@cs``, is used. This change makes the driver compliant with the Python Database API specification `PEP 249 `_. For example, the following code will fail: .. code-block:: python c = oracledb.connect("un", "pw", "cs") and needs to be changed to: .. code-block:: python c = oracledb.connect(user="un", password="pw", dsn="cs") The following example will continue to work without change: .. code-block:: python c = oracledb.connect("un/pw@cs") 4. Review obsolete encoding parameters in calls to :func:`oracledb.connect()`, ``oracledb.Connection()``, and ``oracledb.SessionPool()``: - ``encoding`` and ``nencoding`` are ignored by python-oracledb. The python-oracledb driver uses UTF-8 exclusively. - ``threaded`` is ignored in :func:`oracledb.connect()` and ``oracledb.Connection()`` by python-oracledb. This parameter was already ignored in ``oracledb.SessionPool()`` from cx_Oracle 8.2. 5. Remove all references to :meth:`Cursor.fetchraw()` as this method was deprecated in cx_Oracle 8.2 and has been removed in python-oracledb. Instead, use one of the other fetch methods such as :meth:`Cursor.fetchmany()`. 6. The default value of the ``oracledb.SessionPool()`` parameter :attr:`~Connection.getmode` now waits for an available connection. That is the default is now :data:`~oracledb.SPOOL_ATTRVAL_WAIT` instead of :data:`~oracledb.SPOOL_ATTRVAL_NOWAIT`. The new default value improves the behavior for most applications. If the pool is in the middle of growing, the new value prevents transient connection creation errors from occurring when using the Thin mode, or when using the Thick mode with recent Oracle Client libraries. If the old default value is required, modify any pool creation code to explicitly specify ``getmode=oracledb.POOL_SPOOL_ATTRVAL_NOWAIT``. Note a :ref:`ConnectionPool class ` deprecates the equivalent SessionPool class. The method :meth:`oracledb.create_pool()` deprecates the use of ``oracledb.SessionPool()``. New pool parameter constant names such as :data:`~oracledb.POOL_GETMODE_NOWAIT` and :data:`~oracledb.PURITY_SELF` are now preferred. The old namespaces still work. 7. Review the following sections to see if your application requirements are satisfied by the python-oracledb Thin mode: - :ref:`featuresummary` - :ref:`driverdiff` If your application requirements are not supported by the Thin mode, then use the python-oracledb Thick mode. 8. Review :ref:`compatibility`. If your code base uses an older cx_Oracle version, review the previous :ref:`release notes ` for additional changes to modernize the code. 9. Modernize code as needed or desired. See :ref:`deprecations` for the list of deprecations in python-oracledb 1.0. Additional Upgrade Steps to use python-oracledb Thin Mode +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ To use python-oracledb Thin mode, the following changes need to be made in addition to the common :ref:`commonupgrade`: 1. Remove calls to :func:`~oracledb.init_oracle_client` since this turns on the python-oracledb Thick mode. 2. If the ``config_dir`` parameter of :func:`~oracledb.init_oracle_client` had been used, then set the new :attr:`defaults.config_dir` attribute to the desired value or set the ``config_dir`` parameter when connecting. For example: .. code-block:: python oracledb.defaults.config_dir = "/opt/oracle/config" Also see :ref:`sqlnetclientconfig`. 3. If the application is connecting using an :ref:`Oracle Net service name ` from a ``tnsnames.ora`` file located in a "default" location such as the Instant Client ``network/admin/`` subdirectory, in ``$ORACLE_HOME/network/admin/``, or in ``$ORACLE_BASE/homes/XYZ/network/admin/`` (in a read-only Oracle Database home), then the configuration file directory must now explicitly be set as shown above. 4. Remove calls to :func:`oracledb.clientversion()` which is only available in the python-oracledb Thick mode. Oracle Client libraries are not available in Thin mode. 5. Ensure that any assumptions about when connections are created in the connection pool are eliminated. The python-oracledb Thin mode creates connections in a daemon thread and so the attribute :attr:`ConnectionPool.opened` will change over time and will not be equal to :attr:`ConnectionPool.min` immediately after the pool is created. Note that this behavior is also similar in recent versions of the Oracle Call Interface (OCI) Session Pool used by the Thick mode. Unless the ``oracledb.SessionPool()`` function's parameter ``getmode`` is ``SPOOL_ATTRVAL_WAIT`` (or the new equivalent :data:`oracledb.POOL_GETMODE_WAIT`), then applications should not call :meth:`ConnectionPool.acquire()` until sufficient time has passed for connections in the pool to be created. 6. Review error handling improvements. See :ref:`errorhandling`. 7. Review locale and globalization usage. See :ref:`globalization`. Additional Upgrade Steps to use python-oracledb Thick Mode ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ To use python-oracledb Thick mode, the following changes need to be made in addition to the common :ref:`commonupgrade`: 1. The function :func:`~oracledb.init_oracle_client()` *must* be called. It can be called anywhere before the first call to :func:`~oracledb.connect()`, ``oracledb.Connection()``, and ``oracledb.SessionPool()``. This enables the Thick mode. See :ref:`enablingthick` for more details. The requirement to call ``init_oracle_client()`` means that Oracle Client library loading is not automatically deferred until the driver is first used, such as when a connection is opened. The application must explicitly manage this, if deferral is required. In python-oracledb, ``init_oracle_client()`` can be called multiple times in a Python process as long as arguments are the same. Note that on Linux and related operating systems, the ``init_oracle_client()`` parameter ``lib_dir`` should not be passed. Instead, set the system library search path with ``ldconfig`` or ``LD_LIBRARY_PATH`` prior to running Python. 2. Replace all usages of the deprecated Advanced Queuing API with the new :ref:`AQ API ` originally introduced in cx_Oracle 7.2, see the `cx_Oracle Advanced Queuing (AQ) `_ documentation. 3. Review error handling improvements. See :ref:`errorhandling`. Code to Aid the Upgrade to python-oracledb ------------------------------------------ .. _toggling: Toggling between Drivers ++++++++++++++++++++++++ The sample `oracledb_upgrade.py `__ shows a way to toggle applications between cx_Oracle and the two python-oracledb modes. Note this script cannot map some functionality such as :ref:`obsolete cx_Oracle ` features or error message changes. An example application showing this module in use is: .. code-block:: python # test.py import oracledb_upgrade as cx_Oracle import os un = os.environ.get("PYTHON_USERNAME") pw = os.environ.get("PYTHON_PASSWORD") cs = os.environ.get("PYTHON_CONNECTSTRING") connection = cx_Oracle.connect(user=un, password=pw, dsn=cs) with connection.cursor() as cursor: sql = """SELECT UNIQUE CLIENT_DRIVER FROM V$SESSION_CONNECT_INFO WHERE SID = SYS_CONTEXT('USERENV', 'SID')""" for r, in cursor.execute(sql): print(r) You can then choose what mode is in use by setting the environment variable ``ORA_PYTHON_DRIVER_TYPE`` to one of "cx", "thin", or "thick":: export ORA_PYTHON_DRIVER_TYPE=thin python test.py Output shows the python-oracledb Thin mode was used:: python-oracledb thn : 1.0.0 You can customize ``oracledb_upgrade.py`` to your needs. For example, if your connection and pool creation calls always use keyword parameters, you can remove the shims that map from positional arguments to keyword arguments. The simplest form is shown in :ref:`frameworks`. Testing Which Driver is in Use ++++++++++++++++++++++++++++++ To know whether the driver is cx_Oracle or python-oracledb, you can use code similar to: .. code-block:: python import oracledb as cx_Oracle # or: # import cx_Oracle if cx_Oracle.__name__ == 'cx_Oracle': print('cx_Oracle') else: print('oracledb') Another method that can be used to check which driver is in use is to query ``V$SESSION_CONNECT_INFO``, see :ref:`vsessconinfo`. .. _frameworks: Python Frameworks, SQL Generators, and ORMs ------------------------------------------- The python-oracledb Thin mode features in the python-oracledb cover the needs of frameworks that depend upon the Python Database API. Until SQLAlchemy, Django, other frameworks, object-relational mappers (ORMs), and libraries add native support for python-oracledb, you can add temporary code like this to use python-oracledb in-place of cx_Oracle: .. code-block:: python import sys import oracledb oracledb.version = "8.3.0" sys.modules["cx_Oracle"] = oracledb import cx_Oracle .. note:: The import of cx_Oracle occurs last. This code must be run before the library code does its own import of cx_Oracle. python-oracledb-1.2.1/doc/src/user_guide/aq.rst000066400000000000000000000217701434177474600214570ustar00rootroot00000000000000.. _aqusermanual: *********************************** Using Oracle Advanced Queuing (AQ) *********************************** `Oracle Advanced Queuing `__ is a highly configurable and scalable messaging feature of Oracle Database. It has interfaces in various languages, letting you integrate multiple tools in your architecture. .. note:: Oracle Advanced Queuing is only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. Python-oracledb uses the updated interface for Oracle Advanced Queuing that was first introduced in cx_Oracle 7.2. Starting from Oracle Database 21c, Advanced Queuing also supports the JSON payload type. To use the JSON payload type, the Oracle Client libraries must be version 21 or later. There are Advanced Queuing examples in the `GitHub examples `__ directory. Creating a Queue ================ Before being used, queues need to be created in the database. **Using RAW Payloads** Queues can be created using the RAW payload type, for example in SQL*Plus: .. code-block:: sql begin dbms_aqadm.create_queue_table('MY_QUEUE_TABLE', 'RAW'); dbms_aqadm.create_queue('DEMO_RAW_QUEUE', 'MY_QUEUE_TABLE'); dbms_aqadm.start_queue('DEMO_RAW_QUEUE'); end; / This example creates a RAW queue suitable for sending string or bytes messages. **Using JSON Payloads** Also, queues can be created using the JSON payload type. For example, in SQL*Plus: .. code-block:: sql begin dbms_aqadm.create_queue_table('JSON_QUEUE_TABLE', 'JSON'); dbms_aqadm.create_queue('DEMO_JSON_QUEUE', 'JSON_QUEUE_TABLE'); dbms_aqadm.start_queue('DEMO_JSON_QUEUE'); end; / This example creates a JSON queue suitable for sending JSON data messages. Enqueuing Messages ================== To send messages in Python, you connect and get a :ref:`queue `. The queue can be used for enqueuing, dequeuing, or both as needed. **Using RAW Payloads** You can connect to the database and get the queue that was created with RAW payload type by using: .. code-block:: python queue = connection.queue("DEMO_RAW_QUEUE") Now messages can be queued using :meth:`~Queue.enqone()`. To send three messages: .. code-block:: python PAYLOAD_DATA = [ "The first message", "The second message", "The third message" ] for data in PAYLOAD_DATA: queue.enqone(connection.msgproperties(payload=data)) connection.commit() Since the queue sending the messages is a RAW queue, the strings in this example will be internally encoded to bytes using ``message.encode()`` before being enqueued. **Using JSON Payloads** You can connect to the database and get the queue that was created with JSON payload type by using: .. code-block:: python queue = connection.queue("DEMO_JSON_QUEUE", "JSON") # The second argument (JSON) indicates that the queue is of JSON payload type. Now the message can be enqueued using :meth:`~Queue.enqone()`. .. code-block:: python json_data = [ [ 2.75, True, 'Ocean Beach', b'Some bytes', {'keyA': 1.0, 'KeyB': 'Melbourne'}, datetime.datetime(2022, 8, 1, 0, 0) ], dict(name="John", age=30, city="New York") ] for data in json_data: queue.enqone(connection.msgproperties(payload=data)) connection.commit() Dequeuing Messages ================== Dequeuing is performed similarly. To dequeue a message call the method :meth:`~Queue.deqone()` as shown in the examples below. **Using RAW Payload Type** .. code-block:: python queue = connection.queue("DEMO_RAW_QUEUE") message = queue.deqOne() connection.commit() print(message.payload.decode()) Note that if the message is expected to be a string, the bytes must be decoded using ``message.payload.decode()``, as shown. **Using JSON Payload Type** .. code-block:: python queue = connection.queue("DEMO_JSON_QUEUE", "JSON") message = queue.deqOne() connection.commit() Using Object Queues =================== Named Oracle objects can be enqueued and dequeued as well. Given an object type called ``UDT_BOOK``: .. code-block:: sql CREATE OR REPLACE TYPE udt_book AS OBJECT ( Title VARCHAR2(100), Authors VARCHAR2(100), Price NUMBER(5,2) ); / And a queue that accepts this type: .. code-block:: sql begin dbms_aqadm.create_queue_table('BOOK_QUEUE_TAB', 'UDT_BOOK'); dbms_aqadm.create_queue('DEMO_BOOK_QUEUE', 'BOOK_QUEUE_TAB'); dbms_aqadm.start_queue('DEMO_BOOK_QUEUE'); end; / You can queue messages: .. code-block:: python book_type = connection.gettype("UDT_BOOK") queue = connection.queue("DEMO_BOOK_QUEUE", book_type) book = book_type.newobject() book.TITLE = "Quick Brown Fox" book.AUTHORS = "The Dog" book.PRICE = 123 queue.enqone(connection.msgproperties(payload=book)) connection.commit() Dequeuing is done like this: .. code-block:: python book_type = connection.gettype("UDT_BOOK") queue = connection.queue("DEMO_BOOK_QUEUE", book_type) message = queue.deqone() connection.commit() print(message.payload.TITLE) # will print Quick Brown Fox Using Recipient Lists ===================== A list of recipient names can be associated with a message at the time a message is enqueued. This allows a limited set of recipients to dequeue each message. The recipient list associated with the message overrides the queue subscriber list, if there is one. The recipient names need not be in the subscriber list but can be, if desired. To dequeue a message, the ``consumername`` attribute can be set to one of the recipient names. The original message recipient list is not available on dequeued messages. All recipients have to dequeue a message before it gets removed from the queue. Subscribing to a queue is like subscribing to a magazine: each subscriber can dequeue all the messages placed into a specific queue, just as each magazine subscriber has access to all its articles. However, being a recipient is like getting a letter: each recipient is a designated target of a particular message. For example:: props = self.connection.msgproperties(payload=book,recipients=["sub2", "sub3"]) queue.enqone(props) Later, when dequeuing messages, a specific recipient can be set to get the messages intended for that recipient using the ``consumername`` attribute:: queue.deqoptions.consumername = "sub3" m = queue.deqone() Changing Queue and Message Options ================================== Refer to the :ref:`python-oracledb AQ API ` and `Oracle Advanced Queuing documentation `__ for details on all of the enqueue and dequeue options available. Enqueue options can be set. For example, to make it so that an explicit call to :meth:`~Connection.commit()` on the connection is not needed to commit messages: .. code-block:: python queue = connection.queue("DEMO_RAW_QUEUE") queue.enqoptions.visibility = oracledb.ENQ_IMMEDIATE Dequeue options can also be set. For example, to specify not to block on dequeuing if no messages are available: .. code-block:: python queue = connection.queue("DEMO_RAW_QUEUE") queue.deqoptions.wait = oracledb.DEQ_NO_WAIT Message properties can be set when enqueuing. For example, to set an expiration of 60 seconds on a message: .. code-block:: python queue.enqone(connection.msgproperties(payload="Message", expiration=60)) This means that if no dequeue operation occurs within 60 seconds that the message will be dropped from the queue. Bulk Enqueue and Dequeue ======================== The :meth:`~Queue.enqmany()` and :meth:`~Queue.deqmany()` methods can be used for efficient bulk message handling. :meth:`~Queue.enqmany()` is similar to :meth:`~Queue.enqone()` but accepts an array of messages: .. code-block:: python messages = [ "The first message", "The second message", "The third message", ] queue = connection.queue("DEMO_RAW_QUEUE") queue.enqmany(connection.msgproperties(payload=m) for m in messages) connection.commit() .. warning:: Calling :meth:`~Queue.enqmany()` in parallel on different connections acquired from the same pool may fail due to Oracle bug 29928074. Ensure that this function is not run in parallel, use standalone connections or connections from different pools, or make multiple calls to :meth:`~Queue.enqone()` instead. The function :meth:`~Queue.deqmany()` call is not affected. To dequeue multiple messages at one time, use :meth:`~Queue.deqmany()`. This takes an argument specifying the maximum number of messages to dequeue at one time: .. code-block:: python for message in queue.deqmany(10): print(message.payload.decode()) Depending on the queue properties and the number of messages available to dequeue, this code will print out from zero to ten messages. python-oracledb-1.2.1/doc/src/user_guide/batch_statement.rst000066400000000000000000000270001434177474600242130ustar00rootroot00000000000000.. _batchstmnt: ******************************************* Executing Batch Statements and Bulk Loading ******************************************* Inserting or updating multiple rows can be performed efficiently with :meth:`Cursor.executemany()`, making it easy to work with large data sets with python-oracledb. This method can significantly outperform repeated calls to :meth:`Cursor.execute()` by reducing network transfer costs and database overheads. The :meth:`~Cursor.executemany()` method can also be used to execute PL/SQL statements multiple times at once. There are examples in the `GitHub examples `__ directory. The following tables will be used in the samples that follow: .. code-block:: sql create table ParentTable ( ParentId number(9) not null, Description varchar2(60) not null, constraint ParentTable_pk primary key (ParentId) ); create table ChildTable ( ChildId number(9) not null, ParentId number(9) not null, Description varchar2(60) not null, constraint ChildTable_pk primary key (ChildId), constraint ChildTable_fk foreign key (ParentId) references ParentTable ); Batch Execution of SQL ====================== The following example inserts five rows into the table ``ParentTable``: .. code-block:: python data = [ (10, 'Parent 10'), (20, 'Parent 20'), (30, 'Parent 30'), (40, 'Parent 40'), (50, 'Parent 50') ] cursor.executemany("insert into ParentTable values (:1, :2)", data) This code requires only one :ref:`round-trip ` from the client to the database instead of the five round-trips that would be required for repeated calls to :meth:`~Cursor.execute()`. For very large data sets, there may be an external buffer or network limits to how many rows can be processed, so repeated calls to ``executemany()`` may be required. The limits are based on both the number of rows being processed as well as the "size" of each row that is being processed. Repeated calls to :meth:`~Cursor.executemany()` are still better than repeated calls to :meth:`~Cursor.execute()`. Predefining Memory Areas ------------------------ When multiple rows of data are being processed there is the possibility that the data is not uniform in type and size. In such cases, python-oracledb makes some effort to accommodate such differences. Type determination for each column is deferred until a value that is not ``None`` is found in the column's data. If all values in a particular column are ``None``, then python-oracledb assumes the type is a string and has a length of 1. Python-oracledb will also adjust the size of the buffers used to store strings and bytes when a longer value is encountered in the data. These sorts of operations incur overhead as memory has to be reallocated and data copied. To eliminate this overhead, using :meth:`~Cursor.setinputsizes()` tells python-oracledb about the type and size of the data that is going to be used. Consider the following code: .. code-block:: python data = [ (110, "Parent 110"), (2000, "Parent 2000"), (30000, "Parent 30000"), (400000, "Parent 400000"), (5000000, "Parent 5000000") ] cursor.setinputsizes(None, 20) cursor.executemany(""" insert into ParentTable (ParentId, Description) values (:1, :2)""", data) If this example did not call :meth:`~Cursor.setinputsizes()`, then python-oracledb performs five allocations of increasing size and perform data copies as it discovers each new, longer string. However, ``cursor.setinputsizes(None, 20)`` tells python-oracledb that the maximum size of the strings that will be processed is 20 characters. The first parameter of ``None`` tells python-oracledb that its default processing will be sufficient since numeric data is already stored efficiently. Since python-oracledb allocates memory for each row based on the supplied values, do not oversize them. Batch Execution of PL/SQL ========================= PL/SQL functions and procedures and anonymous PL/SQL blocks can also be called using :meth:`~Cursor.executemany()` in order to improve performance. For example: .. code-block:: python data = [ (10, 'Parent 10'), (20, 'Parent 20'), (30, 'Parent 30'), (40, 'Parent 40'), (50, 'Parent 50') ] cursor.executemany("begin mypkg.create_parent(:1, :2); end;", data) If ``executemany()`` is used for PL/SQL code that returns OUT binds it will have the same performance characteristics as repeated calls to ``execute()``. Note that the ``batcherrors`` parameter (discussed below) cannot be used with PL/SQL block execution. Handling Data Errors ==================== Large datasets may contain some invalid data. When using batch execution as discussed above, the entire batch will be discarded if a single error is detected, potentially eliminating the performance benefits of batch execution and increasing the complexity of the code required to handle those errors. If the parameter ``batchErrors`` is set to the value ``True`` when calling :meth:`~Cursor.executemany()`, however, processing will continue even if there are data errors in some rows, and the rows containing errors can be examined afterwards to determine what course the application should take. Note that if any errors are detected, a transaction will be started but not committed, even if :attr:`Connection.autocommit` is set to ``True``. After examining the errors and deciding what to do with them, the application needs to explicitly commit or roll back the transaction with :meth:`Connection.commit()` or :meth:`Connection.rollback()`, as needed. This example shows how data errors can be identified: .. code-block:: python data = [ (60, 'Parent 60'), (70, 'Parent 70'), (70, 'Parent 70 (duplicate)'), (80, 'Parent 80'), (80, 'Parent 80 (duplicate)'), (90, 'Parent 90') ] cursor.executemany("insert into ParentTable values (:1, :2)", data, batcherrors=True) for error in cursor.getbatcherrors(): print("Error", error.message, "at row offset", error.offset) The output is:: Error ORA-00001: unique constraint (PYTHONDEMO.PARENTTABLE_PK) violated at row offset 2 Error ORA-00001: unique constraint (PYTHONDEMO.PARENTTABLE_PK) violated at row offset 4 The row offset is the index into the array of the data that could not be inserted due to errors. The application could choose to commit or rollback the other rows that were successfully inserted. Alternatively, it could correct the data for the two invalid rows and attempt to insert them again before committing. Identifying Affected Rows ========================= When executing a DML statement using :meth:`~Cursor.execute()`, the number of rows affected can be examined by looking at the attribute :attr:`~Cursor.rowcount`. When performing batch execution with :meth:`Cursor.executemany()`, the row count will return the *total* number of rows that were affected. If you want to know the total number of rows affected by each row of data that is bound you must set the parameter ``arraydmlrowcounts`` to ``True``, as shown: .. code-block:: python parent_ids_to_delete = [20, 30, 50] cursor.executemany("delete from ChildTable where ParentId = :1", [(i,) for i in parent_ids_to_delete], arraydmlrowcounts=True) row_counts = cursor.getarraydmlrowcounts() for parent_id, count in zip(parent_ids_to_delete, row_counts): print("Parent ID:", parent_id, "deleted", count, "rows.") Using the data found in the `GitHub samples `__ the output is as follows:: Parent ID: 20 deleted 3 rows. Parent ID: 30 deleted 2 rows. Parent ID: 50 deleted 4 rows. DML RETURNING ============= DML statements like INSERT, UPDATE, DELETE, and MERGE can return values by using the DML RETURNING syntax. A bind variable can be created to accept this data. See :ref:`bind` for more information. If, instead of merely deleting the rows as shown in the previous example, you also wanted to know some information about each of the rows that were deleted, you can use the following code: .. code-block:: python parent_ids_to_delete = [20, 30, 50] child_id_var = cursor.var(int, arraysize=len(parent_ids_to_delete)) cursor.setinputsizes(None, child_id_var) cursor.executemany(""" delete from ChildTable where ParentId = :1 returning ChildId into :2""", [(i,) for i in parent_ids_to_delete]) for ix, parent_id in enumerate(parent_ids_to_delete): print("Child IDs deleted for parent ID", parent_id, "are", child_id_var.getvalue(ix)) The output will be:: Child IDs deleted for parent ID 20 are [1002, 1003, 1004] Child IDs deleted for parent ID 30 are [1005, 1006] Child IDs deleted for parent ID 50 are [1012, 1013, 1014, 1015] Note that the bind variable created to accept the returned data must have an arraysize large enough to hold data for each row that is processed. Also, the call to :meth:`Cursor.setinputsizes()` binds this variable immediately so that it does not have to be passed in each row of data. Loading CSV Files into Oracle Database ====================================== The :meth:`Cursor.executemany()` method and Python's `csv module `__ can be used to efficiently insert CSV (Comma Separated Values) data. For example, consider the file ``data.csv``:: 101,Abel 154,Baker 132,Charlie 199,Delta . . . And the schema: .. code-block:: sql create table test (id number, name varchar2(25)); Data loading can be done in batches of records since the number of records may prevent all data being inserted at once: .. code-block:: python import oracledb import csv # CSV file FILE_NAME = 'data.csv' # Adjust the number of rows to be inserted in each iteration # to meet your memory and performance requirements BATCH_SIZE = 10000 connection = oracledb.connect(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb") with connection.cursor() as cursor: # Predefine the memory areas to match the table definition. # This can improve performance by avoiding memory reallocations. # Here, one parameter is passed for each of the columns. # "None" is used for the ID column, since the size of NUMBER isn't # variable. The "25" matches the maximum expected data size for the # NAME column cursor.setinputsizes(None, 25) with open(FILE_NAME, 'r') as csv_file: csv_reader = csv.reader(csv_file, delimiter=',') sql = "insert into test (id, name) values (:1, :2)" data = [] for line in csv_reader: data.append((line[0], line[1])) if len(data) % BATCH_SIZE == 0: cursor.executemany(sql, data) data = [] if data: cursor.executemany(sql, data) connection.commit() Depending on data sizes and business requirements, database changes such as temporarily disabling redo logging on the table, or disabling indexes may also be beneficial. See `load_csv.py `__ for a runnable example. python-oracledb-1.2.1/doc/src/user_guide/bind.rst000066400000000000000000000706601434177474600217740ustar00rootroot00000000000000.. _bind: ******************** Using Bind Variables ******************** SQL and PL/SQL statements that pass data to and from Oracle Database should use placeholders in SQL and PL/SQL statements that mark where data is supplied or returned. These placeholders are referred to as bind variables or bind parameters. A bind variable is a colon-prefixed identifier or numeral. For example, there are two bind variables (``dept_id`` and ``dept_name``) in this SQL statement: .. code-block:: python sql = """insert into departments (department_id, department_name) values (:dept_id, :dept_name)""" cursor.execute(sql, [280, "Facility"]) Using bind variables is important for scalability and security. They help avoid SQL Injection security problems because data is never treated as part of an executable statement. Never concatenate or interpolate user data into SQL statements: .. code-block:: python did = 280 dnm = "Facility" # !! Never do this !! sql = f"""insert into departments (department_id, department_name) values ({did}, '{dnm}')""" cursor.execute(sql) Bind variables reduce parsing and execution costs when statements are executed more than once with different data values. If you do not use bind variables, Oracle must reparse and cache multiple statements. When using bind variables, Oracle Database may be able to reuse the statement execution plan and context. Bind variables can be used to substitute data, but cannot be used to substitute the text of the statement. You cannot, for example, use a bind variable where a column name or a table name is required. Bind variables also cannot be used in Data Definition Language (DDL) statements, such as CREATE TABLE or ALTER statements. Binding by Name or Position =========================== Binding can be done by name or by position. A named bind is performed when the bind variables in a statement are associated with a name. For example: .. code-block:: python cursor.execute(""" insert into departments (department_id, department_name) values (:dept_id, :dept_name)""", dept_id=280, dept_name="Facility") # alternatively, the parameters can be passed as a dictionary instead of as # keyword parameters data = dict(dept_id=280, dept_name="Facility") cursor.execute(""" insert into departments (department_id, department_name) values (:dept_id, :dept_name)""", data) In the above example, the keyword parameter names or the keys of the dictionary must match the bind variable names. The advantages of this approach are that the location of the bind variables in the statement is not important, the names can be meaningful and the names can be repeated while still only supplying the value once. A positional bind is performed when a list of bind values are passed to the execute() call. For example: .. code-block:: python cursor.execute(""" insert into departments (department_id, department_name) values (:dept_id, :dept_name)""", [280, "Facility"]) Note that for SQL statements, the order of the bind values must exactly match the order of each bind variable and duplicated names must have their values repeated. For PL/SQL statements, however, the order of the bind values must exactly match the order of each **unique** bind variable found in the PL/SQL block and values should not be repeated. In order to avoid this difference, binding by name is recommended when bind variable names are repeated. Bind Direction ============== The caller can supply data to the database (IN), the database can return data to the caller (OUT) or the caller can supply initial data to the database and the database can supply the modified data back to the caller (IN/OUT). This is known as the bind direction. The examples shown above have all supplied data to the database and are therefore classified as IN bind variables. In order to have the database return data to the caller, a variable must be created. This is done by calling the method :func:`Cursor.var()`, which identifies the type of data that will be found in that bind variable and its maximum size among other things. Here is an example showing how to use OUT binds. It calculates the sum of the integers 8 and 7 and stores the result in an OUT bind variable of type integer: .. code-block:: python out_val = cursor.var(int) cursor.execute(""" begin :out_val := :in_bind_var1 + :in_bind_var2; end;""", out_val=out_val, in_bind_var1=8, in_bind_var2=7) print(out_val.getvalue()) # will print 15 If instead of simply getting data back you wish to supply an initial value to the database, you can set the variable's initial value. This example is the same as the previous one but it sets the initial value first: .. code-block:: python in_out_var = cursor.var(int) in_out_var.setvalue(0, 25) cursor.execute(""" begin :in_out_bind_var := :in_out_bind_var + :in_bind_var1 + :in_bind_var2; end;""", in_out_bind_var=in_out_var, in_bind_var1=8, in_bind_var2=7) print(in_out_var.getvalue()) # will print 40 When binding data to parameters of PL/SQL procedures that are declared as OUT parameters, it is worth noting that any value that is set in the bind variable will be ignored. In addition, any parameters declared as IN/OUT that do not have a value set will start out with a value of ``null``. Binding Null Values =================== In python-oracledb, null values are represented by the Python singleton ``None``. For example: .. code-block:: python cursor.execute(""" insert into departments (department_id, department_name) values (:dept_id, :dept_name)""", dept_id=280, dept_name=None) In this specific case, since the ``DEPARTMENT_NAME`` column is defined as a ``NOT NULL`` column, an error will occur:: oracledb.IntegrityError: ORA-01400: cannot insert NULL into ("HR"."DEPARTMENTS"."DEPARTMENT_NAME") If this value is bound directly, python-oracledb assumes it to be a string (equivalent to a VARCHAR2 column). If you need to use a different Oracle type you will need to make a call to :func:`Cursor.setinputsizes()` or create a bind variable with the correct type by calling :func:`Cursor.var()`. Binding ROWID Values ==================== The pseudo-column ``ROWID`` uniquely identifies a row in a table. In python-oracledb, ROWID values are represented as strings. The example below shows fetching a row and then updating that row by binding its rowid: .. code-block:: python # fetch the row cursor.execute(""" select rowid, manager_id from departments where department_id = :dept_id""", dept_id=280) rowid, manager_id = cursor.fetchone() # update the row by binding ROWID cursor.execute(""" update departments set manager_id = :manager_id where rowid = :rid""", manager_id=205, rid=rowid) Binding UROWID Values ===================== Universal rowids (UROWID) are used to uniquely identify rows in index organized tables. In python-oracledb, UROWID values are represented as strings. The example below shows fetching a row from index organized table ``universal_rowids`` and then updating that row by binding its urowid: .. code-block:: sql CREATE TABLE universal_rowids ( int_col number(9) not null, str_col varchar2(250) not null, date_col date not null, CONSTRAINT universal_rowids_pk PRIMARY KEY(int_col, str_col, date_col) ) ORGANIZATION INDEX .. code-block:: python ridvar = cursor.var(oracledb.DB_TYPE_UROWID) # fetch the row cursor.execute(""" begin select rowid into :rid from universal_rowids where int_col = 3; end;""", rid=ridvar) # update the row by binding UROWID cursor.execute(""" update universal_rowids set str_col = :str_val where rowid = :rowid_val""", str_val="String #33", rowid_val=ridvar) Note that the type :attr:`oracledb.DB_TYPE_UROWID` is only supported in python-oracledb Thin mode. For python-oracledb Thick mode, the database type UROWID can be bound with type :attr:`oracledb.DB_TYPE_ROWID`. See :ref:`querymetadatadiff`. DML RETURNING Bind Variables ============================ When a RETURNING clause is used with a DML statement like UPDATE, INSERT, or DELETE, the values are returned to the application through the use of OUT bind variables. Consider the following example: .. code-block:: python # The RETURNING INTO bind variable is a string dept_name = cursor.var(str) cursor.execute(""" update departments set location_id = :loc_id where department_id = :dept_id returning department_name into :dept_name""", loc_id=1700, dept_id=50, dept_name=dept_name) print(dept_name.getvalue()) # will print ['Shipping'] In the above example, since the WHERE clause matches only one row, the output contains a single item in the list. If the WHERE clause matched multiple rows, the output would contain as many items as there were rows that were updated. No duplicate binds are allowed in a DML statement with a RETURNING clause, and no duplication is allowed between bind variables in the DML section and the RETURNING section of the statement. LOB Bind Variables ================== Database CLOBs, NCLOBS, BLOBs, and BFILEs can be bound with types :attr:`oracledb.DB_TYPE_CLOB`, :attr:`oracledb.DB_TYPE_NCLOB`, :attr:`oracledb.DB_TYPE_BLOB` and :attr:`oracledb.DB_TYPE_BFILE` respectively. LOBs fetched from the database or created with :meth:`Connection.createlob()` can also be bound. LOBs may represent Oracle Database persistent LOBs (those stored in tables) or temporary LOBs (such as those created with :meth:`Connection.createlob()` or returned by some SQL and PL/SQL operations). LOBs can be used as IN, OUT, or IN/OUT bind variables. See :ref:`lobdata` for examples. .. _refcur: REF CURSOR Bind Variables ========================= Python-oracledb provides the ability to bind and define PL/SQL REF cursors. As an example, consider the PL/SQL procedure: .. code-block:: sql CREATE OR REPLACE PROCEDURE find_employees ( p_query IN VARCHAR2, p_results OUT SYS_REFCURSOR ) AS BEGIN OPEN p_results FOR SELECT employee_id, first_name, last_name FROM employees WHERE UPPER(first_name || ' ' || last_name || ' ' || email) LIKE '%' || UPPER(p_query) || '%'; END; / A newly opened cursor can be bound to the REF CURSOR parameter as shown in the following Python code. After the PL/SQL procedure has been called with :meth:`Cursor.callproc()`, the cursor can then be fetched just like any other cursor which had executed a SQL query: .. code-block:: python ref_cursor = connection.cursor() cursor.callproc("find_employees", ['Smith', ref_cursor]) for row in ref_cursor: print(row) With Oracle's `sample HR schema `__ there are two employees with the last name 'Smith' so the result is:: (159, 'Lindsey', 'Smith') (171, 'William', 'Smith') To return a REF CURSOR from a PL/SQL function, use ``oracledb.DB_TYPE_CURSOR`` for the return type of :meth:`Cursor.callfunc()`: .. code-block:: python ref_cursor = cursor.callfunc('example_package.f_get_cursor', oracledb.DB_TYPE_CURSOR) for row in ref_cursor: print(row) See :ref:`tuning` for information on how to tune REF CURSORS. Binding PL/SQL Collections ========================== PL/SQL Collections like Associative Arrays can be bound as IN, OUT, and IN/OUT variables. When binding IN values, an array can be passed directly as shown in this example, which sums up the lengths of all of the strings in the provided array. First the PL/SQL package definition: .. code-block:: sql create or replace package mypkg as type udt_StringList is table of varchar2(100) index by binary_integer; function DemoCollectionIn ( a_Values udt_StringList ) return number; end; / create or replace package body mypkg as function DemoCollectionIn ( a_Values udt_StringList ) return number is t_ReturnValue number := 0; begin for i in 1..a_Values.count loop t_ReturnValue := t_ReturnValue + length(a_Values(i)); end loop; return t_ReturnValue; end; end; / Then the Python code: .. code-block:: python values = ["String One", "String Two", "String Three"] return_val = cursor.callfunc("mypkg.DemoCollectionIn", int, [values]) print(return_val) # will print 32 In order get values back from the database, a bind variable must be created using :meth:`Cursor.arrayvar()`. The first parameter to this method is a Python type that python-oracledb knows how to handle or one of the oracledb :ref:`types`. The second parameter is the maximum number of elements that the array can hold or an array providing the value (and indirectly the maximum length). The final parameter is optional and only used for strings and bytes. It identifies the maximum length of the strings and bytes that can be stored in the array. If not specified, the length defaults to 4000 bytes. Consider the following PL/SQL package: .. code-block:: sql create or replace package mypkg as type udt_StringList is table of varchar2(100) index by binary_integer; procedure DemoCollectionOut ( a_NumElements number, a_Values out nocopy udt_StringList ); procedure DemoCollectionInOut ( a_Values in out nocopy udt_StringList ); end; / create or replace package body mypkg as procedure DemoCollectionOut ( a_NumElements number, a_Values out nocopy udt_StringList ) is begin for i in 1..a_NumElements loop a_Values(i) := 'Demo out element #' || to_char(i); end loop; end; procedure DemoCollectionInOut ( a_Values in out nocopy udt_StringList ) is begin for i in 1..a_Values.count loop a_Values(i) := 'Converted element #' || to_char(i) || ' originally had length ' || length(a_Values(i)); end loop; end; end; / The Python code to process an OUT collection will be as follows. Note the call to :meth:`Cursor.arrayvar()` which creates space for an array of strings. Each string permits up to 100 bytes and only 10 strings are permitted. If the PL/SQL block exceeds the maximum number of strings allowed the error ``ORA-06513: PL/SQL: index for PL/SQL table out of range for host language array`` will be raised. .. code-block:: python out_array_var = cursor.arrayvar(str, 10, 100) cursor.callproc("mypkg.DemoCollectionOut", [5, out_array_var]) for val in out_array_var.getvalue(): print(val) This would produce the following output:: Demo out element #1 Demo out element #2 Demo out element #3 Demo out element #4 Demo out element #5 The Python code to process an IN/OUT collections is similar. Note the different call to :meth:`Cursor.arrayvar()` which creates space for an array of strings, but uses an array to determine both the maximum length of the array and its initial value. .. code-block:: python in_values = ["String One", "String Two", "String Three", "String Four"] in_out_array_var = cursor.arrayvar(str, in_values) cursor.callproc("mypkg.DemoCollectionInOut", [in_out_array_var]) for val in in_out_array_var.getvalue(): print(val) This will produce the following output:: Converted element #1 originally had length 10 Converted element #2 originally had length 10 Converted element #3 originally had length 12 Converted element #4 originally had length 11 If an array variable needs to have an initial value but also needs to allow for more elements than the initial value contains, the following code can be used instead: .. code-block:: python in_out_array_var = cursor.arrayvar(str, 10, 100) in_out_array_var.setvalue(0, ["String One", "String Two"]) All of the collections that have been bound in preceding examples have used contiguous array elements. If an associative array with sparse array elements is needed, a different approach is required. Consider the following PL/SQL code: .. code-block:: sql create or replace package mypkg as type udt_StringList is table of varchar2(100) index by binary_integer; procedure DemoCollectionOut ( a_Value out nocopy udt_StringList ); end; / create or replace package body mypkg as procedure DemoCollectionOut ( a_Value out nocopy udt_StringList ) is begin a_Value(-1048576) := 'First element'; a_Value(-576) := 'Second element'; a_Value(284) := 'Third element'; a_Value(8388608) := 'Fourth element'; end; end; / Note that the collection element indices are separated by large values. The technique used above would fail with the exception ``ORA-06513: PL/SQL: index for PL/SQL table out of range for host language array``. The code required to process this collection looks like this instead: .. code-block:: python collection_type = connection.gettype("MYPKG.UDT_STRINGLIST") collection = collection_type.newobject() cursor.callproc("mypkg.DemoCollectionOut", [collection]) print(collection.aslist()) This produces the output:: ['First element', 'Second element', 'Third element', 'Fourth element'] Note the use of :meth:`Object.aslist()` which returns the collection element values in index order as a simple Python list. The indices themselves are lost in this approach. Starting from cx_Oracle 7.0, the associative array can be turned into a Python dictionary using :meth:`Object.asdict()`. If that value was printed in the previous example instead, the output would be:: {-1048576: 'First element', -576: 'Second element', 284: 'Third element', 8388608: 'Fourth element'} If the elements need to be traversed in index order, the methods :meth:`Object.first()` and :meth:`Object.next()` can be used. The method :meth:`Object.getelement()` can be used to acquire the element at a particular index. This is shown in the following code: .. code-block:: python ix = collection.first() while ix is not None: print(ix, "->", collection.getelement(ix)) ix = collection.next(ix) This produces the output:: -1048576 -> First element -576 -> Second element 284 -> Third element 8388608 -> Fourth element Similarly, the elements can be traversed in reverse index order using the methods :meth:`Object.last()` and :meth:`Object.prev()` as shown in the following code: .. code-block:: python ix = collection.last() while ix is not None: print(ix, "->", collection.getelement(ix)) ix = collection.prev(ix) This produces the output:: 8388608 -> Fourth element 284 -> Third element -576 -> Second element -1048576 -> First element Binding PL/SQL Records ====================== PL/SQL record type objects can also be bound for IN, OUT, and IN/OUT bind variables. For example: .. code-block:: sql create or replace package mypkg as type udt_DemoRecord is record ( NumberValue number, StringValue varchar2(30), DateValue date, BooleanValue boolean ); procedure DemoRecordsInOut ( a_Value in out nocopy udt_DemoRecord ); end; / create or replace package body mypkg as procedure DemoRecordsInOut ( a_Value in out nocopy udt_DemoRecord ) is begin a_Value.NumberValue := a_Value.NumberValue * 2; a_Value.StringValue := a_Value.StringValue || ' (Modified)'; a_Value.DateValue := a_Value.DateValue + 5; a_Value.BooleanValue := not a_Value.BooleanValue; end; end; / Then this Python code can be used to call the stored procedure which will update the record: .. code-block:: python # create and populate a record record_type = connection.gettype("MYPKG.UDT_DEMORECORD") record = record_type.newobject() record.NUMBERVALUE = 6 record.STRINGVALUE = "Test String" record.DATEVALUE = datetime.datetime(2016, 5, 28) record.BOOLEANVALUE = False # show the original values print("NUMBERVALUE ->", record.NUMBERVALUE) print("STRINGVALUE ->", record.STRINGVALUE) print("DATEVALUE ->", record.DATEVALUE) print("BOOLEANVALUE ->", record.BOOLEANVALUE) print() # call the stored procedure which will modify the record cursor.callproc("mypkg.DemoRecordsInOut", [record]) # show the modified values print("NUMBERVALUE ->", record.NUMBERVALUE) print("STRINGVALUE ->", record.STRINGVALUE) print("DATEVALUE ->", record.DATEVALUE) print("BOOLEANVALUE ->", record.BOOLEANVALUE) This will produce the following output:: NUMBERVALUE -> 6 STRINGVALUE -> Test String DATEVALUE -> 2016-05-28 00:00:00 BOOLEANVALUE -> False NUMBERVALUE -> 12 STRINGVALUE -> Test String (Modified) DATEVALUE -> 2016-06-02 00:00:00 BOOLEANVALUE -> True Note that when manipulating records, all of the attributes must be set by the Python program in order to avoid an Oracle Client bug which will result in unexpected values or the Python application segfaulting. .. _spatial: Binding Spatial Data Types ========================== Oracle Spatial data types objects can be represented by Python objects and their attribute values can be read and updated. The objects can further be bound and committed to database. This is similar to the examples above. An example of fetching SDO_GEOMETRY is in :ref:`Oracle Database Objects and Collections `. .. _inputtypehandlers: Changing Bind Data Types using an Input Type Handler ==================================================== Input Type Handlers allow applications to change how data is bound to statements, or even to enable new types to be bound directly. An input type handler is enabled by setting the attribute :attr:`Cursor.inputtypehandler` or :attr:`Connection.inputtypehandler`. Input type handlers can be combined with variable converters to bind Python objects seamlessly: .. code-block:: python # A standard Python object class Building: def __init__(self, build_id, description, num_floors, date_built): self.building_id = build_id self.description = description self.num_floors = num_floors self.date_built = date_built building = Building(1, "Skyscraper 1", 5, datetime.date(2001, 5, 24)) # Get Python representation of the Oracle user defined type UDT_BUILDING obj_type = con.gettype("UDT_BUILDING") # convert a Python Building object to the Oracle user defined type # UDT_BUILDING def building_in_converter(value): obj = obj_type.newobject() obj.BUILDINGID = value.building_id obj.DESCRIPTION = value.description obj.NUMFLOORS = value.num_floors obj.DATEBUILT = value.date_built return obj def input_type_handler(cursor, value, num_elements): if isinstance(value, Building): return cursor.var(obj_type, arraysize=num_elements, inconverter=building_in_converter) # With the input type handler, the bound Python object is converted # to the required Oracle object before being inserted cur.inputtypehandler = input_type_handler cur.execute("insert into myTable values (:1, :2)", (1, building)) Binding Multiple Values to a SQL WHERE IN Clause ================================================ To use a SQL IN clause with multiple values, use one bind variable per value. You cannot directly bind a Python list or dictionary to a single bind variable. For example, to use two values in an IN clause: .. code-block:: python cursor.execute(""" select employee_id, first_name, last_name from employees where last_name in (:name1, :name2)""", name1="Smith", name2="Taylor") for row in cursor: print(row) This gives the output:: (159, 'Lindsey', 'Smith') (171, 'William', 'Smith') (176, 'Jonathon', 'Taylor') (180, 'Winston', 'Taylor') If the query is executed multiple times with differing numbers of values, a bind variable should be included for each possible value. When the statement is executed but the maximum number of values has not been supplied, the value ``None`` can be bound for missing values. For example, if the query above is used for up to 5 values, the code will be: .. code-block:: python cursor.execute(""" select employee_id, first_name, last_name from employees where last_name in (:name1, :name2, :name3, :name4, :name5)""", name1="Smith", name2="Taylor", name3=None, name4=None, name5=None) for row in cursor: print(row) This will produce the same output as the original example. Reusing the same SQL statement like this for a variable number of values, instead of constructing a unique statement per set of values, allows best reuse of Oracle Database resources. However, if the statement is not going to be re-executed or the number of values is only going to be known at runtime, then a SQL statement can be built up as follows: .. code-block:: python bind_values = ["Gates", "Marvin", "Fay"] bind_names = [":" + str(i + 1) for i in range(len(bind_values))] sql = "select employee_id, first_name, last_name from employees " + \ "where last_name in (%s)" % (",".join(bind_names)) cursor.execute(sql, bind_values) for row in cursor: print(row) A general solution for a larger number of values is to construct a SQL statement like:: SELECT ... WHERE col IN ( ) The best way to do the '' depends on how the data is initially represented and the number of items. You might look at using CONNECT BY or at using a global temporary table. One method is to use an Oracle collection with the ``TABLE()`` clause. For example, if the following type was created:: SQL> CREATE OR REPLACE TYPE name_array AS TABLE OF VARCHAR2(25); 2 / then the application could do: .. code-block:: python type_obj = connection.gettype("NAME_ARRAY") obj = type_obj.newobject() obj.extend(["Smith", "Taylor"]) cursor.execute("""select employee_id, first_name, last_name from employees where last_name in (select * from table(:1))""", [obj]) for row in cursor: print(row) For efficiency, retain the return value of ``gettype()`` for reuse instead of making repeated calls to get the type information. Binding Column and Table Names ============================== Column and table names cannot be bound in SQL queries. You can concatenate text to build up a SQL statement, but ensure that you use an Allow List or other means to validate the data in order to avoid SQL Injection security issues: .. code-block:: python table_allow_list = ['employees', 'departments'] table_name = get_table_name() # get the table name from user input if table_name.lower() not in table_allow_list: raise Exception('Invalid table name') sql = f'select * from {table_name}' Binding column names can be done either by using the above method or by using a CASE statement. The example below demonstrates binding a column name in an ORDER BY clause: .. code-block:: python sql = """ SELECT * FROM departments ORDER BY CASE :bindvar WHEN 'department_id' THEN DEPARTMENT_ID ELSE MANAGER_ID END""" col_name = get_column_name() # Obtain a column name from the user cursor.execute(sql, [col_name]) Depending on the name provided by the user, the query results will be ordered either by the column ``DEPARTMENT_ID`` or the column ``MANAGER_ID``. python-oracledb-1.2.1/doc/src/user_guide/connection_handling.rst000066400000000000000000003504161434177474600250630ustar00rootroot00000000000000.. _connhandling: ***************************** Connecting to Oracle Database ***************************** Connections between python-oracledb and Oracle Database are used for executing :ref:`SQL `, :ref:`PL/SQL `, and :ref:`SODA `. By default, python-oracledb runs in a 'Thin' mode which connects directly to Oracle Database. This mode does not need Oracle Client libraries. However, some :ref:`additional functionality ` is available when python-oracledb uses them. Python-oracledb is said to be in 'Thick' mode when Oracle Client libraries are used. See :ref:`enablingthick`. Both modes have comprehensive functionality supporting the Python Database API v2.0 Specification. If you intend to use the Thick mode, then you *must* call :func:`~oracledb.init_oracle_client()` in the application before any standalone connection or pool is created. The python-oracledb Thick mode loads Oracle Client libraries which communicate over Oracle Net to an existing database. The Oracle Client libraries need to be installed separately. See :ref:`installation`. Oracle Net is not a separate product: it is how the Oracle Client and Oracle Database communicate. There are two ways to create a connection to Oracle Database using python-oracledb: * **Standalone connections**: :ref:`Standalone connections ` are useful when the application needs a single connection to a database. Connections are created by calling :meth:`oracledb.connect()`. * **Pooled connections**: :ref:`Connection pooling ` is important for performance when applications frequently connect and disconnect from the database. Pools support Oracle's :ref:`high availability ` features and are recommended for applications that must be reliable. Small pools can also be useful for applications that want a few connections available for infrequent use. Pools are created with :meth:`oracledb.create_pool()` at application initialization time, and then :meth:`ConnectionPool.acquire()` can be called to obtain a connection from a pool. Many connection behaviors can be controlled by python-oracledb connection options. Other settings can be configured in :ref:`optnetfiles` or in :ref:`optclientfiles`. These include limiting the amount of time that opening a connection can take, or enabling :ref:`network encryption `. .. note:: Creating a connection in python-oracledb Thin mode always requires a connection string, or the database host name and service name, to be specified. The Thin mode cannot use "bequeath" connections and does not reference Oracle environment variables ``ORACLE_SID``, ``TWO_TASK``, or ``LOCAL``. .. note:: When using python-oracledb in Thin mode, the ``tnsnames.ora`` file will not be automatically located. The file's directory must explicitly be passed to the application, see :ref:`optnetfiles`. .. _standaloneconnection: Standalone Connections ====================== Standalone connections are database connections that do not use a python-oracledb connection pool. They are useful for simple applications that use a single connection to a database. Simple connections are created by calling :meth:`oracledb.connect()` and passing a database username, the database password for that user, and a 'data source name' :ref:`connection string `. Python-oracledb also supports :ref:`external authentication ` and so passwords do not need to be in the application. Creating a Standalone Connection -------------------------------- Standalone connections are created by calling :meth:`oracledb.connect()`. A simple standalone connection example: .. code-block:: python import oracledb import getpass userpwd = getpass.getpass("Enter password: ") connection = oracledb.connect(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb") You could alternatively read the password from an environment variable: .. code-block:: python userpwd = os.environ.get("PYTHON_PASSWORD") connection = oracledb.connect(user="hr", password=userpwd, dsn="localhost/orclpdb") The :meth:`oracledb.connect()` method allows the database host name and database service name to be passed as separate parameters. The database listener port can also be passed: .. code-block:: python import os userpwd = os.environ.get("PYTHON_PASSWORD") connection = oracledb.connect(user="hr", password=userpwd, host="localhost", port=1521, service_name="orclpdb") If you like to encapsulate values, parameters can be passed using a :ref:`ConnectParams Object `: .. code-block:: python params = oracledb.ConnectParams(host="my_host", port=my_port, service_name="my_service_name") conn = oracledb.connect(user="my_user", password="my_password", params=params) Some values such as the database host name can be specified as ``connect()`` parameters, as part of the connect string, and in the ``params`` object. If a ``dsn`` is passed, the python-oracledb :ref:`Thick ` mode will use the ``dsn`` string to connect. Otherwise, a connection string is internally constructed from the individual parameters and ``params`` object values, with the individual parameters having precedence. In python-oracledb's default Thin mode, a connection string is internally used that contains all relevant values specified. The precedence in Thin mode is that values in any ``dsn`` parameter override values passed as individual parameters, which themselves override values set in the ``params`` object. Similar precedence rules also apply to other values. A single, combined connection string can be passed to ``connect()`` but this may cause complications if the password contains '@' or '/' characters: .. code-block:: python username="hr" userpwd = os.environ.get("PYTHON_PASSWORD") host = "localhost" port = 1521 service_name = "orclpdb" dsn = f'{username}/{userpwd}@{host}:{port}/{service_name}' connection = oracledb.connect(dsn) Closing Connections +++++++++++++++++++ Connections should be released when they are no longer needed. You may prefer to let connections be automatically cleaned up when references to them go out of scope. This lets python-oracledb close dependent resources in the correct order: .. code-block:: python with oracledb.connect(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb") as connection: with connection.cursor() as cursor: cursor.execute("insert into SomeTable values (:1, :2)", (1, "Some string")) connection.commit() This code ensures that once the block is completed, the connection is closed and resources have been reclaimed by the database. In addition, any attempt to use the variable ``connection`` outside of the block will simply fail. Alternatively, you can explicitly close a connection by calling. :meth:`Connection.close()`: .. code-block:: python connection = oracledb.connect(user="hr", password=userpwd, dsn="localhost/orclpdb") # do something with the connection . . . # close the connection connection.close() .. _connerrors: Common Connection Errors ------------------------ Some of the common connection errors that you may encounter in the python-oracledb's default Thin mode are detailed below. Also see :ref:`errorhandling`. Use keyword parameters ++++++++++++++++++++++ If you use: .. code-block:: python connection = oracledb.connect("hr", userpwd, "localhost/orclpdb") then you will get the error:: TypeError: connect() takes from 0 to 1 positional arguments but 3 were given The :meth:`oracledb.connect()` method requires keyword parameters to be used .. code-block:: python connection = oracledb.connect(user="hr", password=userpwd, dsn="localhost/orclpdb") The exception passing a single argument containing the combined credential and connection string. This is supported: .. code-block:: python connection = oracledb.connect("hr/userpwd@localhost/orclpdb") Use the correct credentials +++++++++++++++++++++++++++ If your username or password are not known by the database that you attempted to connect to, then you will get the error:: ORA-01017: invalid username/password; logon denied Find the correct username and password and try reconnecting. Use the correct connection string +++++++++++++++++++++++++++++++++ If the hostname, port, or service name are incorrect, then the connection will fail with the error:: DPY-6001: cannot connect to database. Service "doesnotexist" is not registered with the listener at host "localhost" port 1521. (Similar to ORA-12514) This error means that Python successfully reached a computer (in this case, "localhost" using the default port 1521) that is running a database. However, the database service you wanted ("doesnotexist") does not exist there. Technically, the error means the listener does not know about the service at the moment. So you might also get this error if the database is currently restarting. This error is similar to the ``ORA-12514`` error that you may see when connecting with python-oracledb in Thick mode, or with some other Oracle tools. The solution is to use a valid service name in the connection string. You can: - Check and fix any typos in the service name you used - Check if the hostname and port are correct - Ask your database administrator (DBA) for the correct values - Wait a few moments and re-try in case the database is restarting - Review the connection information in your cloud console or cloud wallet, if you are using a cloud database - Run `lsnrctl status` on the database machine to find the known service names .. _connstr: Connection Strings ================== The data source name parameter ``dsn`` of :meth:`oracledb.connect()` and :meth:`oracledb.create_pool()` is the Oracle Database connection string that identifies which database service to connect to. The ``dsn`` string can be one of: * An Oracle Easy Connect string * An Oracle Net Connect Descriptor string * A Net Service Name mapping to a connect descriptor For more information about naming methods, see `Oracle Net Service Reference `__. .. _easyconnect: Easy Connect Syntax for Connection Strings ------------------------------------------ An Easy Connect string is often the simplest connection string to use for the data source name parameter ``dsn`` of :meth:`oracledb.connect()` and :meth:`oracledb.create_pool()`. This method does not need configuration files such as ``tnsnames.ora``. For example, to connect to the Oracle Database service ``orclpdb`` that is running on the host ``dbhost.example.com`` with the default Oracle Database port 1521, use: .. code-block:: python connection = oracledb.connect(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb") If the database is using a non-default port, it must be specified: .. code-block:: python connection = oracledb.connect(user="hr", password=userpwd, dsn="dbhost.example.com:1984/orclpdb") The Easy Connect syntax supports Oracle Database service names. It cannot be used with the older System Identifiers (SID). The latest `Easy Connect Plus `__ syntax allows the use of multiple hosts or ports, along with optional entries for the wallet location, the distinguished name of the database server, and even allows some network configuration options be set. This means that a :ref:`sqlnet.ora ` file is not needed for some common connection scenarios. In python-oracledb Thin mode, any unknown Easy Connect options are ignored and are not passed to the database. See :ref:`Connection String Differences ` for more information. In python-oracledb Thick mode, it is the Oracle Client libraries that parse the Easy Connect string. Check the Easy Connect Naming method in `Oracle Net Service Administrator's Guide `__ for the syntax to use in your version of the Oracle Client libraries. .. _netservice: Net Service Names for Connection Strings ---------------------------------------- Connect Descriptor Strings are commonly stored in a :ref:`tnsnames.ora ` file and associated with a Net Service Name. This name can be used directly for the data source name parameter ``dsn`` of :meth:`oracledb.connect()` and :meth:`oracledb.create_pool()`. For example, given a file ``/opt/oracle/config/tnsnames.ora`` with the following contents:: ORCLPDB = (DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)(HOST = dbhost.example.com)(PORT = 1521)) (CONNECT_DATA = (SERVER = DEDICATED) (SERVICE_NAME = orclpdb) ) ) Then you could connect in python-oracledb Thin mode by using the following code: .. code-block:: python connection = oracledb.connect(user="hr", password=userpwd, dsn="orclpdb", config_dir="/opt/oracle/config") More options for how python-oracledb locates ``tnsnames.ora`` files is detailed in :ref:`optnetfiles`. Note in python-oracledb Thick mode, the configuration directory must be set during initialization, not at connection time. For more information about Net Service Names, see `Database Net Services Reference `__. Oracle Net Connect Descriptor Strings ------------------------------------- Full Connect Descriptor strings can be embedded directly in python-oracledb applications: .. code-block:: python dsn = """(DESCRIPTION= (FAILOVER=on) (ADDRESS_LIST= (ADDRESS=(PROTOCOL=tcp)(HOST=sales1-svr)(PORT=1521)) (ADDRESS=(PROTOCOL=tcp)(HOST=sales2-svr)(PORT=1521))) (CONNECT_DATA=(SERVICE_NAME=sales.example.com)))""" connection = oracledb.connect(user="hr", password=userpwd, dsn=dsn) The :meth:`oracledb.ConnectParams()` and :meth:`ConnectParams.get_connect_string()` functions can be used to construct a connect descriptor string from the individual components, see :ref:`usingconnparams`. For example: .. code-block:: python cp = oracledb.ConnectParams(host="dbhost.example.com", port=1521, service_name="orclpdb") dsn = cp.get_connect_string() print(dsn) This prints:: (DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=tcp)(HOST=dbhost.example.com)(PORT=1521)))(CONNECT_DATA=(SERVICE_NAME=orclpdb))(SECURITY=(SSL_SERVER_DN_MATCH=True))) JDBC and Oracle SQL Developer Connection Strings ------------------------------------------------ The python-oracledb connection string syntax is different from Java JDBC and the common Oracle SQL Developer syntax. If these JDBC connection strings reference a service name like:: jdbc:oracle:thin:@hostname:port/service_name For example:: jdbc:oracle:thin:@dbhost.example.com:1521/orclpdb then use Oracle's Easy Connect syntax in python-oracledb: .. code-block:: python connection = oracledb.connect(user="hr", password=userpwd, dsn="dbhost.example.com:1521/orclpdb") Alternatively, if a JDBC connection string uses an old-style Oracle Database SID "system identifier", and the database does not have a service name:: jdbc:oracle:thin:@hostname:port:sid For example:: jdbc:oracle:thin:@dbhost.example.com:1521:orcl then connect by using the ``sid`` parameter: .. code-block:: python connection = oracledb.connect(user="hr", password=userpwd, host="dbhost.example.com", port=1521, sid="orcl") Alternatively, create a ``tnsnames.ora`` (see :ref:`optnetfiles`) entry, for example:: finance = (DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)(HOST = dbhost.example.com)(PORT = 1521)) (CONNECT_DATA = (SID = ORCL) ) ) This can be referenced in python-oracledb: .. code-block:: python connection = oracledb.connect(user="hr", password=userpwd, dsn="finance") .. _usingconnparams: Using the ConnectParams Builder Class ====================================== The :ref:`ConnectParams class ` allows you to define connection parameters in a single place. The :func:`oracledb.ConnectParams()` function returns a ``ConnectParams`` object. The object can be passed to :func:`oracledb.connect()`. For example: .. code-block:: python cp = oracledb.ConnectParams(user="hr", password=userpwd, host="dbhost", port=1521, service_name="orclpdb") connection = oracledb.connect(params=cp) The use of the ConnectParams class is optional because you can pass the same parameters directly to :func:`~oracledb.connect()`. For example, the code above is equivalent to: .. code-block:: python connection = oracledb.connect(user="hr", password=userpwd, host="dbhost", port=1521, service_name="orclpdb") If you want to keep credentials separate, you can use ConnectParams just to encapsulate connection string components: .. code-block:: python cp = oracledb.ConnectParams(host="dbhost", port=1521, service_name="orclpdb") connection = oracledb.connect(user="hr", password=userpwd, params=cp) You can use :meth:`ConnectParams.get_connect_string()` to get a connection string from a ConnectParams object: .. code-block:: python cp = oracledb.ConnectParams(host="dbhost", port="my_port", service_name="my_service_name") dsn = cp.get_connect_string() connection = oracledb.connect(user="hr", password=userpwd, dsn=dsn) To parse a connection string and store components as attributes: .. code-block:: python cp = oracledb.ConnectParams() cp.parse_connect_string("host.example.com:1522/orclpdb") Most parameter values of :func:`oracledb.ConnectParams()` are gettable as attributes. For example, to get the stored host name: .. code-block:: python print(cp.host) Attributes such as the password are not gettable. You can set individual attributes using :meth:`ConnectParams.set()`: .. code-block:: python cp = oracledb.ConnectParams(host="localhost", port=1521, service_name="orclpdb") # set a new port cp.set(port=1522) # change both the port and service name cp.set(port=1523, service_name="orclpdb") Some values such as the database host name can be specified as :func:`oracledb.connect()`, parameters, as part of the connect string, and in the ``params`` object. If a ``dsn`` is passed, the python-oracledb :ref:`Thick ` mode will use the ``dsn`` string to connect. Otherwise, a connection string is internally constructed from the individual parameters and ``params`` object values, with the individual parameters having precedence. In python-oracledb's default Thin mode, a connection string is internally used that contains all relevant values specified. The precedence in Thin mode is that values in any ``dsn`` parameter override values passed as individual parameters, which themselves override values set in the ``params`` object. Similar precedence rules also apply to other values. .. _connpooling: Connection Pooling ================== Python-oracledb's connection pooling lets applications create and maintain a pool of open connections to the database. Connection pooling is available in both Thin and :ref:`Thick ` modes. Connection pooling is important for performance and scalability when applications need to handle a large number of users who do database work for short periods of time but have relatively long periods when the connections are not needed. The high availability features of pools also make small pools useful for applications that want a few connections available for infrequent use and requires them to be immediately usable when acquired. In python-oracledb Thick mode, the pool implementation uses Oracle's `session pool technology `__ which supports additional Oracle Database features, for example some advanced :ref:`high availability ` features. Creating a Connection Pool -------------------------- A connection pool is created by calling :meth:`oracledb.create_pool()`. Various pool options can be specified as described in :meth:`~oracledb.create_pool()` and detailed below. For example, to create a pool that initially contains one connection but can grow up to five connections: .. code-block:: python pool = oracledb.create_pool(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb", min=1, max=5, increment=1) After the pool has been created, your application can get a connection from it by calling :meth:`ConnectionPool.acquire()`: .. code-block:: python connection = pool.acquire() These connections can be used in the same way that :ref:`standaloneconnection` are used. By default, :meth:`~ConnectionPool.acquire()` calls wait for a connection to be available before returning to the application. A connection will be available if the pool currently has idle connections, when another user returns a connection to the pool, or after the pool grows. Waiting allows applications to be resilient to temporary spikes in connection load. Users may have to wait a brief time to get a connection but will not experience connection failures. You can change the behavior of :meth:`~ConnectionPool.acquire()` by setting the ``getmode`` option during pool creation. For example, the option can be set so that if all the connections are currently in use by the application, any additional :meth:`~ConnectionPool.acquire()` call will return an error immediately. .. code-block:: python pool = oracledb.create_pool(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb", min=2, max=5, increment=1, getmode=oracledb.POOL_GETMODE_NOWAIT) Note that when using this option value in Thick mode with Oracle Client libraries 12.2 or earlier, the :meth:`~ConnectionPool.acquire()` call will still wait if the pool can grow. However, you will get an error immediately if the pool is at its maximum size. With newer Oracle Client libraries and with Thin mode, an error will be returned if the pool has to, or cannot, grow. When your application has finished performing all required database operations, the pooled connection should be released to make it available for other users of the pool. You can do this with :meth:`ConnectionPool.release()` or :meth:`Connection.close()`. Alternatively you may prefer to let pooled connections be closed implicitly at the end of scope. For example, by using a ``with`` statement: .. code-block:: python with pool.acquire() as connection: with connection.cursor() as cursor: for result in cursor.execute("select * from mytab"): print(result) At application shutdown, the connection pool can be completely closed using :meth:`ConnectionPool.close()`: .. code-block:: python pool.close() To force immediate pool termination when connections are still in use, execute: .. code-block:: python pool.close(force=True) See `connection_pool.py `__ for a runnable example of connection pooling. **Connection Pool Growth** At pool creation, ``min`` connections are established to the database. When a pool needs to grow, new connections are created automatically limited by the ``max`` size. The pool ``max`` size restricts the number of application users that can do work in parallel on the database. The number of connections opened by a pool can shown with the attribute. :attr:`ConnectionPool.opened`. The number of connections the application has obtained with :meth:`~ConnectionPool.acquire()` can be shown with :attr:`ConnectionPool.busy`. The difference in values is the number of connections unused or 'idle' in the pool. These idle connections may be candidates for the pool to close, depending on the pool configuration. Pool growth is normally initiated when :meth:`~ConnectionPool.acquire()` is called and there are no idle connections in the pool that can be returned to the application. The number of new connections created internally will be the value of the :meth:`~oracledb.create_pool()` parameter ``increment``. Depending on whether Thin or Thick mode is used and on the pool creation ``getmode`` value that is set, any :meth:`~ConnectionPool.acquire()` call that initiates pool growth may wait until all ``increment`` new connections are internally opened. However, in this case the cost is amortized because later :meth:`~ConnectionPool.acquire()` calls may not have to wait and can immediately return an available connection. Some users set larger ``increment`` values even for fixed-size pools because it can help a pool re-establish itself if all connections become invalid, for example after a network dropout. In the common case of Thin mode with the default ``getmode`` of ``POOL_GETMODE_WAIT``, any :meth:`~ConnectionPool.acquire()` call that initiates pool growth will return after the first new connection is created, regardless of how big ``increment`` is. The pool will then continue to re-establish connections in a background thread. A connection pool can shrink back to its minimum size when connections opened by the pool are not used by the application. This frees up database resources while allowing pools to retain connections for active users. Note this is currently applicable to Thick mode only. If connections are idle in the pool (i.e. not currently acquired by the application) and are unused for longer than the pool creation attribute ``timeout`` value , then they will be closed. The default ``timeout`` is 0 seconds signifying an infinite time and meaning idle connections will never be closed. The pool creation parameter ``max_lifetime_session`` also allows pools to shrink. This parameter bounds the total length of time that a connection can exist starting from the time the pool created it. If a connection was created ``max_lifetime_session`` or longer seconds ago, then it will be closed when it is idle in the pool. In the case when ``timeout`` and ``max_lifetime_session`` are both set, the connection will be terminated if either the idle timeout happens or the max lifetime setting is exceeded. Note that when using python-oracledb in Thick mode with Oracle Client libraries prior to 21c, pool shrinkage is only initiated when the pool is accessed so pools in fully dormant applications will not shrink until the application is next used. For pools created with :ref:`external authentication `, with :ref:`homogeneous ` set to False, or when using :ref:`drcp`, then the number of connections opened at pool creation is zero even if a larger value is specified for ``min``. Also, in these cases the pool increment unit is always 1 regardless of the value of ``increment``. **Pool Connection Health** Before :meth:`ConnectionPool.acquire()` returns, python-oracledb does a lightweight check similar to :meth:`Connection.is_healthy()` to see if the network transport for the selected connection is still open. If it is not, then :meth:`~ConnectionPool.acquire()` will clean up the connection and return a different one. This check will not detect cases such as where the database session has been terminated by the DBA, or reached a database resource manager quota limit. To help in those cases, :meth:`~ConnectionPool.acquire()` will also do a full :ref:`round-trip ` database ping similar to :meth:`Connection.ping()` when it is about to return a connection that was idle in the pool (i.e. not currently acquired by the application) for :data:`ConnectionPool.ping_interval` seconds. If the ping fails, the connection will be discarded and another one obtained before :meth:`~ConnectionPool.acquire()` returns to the application. Because this full ping is time based and may not occur for each :meth:`~ConnectionPool.acquire()`, the application may still get an unusable connection. Also, network timeouts and session termination may occur between the calls to :meth:`~ConnectionPool.acquire()` and :meth:`Cursor.execute()`. To handle these cases, applications need to check for errors after each :meth:`~Cursor.execute()` and make application-specific decisions about retrying work if there was a connection failure. When using python-oracledb in Thick mode, Oracle Database features like :ref:`Application Continuity ` can do this automatically in some cases. You can explicitly initiate a full round-trip ping at any time with :meth:`Connection.ping()` to check connection liveness but the overuse will impact performance and scalability. Ensure that the :ref:`firewall `, `resource manager `__ or user profile `IDLE_TIME `__ do not expire idle sessions, since this will require connections to be recreated which will impact performance and scalability. A pool's internal connection re-establishment after lightweight and full pings can mask performance-impacting configuration issues such as firewalls terminating connections. You should monitor `AWR `__ reports for an unexpectedly large connection rate. .. _connpoolsize: Connection Pool Sizing ---------------------- The Oracle Real-World Performance Group's recommendation is to use fixed size connection pools. The values of ``min`` and ``max`` should be the same. When using older versions of Oracle Client libraries the ``increment`` parameter will need to be zero (which is internally treated as a value of one), but otherwise you may prefer a larger size since this will affect how the connection pool is re-established after, for example, a network dropout invalidates all connections. Fixed size pools avoid connection storms on the database which can decrease throughput. See `Guideline for Preventing Connection Storms: Use Static Pools `__, which contains more details about sizing of pools. Having a fixed size will also guarantee that the database can handle the upper pool size. For example, if a dynamically sized pool needs to grow but the database resources are limited, then :meth:`ConnectionPool.acquire()` may return errors such as ``ORA-28547``. With a fixed pool size, this class of error will occur when the pool is created, allowing you to change the pool size or reconfigure the database before users access the application. With a dynamically growing pool, the error may occur much later while the application is in use. The Real-World Performance Group also recommends keeping pool sizes small because they may perform better than larger pools. The pool attributes should be adjusted to handle the desired workload within the bounds of available resources in python-oracledb and the database. .. _poolreconfiguration: Connection Pool Reconfiguration ------------------------------- Some pool settings can be changed dynamically with :meth:`ConnectionPool.reconfigure()`. This allows the pool size and other attributes to be changed during application runtime without needing to restart the pool or application. For example a pool's size can be changed like: .. code-block:: python pool.reconfigure(min=10, max=10, increment=0) After any size change has been processed, reconfiguration on the other parameters is done sequentially. If an error such as an invalid value occurs when changing one attribute, then an exception will be generated but any already changed attributes will retain their new values. During reconfiguration of a pool's size, the behavior of :meth:`ConnectionPool.acquire()` depends on the pool creation ``getmode`` value in effect when :meth:`~ConnectionPool.acquire()` is called, see :meth:`ConnectionPool.reconfigure()`. Closing connections or closing the pool will wait until after pool reconfiguration is complete. Calling ``reconfigure()`` is the only way to change a pool's ``min``, ``max`` and ``increment`` values. Other attributes such as :data:`~ConnectionPool.wait_timeout` can be passed to ``reconfigure()`` or they can be set directly, for example: .. code-block:: python pool.wait_timeout = 1000 .. _sessioncallback: Session CallBacks for Setting Pooled Connection State ----------------------------------------------------- Applications can set "session" state in each connection. Examples of session state are NLS globalization settings from ``ALTER SESSION`` statements. Pooled connections will retain their session state after they have been released back to the pool. However, because pools can grow or connections in the pool can be recreated, there is no guarantee a subsequent :meth:`~ConnectionPool.acquire()` call will return a database connection that has any particular state. The :meth:`~oracledb.create_pool()` parameter ``session_callback`` enables efficient setting of session state so that connections have a known session state, without requiring that state to be explicitly set after every :meth:`~ConnectionPool.acquire()` call. The callback is internally invoked when :meth:`~ConnectionPool.acquire()` is called and runs first. The session callback can be a Python function or a PL/SQL procedure. Connections can also be tagged when they are released back to the pool. The tag is a user-defined string that represents the session state of the connection. When acquiring connections, a particular tag can be requested. If a connection with that tag is available, it will be returned. If not, then another session will be returned. By comparing the actual and requested tags, applications can determine what exact state a session has, and make any necessary changes. Connection tagging and PL/SQL callbacks are only available in python-oracledb Thick mode. Python callbacks can be used in python-oracledb Thin and Thick modes. There are three common scenarios for ``session_callback``: - When all connections in the pool should have the same state, use a Python callback without tagging. - When connections in the pool require different state for different users, use a Python callback with tagging. - With :ref:`drcp`, use a PL/SQL callback with tagging. Python Callback +++++++++++++++ If the ``session_callback`` parameter is a Python procedure, it will be called whenever :meth:`~ConnectionPool.acquire()` will return a newly created database connection that has not been used before. It is also called when connection tagging is being used and the requested tag is not identical to the tag in the connection returned by the pool. An example is: .. code-block:: python # Set the NLS_DATE_FORMAT for a session def init_session(connection, requested_tag): with connection.cursor() as cursor: cursor.execute("alter session set nls_date_format = 'YYYY-MM-DD HH24:MI'") # Create the pool with session callback defined pool = oracledb.create_pool(user="hr", password=userpwd, dsn="localhost/orclpdb", session_callback=init_session) # Acquire a connection from the pool (will always have the new date format) connection = pool.acquire() If needed, the ``init_session()`` procedure is called internally before :meth:`~ConnectionPool.acquire()` returns. It will not be called when previously used connections are returned from the pool. This means that the ALTER SESSION does not need to be executed after every :meth:`~ConnectionPool.acquire()` call. This improves performance and scalability. In this example tagging was not being used, so the ``requested_tag`` parameter is ignored. Note that if you need to execute multiple SQL statements in the callback, use an anonymous PL/SQL block to save :ref:`round-trips ` of repeated ``execute()`` calls. With ALTER SESSION, pass multiple settings in the one statement: .. code-block:: python cursor.execute(""" begin execute immediate 'alter session set nls_date_format = ''YYYY-MM-DD'' nls_language = AMERICAN'; -- other SQL statements could be put here end;""") .. _conntagging: Connection Tagging ++++++++++++++++++ Connection tagging is used when connections in a pool should have differing session states. In order to retrieve a connection with a desired state, the ``tag`` attribute in :meth:`~ConnectionPool.acquire()` needs to be set. .. note:: Connection tagging is only supported in the python-oracledb Thick mode. See :ref:`enablingthick` . When python-oracledb is using Oracle Client libraries 12.2 or later, then python-oracledb uses 'multi-property tags' and the tag string must be of the form of one or more "name=value" pairs separated by a semi-colon, for example ``"loc=uk;lang=cy"``. When a connection is requested with a given tag, and a connection with that tag is not present in the pool, then a new connection, or an existing connection with cleaned session state, will be chosen by the pool and the session callback procedure will be invoked. The callback can then set desired session state and update the connection's tag. However, if the ``matchanytag`` parameter of :meth:`~ConnectionPool.acquire()` is True, then any other tagged connection may be chosen by the pool and the callback procedure should parse the actual and requested tags to determine which bits of session state should be reset. The example below demonstrates connection tagging: .. code-block:: python def init_session(connection, requested_tag): if requested_tag == "NLS_DATE_FORMAT=SIMPLE": sql = "ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD'" elif requested_tag == "NLS_DATE_FORMAT=FULL": sql = "ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI'" cursor = connection.cursor() cursor.execute(sql) connection.tag = requested_tag pool = oracledb.create_pool(user="hr", password=userpwd, dsn="orclpdb", session_callback=init_session) # Two connections with different session state: connection1 = pool.acquire(tag="NLS_DATE_FORMAT=SIMPLE") connection2 = pool.acquire(tag="NLS_DATE_FORMAT=FULL") See `session_callback.py `__ for an example. PL/SQL Callback +++++++++++++++ .. note:: PL/SQL Callbacks are only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. When python-oracledb uses Oracle Client 12.2 or later, the session callback can also be the name of a PL/SQL procedure. A PL/SQL callback will be initiated only when the tag currently associated with a connection does not match the tag that is requested. A PL/SQL callback is most useful when using :ref:`drcp` because DRCP does not require a :ref:`round-trip ` to invoke a PL/SQL session callback procedure. The PL/SQL session callback should accept two VARCHAR2 arguments: .. code-block:: sql PROCEDURE myPlsqlCallback ( requestedTag IN VARCHAR2, actualTag IN VARCHAR2 ); The logic in this procedure can parse the actual tag in the session that has been selected by the pool and compare it with the tag requested by the application. The procedure can then change any state required before the connection is returned to the application from :meth:`~ConnectionPool.acquire()`. If the ``matchanytag`` attribute of :meth:`~ConnectionPool.acquire()` is *True*, then a connection with any state may be chosen by the pool. Oracle 'multi-property tags' must be used. The tag string must be of the form of one or more "name=value" pairs separated by a semi-colon, for example ``"loc=uk;lang=cy"``. In python-oracledb set ``session_callback`` to the name of the PL/SQL procedure. For example: .. code-block:: python pool = oracledb.create_pool(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb:pooled", session_callback="MyPlsqlCallback") connection = pool.acquire(tag="NLS_DATE_FORMAT=SIMPLE", # DRCP options, if you are using DRCP cclass='MYCLASS', purity=oracledb.ATTR_PURITY_SELF) See `session_callback_plsql.py `__ for an example. .. _connpooltypes: Heterogeneous and Homogeneous Connection Pools ---------------------------------------------- By default, connection pools are 'homogeneous', meaning that all connections use the same database credentials. Both python-oracledb Thin and :ref:`Thick ` modes support homogeneous pools. **Creating Heterogeneous Pools** The python-oracledb Thick mode additionally supports Heterogeneous pools, allowing different user names and passwords to be passed to each :meth:`~ConnectionPool.acquire()` call. To create an heterogeneous pool, set the :meth:`~oracledb.create_pool()` parameter ``homogeneous`` to False: .. code-block:: python pool = oracledb.create_pool(dsn="dbhost.example.com/orclpdb", homogeneous=False) connection = pool.acquire(user="hr", password=userpwd) .. _usingpoolparams: Using the PoolParams Builder Class ---------------------------------- The :ref:`PoolParams class ` allows you to define connection and pool parameters in a single place. The :func:`oracledb.PoolParams()` function returns a ``PoolParams`` object. This is a subclass of the :ref:`ConnectParams class ` with additional pool-specific attributes such as the pool size. A ``PoolParams`` object can be passed to :func:`oracledb.create_pool()`. For example: .. code-block:: python pp = oracledb.PoolParams(min=1, max=2, increment=1) pool = oracledb.create_pool(user="hr", password=userpw, dsn="dbhost.example.com/orclpdb", params=pp) The use of the PoolParams class is optional because you can pass the same parameters directly to :func:`~oracledb.create_pool()`. For example, the code above is equivalent to: .. code-block:: python pool = oracledb.create_pool(user="hr", password=userpw, dsn="dbhost.example.com/orclpdb", min=1, max=2, increment=1) Most PoolParams arguments are gettable as properties. They may be set individually using the ``set()`` method: .. code-block:: python pp = oracledb.PoolParams() pp.set(min=5) print(pp.min) # 5 Some values such as the database host name, can be specified as :func:`oracledb.create_pool()` parameters, as part of the connect string, and in the ``params`` object. If a ``dsn`` is passed, the python-oracledb :ref:`Thick ` mode will use the ``dsn`` string to connect. Otherwise, a connection string is internally constructed from the individual parameters and ``params`` object values, with the individual parameters having precedence. In python-oracledb's default Thin mode, a connection string is internally used that contains all relevant values specified. The precedence in Thin mode is that values in any ``dsn`` parameter override values passed as individual parameters, which themselves override values set in the ``params`` object. Similar precedence rules also apply to other values. .. _drcp: Database Resident Connection Pooling (DRCP) =========================================== `Database Resident Connection Pooling (DRCP) `__ enables database resource sharing for applications which use a large number of connections that run in multiple client processes or run on multiple middle-tier application servers. By default, each connection from Python will use one database server process. DRCP allows pooling of these server processes. This reduces the amount of memory required on the database host. The DRCP pool can be shared by multiple applications. DRCP is useful for applications which share the same database credentials, have similar session settings (for example date format settings or PL/SQL package state), and where the application gets a database connection, works on it for a relatively short duration, and then releases it. For efficiency, it is recommended that DRCP connections should be used in conjunction with python-oracledb's local :ref:`connection pool `. However, although using DRCP with standalone connections is not as efficient it does allow the database to reuse database server processes which can provide a performance benefit for applications that cannot use a local connection pool. Although applications can choose whether or not to use pooled connections at runtime, care must be taken to configure the database appropriately for the number of expected connections, and also to stop inadvertent use of non-DRCP connections leading to a database server resource shortage. Conversely, avoid using DRCP connections for long-running operations. For more information about DRCP, see `Oracle Database Concepts Guide `__ and for DRCP Configuration, see `Oracle Database Administrator's Guide `__. Using DRCP with python-oracledb applications involves the following steps: 1. Configuring and enabling DRCP in the database 2. Configuring the application to use a DRCP connection 3. Deploying the application Enabling DRCP in Oracle Database -------------------------------- Every Oracle Database uses a single, default DRCP connection pool. From Oracle Database 21c, each pluggable database can optionally have its own pool. Note that DRCP is already enabled in Oracle Autonomous Database and pool management is different to the steps below. DRCP pools can be configured and administered by a DBA using the ``DBMS_CONNECTION_POOL`` package: .. code-block:: sql EXECUTE DBMS_CONNECTION_POOL.CONFIGURE_POOL( pool_name => 'SYS_DEFAULT_CONNECTION_POOL', minsize => 4, maxsize => 40, incrsize => 2, session_cached_cursors => 20, inactivity_timeout => 300, max_think_time => 600, max_use_session => 500000, max_lifetime_session => 86400) Alternatively, the method ``DBMS_CONNECTION_POOL.ALTER_PARAM()`` can set a single parameter: .. code-block:: sql EXECUTE DBMS_CONNECTION_POOL.ALTER_PARAM( pool_name => 'SYS_DEFAULT_CONNECTION_POOL', param_name => 'MAX_THINK_TIME', param_value => '1200') The ``inactivity_timeout`` setting terminates idle pooled servers, helping optimize database resources. To avoid pooled servers permanently being held onto by a selfish Python script, the ``max_think_time`` parameter can be set. The parameters ``num_cbrok`` and ``maxconn_cbrok`` can be used to distribute the persistent connections from the clients across multiple brokers. This may be needed in cases where the operating system per-process descriptor limit is small. Some customers have found that having several connection brokers improves performance. The ``max_use_session`` and ``max_lifetime_session`` parameters help protect against any unforeseen problems affecting server processes. The default values will be suitable for most users. See the `Oracle DRCP documentation `__ for details on parameters. In general, if pool parameters are changed, then the pool should be restarted. Otherwise, server processes will continue to use old settings. There is a ``DBMS_CONNECTION_POOL.RESTORE_DEFAULTS()`` procedure to reset all values. When DRCP is used with RAC, each database instance has its own connection broker and pool of servers. Each pool has the identical configuration. For example, all pools start with ``minsize`` server processes. A single DBMS_CONNECTION_POOL command will alter the pool of each instance at the same time. The pool needs to be started before connection requests begin. The command below does this by bringing up the broker, which registers itself with the database listener: .. code-block:: sql EXECUTE DBMS_CONNECTION_POOL.START_POOL() Once enabled this way, the pool automatically restarts when the database instance restarts, unless explicitly stopped with the ``DBMS_CONNECTION_POOL.STOP_POOL()`` command: .. code-block:: sql EXECUTE DBMS_CONNECTION_POOL.STOP_POOL() The pool cannot be stopped while connections are open. Coding Applications to use DRCP ------------------------------- To use DRCP, application connection establishment must request a DRCP pooled server. The best practice is also to specify a user-chosen connection class name. A 'purity' of the connection session state can optionally be specified. See the Oracle Database documentation on `benefiting from scalability `__ for more information on purity and connection classes. To request the database, use a DRCP pooled server and you can use a specific connection string in :meth:`oracledb.create_pool()` or :meth:`oracledb.connect()` like one of the following syntaxes. For example with the :ref:`Easy Connect syntax `: .. code-block:: python dsn = "dbhost.example.com/orcl:pooled" Alternatively, add ``(SERVER=POOLED)`` to the connect descriptor such as used in an Oracle Network configuration file ``tnsnames.ora``:: customerpool = (DESCRIPTION=(ADDRESS=(PROTOCOL=tcp) (HOST=dbhost.example.com) (PORT=1521))(CONNECT_DATA=(SERVICE_NAME=CUSTOMER) (SERVER=POOLED))) You can also specify to use a DRCP pooled server by setting the ``server_type`` parameter when creating a standalone connection or creating a python-oracledb connection pool. For example: .. code-block:: python pool = oracledb.create_pool(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb", min=2, max=5, increment=1, server_type="pooled") **DRCP Connection Class Names** The best practice is to specify a ``cclass`` class name when creating a python-oracledb connection pool. This user-chosen name provides some partitioning of DRCP session memory so reuse is limited to similar applications. It provides maximum pool sharing if multiple application processes are started. A class name also allows better DRCP usage tracking in the database. In the database monitoring views, the class name shown will be the value specified in the application prefixed with the user name. To create a connection pool requesting a DRCP pooled server and specifying a class name you can call: .. code-block:: python pool = oracledb.create_pool(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb:pooled", min=2, max=5, increment=1, cclass="MYAPP") The python-oracledb connection pool size does not need to match the DRCP pool size. The limit on overall execution parallelism is determined by the DRCP pool size. Connection class names can also be passed to :meth:`~ConnectionPool.acquire()`, if desired: .. code-block:: python pool = oracledb.create_pool(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb:pooled", min=2, max=5, increment=1, cclass="MYAPP") connection = mypool.acquire(cclass="OTHERAPP") If a pooled server of a requested class is not available, a server with new session state is used. If the DRCP pool cannot grow, a server with a different class may be used and its session state cleared. If ``cclass`` is not set, then the pooled server sessions will not be reused optimally, and the DRCP statistic views may record large values for NUM_MISSES. **DRCP Connection Purity** DRCP allows the connection session memory to be reused or cleaned each time a connection is acquired from the pool. The pool or connection creation ``purity`` parameter can be one of ``PURITY_NEW``, ``PURITY_SELF``, or ``PURITY_DEFAULT``. The value ``PURITY_SELF`` allows reuse of both the pooled server process and session memory, giving maximum benefit from DRCP. By default, python-oracledb pooled connections use ``PURITY_SELF`` and standalone connections use ``PURITY_NEW``. To limit session sharing, you can explicitly require that new session memory be allocated each time :meth:`~ConnectionPool.acquire()` is called: .. code-block:: python pool = oracledb.create_pool(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb:pooled", min=2, max=5, increment=1, cclass="MYAPP", purity=oracledb.PURITY_NEW) **Setting the Connection Class and Purity in the Connection String** For the python-oracledb Thin mode, you can specify the class and purity in the connection string itself. This removes the need to modify an existing application when you want to use DRCP: .. code-block:: python dsn = "localhost/orclpdb:pooled?pool_connection_class=MYAPP&pool_purity=self" Recent versions of Oracle Client libraries also support this syntax. However, explicitly specifying the purity as SELF in this way may cause some unusable connections in a python-oracledb Thick mode connection pool not to be terminated. In summary, if you cannot programmatically set the class name and purity, or cannot use python-oracledb Thin mode, then avoid explicitly setting the purity as a connection string parameter when using a python-oracledb connection pooling in Thick mode. **Closing Connections when using DRCP** Similar to using a python-oracledb connection pool, Python scripts where python-oracledb connections do not go out of scope quickly (which releases them), or do not currently use :meth:`Connection.close()` or :meth:`ConnectionPool.release()` should be examined to see if the connections can be closed earlier. This allows maximum reuse of DRCP pooled servers by other users: .. code-block:: python pool = oracledb.create_pool(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb:pooled", min=2, max=5, increment=1, cclass="MYAPP") # Do some database operations connection = mypool.acquire() . . . connection.close(); # <- Add this to release the DRCP pooled server # Do lots of non-database work . . . # Do some more database operations connection = mypool.acquire() # <- And get a new pooled server only when needed . . . connection.close(); .. _monitoringdrcp: Monitoring DRCP --------------- Data dictionary views are available to monitor the performance of DRCP. Database administrators can check statistics such as the number of busy and free servers, and the number of hits and misses in the pool against the total number of requests from clients. The views include: * ``DBA_CPOOL_INFO`` * ``V$PROCESS`` * ``V$SESSION`` * ``V$CPOOL_STATS`` * ``V$CPOOL_CC_STATS`` * ``V$CPOOL_CONN_INFO`` **DBA_CPOOL_INFO View** ``DBA_CPOOL_INFO`` displays configuration information about the DRCP pool. The columns are equivalent to the ``dbms_connection_pool.configure_pool()`` settings described in the table of DRCP configuration options, with the addition of a ``STATUS`` column. The status is ``ACTIVE`` if the pool has been started and ``INACTIVE`` otherwise. Note that the pool name column is called ``CONNECTION_POOL``. This example checks whether the pool has been started and finds the maximum number of pooled servers:: SQL> SELECT connection_pool, status, maxsize FROM dba_cpool_info; CONNECTION_POOL STATUS MAXSIZE ---------------------------- ---------- ---------- SYS_DEFAULT_CONNECTION_POOL ACTIVE 40 **V$PROCESS and V$SESSION Views** The ``V$SESSION`` view shows information about the currently active DRCP sessions. It can also be joined with ``V$PROCESS`` through ``V$SESSION.PADDR = V$PROCESS.ADDR`` to correlate the views. **V$CPOOL_STATS View** The ``V$CPOOL_STATS`` view displays information about the DRCP statistics for an instance. The V$CPOOL_STATS view can be used to assess how efficient the pool settings are. This example query shows an application using the pool effectively. The low number of misses indicates that servers and sessions were reused. The wait count shows just over 1% of requests had to wait for a pooled server to become available:: NUM_REQUESTS NUM_HITS NUM_MISSES NUM_WAITS ------------ ---------- ---------- ---------- 10031 99990 40 1055 If ``cclass`` was set (allowing pooled servers and sessions to be reused), then NUM_MISSES will be low. If the pool maxsize is too small for the connection load, then NUM_WAITS will be high. **V$CPOOL_CC_STATS View** The view ``V$CPOOL_CC_STATS`` displays information about the connection class level statistics for the pool per instance:: SQL> SELECT cclass_name, num_requests, num_hits, num_misses FROM v$cpool_cc_stats; CCLASS_NAME NUM_REQUESTS NUM_HITS NUM_MISSES -------------------------------- ------------ ---------- ---------- HR.MYCLASS 100031 99993 38 The class name columns shows the database user name appended with the connection class name. **V$CPOOL_CONN_INFO View** The ``V$POOL_CONN_INFO`` view gives insight into client processes that are connected to the connection broker, making it easier to monitor and trace applications that are currently using pooled servers or are idle. This view was introduced in Oracle 11gR2. You can monitor the view ``V$CPOOL_CONN_INFO`` to, for example, identify misconfigured machines that do not have the connection class set correctly. This view maps the machine name to the class name. In python-oracledb Thick mode, the class name will be default to one like shown below:: SQL> SELECT cclass_name, machine FROM v$cpool_conn_info; CCLASS_NAME MACHINE --------------------------------------- ------------ CJ.OCI:SP:wshbIFDtb7rgQwMyuYvodA cjlinux In this example, you would examine applications on ``cjlinux`` and make them set ``cclass``. When connecting to Oracle Autonomous Database on shared infrastructure (ADB-S), the ``V$CPOOL_CONN_INFO`` view can be used to track the number of connection hits and misses to show the pool efficiency. .. _proxyauth: Connecting Using Proxy Authentication ===================================== Proxy authentication allows a user (the "session user") to connect to Oracle Database using the credentials of a "proxy user". Statements will run as the session user. Proxy authentication is generally used in three-tier applications where one user owns the schema while multiple end-users access the data. For more information about proxy authentication, see the `Oracle documentation `__. An alternative to using proxy users is to set :attr:`Connection.client_identifier` after connecting and use its value in statements and in the database, for example for :ref:`monitoring `. The following proxy examples use these schemas. The ``mysessionuser`` schema is granted access to use the password of ``myproxyuser``: .. code-block:: sql CREATE USER myproxyuser IDENTIFIED BY myproxyuserpw; GRANT CREATE SESSION TO myproxyuser; CREATE USER mysessionuser IDENTIFIED BY itdoesntmatter; GRANT CREATE SESSION TO mysessionuser; ALTER USER mysessionuser GRANT CONNECT THROUGH myproxyuser; After connecting to the database, the following query can be used to show the session and proxy users: .. code-block:: sql SELECT SYS_CONTEXT('USERENV', 'PROXY_USER'), SYS_CONTEXT('USERENV', 'SESSION_USER') FROM DUAL; Standalone connection examples: .. code-block:: python # Basic Authentication without a proxy connection = oracledb.connect(user="myproxyuser", password="myproxyuserpw", dsn="dbhost.example.com/orclpdb") # PROXY_USER: None # SESSION_USER: MYPROXYUSER # Basic Authentication with a proxy connection = oracledb.connect(user="myproxyuser[mysessionuser]", password="myproxyuserpw", dsn="dbhost.example.com/orclpdb") # PROXY_USER: MYPROXYUSER # SESSION_USER: MYSESSIONUSER Pooled connection examples: .. code-block:: python # Basic Authentication without a proxy pool = oracledb.create_pool(user="myproxyuser", password="myproxyuserpw", dsn="dbhost.example.com/orclpdb") connection = pool.acquire() # PROXY_USER: None # SESSION_USER: MYPROXYUSER # Basic Authentication with proxy pool = oracledb.create_pool(user="myproxyuser[mysessionuser]", password="myproxyuserpw", dsn="dbhost.example.com/orclpdb", homogeneous=False) connection = pool.acquire() # PROXY_USER: MYPROXYUSER # SESSION_USER: MYSESSIONUSER Note the use of a :ref:`heterogeneous ` pool in the example above. This is required in this scenario. .. _extauth: Connecting Using External Authentication ======================================== Instead of storing the database username and password in Python scripts or environment variables, database access can be authenticated by an outside system. External Authentication allows applications to validate user access by an external password store (such as an Oracle Wallet), by the operating system, or with an external authentication service. .. note:: Connecting to Oracle Database using external authentication is only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. Using an Oracle Wallet for External Authentication -------------------------------------------------- The following steps give an overview of using an Oracle Wallet. Wallets should be kept securely. Wallets can be managed with `Oracle Wallet Manager `__. In this example the wallet is created for the ``myuser`` schema in the directory ``/home/oracle/wallet_dir``. The ``mkstore`` command is available from a full Oracle client or Oracle Database installation. If you have been given wallet by your DBA, skip to step 3. 1. First create a new wallet as the ``oracle`` user:: mkstore -wrl "/home/oracle/wallet_dir" -create This will prompt for a new password for the wallet. 2. Create the entry for the database user name and password that are currently hardcoded in your Python scripts. Use either of the methods shown below. They will prompt for the wallet password that was set in the first step. **Method 1 - Using an Easy Connect string**:: mkstore -wrl "/home/oracle/wallet_dir" -createCredential dbhost.example.com/orclpdb myuser myuserpw **Method 2 - Using a connect name identifier**:: mkstore -wrl "/home/oracle/wallet_dir" -createCredential mynetalias myuser myuserpw The alias key ``mynetalias`` immediately following the ``-createCredential`` option will be the connect name to be used in Python scripts. If your application connects with multiple different database users, you could create a wallet entry with different connect names for each. You can see the newly created credential with:: mkstore -wrl "/home/oracle/wallet_dir" -listCredential 3. Skip this step if the wallet was created using an Easy Connect String. Otherwise, add an entry in :ref:`tnsnames.ora ` for the connect name as follows:: mynetalias = (DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)(HOST = dbhost.example.com)(PORT = 1521)) (CONNECT_DATA = (SERVER = DEDICATED) (SERVICE_NAME = orclpdb) ) ) The file uses the description for your existing database and sets the connect name alias to ``mynetalias``, which is the identifier used when adding the wallet entry. 4. Add the following wallet location entry in the :ref:`sqlnet.ora ` file, using the ``DIRECTORY`` you created the wallet in:: WALLET_LOCATION = (SOURCE = (METHOD = FILE) (METHOD_DATA = (DIRECTORY = /home/oracle/wallet_dir) ) ) SQLNET.WALLET_OVERRIDE = TRUE Examine the Oracle documentation for full settings and values. 5. Ensure the configuration files are in a default location or TNS_ADMIN is set to the directory containing them. See :ref:`optnetfiles`. With an Oracle wallet configured, and readable by you, your scripts can connect using: .. code-block:: python connection = oracledb.connect(externalauth=True, dsn="mynetalias") or: .. code-block:: python pool = oracledb.create_pool(externalauth=True, homogeneous=False, dsn="mynetalias") pool.acquire() The ``dsn`` must match the one used in the wallet. After connecting, the query:: SELECT SYS_CONTEXT('USERENV', 'SESSION_USER') FROM DUAL; will show:: MYUSER .. note:: Wallets are also used to configure Transport Layer Security (TLS) connections. If you are using a wallet like this, you may need a database username and password in :meth:`oracledb.connect()` and :meth:`oracledb.create_pool()` calls. **External Authentication and Proxy Authentication** The following examples show external wallet authentication combined with :ref:`proxy authentication `. These examples use the wallet configuration from above, with the addition of a grant to another user:: ALTER USER mysessionuser GRANT CONNECT THROUGH myuser; After connection, you can check who the session user is with: .. code-block:: sql SELECT SYS_CONTEXT('USERENV', 'PROXY_USER'), SYS_CONTEXT('USERENV', 'SESSION_USER') FROM DUAL; Standalone connection example: .. code-block:: python # External Authentication with proxy connection = oracledb.connect(user="[mysessionuser]", dsn="mynetalias") # PROXY_USER: MYUSER # SESSION_USER: MYSESSIONUSER You can also explicitly set the ``externalauth`` parameter to True in standalone connections as shown below. The ``externalauth`` parameter is optional. .. code-block:: python # External Authentication with proxy when externalauth is set to True connection = oracledb.connect(user="[mysessionuser]", dsn="mynetalias", externalauth=True) # PROXY_USER: MYUSER # SESSION_USER: MYSESSIONUSER Pooled connection example: .. code-block:: python # External Authentication with proxy pool = oracledb.create_pool(externalauth=True, homogeneous=False, dsn="mynetalias") pool.acquire(user="[mysessionuser]") # PROXY_USER: MYUSER # SESSION_USER: MYSESSIONUSER The following usage is not supported: .. code-block:: python pool = oracledb.create_pool(user="[mysessionuser]", externalauth=True, homogeneous=False, dsn="mynetalias") pool.acquire() Operating System Authentication ------------------------------- With Operating System authentication, Oracle allows user authentication to be performed by the operating system. The following steps give an overview of how to implement OS Authentication on Linux. 1. Log in to your computer. The commands used in these steps assume the operating system user name is "oracle". 2. Log in to SQL*Plus as the SYSTEM user and verify the value for the ``OS_AUTHENT_PREFIX`` parameter:: SQL> SHOW PARAMETER os_authent_prefix NAME TYPE VALUE ------------------------------------ ----------- ------------------------------ os_authent_prefix string ops$ 3. Create an Oracle database user using the ``os_authent_prefix`` determined in step 2, and the operating system user name: .. code-block:: sql CREATE USER ops$oracle IDENTIFIED EXTERNALLY; GRANT CONNECT, RESOURCE TO ops$oracle; In Python, connect using the following code: .. code-block:: python connection = oracledb.connect(dsn="mynetalias") Your session user will be ``OPS$ORACLE``. If your database is not on the same computer as Python, you can perform testing by setting the database configuration parameter ``remote_os_authent=true``. Beware of security concerns because this is insecure. See `Oracle Database Security Guide `__ for more information about Operating System Authentication. .. _tokenauth: Token-Based Authentication ========================== Token-Based Authentication allows users to connect to a database by using an encrypted authentication token without having to enter a database username and password. The authentication token must be valid and not expired for the connection to be successful. Users already connected will be able to continue work after their token has expired but they will not be able to reconnect without getting a new token. The two authentication methods supported by python-oracledb are :ref:`Open Authorization (OAuth 2.0) ` and :ref:`Oracle Cloud Infrastructure (OCI) Identity and Access Management (IAM) `. .. _oauth2: Connecting Using OAuth 2.0 Token-Based Authentication ----------------------------------------------------- Oracle Cloud Infrastructure (OCI) users can be centrally managed in a Microsoft Azure Active Directory (Azure AD) service. Open Authorization (OAuth 2.0) token-based authentication allows users to authenticate to Oracle Database using Azure AD OAuth2 tokens. Currently, only Azure AD tokens are supported. Ensure that you have a Microsoft Azure account and your Oracle Database is registered with Azure AD. See `Configuring the Oracle Autonomous Database for Microsoft Azure AD Integration `_ for more information. Both Thin and Thick modes of the python-oracledb driver support OAuth 2.0 token-based authentication. When using python-oracledb in Thick mode, Oracle Client libraries 19.15 (or later), or 21.7 (or later) are needed. OAuth 2.0 token-based authentication can be used for both standalone connections and connection pools. Tokens can be specified using the connection parameter introduced in python-oracledb 1.1. Users of earlier python-oracledb versions can alternatively use :ref:`OAuth 2.0 Token-Based Authentication Connection Strings`. OAuth2 Token Generation And Extraction ++++++++++++++++++++++++++++++++++++++ There are different ways to retrieve Azure AD OAuth2 tokens. Some of the ways to retrieve the OAuth2 tokens are detailed in `Examples of Retrieving Azure AD OAuth2 Tokens `_. You can also retrieve Azure AD OAuth2 tokens by using `Azure Identity client library for Python `_. .. _oauthhandler: Example of Using a TokenHandlerOAuth Class ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Here, as an example, we are using a Python script to automate the process of generating and reading the Azure AD OAuth2 tokens. .. code:: python import json import os import oracledb import requests class TokenHandlerOAuth: def __init__(self, file_name="cached_token_file_name", api_key="api_key", client_id="client_id", client_secret="client_secret"): self.token = None self.file_name = file_name self.url = \ f"https://login.microsoftonline.com/{api_key}/oauth2/v2.0/token" self.scope = \ f"https://oracledevelopment.onmicrosoft.com/{client_id}/.default" if os.path.exists(file_name): with open(file_name) as f: self.token = f.read().strip() self.api_key = api_key self.client_id = client_id self.client_secret = client_secret def __call__(self, refresh): if self.token is None or refresh: post_data = dict(client_id=self.client_id, grant_type="client_credentials", scope=self.scope, client_secret=self.client_secret) r = requests.post(url=self.url, data=post_data) result = json.loads(r.text) self.token = result["access_token"] with open(self.file_name, "w") as f: f.write(self.token) return self.token The TokenHandlerOAuth class uses a callable to generate and read the OAuth2 tokens. When the callable in the TokenHandlerAuth class is invoked for the first time to create a standalone connection or pool, the ``refresh`` parameter is False which allows the callable to return a cached token, if desired. The expiry date is then extracted from this token and compared with the current date. If the token has not expired, then it will be used directly. If the token has expired, the callable is invoked the second time with the ``refresh`` parameter set to True. See :ref:`curl` for an alternative way to generate the tokens. Standalone Connection Creation with OAuth2 Access Tokens ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ For OAuth 2.0 Token-Based Authentication, the ``access_token`` connection parameter must be specified. This parameter should be a string (or a callable that returns a string) specifying an Azure AD OAuth2 token. Standalone connections can be created in the python-oracledb Thick and Thin modes using OAuth 2.0 token-based authentication. In the examples below, the ``access_token`` parameter is set to a callable. **In python-oracledb Thin mode** When connecting to Oracle Cloud Database with mutual TLS (mTLS) using OAuth2 tokens in the python-oracledb Thin mode, you need to explicitly set the ``config_dir``, ``wallet_location``, and ``wallet_password`` parameters of :func:`~oracledb.connect`. See, :ref:`autonomousdb`. The following example shows a standalone connection creation using OAuth 2.0 token based authentication in the python-oracledb Thin mode. For information on TokenHandlerOAuth() used in the example, see :ref:`oauthhandler`. .. code:: python connection = oracledb.connect(access_token=TokenHandlerOAuth(), dsn=mydb_low, config_dir="path_to_extracted_wallet_zip", wallet_location="location_of_pem_file", wallet_password=wp) **In python-oracledb Thick mode** In the python-oracledb Thick mode, you can create a standalone connection using OAuth2 tokens as shown in the example below. For information on TokenHandlerOAuth() used in the example, see :ref:`oauthhandler`. .. code:: python connection = oracledb.connect(access_token=TokenHandlerOAuth(), externalauth=True, dsn=mydb_low) Connection Pool Creation with OAuth2 Access Tokens ++++++++++++++++++++++++++++++++++++++++++++++++++ For OAuth 2.0 Token-Based Authentication, the ``access_token`` connection parameter must be specified. This parameter should be a string (or a callable that returns a string) specifying an Azure AD OAuth2 token. The ``externalauth`` parameter must be set to True in the python-oracledb Thick mode. The ``homogeneous`` parameter must be set to True in both the python-oracledb Thin and Thick modes. Connection pools can be created in the python-oracledb Thick and Thin modes using OAuth 2.0 token-based authentication. In the examples below, the ``access_token`` parameter is set to a callable. Note that the ``access_token`` parameter should be set to a callable. This is useful when the connection pool needs to expand and create new connections but the current token has expired. In such case, the callable should return a string specifying the new, valid Azure AD OAuth2 token. **In python-oracledb Thin mode** When connecting to Oracle Cloud Database with mutual TLS (mTLS) using OAuth2 tokens in the python-oracledb Thin mode, you need to explicitly set the ``config_dir``, ``wallet_location``, and ``wallet_password`` parameters of :func:`~oracledb.create_pool`. See, :ref:`autonomousdb`. The following example shows a connection pool creation using OAuth 2.0 token based authentication in the python-oracledb Thin mode. For information on TokenHandlerOAuth() used in the example, see :ref:`oauthhandler`. .. code:: python connection = oracledb.create_pool(access_token=TokenHandlerOAuth(), homogeneous=True, dsn=mydb_low, config_dir="path_to_extracted_wallet_zip", wallet_location="location_of_pem_file", wallet_password=wp min=1, max=5, increment=2) **In python-oracledb Thick mode** In the python-oracledb Thick mode, you can create a connection pool using OAuth2 tokens as shown in the example below. For information on TokenHandlerOAuth() used in the example, see :ref:`oauthhandler`. .. code:: python pool = oracledb.create_pool(access_token=TokenHandlerOAuth(), externalauth=True, homogeneous=True, dsn=mydb_low, min=1, max=5, increment=2) .. _oauth2connstr: OAuth 2.0 Token-Based Authentication Connection Strings +++++++++++++++++++++++++++++++++++++++++++++++++++++++ The connection string used by python-oracledb can specify the directory where the token file is located. This syntax is usable with older versions of python-oracledb. However, it is recommended to use connection parameters introduced in python-oracledb 1.1 instead. See :ref:`OAuth 2.0 Token-Based Authentication`. .. note:: OAuth 2.0 Token-Based Authentication Connection Strings is only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. There are different ways to retrieve Azure AD OAuth2 tokens. Some of the ways to retrieve the OAuth2 tokens are detailed in `Examples of Retrieving Azure AD OAuth2 Tokens `_. You can also retrieve Azure AD OAuth2 tokens by using `Azure Identity client library for Python `_. .. _curl: Example of Using a Curl Command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Here, as an example, we are using Curl with a Resource Owner Password Credential (ROPC) Flow, that is, a ``curl`` command is used against the Azure AD API to get the Azure AD OAuth2 token:: curl -X POST -H 'Content-Type: application/x-www-form-urlencoded' https://login.microsoftonline.com/your_tenant_id/oauth2/v2.0/token -d 'client_id=your_client_id' -d 'grant_type=client_credentials' -d 'scope=https://oracledevelopment.onmicrosoft.com/your_client_id/.default' -d 'client_secret=your_client_secret' This command generates a JSON response with token type, expiration, and access token values. The JSON response needs to be parsed so that only the access token is written and stored in a file. You can save the value of ``access_token`` generated to a file and set ``TOKEN_LOCATION`` to the location of token file. See :ref:`oauthhandler` for an example of using the TokenHandlerOAuth class to generate and read tokens. The Oracle Net parameters ``TOKEN_AUTH`` and ``TOKEN_LOCATION`` must be set when you are using the connection string syntax. Also, the ``PROTOCOL`` parameter must be ``tcps`` and ``SSL_SERVER_DN_MATCH`` should be ``ON``. You can set ``TOKEN_AUTH=OAUTH``. There is no default location set in this case, so you must set ``TOKEN_LOCATION`` to either of the following: * A directory, in which case, you must create a file named ``token`` which contains the token value * A fully qualified file name, in which case, you must specify the entire path of the file which contains the token value You can either set ``TOKEN_AUTH`` and ``TOKEN_LOCATION`` in a sqlnet.ora file or alternatively, you can specify it inside a connect descriptor stored in :ref:`tnsnames.ora` file, for example:: db_alias = (DESCRIPTION = (ADDRESS=(PROTOCOL=TCPS)(PORT=1522)(HOST=xxx.oraclecloud.com)) (CONNECT_DATA=(SERVICE_NAME=xxx.adb.oraclecloud.com)) (SECURITY = (SSL_SERVER_CERT_DN="CN=xxx.oraclecloud.com,OU=Oracle BMCS US, \ O=Oracle Corporation,L=Redwood City,ST=California,C=US") (TOKEN_AUTH=OAUTH) (TOKEN_LOCATION="/home/user1/mytokens/oauthtoken") ) ) The ``TOKEN_AUTH`` and ``TOKEN_LOCATION`` values in a connection string take precedence over the ``sqlnet.ora`` settings. Standalone connection example: .. code-block:: python connection = oracledb.connect(dsn=db_alias, externalauth=True) Connection pool example: .. code-block:: python pool = oracledb.create_pool(dsn=db_alias, externalauth=True, homogeneous=False, min=1, max=2, increment=1) connection = pool.acquire() .. _iamauth: Connecting Using OCI IAM Token-Based Authentication --------------------------------------------------- Oracle Cloud Infrastructure (OCI) Identity and Access Management (IAM) provides its users with a centralized database authentication and authorization system. Using this authentication method, users can use the database access token issued by OCI IAM to authenticate to the Oracle Cloud Database. Both Thin and Thick modes of the python-oracledb driver support OCI IAM token-based authentication. When using python-oracledb in Thick mode, Oracle Client libraries 19.14 (or later), or 21.5 (or later) are needed. OCI IAM token-based authentication can be used for both standalone connections and connection pools. Tokens can be specified using the connection parameter introduced in python-oracledb 1.1. Users of earlier python-oracledb versions can alternatively use :ref:`OCI IAM Token-Based Authentication Connection Strings `. OCI IAM Token Generation and Extraction +++++++++++++++++++++++++++++++++++++++ Authentication tokens can be generated through execution of an Oracle Cloud Infrastructure command line interface (OCI-CLI) command :: oci iam db-token get On Linux, a folder ``.oci/db-token`` will be created in your home directory. It will contain the token and private key files needed by python-oracledb. .. _iamhandler: Example of Using a TokenHandlerIAM Class ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Here, as an example, we are using a Python script to automate the process of generating and reading the OCI IAM tokens. .. code:: python import os import oracledb class TokenHandlerIAM: def __init__(self, dir_name="dir_name", command="oci iam db-token get"): self.dir_name = dir_name self.command = command self.token = None self.private_key = None def __call__(self, refresh): if refresh: if os.system(self.command) != 0: raise Exception("token command failed!") if self.token is None or refresh: self.read_token_info() return (self.token, self.private_key) def read_token_info(self): token_file_name = os.path.join(self.dir_name, "token") pkey_file_name = os.path.join(self.dir_name, "oci_db_key.pem") with open(token_file_name) as f: self.token = f.read().strip() with open(pkey_file_name) as f: if oracledb.is_thin_mode(): self.private_key = f.read().strip() else: lines = [s for s in f.read().strip().split("\n") if s not in ('-----BEGIN PRIVATE KEY-----', '-----END PRIVATE KEY-----')] self.private_key = "".join(lines) The TokenHandlerIAM class uses a callable to generate and read the OCI IAM tokens. When the callable in the TokenHandlerIAM class is invoked for the first time to create a standalone connection or pool, the ``refresh`` parameter is False which allows the callable to return a cached token, if desired. The expiry date is then extracted from this token and compared with the current date. If the token has not expired, then it will be used directly. If the token has expired, the callable is invoked the second time with the ``refresh`` parameter set to True. Standalone Connection Creation with OCI IAM Access Tokens +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ For OCI IAM Token-Based Authentication, the ``access_token`` connection parameter must be specified. This parameter should be a 2-tuple (or a callable that returns a 2-tuple) containing the token and private key. Standalone connections can be created in the python-oracledb Thick and Thin modes using OCI IAM token-based authentication. In the examples below, the ``access_token`` parameter is set to a callable. **In python-oracledb Thin mode** When connecting to Oracle Cloud Database with mutual TLS (mTLS) using OCI IAM tokens in the python-oracledb Thin mode, you need to explicitly set the ``config_dir``, ``wallet_location``, and ``wallet_password`` parameters of :func:`~oracledb.connect`. See, :ref:`autonomousdb`. The following example shows a standalone connection creation using OCI IAM token based authentication in the python-oracledb Thin mode. For information on TokenHandlerIAM() used in the example, see :ref:`iamhandler`. .. code:: python connection = oracledb.connect(access_token=TokenHandlerIAM(), dsn=mydb_low, config_dir="path_to_extracted_wallet_zip", wallet_location="location_of_pem_file", wallet_password=wp) **In python-oracledb Thick mode** In the python-oracledb Thick mode, you can create a standalone connection using OCI IAM tokens as shown in the example below. For information on TokenHandlerIAM() used in the example, see :ref:`iamhandler`. .. code:: python connection = oracledb.connect(access_token=TokenHandlerIAM(), externalauth=True, dsn=mydb_low) Connection Pool Creation with OCI IAM Access Tokens +++++++++++++++++++++++++++++++++++++++++++++++++++ For OCI IAM Token-Based Authentication, the ``access_token`` connection parameter must be specified. This parameter should be a 2-tuple (or a callable that returns a 2-tuple) containing the token and private key. The ``externalauth`` parameter must be set to True in the python-oracledb Thick mode. The ``homogeneous`` parameter must be set to True in both the python-oracledb Thin and Thick modes. Connection pools can be created in the python-oracledb Thick and Thin modes using OCI IAM token-based authentication. In the examples below, the ``access_token`` parameter is set to a callable. Note that the ``access_token`` parameter should be set to a callable. This is useful when the connection pool needs to expand and create new connections but the current token has expired. In such case, the callable should return a 2-tuple (token, private key) specifying the new, valid access token. **In python-oracledb Thin mode** When connecting to Oracle Cloud Database with mutual TLS (mTLS) using OCI IAM tokens in the python-oracledb Thin mode, you need to explicitly set the ``config_dir``, ``wallet_location``, and ``wallet_password`` parameters of :func:`~oracledb.create_pool`. See, :ref:`autonomousdb`. The following example shows a connection pool creation using OCI IAM token based authentication in the python-oracledb Thin mode. For information on TokenHandlerIAM() used in the example, see :ref:`iamhandler`. .. code:: python connection = oracledb.connect(access_token=TokenHandlerIAM(), homogeneous=True, dsn=mydb_low, config_dir="path_to_extracted_wallet_zip", wallet_location="location_of_pem_file", wallet_password=wp min=1, max=5, increment=2) **In python-oracledb Thick mode** In the python-oracledb Thick mode, you can create a connection pool using OCI IAM tokens as shown in the example below. For information on TokenHandlerIAM() used in the example, see :ref:`iamhandler`. .. code:: python pool = oracledb.create_pool(access_token=TokenHandlerIAM(), externalauth=True, homogeneous=True, dsn=mydb_low, min=1, max=5, increment=2) .. _iamauthconnstr: OCI IAM Token-Based Authentication Connection Strings +++++++++++++++++++++++++++++++++++++++++++++++++++++ The connection string used by python-oracledb can specify the directory where the token and private key files are located. This syntax is usable with older versions of python-oracledb. However, it is recommended to use connection parameters introduced in python-oracledb 1.1 instead. See :ref:`OCI IAM Token-Based Authentication`. .. note:: OCI IAM Token-Based Authentication Connection Strings is only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. The Oracle Cloud Infrastructure command line interface (OCI-CLI) can be used externally to get tokens and private keys from OCI IAM, for example with the OCI-CLI ``oci iam db-token get`` command. The Oracle Net parameter ``TOKEN_AUTH`` must be set when you are using the connection string syntax. Also, the ``PROTOCOL`` parameter must be ``tcps`` and ``SSL_SERVER_DN_MATCH`` should be ``ON``. You can set ``TOKEN_AUTH=OCI_TOKEN`` in a ``sqlnet.ora`` file. Alternatively, you can specify it in a connect descriptor, for example:: db_alias = (DESCRIPTION = (ADDRESS=(PROTOCOL=TCPS)(PORT=1522)(HOST=xxx.oraclecloud.com)) (CONNECT_DATA=(SERVICE_NAME=xxx.adb.oraclecloud.com)) (SECURITY = (SSL_SERVER_CERT_DN="CN=xxx.oraclecloud.com,OU=Oracle BMCS US, \ O=Oracle Corporation,L=Redwood City,ST=California,C=US") (TOKEN_AUTH=OCI_TOKEN) ) ) The default location for the token and private key is the same default location that the OCI-CLI tool writes to. For example ``~/.oci/db-token/`` on Linux. If the token and private key files are not in the default location then their directory must be specified with the ``TOKEN_LOCATION`` parameter in a sqlnet.ora file or in a connect descriptor stored inside :ref:`tnsnames.ora` file, for example:: db_alias = (DESCRIPTION = (ADDRESS=(PROTOCOL=TCPS)(PORT=1522)(HOST=xxx.oraclecloud.com)) (CONNECT_DATA=(SERVICE_NAME=xxx.adb.oraclecloud.com)) (SECURITY = (SSL_SERVER_CERT_DN="CN=xxx.oraclecloud.com,OU=Oracle BMCS US, \ O=Oracle Corporation,L=Redwood City,ST=California,C=US") (TOKEN_AUTH=OCI_TOKEN) (TOKEN_LOCATION="/path/to/token/folder") ) ) The ``TOKEN_AUTH`` and ``TOKEN_LOCATION`` values in a connection string take precedence over the ``sqlnet.ora`` settings. Standalone connection example: .. code-block:: python connection = oracledb.connect(dsn=db_alias, externalauth=True) Connection pool example: .. code-block:: python pool = oracledb.create_pool(dsn=db_alias, externalauth=True, homogeneous=False, min=1, max=2, increment=1) connection = pool.acquire() Privileged Connections ====================== The ``mode`` parameter of the function :meth:`oracledb.connect()` specifies the database privilege that you want to associate with the user. The example below shows how to connect to Oracle Database as SYSDBA: .. code-block:: python connection = oracledb.connect(user="sys", password=syspwd, dsn="dbhost.example.com/orclpdb", mode=oracledb.AUTH_MODE_SYSDBA) with connection.cursor() as cursor: cursor.execute("GRANT SYSOPER TO hr") This is equivalent to executing the following in SQL*Plus: .. code-block:: sql CONNECT sys/syspwd AS SYSDBA GRANT SYSOPER TO hr; .. _netencrypt: Securely Encrypting Network Traffic to Oracle Database ====================================================== You can encrypt data transferred between the Oracle Database and python-oracledb so that unauthorized parties are not able to view plain text values as the data passes over the network. Both python-oracledb Thin and Thick modes support TLS. Refer to the `Oracle Database Security Guide `__ for more configuration information. .. _nne: Native Network Encryption ------------------------- The python-oracledb :ref:`Thick mode ` can additionally use Oracle Database's `native network encryption `__. With native network encryption, the client and database server negotiate a key using Diffie-Hellman key exchange. This provides protection against man-in-the-middle attacks. Native network encryption can be configured by editing Oracle Net's optional :ref:`sqlnet.ora ` configuration file. The file on either the database server and/or on each python-oracledb 'client' machine can be configured. Parameters control whether data integrity checking and encryption is required or just allowed, and which algorithms the client and server should consider for use. As an example, to ensure all connections to the database are checked for integrity and are also encrypted, create or edit the Oracle Database ``$ORACLE_HOME/network/admin/sqlnet.ora`` file. Set the checksum negotiation to always validate a checksum and set the checksum type to your desired value. The network encryption settings can similarly be set. For example, to use the SHA512 checksum and AES256 encryption use:: SQLNET.CRYPTO_CHECKSUM_SERVER = required SQLNET.CRYPTO_CHECKSUM_TYPES_SERVER = (SHA512) SQLNET.ENCRYPTION_SERVER = required SQLNET.ENCRYPTION_TYPES_SERVER = (AES256) If you definitely know that the database server enforces integrity and encryption, then you do not need to configure python-oracledb separately. However, you can also, or alternatively do so, depending on your business needs. Create a ``sqlnet.ora`` on your client machine and locate it with other :ref:`optnetfiles`:: SQLNET.CRYPTO_CHECKSUM_CLIENT = required SQLNET.CRYPTO_CHECKSUM_TYPES_CLIENT = (SHA512) SQLNET.ENCRYPTION_CLIENT = required SQLNET.ENCRYPTION_TYPES_CLIENT = (AES256) The client and server sides can negotiate the protocols used if the settings indicate more than one value is accepted. Note that these are example settings only. You must review your security requirements and read the documentation for your Oracle version. In particular, review the available algorithms for security and performance. The ``NETWORK_SERVICE_BANNER`` column of the database view `V$SESSION_CONNECT_INFO `__ can be used to verify the encryption status of a connection. Resetting Passwords =================== After connecting to Oracle Database, passwords can be changed by calling :meth:`Connection.changepassword()`: .. code-block:: python # Get the passwords from somewhere, such as prompting the user oldpwd = getpass.getpass(f"Old Password for {username}: ") newpwd = getpass.getpass(f"New Password for {username}: ") connection.changepassword(oldpwd, newpwd) When a password has expired and you cannot connect directly, you can connect and change the password in one operation by using the ``newpassword`` parameter of the function :meth:`oracledb.connect()` constructor: .. code-block:: python # Get the passwords from somewhere, such as prompting the user oldpwd = getpass.getpass(f"Old Password for {username}: ") newpwd = getpass.getpass(f"New Password for {username}: ") connection = oracledb.connect(user=username, password=oldpwd, dsn="dbhost.example.com/orclpdb", newpassword=newpwd) .. _autonomousdb: Connecting to Oracle Cloud Autonomous Databases ================================================ Python applications can connect to Oracle Autonomous Database (ADB) in Oracle Cloud using one-way TLS (Transport Layer Security) or mutual TLS (mTLS). One-way TLS and mTLS provide enhanced security for authentication and encryption. A database username and password are still required for your application connections. If you need to create a new database schema so you do not login as the privileged ADMIN user, refer to the relevant Oracle Cloud documentation, for example see `Create Database Users `__ in the Oracle Autonomous Database manual. .. _onewaytls: One-way TLS Connection to Oracle Autonomous Database ---------------------------------------------------- With one-way TLS, python-oracledb applications can connect to Oracle ADB without using a wallet. Both Thin and Thick modes of the python-oracledb driver support one-way TLS. Applications that use the python-oracledb Thick mode, can connect to the Oracle ADB through one-way TLS only when using Oracle Client library versions 19.14 (or later) or 21.5 (or later). To enable one-way TLS for an ADB instance, complete the following steps in an Oracle Cloud console in the **Autonomous Database Information** section of the ADB instance details: 1. Click the **Edit** link next to *Access Control List* to update the Access Control List (ACL). The **Edit Access Control List** dialog box is displayed. 2. In the **Edit Access Control List** dialog box, select the type of address list entries and the corresponding values. You can include the required IP addresses, hostnames, or Virtual Cloud Networks (VCNs). The ACL limits access to only the IP addresses or VCNs that have been defined and blocks all other incoming traffic. 3. Navigate back to the ADB instance details page and click the **Edit** link next to *Mutual TLS (mTLS) Authentication*. The **Edit Mutual TLS Authentication** is displayed. 4. In the **Edit Mutual TLS Authentication** dialog box, deselect the **Require mutual TLS (mTLS) authentication** check box to disable the mTLS requirement on Oracle ADB and click **Save Changes**. 5. Navigate back to the ADB instance details page and click **DB Connection** on the top of the page. A **Database Connection** dialog box is displayed. 6. In the Database Connection dialog box, select TLS from the **Connection Strings** drop-down list. 7. Copy the appropriate Connection String of the database instance used by your application. Applications can connect to your Oracle ADB instance using the database credentials and the copied connect descriptor. For example, to connect as the ADMIN user: .. code-block:: python cs = '''(description = (retry_count=20)(retry_delay=3)(address=(protocol=tcps) (port=1522)(host=xxx.oraclecloud.com))(connect_data=(service_name=xxx.adb.oraclecloud.com)) (security=(ssl_server_dn_match=yes)(ssl_server_cert_dn="CN=xxx.oraclecloud.com,OU=Oracle BMCS US, O=Oracle Corporation, L=Redwood City, T=California, C=US")))''' connection = oracledb.connect(user="admin", password=pw, dsn=cs) You can download the ADB connection wallet using the **DB Connection** button and extract the ``tnsnames.ora`` file, or create one yourself if you prefer to keep connections strings out of application code, see :ref:`netservice`. You may be interested in the blog post `Easy wallet-less connections to Oracle Autonomous Databases in Python `__. .. _twowaytls: Mutual TLS (mTLS) Connection to Oracle Autonomous Database ---------------------------------------------------------- To enable python-oracledb connections to Oracle Autonomous Database in Oracle Cloud using mTLS, a wallet needs to be downloaded from the cloud console. mTLS is sometimes called Two-way TLS. Install the Wallet and Network Configuration Files ++++++++++++++++++++++++++++++++++++++++++++++++++ From the Oracle Cloud console for the database, download the wallet zip file using the **DB Connection** button. The zip contains the wallet and network configuration files. When downloading the zip, the cloud console will ask you to create a wallet password. This password is used by python-oracledb in Thin mode, but not in Thick mode. Note: keep wallet files in a secure location and only share them and the password with authorized users. **In python-oracledb Thin mode** For python-oracledb in Thin mode, only two files from the zip are needed: - ``tnsnames.ora`` - Maps net service names used for application connection strings to your database services - ``ewallet.pem`` - Enables SSL/TLS connections in Thin mode. Keep this file secure If you do not have a PEM file, see :ref:`createpem`. Unzip the wallet zip file and move the required files to a location such as ``/opt/OracleCloud/MYDB``. Connection can be made using your database credentials and setting the ``dsn`` parameter to the desired network alias from the ``tnsnames.ora`` file. The ``config_dir`` parameter indicates the directory containing ``tnsnames.ora``. The ``wallet_location`` parameter is the directory containing the PEM file. In this example the files are in the same directory. The ``wallet_password`` parameter should be set to the password created in the cloud console when downloading the wallet. For example, to connect as the ADMIN user using the ``mydb_low`` network service name: .. code-block:: python connection = oracledb.connect(user="admin", password=pw, dsn="mydb_low", config_dir="/opt/OracleCloud/MYDB", wallet_location="/opt/OracleCloud/MYDB", wallet_password=wp) **In python-oracledb Thick mode** For python-oracledb in Thick mode, only these files from the zip are needed: - ``tnsnames.ora`` - Maps net service names used for application connection strings to your database services - ``sqlnet.ora`` - Configures Oracle Network settings - ``cwallet.sso`` - Enables SSL/TLS connections in Thick mode. Keep this file secure Unzip the wallet zip file. There are two options for placing the required files: - Move the three files to the ``network/admin`` directory of the client libraries used by your application. For example if you are using Instant Client 19c and it is in ``$HOME/instantclient_19_15``, then you would put the wallet files in ``$HOME/instantclient_19_15/network/admin/``. Connection can be made using your database credentials and setting the ``dsn`` parameter to the desired network alias from the ``tnsnames.ora`` file. For example, to connect as the ADMIN user using the ``mydb_low`` network service name: .. code-block:: python connection = oracledb.connect(user="admin", password=pw, dsn="mydb_low") - Alternatively, move the three files to any accessible directory, for example ``/opt/OracleCloud/MYDB``. Then edit ``sqlnet.ora`` and change the wallet location directory to the directory containing the ``cwallet.sso`` file. For example:: WALLET_LOCATION = (SOURCE = (METHOD = file) (METHOD_DATA = (DIRECTORY="/opt/OracleCloud/MYDB"))) SSL_SERVER_DN_MATCH=yes Since the ``tnsnames.ora`` and ``sqlnet.ora`` files are not in the default location, your application needs to indicate where they are, either with the ``config_dir`` parameter to :meth:`oracledb.init_oracle_client()`, or using the ``TNS_ADMIN`` environment variable. See :ref:`Optional Oracle Net Configuration Files `. (Neither of these settings are needed, and you do not need to edit ``sqlnet.ora``, if you have put all the files in the ``network/admin`` directory.) For example, to connect as the ADMIN user using the ``mydb_low`` network service name: .. code-block:: python oracledb.init_oracle_client(config_dir="/opt/OracleCloud/MYDB") connection = oracledb.connect(user="admin", password=pw, dsn="mydb_low") In python-oracle Thick mode, to create mTLS connections in one Python process to two or more Oracle Autonomous Databases, move each ``cwallet.sso`` file to its own directory. For each connection use different connection string ``WALLET_LOCATION`` parameters to specify the directory of each ``cwallet.sso`` file. It is recommended to use Oracle Client libraries 19.17 (or later) when using multiple wallets. Access Through a Proxy +++++++++++++++++++++++ If you are behind a firewall, you can tunnel TLS/SSL connections via a proxy using `HTTPS_PROXY `__ in the connect descriptor or setting connection attributes. Successful connection depends on specific proxy configurations. Oracle does not recommend doing this when performance is critical. **In python-oracledb Thin mode** The proxy settings can be passed during connection creation: .. code-block:: python connection = oracledb.connect(user="admin", password=pw, dsn="mydb_low", config_dir="/opt/OracleCloud/MYDB", wallet_location="/opt/OracleCloud/MYDB", wallet_password=wp, https_proxy='myproxy.example.com', https_proxy_port=80) Alternatively, edit ``tnsnames.ora`` and add an ``HTTPS_PROXY`` proxy name and ``HTTPS_PROXY_PORT`` port to the connect descriptor address list of any service name you plan to use, for example:: mydb_low = (description= (address= (https_proxy=myproxy.example.com)(https_proxy_port=80) (protocol=tcps)(port=1522)(host= . . . ) .. code-block:: python connection = oracledb.connect(user="admin", password=pw, dsn="mydb_low", config_dir="/opt/OracleCloud/MYDB", wallet_location="/opt/OracleCloud/MYDB", wallet_password=wp) **In python-oracledb Thick mode** Edit ``sqlnet.ora`` and add a line:: SQLNET.USE_HTTPS_PROXY=on Edit ``tnsnames.ora`` and add an ``HTTPS_PROXY`` proxy name and ``HTTPS_PROXY_PORT`` port to the connect descriptor address list of any service name you plan to use, for example:: mydb_high = (description= (address= (https_proxy=myproxy.example.com)(https_proxy_port=80) (protocol=tcps)(port=1522)(host= . . . ) Using the Easy Connect Syntax with Autonomous Database +++++++++++++++++++++++++++++++++++++++++++++++++++++++ When python-oracledb is using Oracle Client libraries 19c or later, you can optionally use the :ref:`Easy Connect ` syntax to connect to Oracle Autonomous Database. The mapping from the cloud ``tnsnames.ora`` entries to an Easy Connect Plus string is:: protocol://host:port/service_name?wallet_location=/my/dir&retry_count=N&retry_delay=N For example, if your ``tnsnames.ora`` file had an entry:: cjjson_high = (description=(retry_count=20)(retry_delay=3) (address=(protocol=tcps)(port=1522) (host=adb.ap-sydney-1.oraclecloud.com)) (connect_data=(service_name=abc_cjjson_high.adb.oraclecloud.com)) (security=(ssl_server_cert_dn="CN=adb.ap-sydney-1.oraclecloud.com,OU=Oracle ADB SYDNEY,O=Oracle Corporation,L=Redwood City,ST=California,C=US"))) Then your applications can connect using the connection string: .. code-block:: python dsn = "tcps://adb.ap-sydney-1.oraclecloud.com:1522/abc_cjjson_high.adb.oraclecloud.com?wallet_location=/Users/cjones/Cloud/CJJSON&retry_count=20&retry_delay=3" connection = oracledb.connect(user="hr", password=userpwd, dsn=dsn) The ``wallet_location`` parameter needs to be set to the directory containing the ``cwallet.sso`` or ``ewallet.pem`` file from the wallet zip. The other wallet files, including ``tnsnames.ora``, are not needed when you use the Easy Connect Plus syntax. You can add other Easy Connect parameters to the connection string, for example:: dsn = dsn + "&https_proxy=myproxy.example.com&https_proxy_port=80" With python-oracledb Thin mode, the wallet password needs to be passed as a connection parameter. .. _createpem: Creating a PEM File for python-oracledb Thin Mode +++++++++++++++++++++++++++++++++++++++++++++++++ For mutual TLS in python-oracledb Thin mode, the certificate must be Privacy Enhanced Mail (PEM) format. If you are using Oracle Autonomous Database and your wallet zip file does not already include a PEM file, then you can convert the PKCS12 ``ewallet.p12`` file to PEM format using third party tools or the script below. For example, invoke the conversion script by passing the wallet password and the directory containing the PKCS file:: python create_pem.py --wallet-password 'xxxxx' /Users/scott/cloud_configs/MYDBDIR Once the PEM file has been created, you can use it by passing its directory location as the ``wallet_location`` parameter to :func:`oracledb.connect()` or :func:`oracledb.create_pool()`. These methods also accept a ``wallet_password`` parameter. See :ref:`twowaytls`. **Script to convert from PKCS12 to PEM** .. code-block:: python # create_pem.py import argparse import getpass import os from cryptography.hazmat.primitives.serialization \ import pkcs12, Encoding, PrivateFormat, BestAvailableEncryption, \ NoEncryption # parse command line parser = argparse.ArgumentParser(description="convert PKCS#12 to PEM") parser.add_argument("wallet_location", help="the directory in which the PKCS#12 encoded " "wallet file ewallet.p12 is found") parser.add_argument("--wallet-password", help="the password for the wallet which is used to " "decrypt the PKCS#12 encoded wallet file; if not " "specified, it will be requested securely") parser.add_argument("--no-encrypt", dest="encrypt", action="store_false", default=True, help="do not encrypt the converted PEM file with the " "wallet password") args = parser.parse_args() # validate arguments and acquire password if one was not specified pkcs12_file_name = os.path.join(args.wallet_location, "ewallet.p12") if not os.path.exists(pkcs12_file_name): msg = f"wallet location {args.wallet_location} does not contain " \ "ewallet.p12" raise Exception(msg) if args.wallet_password is None: args.wallet_password = getpass.getpass() pem_file_name = os.path.join(args.wallet_location, "ewallet.pem") pkcs12_data = open(pkcs12_file_name, "rb").read() result = pkcs12.load_key_and_certificates(pkcs12_data, args.wallet_password.encode()) private_key, certificate, additional_certificates = result if args.encrypt: encryptor = BestAvailableEncryption(args.wallet_password.encode()) else: encryptor = NoEncryption() with open(pem_file_name, "wb") as f: f.write(private_key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, encryptor)) f.write(certificate.public_bytes(Encoding.PEM)) for cert in additional_certificates: f.write(cert.public_bytes(Encoding.PEM)) print("PEM file", pem_file_name, "written.") .. _connsharding: Connecting to Sharded Databases =============================== `Oracle Sharding `__ can be used to horizontally partition data across independent databases. A database table can be split so each shard contains a table with the same columns but a different subset of rows. These tables are known as sharded tables. Sharding is configured in Oracle Database, see the `Oracle Sharding `__ manual. Sharding requires Oracle Database and Oracle Client libraries 12.2, or later. .. note:: Sharding is only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. The :meth:`oracledb.connect()` and :meth:`ConnectionPool.acquire()` functions accept ``shardingkey`` and ``supershardingkey`` parameters that are a sequence of values used to route the connection directly to a given shard. A sharding key is always required. A super sharding key is additionally required when using composite sharding, which is when data has been partitioned by a list or range (the super sharding key), and then further partitioned by a sharding key. When creating a connection pool, the :meth:`oracledb.create_pool()` attribute ``max_sessions_per_shard`` can be set. This is used to balance connections in the pool equally across shards. It requires Oracle Client libraries 18.3 or later. Shard key values may be of type string (mapping to VARCHAR2 shard keys), number (NUMBER), bytes (RAW), or date (DATE). Multiple types may be used in each array. Sharding keys of TIMESTAMP type are not supported. When connected to a shard, queries will only return data from that shard. For queries that need to access data from multiple shards, connections can be established to the coordinator shard catalog database. In this case, no shard key or super shard key is used. As an example of direct connection, if sharding had been configured on a single VARCHAR2 column like: .. code-block:: sql CREATE SHARDED TABLE customers ( cust_id NUMBER, cust_name VARCHAR2(30), class VARCHAR2(10) NOT NULL, signup_date DATE, cust_code RAW(20), CONSTRAINT cust_name_pk PRIMARY KEY(cust_name)) PARTITION BY CONSISTENT HASH (cust_name) PARTITIONS AUTO TABLESPACE SET ts1; then direct connection to a shard can be made by passing a single sharding key: .. code-block:: python connection = oracledb.connect(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb", shardingkey=["SCOTT"]) Numbers keys can be used in a similar way: .. code-block:: python connection = oracledb.connect(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb", shardingkey=[110]) When sharding by DATE, you can connect like: .. code-block:: python import datetime d = datetime.datetime(2014, 7, 3) connection = oracledb.connect(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb", shardingkey=[d]) When sharding by RAW, you can connect like: .. code-block:: python b = b'\x01\x04\x08'; connection = oracledb.connect(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb", shardingkey=[b]) Multiple keys can be specified, for example: .. code-block:: python key_list = [70, "SCOTT", "gold", b'\x00\x01\x02'] connection = oracledb.connect(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb", shardingkey=key_list) A super sharding key example is: .. code-block:: python connection = oracledb.connect(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb", supershardingkey=["goldclass"], shardingkey=["SCOTT"]) python-oracledb-1.2.1/doc/src/user_guide/cqn.rst000066400000000000000000000142561434177474600216400ustar00rootroot00000000000000.. _cqn: ************************************************ Working with Continuous Query Notification (CQN) ************************************************ `Continuous Query Notification (CQN) `__ allows applications to receive notifications when a table changes, such as when rows have been updated, regardless of the user or the application that made the change. This can be useful in many circumstances, such as near real-time monitoring, auditing applications, or for such purposes as mid-tier cache invalidation. A cache might hold some values that depend on data in a table. If the data in the table changes, the cached values must then be updated with the new information. .. note:: Continuous Query Notification (CQN) is only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. CQN notification behavior is widely configurable. Choices include specifying what types of SQL should trigger a notification, whether notifications should survive database loss, and control over unsubscription. You can also choose whether notification messages will include ROWIDs of affected rows. By default, object-level notification (previously known as Database Change Notification) occurs. With this mode, a Python notification method is invoked whenever a database transaction is committed that changes an object referenced by a registered query. However if the :meth:`subscription ` option ``qos`` is :data:`oracledb.SUBSCR_QOS_QUERY` then query-level notification occurs. In this mode, the database notifies the application whenever a committed transaction changes the result of a registered query. CQN is best used to track infrequent data changes. Requirements ============ Before using CQN, users must have appropriate permissions: .. code-block:: sql GRANT CHANGE NOTIFICATION TO ; To use CQN, connections must have ``events`` mode set to True, for example: .. code-block:: python connection = oracledb.connect(user=user, password=password, dsn="dbhost.example.com/orclpdb", events=True) The default CQN connection mode means the database must be able to connect back to the application using python-oracledb in order to receive notification events. Alternatively, when using Oracle Database and Oracle client libraries 19.4, or later, subscriptions can set the optional ``client_initiated`` parameter to True, see ``Connection.subscribe()`` below. The default CQN connection mode typically means that the machine running python-oracledb needs a fixed IP address. Note :meth:`Connection.subscribe()` does not verify that this reverse connection is possible. If there is any problem sending a notification, then the callback method will not be invoked. Configuration options can include an IP address and port on which python-oracledb will listen for notifications; otherwise, the database chooses values. Creating a Subscription ======================= Subscriptions allow Python to receive notifications for events that take place in the database that match the given parameters. For example, a basic CQN subscription might be created like: .. code-block:: python connection.subscribe(callback=my_callback) See :meth:`Connection.subscribe()` for details on all of the parameters. See :ref:`cqn-operation-codes` for the types of operations that are supported. See :ref:`subscr-qos` for the quality of service values that are supported. See :ref:`subscr-namespaces` and :ref:`subscr-protocols` for the namespaces and protocols that are supported. See :ref:`Subscription Objects ` for more details on the subscription object that is created. When using Oracle Database and Oracle client libraries 19.4, or later, the optional subscription parameter ``client_initiated`` can be set: .. code-block:: python connection.subscribe(callback=my_callback, client_initiated=True) This enables CQN "client initiated" connections which internally use the same approach as normal python-oracledb connections to the database, and do not require the database to be able to connect back to the application. Since client initiated connections do not need special network configuration they have ease-of-use and security advantages. Registering Queries =================== Once a subscription has been created, one or more queries must be registered by calling :meth:`Subscription.registerquery()`. Registering a query behaves similarly to :meth:`Cursor.execute()`, but only queries are permitted and the ``args`` parameter must be a sequence or dictionary. An example script to receive query notifications when the 'REGIONS' table data changes is: .. code-block:: python def cqn_callback(message): print("Notification:") for query in message.queries: for tab in query.tables: print("Table:", tab.name) print("Operation:", tab.operation) for row in tab.rows: if row.operation & oracledb.OPCODE_INSERT: print("INSERT of rowid:", row.rowid) if row.operation & oracledb.OPCODE_DELETE: print("DELETE of rowid:", row.rowid) subscr = connection.subscribe(callback=cqn_callback, operations=oracledb.OPCODE_INSERT | oracledb.OPCODE_DELETE, qos=oracledb.SUBSCR_QOS_QUERY | oracledb.SUBSCR_QOS_ROWIDS) subscr.registerquery("select * from regions") input("Hit enter to stop CQN demo\n") Running the above script shows the initial output as:: Hit enter to stop CQN demo Use SQL*Plus or another tool to commit a change to the table: .. code-block:: sql insert into regions values(120, 'L'); commit; When the commit is executed, a notification will be received by the callback which should print something like the following:: Hit enter to stop CQN demo Notification: Table: HR.REGIONS Operation: 2 INSERT of rowid: AAA7EsAAHAAAFS/AAA See `GitHub Samples `__ for a runnable CQN example. python-oracledb-1.2.1/doc/src/user_guide/exception_handling.rst000066400000000000000000000103611434177474600247120ustar00rootroot00000000000000.. _exception: ******************* Catching Exceptions ******************* All exceptions raised by python-oracledb are inherited from :attr:`oracledb.Error`. See :ref:`exceptions` and :ref:`exchandling` for information about attributes. See :ref:`errorhandling` for differences between the python-oracledb Thin and :ref:`Thick ` modes. Applications can catch exceptions as needed. For example, when trying to add a customer that already exists in the database, the following could be used to catch the exception: .. code-block:: python try: cursor.execute("insert into customer values (101, 'Customer A')") except oracledb.IntegrityError: print("Customer ID already exists") else: print("Customer added") If information about the exception needs to be processed instead, the following code can be used: .. code-block:: python try: cursor.execute("insert into customer values (101, 'Customer A')") except oracledb.IntegrityError as e: error_obj, = e.args print("Customer ID already exists") print("Error Code:", error_obj.code) print("Error Full Code:", error_obj.full_code) print("Error Message:", error_obj.message) else: print("Customer added") This will print output like:: Customer ID already exists Error Code: 1 Error Full Code: ORA-00001 Error Message: ORA-00001: unique constraint (CJ.PK) violated .. _errorhandling: Error Handling in Thin and Thick Modes ====================================== The Thin and Thick modes of python-oracledb return some errors differently. The python-oracledb Thin mode code generates error messages with the prefix "DPY". In python-oracledb :ref:`Thick ` mode: - The Oracle Call Interface (OCI) libraries generate error messages with the prefix "ORA". - The `ODPI-C `__ code layer generates error messages with the prefix "DPI". - The python-oracledb Thick mode code generates error messages with the prefix "DPY". Errors generated by the Oracle Database itself commonly have the error prefix "ORA". Some differences between python-oracledb Thin and Thick mode errors are shown in the examples below: * Binding: When binding is incorrect, the python-oracledb Thick mode may generate an Oracle Client library error such as:: ORA-01008: not all variables bound In contrast, the python-oracledb Thin mode might generate:: DPY-4010: a bind variable replacement value for placeholder ":1" was not provided * Connection messages: The python-oracledb Thin mode connection and networking is handled by Python itself. Some errors portable accross operating systems and Python versions have DPY-prefixed errors displayed by python-oracledb. Other messages are returned directly from Python and may vary accordingly. The traditional Oracle connection errors with prefix "ORA" are not shown. For example, the scenarios detailed below show how the connection and network error messages might differ between the python-oracledb Thin and Thick modes. * Scenario 1: The given host does not have a database listener running. python-oracledb Thin mode Error:: DPY-6005: cannot connect to database. Connection failed with "[Errno 61] Connection refused" python-oracledb Thick mode Error:: ORA-12541: TNS:no listener * Scenario 2: The requested connection alias was not found in the tnsnames.ora file. python-oracledb Thin mode Error:: DPY-4000: cannot connect to database. Unable to find "{name}" in {file_name} python-oracledb Thick mode Error:: ORA-12154: TNS:could not resolve the connect identifier specified * Scenario 3: The Oracle Database listener does not know of the requested service name. python-oracledb Thin mode Error:: DPY-6001: cannot connect to database. Service "{service_name}" is not registered with the listener at host "{host}" port {port}. (Similar to ORA-12514) python-oracledb Thick mode Error:: ORA-12514: TNS:listener does not currently know of service requested in connect descriptor * Connection Pooling: The python-oracledb Thin mode pool is not based on the Oracle Call Interface (OCI) Session Pool and has its own DPY messages. python-oracledb-1.2.1/doc/src/user_guide/globalization.rst000066400000000000000000000173471434177474600237210ustar00rootroot00000000000000.. _globalization: ******************************** Character Sets and Globalization ******************************** Character Sets ============== Database Character Set ---------------------- Data fetched from and sent to Oracle Database will be mapped between the `database character set `__ and the "Oracle client" character set of the Oracle Client libraries used by python-oracledb. If data cannot be correctly mapped between client and server character sets, then it may be corrupted or queries may fail with :ref:`"codec can't decode byte" `. All database character sets are supported by the python-oracledb. .. _findingcharset: To find the database character set, execute the query: .. code-block:: sql SELECT value AS db_charset FROM nls_database_parameters WHERE parameter = 'NLS_CHARACTERSET'; Database National Character Set ------------------------------- For the secondary `national character set `__ used for NCHAR, NVARCHAR2, and NCLOB data types: - AL16UTF16 is supported by both the python-oracledb Thin and Thick modes - UTF8 is not supported by the python-oracledb Thin mode To find the database's national character set, execute the query: .. code-block:: sql SELECT value AS db_ncharset FROM nls_database_parameters WHERE parameter = 'NLS_NCHAR_CHARACTERSET'; Setting the Client Character Set -------------------------------- In python-oracledb, the encoding used for all character data is "UTF-8". The ``encoding`` and ``nencoding`` parameters of the :meth:`oracledb.connect` and :meth:`oracledb.create_pool` methods are deprecated and ignored. Setting the Client Locale ========================= Thick Mode Oracle Database National Language Support (NLS) ---------------------------------------------------------- The python-oracledb Thick mode uses Oracle Database's National Language Support (NLS) functionality to assist in globalizing applications, for example to convert numbers and dates to strings in the locale specific format. You can use the ``NLS_LANG`` environment variable to set the language and territory used by the Oracle Client libraries. For example, on Linux you could set:: export NLS_LANG=JAPANESE_JAPAN The language ("JAPANESE" in this example) specifies conventions such as the language used for Oracle Database messages, sorting, day names, and month names. The territory ("JAPAN") specifies conventions such as the default date, monetary, and numeric formats. If the language is not specified, then the value defaults to AMERICAN. If the territory is not specified, then the value is derived from the language value. See `Choosing a Locale with the NLS_LANG Environment Variable `__ If the ``NLS_LANG`` environment variable is set in the application with ``os.environ['NLS_LANG']``, it must be set before any connection pool is created, or before any standalone connections are created. Any client character set value in the ``NLS_LANG`` variable, for example ``JAPANESE_JAPAN.JA16SJIS``, is ignored by python-oracledb. See `Setting the Client Character Set`_. Other Oracle globalization variables, such as ``NLS_DATE_FORMAT`` can also be set to change the behavior of python-oracledb Thick, see `Setting NLS Parameters `__. For more information, see the `Database Globalization Support Guide `__. .. _thindatenumber: Thin Mode Locale-aware Number and Date Conversions -------------------------------------------------- .. note:: All NLS environment variables are ignored by the python-oracledb Thin mode. Also the ``ORA_SDTZ`` and ``ORA_TZFILE`` variables are ignored. In the python-oracledb Thin mode, output type handlers need to be used to perform similar conversions. The examples below show a simple conversion and also how the Python locale module can be used. Type handlers like those below can also be used in python-oracledb Thick mode. To convert numbers: .. code-block:: python import locale import oracledb # use this if the environment variable LANG is already set #locale.setlocale(locale.LC_ALL, '') # use this for programmatic setting of locale locale.setlocale(locale.LC_ALL, 'de_DE.UTF-8') # simple naive conversion def type_handler1(cursor, name, default_type, size, precision, scale): if default_type == oracledb.DB_TYPE_NUMBER: return cursor.var(oracledb.DB_TYPE_VARCHAR, arraysize=cursor.arraysize, outconverter=lambda v: v.replace('.', ',')) # locale conversion def type_handler2(cursor, name, default_type, size, precision, scale): if default_type == oracledb.DB_TYPE_NUMBER: return cursor.var(default_type, arraysize=cursor.arraysize, outconverter=lambda v: locale.format_string("%g", v)) connection = oracledb.connect(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb") with connection.cursor() as cursor: print("no type handler...") cursor.execute("select 2.5 from dual") for row in cursor: print(row) # gives 2.5 print() print("with naive type handler...") connection.outputtypehandler = type_handler1 cursor.execute("select 2.5 from dual") for row in cursor: print(row) # gives '2,5' print() print("with locale type handler...") connection.outputtypehandler = type_handler2 cursor.execute("select 2.5 from dual") for row in cursor: print(row) # gives '2,5' print() To convert dates: .. code-block:: python import locale import oracledb # use this if the environment variable LANG is already set #locale.setlocale(locale.LC_ALL, '') # use this for programmatic setting of locale locale.setlocale(locale.LC_ALL, 'de_DE.UTF-8') locale_date_format = locale.nl_langinfo(locale.D_T_FMT) # simple naive conversion def type_handler3(cursor, name, default_type, size, precision, scale): if default_type == oracledb.DB_TYPE_DATE: return cursor.var(default_type, arraysize=cursor.arraysize, outconverter=lambda v: v.strftime("%Y-%m-%d %H:%M:%S")) # locale conversion def type_handler4(cursor, name, default_type, size, precision, scale): if default_type == oracledb.DB_TYPE_DATE: return cursor.var(default_type, arraysize=cursor.arraysize, outconverter=lambda v: v.strftime(locale_date_format)) connection = oracledb.connect(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb") with connection.cursor() as cursor: print("no type handler...") cursor.execute("select sysdate from dual") for row in cursor: print(row) # gives datetime.datetime(2021, 12, 15, 19, 49, 37) print() print("with naive type handler...") connection.outputtypehandler = type_handler3 cursor.execute("select sysdate from dual") for row in cursor: print(row) # gives '2021-12-15 19:49:37' print() print("with locale type handler...") connection.outputtypehandler = type_handler4 cursor.execute("select sysdate from dual") for row in cursor: print(row) # gives 'Mi 15 Dez 19:57:56 2021' print() python-oracledb-1.2.1/doc/src/user_guide/ha.rst000066400000000000000000000241741434177474600214470ustar00rootroot00000000000000.. _highavailability: ********************************************* Using High Availability with python-oracledb ********************************************* Applications can use many features for high availability (HA) during planned and unplanned outages in order to: * Reduce application downtime * Eliminate compromises between high availability and performance * Increase operational productivity .. _harecommend: General HA Recommendations -------------------------- General recommendations for creating highly available python-oracledb programs are: * Tune operating system and Oracle Network parameters to avoid long TCP timeouts, to prevent firewalls killing connections, and to avoid connection storms. * Implement application error handling and recovery. * Use the most recent version of the Oracle Client libraries. New versions have improvements to features such as dead database server detection, and make it easier to set connection options. * Use the most recent version of Oracle Database. New database versions introduce, and enhance, features such as Application Continuity (AC) and Transparent Application Continuity (TAC). * Use Oracle Database technologies such as `RAC `__ or standby databases. * Configure database services to emit :ref:`FAN ` events. * Use a :ref:`connection pool ` because pools can handle database events and take proactive and corrective action for draining, run time load balancing, and fail over. Set the minimum and maximum pool sizes to the same values to avoid connection storms. Remove resource manager or user profiles that prematurely close sessions. * Test all scenarios thoroughly. .. _hanetwork: Network Configuration --------------------- The operating system TCP and :ref:`Oracle Net configuration ` should be configured for performance and availability. Options such as `SQLNET.OUTBOUND_CONNECT_TIMEOUT `__, `SQLNET.RECV_TIMEOUT `__ and `SQLNET.SEND_TIMEOUT `__ can be explored. `Oracle Net Services `__ options may also be useful for high availability and performance tuning. For example, the database's ``listener.ora`` file can have `RATE_LIMIT `__ and `QUEUESIZE `__ parameters that can help handle connection storms. With Oracle Client 19c, `EXPIRE_TIME `__ can be used in :ref:`tnsnames.ora ` connect descriptors to prevent firewalls from terminating idle connections and to adjust keepalive timeouts. The general recommendation for ``EXPIRE_TIME`` is to use a value that is slightly less than half of the termination period. In older versions of Oracle Client, a ``tnsnames.ora`` connect descriptor option `ENABLE=BROKEN `_ can be used instead of ``EXPIRE_TIME``. These settings can also aid detection of a terminated remote database server. When python-oracledb uses :ref:`Oracle Client libraries 19c `, then the :ref:`Easy Connect Plus syntax ` syntax enables some options to be used without needing a ``sqlnet.ora`` file. For example, if your firewall times out every 4 minutes, and you cannot alter the firewall settings, then you may decide to use ``EXPIRE_TIME`` in your connect string to send a probe every 2 minutes to the database to keep connections 'alive':: connection = oracledb.connect("hr", userpwd, "dbhost.example.com/orclpdb?expire_time=2") .. _fan: Fast Application Notification (FAN) ----------------------------------- Users of `Oracle Database FAN `__ must connect to a FAN-enabled database service. The application should have ``events`` set to `True` when connecting. This value can also be changed via :ref:`Oracle Client Configuration `. .. note:: FAN is only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. FAN support is useful for planned and unplanned outages. It provides immediate notification to python-oracledb following outages related to the database, computers, and networks. Without FAN, python-oracledb can hang until a TCP timeout occurs and an error is returned, which might be several minutes. FAN allows python-oracledb to provide high availability features without the application being aware of an outage. Unused, idle connections in a :ref:`connection pool ` will be automatically cleaned up. A future :meth:`ConnectionPool.acquire()` call will establish a fresh connection to a surviving database instance without the application being aware of any service disruption. To handle errors that affect active connections, you can add application logic to re-connect (this will connect to a surviving database instance) and replay application logic without having to return an error to the application user. FAN benefits users of Oracle Database's clustering technology `Oracle RAC `__ because connections to surviving database instances can be immediately made. Users of Oracle's Data Guard with a broker will get FAN events generated when the standby database goes online. Standalone databases will send FAN events when the database restarts. For more information on FAN, see the `white paper on Fast Application Notification `__. .. _appcont: Application Continuity (AC) --------------------------- Oracle Application Continuity (AC) and Transparent Application Continuity (TAC) are Oracle Database technologies that record application interaction with the database and, in the event of a database instance outage, attempt to replay the interaction on a surviving database instance. If successful, users will be unaware of any database issue. AC and TAC are best suited for OLTP applications. .. note:: Oracle AC and TAC functionality is only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. When AC or TAC are configured on the database service, they are transparently available to python-oracledb applications. You must thoroughly test your application because not all lower level calls in the python-oracledb implementation can be replayed. See `OCI and Application Continuity `__ for more information. .. _tg: Transaction Guard ----------------- Python-oracledb supports `Transaction Guard `__ which enables Python application to verify the success or failure of the last transaction in the event of an unplanned outage. This feature is available when both client and database are 12.1 or higher. .. note:: The Transaction Guard feature is only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. Using Transaction Guard helps to: * Preserve the commit outcome * Ensure a known outcome for every transaction See `Oracle Database Development Guide `__ for more information about using Transaction Guard. When an error occurs during commit, the Python application can acquire the logical transaction id (``ltxid``) from the connection and then call a procedure to determine the outcome of the commit for this logical transaction id. Follow the steps below to use the Transaction Guard feature in Python: 1. Grant execute privileges to the database users who will be checking the outcome of the commit. Log in as SYSDBA and run the following command: .. code-block:: sql GRANT EXECUTE ON DBMS_APP_CONT TO ; 2. Create a new service by executing the following PL/SQL block as SYSDBA. Replace the ````, ```` and ```` values with suitable values. It is important that the ``COMMIT_OUTCOME`` parameter be set to true for Transaction Guard to function properly. .. code-block:: sql DECLARE t_Params dbms_service.svc_parameter_array; BEGIN t_Params('COMMIT_OUTCOME') := 'true'; t_Params('RETENTION_TIMEOUT') := ; DBMS_SERVICE.CREATE_SERVICE('', '', t_Params); END; / 3. Start the service by executing the following PL/SQL block as SYSDBA: .. code-block:: sql BEGIN DBMS_SERVICE.start_service(''); END; / Ensure that the service is running by examining the output of the following query: .. code-block:: sql SELECT name, network_name FROM V$ACTIVE_SERVICES ORDER BY 1; **Python Application code requirements to use Transaction Guard** In the Python application code: * Use the connection attribute :attr:`~Connection.ltxid` to determine the logical transaction id. * Call the ``DBMS_APP_CONT.GET_LTXID_OUTCOME`` PL/SQL procedure with the logical transaction id acquired from the connection attribute. This returns a boolean value indicating if the last transaction was committed and whether the last call was completed successfully or not. See the `Transaction Guard Sample `__ for further details. python-oracledb-1.2.1/doc/src/user_guide/initialization.rst000066400000000000000000000571421434177474600241070ustar00rootroot00000000000000.. _initialization: **************************** Initializing python-oracledb **************************** By default, python-oracledb runs in a 'Thin' mode which connects directly to Oracle Database. This mode does not need Oracle Client libraries. However, some :ref:`additional functionality ` is available when python-oracledb uses them. Python-oracledb is said to be in 'Thick' mode when Oracle Client libraries are used. Both modes have comprehensive functionality supporting the Python Database API v2.0 Specification. .. _enablingthick: Enabling python-oracledb Thick mode =================================== Changing from the default Thin mode to the Thick mode requires the addition of a call to :func:`oracledb.init_oracle_client()` as shown in sections below. Other small code updates may also be required, see :ref:`driverdiff` . If you are upgrading a cx_Oracle application to python-oracledb, then refer to :ref:`upgrading83` for changes that may be needed. .. note:: To use python-oracledb in Thick mode you *must* call :func:`~oracledb.init_oracle_client()` in the application. It must be called before any :ref:`standalone connection ` or :ref:`connection pool ` is created. If a connection or pool is first created, then the Thick mode cannot be enabled. All connections in an application use the same mode. Once the Thick mode is enabled, you cannot go back to Thin mode. Enabling the python-oracledb Thick mode loads Oracle Client libraries which handle communication to your database. The Oracle Client libraries need to be installed separately. See :ref:`installation`. .. figure:: /images/python-oracledb-thick-arch.png :alt: architecture of the python-oracledb driver in Thick mode Architecture of the python-oracledb driver in Thick mode You can validate the python-oracledb mode by querying the ``CLIENT_DRIVER`` column of ``V$SESSION_CONNECT_INFO`` and verifying the value of the column begins with ``python-oracledb thk``. See :ref:`vsessconinfo`. .. _libinit: Setting the Oracle Client Library Directory ------------------------------------------- When :meth:`~oracledb.init_oracle_client()` is called, python-oracledb dynamically loads Oracle Client libraries using a search heuristic. Only the first set of libraries found are loaded. The libraries can be: - in an installation of Oracle Instant Client - or in a full Oracle Client installation - or in an Oracle Database installation (if Python is running on the same machine as the database). The versions of Oracle Client and Oracle Database do not have to be the same. For certified configurations see Oracle Support's `Doc ID 207303.1 `__. See :ref:`installation` for information about installing Oracle Client libraries. .. _wininit: Setting the Oracle Client Directory on Windows ++++++++++++++++++++++++++++++++++++++++++++++ On Windows, python-oracledb Thick mode can be enabled as follows: - By passing the ``lib_dir`` parameter in a call to :meth:`~oracledb.init_oracle_client()`, for example: .. code-block:: python import oracledb oracledb.init_oracle_client(lib_dir=r"C:\instantclient_19_14") This directory should contain the libraries from an unzipped Instant Client 'Basic' or 'Basic Light' package. If you pass the library directory from a full client or database installation, such as Oracle Database "XE" Express Edition, then you will need to have previously set your environment to use that same software installation. Otherwise, files such as message files will not be located and you may have library version clashes. On Windows, when the path contains backslashes, use a 'raw' string like ``r"C:\instantclient_19_14"``. If the Oracle Client libraries cannot be loaded from ``lib_dir``, then an exception is raised. - By calling :meth:`~oracledb.init_oracle_client()` without passing a ``lib_dir`` parameter: .. code-block:: python import oracledb oracledb.init_oracle_client() In this case, Oracle Client libraries are first looked for in the directory where the python-oracledb binary module is installed. This directory should contain the libraries from an unzipped Instant Client 'Basic' or 'Basic Light' package. If the libraries are not found there, the search looks at the directories on the system library search path, for example, the ``PATH`` environment variable. If the Oracle Client libraries cannot be loaded, then an exception is raised. .. _macinit: Setting the Oracle Client Directory on macOS ++++++++++++++++++++++++++++++++++++++++++++ On macOS, python-oracledb Thick mode can be enabled as follows: - By passing the ``lib_dir`` parameter in a call to :meth:`~oracledb.init_oracle_client()`, for example: .. code-block:: python import oracledb oracledb.init_oracle_client(lib_dir="/Users/your_username/Downloads/instantclient_19_8") This directory should contain the libraries from an unzipped Instant Client 'Basic' or 'Basic Light' package. If the Oracle Client libraries cannot be loaded from ``lib_dir``, then an exception is raised. - By calling :meth:`~oracledb.init_oracle_client()` without passing a ``lib_dir`` parameter: .. code-block:: python import oracledb oracledb.init_oracle_client() In this case, the Oracle Client libraries are first looked for in the directory where the python-oracledb Thick mode binary module is installed. This directory should contain the libraries from an unzipped Instant Client 'Basic' or 'Basic Light' package, or a symbolic link to the main Oracle Client library if Instant Client is in a different directory. You can find the directory containing the Thick mode binary module by calling the python CLI without specifying a Python script, executing ``import oracledb``, and then typing ``oracledb`` at the prompt. For example if ``/Users/yourname/Library/3.9.6/lib/python3.9/site-packages/oracledb-1.0.0-py3.9-macosx-11.5-x86_64.egg/oracledb`` contains ``thick_impl.cpython-39-darwin.so``, then you could run ``ln -s ~/Downloads/instantclient_19_8/libclntsh.dylib ~/Library/3.9.6/lib/python3.9/site-packages/oracledb-1.0.0-py3.9-macosx-11.5-x86_64.egg/oracledb/``. If python-oracledb does not find the Oracle Client library in that directory, the directories on the system library search path may be used, for example, ``~/lib/`` and ``/usr/local/lib``, or in ``$DYLD_LIBRARY_PATH``. These paths will vary with macOS version and Python version. Any value in ``DYLD_LIBRARY_PATH`` will not propagate to a sub-shell. If the Oracle Client libraries cannot be loaded, then an exception is raised. .. _linuxinit: Setting the Oracle Client Directory on Linux and Related Platforms ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ On Linux and related platforms, python-oracledb Thick mode can be enabled as follows: - By calling :meth:`~oracledb.init_oracle_client()` without passing a ``lib_dir`` parameter: .. code-block:: python import oracledb oracledb.init_oracle_client() Oracle Client libraries are looked for in the operating system library search path, such as configured with ``ldconfig`` or set in the environment variable ``LD_LIBRARY_PATH``. On some UNIX platforms an OS specific equivalent, such as ``LIBPATH`` or ``SHLIB_PATH`` is used instead of ``LD_LIBRARY_PATH``. If libraries are not found in the system library search path, then ``$ORACLE_HOME/lib`` will be used. Note that the environment variable ``ORACLE_HOME`` should only ever be set when you have a full database installation or full client installation (such as installed with the Oracle GUI installer). It should not be set if you are using Oracle Instant Client. The ``ORACLE_HOME`` variable, and other necessary variables, should be set before starting Python. See :ref:`envset`. If the Oracle Client libraries cannot be loaded, then an exception is raised. Ensure that the Python process has directory and file access permissions for the Oracle Client libraries. On Linux ensure a ``libclntsh.so`` file exists. On macOS ensure a ``libclntsh.dylib`` file exists. python-oracledb Thick will not directly load ``libclntsh.*.XX.1`` files in ``lib_dir`` or from the directory where the python-oracledb binary module is available. Note that other libraries used by ``libclntsh*`` are also required. .. _usinginitoracleclient: Calling oracledb.init_oracle_client() to Set the Oracle Client Directory ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Oracle Client Libraries are loaded when :meth:`oracledb.init_oracle_client()` is called. In some environments, applications can use the ``lib_dir`` parameter to specify the directory containing the Oracle Client libraries. Otherwise, the system library search path should contain the relevant library directory before Python is invoked. For example, if the Oracle Instant Client Libraries are in ``C:\oracle\instantclient_19_9`` on Windows or ``$HOME/Downloads/instantclient_19_8`` on macOS (Intel x86), then you can use: .. code-block:: python import oracledb import os import platform d = None # default suitable for Linux if platform.system() == "Darwin" and platform.machine() == "x86_64": d = os.environ.get("HOME")+"/Downloads/instantclient_19_8") elif platform.system() == "Windows": d = r"C:\oracle\instantclient_19_14" oracledb.init_oracle_client(lib_dir=d) Note the use of a 'raw' string ``r"..."`` on Windows so that backslashes are treated as directory separators. **Note that if you set** ``lib_dir`` **on Linux and related platforms, you must still have configured the system library search path to include that directory before starting Python**. On any operating system, if you set ``lib_dir`` to the library directory of a full database or full client installation, you will need to have previously set the Oracle environment, for example by setting the ``ORACLE_HOME`` environment variable. Otherwise, you will get errors like ``ORA-1804``. You should set this along with other Oracle environment variables before starting Python as shown in :ref:`envset`. **Tracing Oracle Client Libraries Loading** To trace the loading of Oracle Client libraries, the environment variable ``DPI_DEBUG_LEVEL`` can be set to 64 before starting Python. For example, on Linux, you might use:: $ export DPI_DEBUG_LEVEL=64 $ python myapp.py 2> log.txt .. _optnetfiles: Optional Oracle Net Configuration Files ======================================= Optional Oracle Net configuration files may be read by python-oracledb. These files affect connections and applications. The common files are: * ``tnsnames.ora``: A configuration file that defines databases addresses for establishing connections. See :ref:`Net Service Name for Connection Strings `. * ``sqlnet.ora``: A profile configuration file that may contain information on features such as connection failover, network encryption, logging, and tracing. The files should be in a directory accessible to Python, not on the database server host. See `Oracle Net Services Reference `__ for more information. .. note:: The ``sqlnet.ora`` file is only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. In the python-oracledb Thin mode, many of the equivalent settings can be defined as connection time parameters, for example by using the :ref:`ConnectParams Class `. **python-oracledb Thin mode** In python-oracledb Thin mode applications, you specify the directory that contains the ``tnsnames.ora`` file by: - setting the `TNS_ADMIN `__ environment variable to the directory containing the file - setting :attr:`defaults.config_dir` to the directory containing the file - setting the ``config_dir`` parameter to the directory containing the file when :func:`connecting ` or creating a :func:`connection pool `. For example: .. code-block:: python import oracledb oracledb.defaults.config_dir = "/opt/oracle/config" .. note:: In Thin mode, you must explicitly set the directory because traditional "default" locations such as the Instant Client ``network/admin/`` subdirectory, or ``$ORACLE_HOME/network/admin/``, or ``$ORACLE_BASE/homes/XYZ/network/admin/`` (in a read-only Oracle Database home) are not automatically looked in. **python-oracledb Thick mode** In python-oracledb Thick mode, the files are loaded from default locations (shown below), from the directory also specified in the ``$TNS_ADMIN`` environment variable, or from the directory specified as a parameter in the :meth:`oracledb.init_oracle_client()` call. For example, if the file ``/opt/oracle/config/tnsnames.ora`` should be used, you can call: .. code-block:: python import oracledb import sys try: oracledb.init_oracle_client(config_dir="/opt/oracle/config") except Exception as err: print("Whoops!") print(err) sys.exit(1) .. note:: In python-oracledb Thick mode, once an application has created its first connection, trying to change the configuration directory will not have any effect. If :meth:`~oracledb.init_oracle_client()` is called to enable Thick mode but ``config_dir`` is not specified, then default directories are searched for the configuration files. They include: - ``$TNS_ADMIN`` - ``/opt/oracle/instantclient_19_14/network/admin`` if Instant Client is in ``/opt/oracle/instantclient_19_14``. - ``/usr/lib/oracle/19.14/client64/lib/network/admin`` if Oracle 19.6 Instant Client RPMs are used on Linux. - ``$ORACLE_HOME/network/admin`` if python-oracledb Thick is using libraries from a database installation. Note that the :ref:`easyconnect` can set many common configuration options without needing ``tnsnames.ora`` or ``sqlnet.ora`` files. The section :ref:`Network Configuration ` has additional information about Oracle Net configuration. .. _optclientfiles: Optional Oracle Client Configuration File ========================================= When python-oracledb uses Oracle Client libraries version 12.1 or later, an optional client parameter file called ``oraaccess.xml`` can be used to configure some behaviors of those libraries, such as statement caching and prefetching. This can be useful if the application cannot be altered. The file is read from the same directory as the `Optional Oracle Net Configuration Files`_. .. note:: The ``oraaccess.xml`` file is only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. A sample ``oraaccess.xml`` file that sets the Oracle client 'prefetch' value to 1000 rows. This value affects every SQL query in the application:: 1000 Prefetching is the number of additional rows that the underlying Oracle Client library fetches whenever python-oracledb Thick requests query data from the database. Prefetching is a tuning option to maximize data transfer efficiency and minimize :ref:`round-trips ` to the database. The prefetch size does not affect when or how many rows are returned by the Thick mode to the application. The cache management is transparently handled by the Oracle Client libraries. Note that standard Thick mode fetch tuning is done using :attr:`Cursor.arraysize`, but changing the prefetch value can be useful in some cases such as when modifying the application is not feasible. The `oraaccess.xml` file has other uses including: - Changing the value of Fast Application Notification :ref:`FAN ` events which affects notifications and Runtime Load Balancing (RLB). - Configuring `Client Result Caching `__ parameters - Turning on `Client Statement Cache Auto-tuning `__ Refer to the documentation on `oraaccess.xml `__ for more details. .. _envset: Oracle Environment Variables for python-oracledb Thick Mode =========================================================== Some common environment variables that influence python-oracledb are shown below. The variables that may be needed depend on how Python is installed, how you connect to the database, and what optional settings are desired. It is recommended to set Oracle variables in the environment before calling Python. However, they may also be set in the application with ``os.putenv()`` before the first connection is established. System environment variables like ``LD_LIBRARY_PATH`` must be set before Python starts. .. note:: These variables, with the exception of ``TNS_ADMIN``, are only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. .. list-table-with-summary:: Common Oracle environment variables :header-rows: 1 :class: wy-table-responsive :widths: 1 2 :summary: The first column displays the Oracle Environment Variable. The second column, Purpose, describes what the environment variableis used for. :align: left * - Oracle Environment Variables - Purpose * - LD_LIBRARY_PATH - The library search path for platforms like Linux should include the Oracle libraries, for example ``$ORACLE_HOME/lib`` or ``/opt/instantclient_19_3``. This variable is not needed if the libraries are located by an alternative method, such as with ``ldconfig``. On other UNIX platforms, you may need to set an OS specific equivalent such as ``LIBPATH`` or ``SHLIB_PATH``. * - PATH - The library search path for Windows should include the location where ``OCI.DLL`` is found. Not needed if you set ``lib_dir`` in a call to :meth:`oracledb.init_oracle_client()` * - TNS_ADMIN - The directory of optional Oracle Client configuration files such as ``tnsnames.ora`` and ``sqlnet.ora``. Not needed if the configuration files are in a default location or if ``config_dir`` was not used in :meth:`oracledb.init_oracle_client()`. See :ref:`optnetfiles`. * - ORA_SDTZ - The default session time zone. * - ORA_TZFILE - The name of the Oracle time zone file to use. See below. * - ORACLE_HOME - The directory containing the Oracle Database software. The directory and various configuration files must be readable by the Python process. This variable should not be set if you are using Oracle Instant Client. * - NLS_LANG - Determines the 'national language support' globalization options for python-oracledb. Note that from cx_Oracle 8, the character set component is ignored and only the language and territory components of ``NLS_LANG`` are used. The character set can instead be specified during connection or connection pool creation. See :ref:`globalization`. * - NLS_DATE_FORMAT, NLS_TIMESTAMP_FORMAT - Often set in Python applications to force a consistent date format independent of the locale. The variables are ignored if the environment variable ``NLS_LANG`` is not set. Oracle Instant Client includes a small and big time zone file, for example ``timezone_32.dat`` and ``timezlrg_32.dat``. The versions can be shown by running the utility ``genezi -v`` located in the Instant Client directory. The small file contains only the most commonly used time zones. By default, the larger ``timezlrg_n.dat`` file is used. If you want to use the smaller ``timezone_n.dat`` file, then set the ``ORA_TZFILE`` environment variable to the name of the file without any directory prefix. For example ``export ORA_TZFILE=timezone_32.dat``. With Oracle Instant Client 12.2 or later, you can also use an external time zone file. Create a subdirectory ``oracore/zoneinfo`` under the Instant Client directory, and move the file into it. Then set ``ORA_TZFILE`` to the file name, without any directory prefix. The ``genezi -v`` utility will show the time zone file in use. If python-oracledb Thick mode is using Oracle Client libraries from an Oracle Database or full Oracle Client software installation (such as installed with Oracle's GUI installer), and you want to use a non-default time zone file, then set ``ORA_TZFILE`` to the file name with a directory prefix. For example: ``export ORA_TZFILE=/opt/oracle/myconfig/timezone_31.dat``. The Oracle Database documentation contains more information about time zone files, see `Choosing a Time Zone File `__. .. _otherinit: Other python-oracledb Thick Mode Initialization =============================================== The :meth:`oracledb.init_oracle_client()` function allows ``driver_name`` and ``error_url`` parameters to be set. These are useful for applications whose end-users are not aware that python-oracledb is being used. An example of setting the parameters is: .. code-block:: python import oracledb import sys try: oracledb.init_oracle_client(driver_name="My Great App : 3.1.4", error_url="https://example.com/MyInstallInstructions.html") except Exception as err: print("Whoops!") print(err) sys.exit(1) The convention for ``driver_name`` is to separate the product name from the product version by a colon and single blank characters. The value will be shown in Oracle Database views like ``V$SESSION_CONNECT_INFO``. If this parameter is not specified, then a value like "python-oracledb thk : 1.0.0" is shown, see :ref:`vsessconinfo`. The ``error_url`` string will be shown in the exception raised if ``init_oracle_client()`` cannot load the Oracle Client libraries. This allows applications that use python-oracledb in Thick mode to refer users to application-specific installation instructions. If this value is not specified, then the :ref:`installation` URL is used. Changing from python-oracledb Thick Mode to python-oracledb Thin Mode ===================================================================== Changing an application that currently uses Thin mode requires the removal of calls to :func:`oracledb.init_oracle_client()` and an application restart. Other small changes may be required. All connections in a python-oracledb application must use the same mode. If you have been using python-oracledb in Thick mode, you can use Thin mode by: 1. Reviewing :ref:`featuresummary` and :ref:`driverdiff` for code changes that may be needed. Also read :ref:`toggling`. 2. Removing all calls to :func:`oracledb.init_oracle_client` from the application. 3. Make other necessary changes identified in step 1. 4. When you are satisfied, you can optionally remove Oracle Client libraries. For example, delete your Oracle Instant Client directory. You can validate the python-oracledb mode by querying the ``CLIENT_DRIVER`` column of ``V$SESSION_CONNECT_INFO`` and verifying if the value of the column begins with ``python-oracledb thn``. See :ref:`vsessconinfo`. python-oracledb-1.2.1/doc/src/user_guide/installation.rst000066400000000000000000001105351434177474600235550ustar00rootroot00000000000000.. _installation: *************************** Installing python-oracledb *************************** The python-oracledb driver allows Python 3 applications to connect to Oracle Database. Python-oracledb is the new name for the Python `cx_Oracle driver `__. If you are upgrading from cx_Oracle, see :ref:`upgrading83`. .. figure:: /images/python-oracledb-thin-arch.png :alt: architecture of the python-oracledb driver Architecture of the python-oracledb driver By default, python-oracledb runs in a 'Thin' mode which connects directly to Oracle Database. This mode does not need Oracle Client libraries. However, some :ref:`additional functionality ` is available when python-oracledb uses them. Python-oracledb is said to be in 'Thick' mode when Oracle Client libraries are used. See :ref:`enablingthick`. Both modes have comprehensive functionality supporting the Python Database API v2.0 Specification. .. _quickstart: Quick Start python-oracledb Installation ======================================== This section contains the steps that you need to perform to install python-oracledb quickly. 1. Install `Python 3 `__, if it is not already available. The version of Python to be used depends on the operating system (OS): - On Windows, use Python 3.7 to 3.11 - On macOS, use Python 3.7 to 3.11 - On Linux, use Python 3.6 to 3.11 By default, python-oracledb connects directly to Oracle Database. This lets it be used when Oracle Client libraries are not available (such Apple M1 or Alpine Linux), or where the client libraries are not easily installable (such as some cloud environments). Note not all environments are tested. 2. Install python-oracledb from `PyPI `__: .. code-block:: shell python -m pip install oracledb --upgrade If a binary package is not available for your platform, the source package will be downloaded instead. This will be compiled and the resulting binary installed. The ``--user`` option may be useful if you do not have permission to write to system directories: .. code-block:: shell python -m pip install oracledb --upgrade --user If you are behind a proxy, add a proxy server to the command, for example add ``--proxy=http://proxy.example.com:80`` 3. Create a file ``test.py`` such as: .. code-block:: python # test.py import oracledb import os un = os.environ.get('PYTHON_USERNAME') pw = os.environ.get('PYTHON_PASSWORD') cs = os.environ.get('PYTHON_CONNECTSTRING') with oracledb.connect(user=un, password=pw, dsn=cs) as connection: with connection.cursor() as cursor: sql = """select sysdate from dual""" for r in cursor.execute(sql): print(r) 4. In your integrated development environment (IDE) or terminal window, set the three environment variables used by the test program. A simple :ref:`connection ` to the database requires an Oracle Database `user name and password `_ and a database :ref:`connection string `. Set the environment variables to your values. For python-oracledb, the connection string is commonly of the format ``hostname/servicename``, using the host name where the database is running and the Oracle Database service name of the database instance. The database can be on-premises or in the Cloud. It should be version 12.1 or later. The python-oracledb driver does not include a database. 5. Run the program as shown below: .. code-block:: shell python test.py The date will be shown. You can learn more about python-oracledb from the `python-oracledb documentation `__ and `samples `__. If you run into installation trouble, see `Troubleshooting`_. Supported Oracle Database Versions ================================== When python-oracledb is used in the default Thin mode, it connects directly to the Oracle Database and does not require Oracle Client libraries. Connections in this mode can be made to Oracle Database 12.1 or later. To use the :ref:`Thick mode features ` of python-oracledb, additional Oracle Client libraries must be installed, as detailed in the subsequent sections. Connections in this mode can be made to Oracle Database 9.2, or later, depending on the Oracle Client library version. Oracle's standard client-server network interoperability allows connections between different versions of Oracle Client libraries and Oracle Database. For currently certified configurations, see Oracle Support's `Doc ID 207303.1 `__. In summary: - Oracle Client 21 can connect to Oracle Database 12.1 or later - Oracle Client 19, 18 and 12.2 can connect to Oracle Database 11.2 or later - Oracle Client 12.1 can connect to Oracle Database 10.2 or later - Oracle Client 11.2 can connect to Oracle Database 9.2 or later The technical restrictions on creating connections may be more flexible. For example, Oracle Client 12.2 can successfully connect to Oracle Database 10.2. The python-oracledb attribute :attr:`Connection.thin` can be used to see what mode a connection is in. In the Thick mode, the function :func:`oracledb.clientversion()` can be used to determine which Oracle Client version is in use. The attribute :attr:`Connection.version` can be used to determine which Oracle Database version a connection is accessing. These can then be used to adjust the application behavior accordingly. Any attempt to use Oracle features that are not supported by a particular mode or client library/database combination will result in runtime errors. Installation Requirements ========================== To use python-oracledb, you need: - Python 3.6, 3.7, 3.8, 3.9, 3.10 or 3.11 depending on the operating system: - Windows: Use Python 3.7 to 3.11 - macOS: Use Python 3.7 to 3.11 - Linux: Use Python 3.6 to 3.11 - The Python cryptography package. This package is automatically installed as a dependency of python-oracledb. It is strongly recommended that you keep the cryptography package up to date whenever new versions are released. If the cryptography package is not available, you can still install python-oracledb but can only use it in Thick mode, see :ref:`nocrypto`. - Optionally, Oracle Client libraries can be installed to enable some additional advanced functionality. These can be from the free `Oracle Instant Client `__, from a full Oracle Client installation (such as installed by Oracle's GUI installer), or from those included in Oracle Database if Python is on the same machine as the database. Oracle Client libraries versions 21, 19, 18, 12, and 11.2 are supported where available on Linux, Windows and macOS (Intel x86). Oracle's standard client-server version interoperability allows connection to both older and newer databases. - An Oracle Database either local or remote, on-premises or in the Cloud. Installing python-oracledb on Linux =================================== This section discusses the generic installation methods on Linux. Install python-oracledb ------------------------ The generic way to install python-oracledb on Linux is to use Python's `pip `__ package to install from Python's package repository `PyPI `__: .. code-block:: shell python -m pip install oracledb This will download and install a pre-compiled binary from `PyPI `__ if one is available for your architecture. Otherwise, the source will be downloaded, compiled, and the resulting binary installed. Compiling python-oracledb requires the ``Python.h`` header file. If you are using the default ``python`` package, this file is in the ``python-devel`` package or equivalent. On Oracle Linux 8, to use the default Python 3.6 installation, install with: .. code-block:: shell python3 -m pip install oracledb --user The ``--user`` option is useful when you do not have permission to write to system directories. Other versions of Python can be used on Oracle Linux, see `Python for Oracle Linux `__. If you are behind a proxy, add a proxy server to the command, for example add ``--proxy=http://proxy.example.com:80`` Optionally Install Oracle Client -------------------------------- By default, python-oracledb runs in a Thin mode which connects directly to Oracle Database so no further installation steps are required. However, to use additional features available in :ref:`Thick mode ` you need Oracle Client libraries installed. Oracle Client versions 21, 19, 18, 12 and 11.2 are supported. - If your database is on a remote computer, then download the free `Oracle Instant Client `__ "Basic" or "Basic Light" package for your operating system architecture. - Alternatively, use the client libraries already available in a locally installed database such as the free `Oracle Database Express Edition ("XE") `__ release. To use python-oracledb in Thick mode you must call :meth:`oracledb.init_oracle_client()` in your application, see :ref:`enablingthick`. For example: .. code-block:: python import oracledb oracledb.init_oracle_client() On Linux, do not pass the ``lib_dir`` parameter in the call: the Oracle Client libraries on Linux must be in the system library search path *before* the Python process starts. Oracle Instant Client Zip Files +++++++++++++++++++++++++++++++ To use python-oracledb Thick mode with Oracle Instant Client zip files: 1. Download an Oracle 21, 19, 18, 12, or 11.2 "Basic" or "Basic Light" zip file matching your Python 64-bit or 32-bit architecture: - `x86-64 64-bit `__ - `x86 32-bit `__ - `ARM (aarch64) 64-bit `__ The latest version is recommended. Oracle Instant Client 21 will connect to Oracle Database 12.1 or later. 2. Unzip the package into a single directory that is accessible to your application. For example: .. code-block:: shell mkdir -p /opt/oracle cd /opt/oracle unzip instantclient-basic-linux.x64-21.6.0.0.0.zip 3. Install the ``libaio`` package with sudo or as the root user. For example:: sudo yum install libaio On some Linux distributions this package is called ``libaio1`` instead. On recent Linux versions such as Oracle Linux 8, you may also need to install the ``libnsl`` package when using Oracle Instant Client 19. 4. If there is no other Oracle software on the machine that will be impacted, permanently add Instant Client to the runtime link path. For example, with sudo or as the root user: .. code-block:: shell sudo sh -c "echo /opt/oracle/instantclient_21_6 > /etc/ld.so.conf.d/oracle-instantclient.conf" sudo ldconfig Alternatively, set the environment variable ``LD_LIBRARY_PATH`` to the appropriate directory for the Instant Client version. For example:: export LD_LIBRARY_PATH=/opt/oracle/instantclient_21_6:$LD_LIBRARY_PATH 5. If you use optional Oracle configuration files such as ``tnsnames.ora``, ``sqlnet.ora``, or ``oraaccess.xml`` with Instant Client, then put the files in an accessible directory, for example in ``/opt/oracle/your_config_dir``. Then use: .. code-block:: python import oracledb oracledb.init_oracle_client(config_dir="/home/your_username/oracle/your_config_dir") or set the environment variable ``TNS_ADMIN`` to that directory name. Alternatively, put the files in the ``network/admin`` subdirectory of Instant Client, for example in ``/opt/oracle/instantclient_21_6/network/admin``. This is the default Oracle configuration directory for executables linked with this Instant Client. 6. Call :meth:`oracledb.init_oracle_client()` in your application, if it is not already used. Oracle Instant Client RPMs ++++++++++++++++++++++++++ To use python-oracledb with Oracle Instant Client RPMs: 1. Download an Oracle 21, 19, 18, 12, or 11.2 "Basic" or "Basic Light" RPM matching your Python architecture: - `x86-64 64-bit `__ - `x86 32-bit `__ - `ARM (aarch64) 64-bit `__ Oracle's yum server has convenient repositories: - `Instant Client 21 RPMs for Oracle Linux x86-64 8 `__, `Older Instant Client RPMs for Oracle Linux x86-64 8 `__ - `Instant Client 21 RPMs for Oracle Linux x86-64 7 `__, `Older Instant Client RPMs for Oracle Linux x86-64 7 `__ - `Instant Client RPMs for Oracle Linux x86-64 6 `__ - `Instant Client RPMs for Oracle Linux ARM (aarch64) 8 `__ - `Instant Client RPMs for Oracle Linux ARM (aarch64) 7 `__ The latest version is recommended. Oracle Instant Client 21 will connect to Oracle Database 12.1 or later. 2. Install the downloaded RPM with sudo or as the root user. For example: .. code-block:: shell sudo yum install oracle-instantclient-basic-21.6.0.0.0-1.x86_64.rpm Yum will automatically install required dependencies, such as ``libaio``. On recent Linux versions such as Oracle Linux 8, you may need to manually install the ``libnsl`` package when using Oracle Instant Client 19. 3. For Instant Client 19 or later, the system library search path is automatically configured during installation. For older versions, if there is no other Oracle software on the machine that will be impacted, permanently add Instant Client to the runtime link path. For example, with sudo or as the root user: .. code-block:: shell sudo sh -c "echo /usr/lib/oracle/18.5/client64/lib > /etc/ld.so.conf.d/oracle-instantclient.conf" sudo ldconfig Alternatively, for version 18 and earlier, every shell running Python will need to have the environment variable ``LD_LIBRARY_PATH`` set to the appropriate directory for the Instant Client version. For example:: export LD_LIBRARY_PATH=/usr/lib/oracle/18.5/client64/lib:$LD_LIBRARY_PATH 4. If you use optional Oracle configuration files such as ``tnsnames.ora``, ``sqlnet.ora`` or ``oraaccess.xml`` with Instant Client, then put the files in an accessible directory, for example in ``/opt/oracle/your_config_dir``. Then use: .. code-block:: python import oracledb oracledb.init_oracle_client(config_dir="/opt/oracle/your_config_dir") or set the environment variable ``TNS_ADMIN`` to that directory name. Alternatively, put the files in the ``network/admin`` subdirectory of Instant Client, for example in ``/usr/lib/oracle/21/client64/lib/network/admin``. This is the default Oracle configuration directory for executables linked with this Instant Client. 5. Call :meth:`oracledb.init_oracle_client()` in your application, if it is not already used. Local Database or Full Oracle Client ++++++++++++++++++++++++++++++++++++ Python-oracledb applications can use Oracle Client 21, 19, 18, 12, or 11.2 libraries from a local Oracle Database or full Oracle Client installation (such as installed by Oracle's GUI installer). The libraries must be either 32-bit or 64-bit, matching your Python architecture. 1. Set required Oracle environment variables by running the Oracle environment script. For example: .. code-block:: shell source /usr/local/bin/oraenv For Oracle Database Express Edition ("XE") 11.2, run: .. code-block:: shell source /u01/app/oracle/product/11.2.0/xe/bin/oracle_env.sh 2. Optional Oracle configuration files such as ``tnsnames.ora``, ``sqlnet.ora``, or ``oraaccess.xml`` can be placed in ``$ORACLE_HOME/network/admin``. Alternatively, Oracle configuration files can be put in another, accessible directory. Then set the environment variable ``TNS_ADMIN`` to that directory name. 3. Call :meth:`oracledb.init_oracle_client()` in your application, if it is not already used. .. _wininstall: Installing python-oracledb on Windows ===================================== Install python-oracledb ------------------------ Use Python's `pip `__ package to install python-oracledb from Python's package repository `PyPI `__:: python -m pip install oracledb If you are behind a proxy, add a proxy server to the command, for example add ``--proxy=http://proxy.example.com:80`` .. code-block:: shell python -m pip install oracledb --proxy=http://proxy.example.com:80 --upgrade This will download and install a pre-compiled binary `if one is available `__ for your architecture. If a pre-compiled binary is not available, the source will be downloaded, compiled, and the resulting binary installed. Optionally Install Oracle Client -------------------------------- By default, python-oracledb runs in a Thin mode which connects directly to Oracle Database so no further installation steps are required. However, to use additional features available in :ref:`Thick mode ` you need Oracle Client libraries installed. Oracle Client versions 21, 19, 18, 12, and 11.2 are supported. - If your database is on a remote computer, then download the free `Oracle Instant Client `__ "Basic" or "Basic Light" package for your operating system architecture. - Alternatively, use the client libraries already available in a locally installed database such as the free `Oracle Database Express Edition ("XE") `__ release. To use python-oracledb in Thick mode you must call :meth:`oracledb.init_oracle_client()` in your application, see :ref:`enablingthick`. For example: .. code-block:: python import oracledb oracledb.init_oracle_client() On Windows, you may prefer to pass the ``lib_dir`` parameter in the call as shown below. Oracle Instant Client Zip Files +++++++++++++++++++++++++++++++ To use python-oracledb in Thick mode with Oracle Instant Client zip files: 1. Download an Oracle 21, 19, 18, 12, or 11.2 "Basic" or "Basic Light" zip file: `64-bit `__ or `32-bit `__, matching your Python architecture. The latest version is recommended. Oracle Instant Client 19 will connect to Oracle Database 11.2 or later. Windows 7 users: Note that Oracle 19c is not supported on Windows 7. 2. Unzip the package into a directory that is accessible to your application. For example unzip ``instantclient-basic-windows.x64-19.11.0.0.0dbru.zip`` to ``C:\oracle\instantclient_19_11``. 3. Oracle Instant Client libraries require a Visual Studio redistributable with a 64-bit or 32-bit architecture to match Instant Client's architecture. Each Instant Client version requires a different redistributable version: - For Instant Client 21, install `VS 2019 `__ or later - For Instant Client 19, install `VS 2017 `__ - For Instant Client 18 or 12.2, install `VS 2013 `__ - For Instant Client 12.1, install `VS 2010 `__ - For Instant Client 11.2, install `VS 2005 64-bit `__ Configure Oracle Instant Client ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1. There are several alternative ways to tell python-oracledb where your Oracle Client libraries are, see :ref:`initialization`. * With Oracle Instant Client you can use :meth:`oracledb.init_oracle_client()` in your application, for example: .. code-block:: python import oracledb oracledb.init_oracle_client(lib_dir=r"C:\oracle\instantclient_19_14") Note that a 'raw' string is used because backslashes occur in the path. * Alternatively, add the Oracle Instant Client directory to the ``PATH`` environment variable. The directory must occur in ``PATH`` before any other Oracle directories. Restart any open command prompt windows. Update your application to call ``init_oracle_client()``, which enables python-oracledb Thick mode: .. code-block:: python import oracledb oracledb.init_oracle_client() * Another way to set ``PATH`` is to use a batch file that sets it before Python is executed, for example:: REM mypy.bat SET PATH=C:\oracle\instantclient_19_14;%PATH% python %* Invoke this batch file every time you want to run Python. Update your application to call ``init_oracle_client()``, which enables python-oracledb Thick mode: .. code-block:: python import oracledb oracledb.init_oracle_client() 2. If you use optional Oracle configuration files such as ``tnsnames.ora``, ``sqlnet.ora``, or ``oraaccess.xml`` with Instant Client, then put the files in an accessible directory, for example in ``C:\oracle\your_config_dir``. Then use: .. code-block:: python import oracledb oracledb.init_oracle_client(lib_dir=r"C:\oracle\instantclient_19_14", config_dir=r"C:\oracle\your_config_dir") or set the environment variable ``TNS_ADMIN`` to that directory name. Alternatively, put the files in a ``network\admin`` subdirectory of Instant Client, for example in ``C:\oracle\instantclient_19_11\network\admin``. This is the default Oracle configuration directory for executables linked with this Instant Client. Local Database or Full Oracle Client ++++++++++++++++++++++++++++++++++++ Python-oracledb Thick mode applications can use Oracle Client 21, 19, 18, 12, or 11.2 libraries from a local Oracle Database or full Oracle Client (such as installed by Oracle's GUI installer). The Oracle libraries must be either 32-bit or 64-bit, matching your Python architecture. 1. Set the environment variable ``PATH`` to include the path that contains ``OCI.DLL``, if it is not already set. Restart any open command prompt windows. 2. Optional Oracle configuration files such as ``tnsnames.ora``, ``sqlnet.ora``, or ``oraaccess.xml`` can be placed in the ``network\admin`` subdirectory of the Oracle Database software installation. Alternatively, pass ``config_dir`` to :meth:`oracledb.init_oracle_client()` as shown in the previous section, or set ``TNS_ADMIN`` to the directory name. 3. To use python-oracledb in Thick mode you must call :meth:`oracledb.init_oracle_client()` in your application, see :ref:`enablingthick`. .. code-block:: python import oracledb oracledb.init_oracle_client() Installing python-oracledb on macOS =================================== Python-oracledb is available as a Universal binary for Python 3.8, or later, on Apple Intel and M1 architectures. A binary is also available for Python 3.7 on Apple Intel. Install python-oracledb ----------------------- Use Python's `pip `__ package to install python-oracledb from Python's package repository `PyPI `__: .. code-block:: shell python -m pip install oracledb The ``--user`` option may be useful if you do not have permission to write to system directories: .. code-block:: shell python -m pip install oracledb --user To install into the system Python, you may need to use ``/usr/bin/python3`` instead of ``python``: .. code-block:: shell /usr/bin/python3 -m pip install oracledb --user If you are behind a proxy, add a proxy server to the command, for example add ``--proxy=http://proxy.example.com:80`` The source will be downloaded, compiled, and the resulting binary installed. Optionally Install Oracle Client -------------------------------- By default, python-oracledb runs in a Thin mode which connects directly to Oracle Database so no further installation steps are required. However, to use additional features available in :ref:`Thick mode ` you need Oracle Client libraries installed. Note that to use Thick mode on the M1 architecture you will need to use Rosetta with Python 64-bit Intel and the Instant Client (Intel x86) libraries. Manual Installation +++++++++++++++++++ * Download the **Basic** 64-bit DMG from `Oracle `__. * In Finder, double-click DMG to mount it. * Open a terminal window and run the install script in the mounted package, for example: .. code-block:: shell /Volumes/instantclient-basic-macos.x64-19.8.0.0.0dbru/install_ic.sh This copies the contents to ``$HOME/Downloads/instantclient_19_8``. Applications may not have access to the ``Downloads`` directory, so you should move Instant Client somewhere convenient. * In Finder, eject the mounted Instant Client package. If you have multiple Instant Client DMG packages mounted, you only need to run ``install_ic.sh`` once. It will copy all mounted Instant Client DMG packages at the same time. Scripted Installation +++++++++++++++++++++ Instant Client installation can alternatively be scripted, for example: .. code-block:: shell cd $HOME/Downloads curl -O https://download.oracle.com/otn_software/mac/instantclient/198000/instantclient-basic-macos.x64-19.8.0.0.0dbru.dmg hdiutil mount instantclient-basic-macos.x64-19.8.0.0.0dbru.dmg /Volumes/instantclient-basic-macos.x64-19.8.0.0.0dbru/install_ic.sh hdiutil unmount /Volumes/instantclient-basic-macos.x64-19.8.0.0.0dbru The Instant Client directory will be ``$HOME/Downloads/instantclient_19_8``. Applications may not have access to the ``Downloads`` directory, so you should move Instant Client somewhere convenient. Configure Oracle Instant Client ------------------------------- 1. Call :meth:`oracledb.init_oracle_client()` in your application: .. code-block:: python import oracledb oracledb.init_oracle_client(lib_dir="/Users/your_username/Downloads/instantclient_19_8") 2. If you use optional Oracle configuration files such as ``tnsnames.ora``, ``sqlnet.ora``, or ``oraaccess.xml`` with Oracle Instant Client, then put the files in an accessible directory, for example in ``/Users/your_username/oracle/your_config_dir``. Then use: .. code-block:: python import oracledb oracledb.init_oracle_client(lib_dir="/Users/your_username/Downloads/instantclient_19_8", config_dir="/Users/your_username/oracle/your_config_dir") Or set the environment variable ``TNS_ADMIN`` to that directory name. Alternatively, put the files in the ``network/admin`` subdirectory of Oracle Instant Client, for example in ``/Users/your_username/Downloads/instantclient_19_8/network/admin``. This is the default Oracle configuration directory for executables linked with this Instant Client. Installing python-oracledb without Internet Access =================================================== To install python-oracledb on a computer that is not connected to the internet, download the appropriate python-oracledb file from Python's package repository `PyPI `__. Transfer this file to the offline computer and install it with:: python -m pip install "" Then follow the general python-oracledb platform installation instructions to install Oracle client libraries. .. _nocrypto: Installing python-oracledb without the Cryptography Package =========================================================== If the Python cryptography package is not available, python-oracledb can still be installed but can only be used in Thick mode. To install without the cryptography package, use pip's ``--no-deps`` option, for example: .. code-block:: python python -m pip install oracledb --no-deps Oracle Client libraries must then be installed. See previous sections. To use python-oracledb in Thick mode you must call :meth:`oracledb.init_oracle_client()` in your application, see :ref:`enablingthick`. Without this, your application will get the error ``DPY-3016: python-oracledb thin mode cannot be used because the cryptography package is not installed``. Installing from Source Code =========================== The following dependencies are required to build python-oracledb from source code: - Cython Package: Cython is a standard Python package from PyPI. - The Python cryptography package. This will need to be installed manually before building python-oracledb. For example install with ``pip``. - C Compiler: A C99 compiler is needed. Install Using GitHub -------------------- In order to install using the source on GitHub, use the following commands:: git clone --recurse-submodules https://github.com/oracle/python-oracledb.git cd python-oracledb python setup.py build python setup.py install Note that if you download a source zip file directly from GitHub then you will also need to download an `ODPI-C `__ source zip file and put the extracted contents inside the "odpi" subdirectory, for example in "python-oracledb-main/src/oracledb/impl/thick/odpi". Python-oracledb source code is also available from opensource.oracle.com. This can be installed with:: git clone --recurse-submodules https://opensource.oracle.com/git/oracle/python-oracledb.git cd python-oracledb python setup.py build python setup.py install If you do not have access to system directories, the ``--user`` option can be used to install into a local directory:: python setup.py install --user Install Using Source from PyPI ------------------------------ The source package can be downloaded manually from `PyPI `__ and extracted, after which the following commands should be run:: python setup.py build python setup.py install If you do not have access to system directories, the ``--user`` option can be used to install into a local directory:: python setup.py install --user Troubleshooting =============== If installation fails: - An error such as ``not a supported wheel on this platform.`` indicates that you may be using an older `pip` version. Upgrade it with the following command: .. code-block:: shell python -m pip install pip --upgrade --user - Use option ``-v`` with pip. Review your output and logs. Try to install using a different method. **Google anything that looks like an error.** Try some potential solutions. - If there was a network connection error, check if you need to set the environment variables ``http_proxy`` and/or ``https_proxy`` or try ``python -m pip install --proxy=http://proxy.example.com:80 oracledb --upgrade``. - If the upgrade did not give any errors but the old version is still installed, try ``python -m pip install oracledb --upgrade --force-reinstall``. - If you do not have access to modify your system version of Python, then use ``python -m pip install oracledb --upgrade --user`` or venv. - If you get the error ``No module named pip``, it means that the pip module that is built into Python may sometimes be removed by the OS. Use the venv module (built into Python 3.x) or virtualenv module instead. - If you get the error ``fatal error: dpi.h: No such file or directory`` when building from source code, then ensure that your source installation has a subdirectory called "odpi" containing files. If this is missing, review the section on `Install Using GitHub`_. If using python-oracledb fails: - If you have multiple versions of Python installed, ensure that you are using the correct python and pip (or python3 and pip3) executables. - If you get the error ``DPI-1047: Oracle Client library cannot be loaded``: - Review the :ref:`features available in python-oracledb's default Thin mode `. If Thin mode suits your requirements, then remove calls in your application to :meth:`oracledb.init_oracle_client()` since this loads the Oracle Client library to enable Thick mode. - On Windows and macOS, pass the ``lib_dir`` library directory parameter in your :meth:`oracledb.init_oracle_client()` call. The parameter should be the location of your Oracle Client libraries. Do not pass this parameter on Linux. - Check if Python and your Oracle Client libraries are both 64-bit or both 32-bit. The ``DPI-1047`` message will tell you whether the 64-bit or 32-bit Oracle Client is needed for your Python. - For Thick mode connections, set the environment variable ``DPI_DEBUG_LEVEL`` to 64 and restart python-oracledb. The trace messages will show how and where python-oracledb is looking for the Oracle Client libraries. At a Windows command prompt, this could be done with:: set DPI_DEBUG_LEVEL=64 On Linux and macOS, you might use:: export DPI_DEBUG_LEVEL=64 - On Windows, if you have a full database installation, ensure that this database is the `currently configured database `__. - On Windows, if you are not using passing a library directory parameter to :meth:`oracledb.init_oracle_client()`, then restart your command prompt and use ``set PATH`` to check if the environment variable has the correct Oracle Client listed before any other Oracle directories. - On Windows, use the ``DIR`` command to verify that ``OCI.DLL`` exists in the directory passed to :meth:`oracledb.init_oracle_client()` or set in ``PATH``. - On Windows, check that the correct `Windows Redistributables `__ have been installed. - On Linux, check if the ``LD_LIBRARY_PATH`` environment variable contains the Oracle Client library directory. Or, if you are using Oracle Instant Client, a preferred alternative is to ensure that a file in the ``/etc/ld.so.conf.d`` directory contains the path to the Instant Client directory, and then run ``ldconfig``. - If you get the error ``DPY-3010: connections to this database server version are not supported by python-oracledb in thin mode`` when connecting to Oracle Database 11.2, then you need to enable Thick mode by installing Oracle Client libraries and calling :meth:`oracledb.init_oracle_client()` in your code. Alternatively, upgrade your database. - If you get the error "``DPI-1072: the Oracle Client library version is unsupported``", then review the installation requirements. The Thick mode of python-oracledb needs Oracle Client libraries 11.2 or later. Note that version 19 is not supported on Windows 7. Similar steps shown above for ``DPI-1047`` may help. You may be able to use Thin mode which can be done by removing calls :meth:`oracledb.init_oracle_client()` from your code. python-oracledb-1.2.1/doc/src/user_guide/introduction.rst000066400000000000000000000127221434177474600235740ustar00rootroot00000000000000.. _introduction: ***************************************************** Introduction to the Python Driver for Oracle Database ***************************************************** The python-oracledb driver is a Python extension module that enables access to Oracle Database. It has comprehensive functionality supporting the `Python Database API v2.0 Specification `__ with a considerable number of additions and a couple of exclusions. The python-oracledb driver is the renamed, major version successor to `cx_Oracle 8.3 `__. For upgrade information, see :ref:`upgrading83`. Python-oracledb is typically installed from Python's package repository `PyPI `__ using `pip `__. See :ref:`installation` for more information. Architecture ============ Python-oracledb is a 'Thin' driver with an optional 'Thick' mode enabled by an application setting. python-oracledb Thin Mode Architecture -------------------------------------- By default, python-oracledb allows connecting directly to Oracle Database 12.1 or later. This Thin mode does not need Oracle Client libraries. .. _thinarchfig: .. figure:: /images/python-oracledb-thin-arch.png :alt: architecture of the python-oracledb driver in Thin mode Architecture of the python-oracledb driver in Thin mode The figure shows the architecture of python-oracledb. Users interact with a Python application, for example by making web requests. The application program makes calls to python-oracledb functions. The connection from python-oracledb Thin mode to the Oracle Database is established directly. The database can be on the same machine as Python, or it can be remote. The Oracle Net behavior can optionally be configured by using a ``tnsnames.ora`` file and with application settings. See :ref:`optnetfiles`. python-oracledb Thick Mode Architecture --------------------------------------- Python-oracledb is said to be in 'Thick' mode when it links with Oracle Client libraries. An application script runtime option enables this mode by loading the libraries, see :ref:`enablingthick`. This gives you some :ref:`additional functionality `. Depending on the version of the Oracle Client libraries, this mode of python-oracledb can connect to Oracle Database 9.2 or later. .. _thickarchfig: .. figure:: /images/python-oracledb-thick-arch.png :alt: architecture of the python-oracledb driver in Thick mode Architecture of the python-oracledb driver in Thick mode The figure shows the architecture of the python-oracledb Thick mode. Users interact with a Python application, for example by making web requests. The application program makes calls to python-oracledb functions. Internally, python-oracledb dynamically loads Oracle Client libraries. Connections from python-oracledb Thick mode to Oracle Database are established using the Oracle Client libraries. The database can be on the same machine as Python, or it can be remote. To use python-oracledb Thick mode, the Oracle Client libraries must be installed separately, see :ref:`installation`. The libraries can be from an installation of `Oracle Instant Client `__, from a full Oracle Client installation (such as installed by Oracle's GUI installer), or even from an Oracle Database installation (if Python is running on the same machine as the database). Oracle's standard client-server version interoperability allows connection to both older and newer databases from different Oracle Client library versions. Some behaviors of the Oracle Client libraries can optionally be configured with an ``oraaccess.xml`` file, for example to enable auto-tuning of a statement cache. See :ref:`optclientfiles`. The Oracle Net behavior can optionally be configured with files such as ``tnsnames.ora`` and ``sqlnet.ora``, for example to enable :ref:`network encryption `. See :ref:`optnetfiles`. Oracle environment variables that are set before python-oracledb first creates a database connection may affect python-oracledb Thick mode behavior. See :ref:`envset`. Feature Highlights of python-oracledb ====================================== The python-oracledb feature highlights are: * Easy installation from PyPI * Support for multiple Oracle Database versions * Supports the `Python Database API v2.0 Specification `__ with a considerable number of additions and a couple of exclusions. * Works with common frameworks and ORMs * Execution of SQL and PL/SQL statements * Extensive Oracle data type support, including JSON, large objects (``CLOB`` and ``BLOB``) and binding of SQL objects * Connection management, including connection pooling * Oracle Database High Availability features * Full use of Oracle Network Service infrastructure, including encrypted network traffic See :ref:`featuresummary` for more information. Getting Started =============== See :ref:`quickstart`. Examples and Tutorial ===================== Runnable examples are in the `GitHub samples directory `__. A tutorial `Python and Oracle Database Tutorial: The New Wave of Scripting `__ is also available. python-oracledb-1.2.1/doc/src/user_guide/json_data_type.rst000066400000000000000000000272071434177474600240620ustar00rootroot00000000000000.. _jsondatatype: *************** Using JSON Data *************** Native support for JavaScript Object Notation (JSON) data was introduced in Oracle Database 12c. You can use JSON with relational database features, including transactions, indexing, declarative querying, and views. You can project JSON data relationally, making it available for relational processes and tools. Also see :ref:`Simple Oracle Document Access (SODA) `, which allows access to JSON documents through a set of NoSQL-style APIs. For more information about using JSON in Oracle Database see the `Database JSON Developer's Guide `__. **Oracle Database 12c JSON Data Type** In Oracle Database 12c, JSON in relational tables is stored as BLOB, CLOB or VARCHAR2 data. All of these types can be used with python-oracledb in Thin or Thick modes. The older syntax to create a table with a JSON column is like: .. code-block:: sql create table CustomersAsBlob ( id integer not null primary key, json_data blob check (json_data is json) ); The check constraint with the clause ``IS JSON`` ensures only JSON data is stored in that column. The older syntax can still be used in Oracle Database 21c; however, the recommendation is to move to the new JSON type. With the old syntax, the storage can be BLOB, CLOB, or VARCHAR2. Of these, BLOB is preferred to avoid character set conversion overheads. **Oracle Database 21c JSON Data Type** Oracle Database 21c introduced a dedicated JSON data type with a new `binary storage format `__ that improves performance and functionality. To fully take advantage of the dedicated JSON type, use python-oracledb in Thick mode with Oracle Client libraries version 21, or later. In Oracle Database 21, to create a table with a column called ``JSON_DATA`` for JSON data you might use: .. code-block:: sql create table CustomersAsJson ( id integer not null primary key, json_data json ); Using the Oracle Database 21c JSON Type in python-oracledb Thick Mode ===================================================================== Using python-oracledb Thick mode with Oracle Database 21c and Oracle Client 21c (or later), you can insert by binding as shown below: .. code-block:: python data = [ (5, dict(name="Sally", dept="Sales", location="France")), ] insert_sql = "insert into CustomersAsJson values (:1, :2)" # Take advantage of direct binding cursor.setinputsizes(None, oracledb.DB_TYPE_JSON) cursor.execute(insert_sql, [1, data]) To fetch a JSON column, use: .. code-block:: python for row in cursor.execute("select * from CustomersAsJson"): print(row) See `json_direct.py `__ for a runnable example. The example also shows how to use this type when python-oracledb Thick mode uses older Oracle Client libraries. Using the Oracle Database 21c JSON Type and python-oracledb Thin Mode ===================================================================== Using python-oracledb Thin mode with Oracle Database 21c, you can insert into a JSON column as shown below: .. code:: python data = [ (1, dict(name="Rod", dept="Sales", location="Germany")), (2, dict(name="George", dept="Marketing", location="Bangalore")), (3, dict(name="Sam", dept="Sales", location="Mumbai")), (4, dict(name="Jill", dept="Marketing", location="Germany")) ] insert_sql = "insert into CustomersAsJson values (:1, :2)" # Insert the data as a JSON string cursor.executemany(insert_sql, [(i, json.dumps(j)) for i, j in data]) For python-oracledb Thin mode, a type handler is required to fetch the Oracle 21c JSON datatype. If a type handler is used in the python-oracledb Thick mode, then the behavior is same in both the python-oracledb modes. The following example shows a type handler: .. code-block:: python def my_type_handler(cursor, name, default_type, size, precision, scale): if default_type == oracledb.DB_TYPE_JSON: return cursor.var(str, arraysize=cursor.arraysize, outconverter=json.loads) cursor.outputtypehandler = my_type_handler for row in cursor.execute("select * from CustomersAsJson"): print(row) With a type handler, the python-oracledb Thin mode is equivalent to using the python-oracledb Thick mode with Oracle Client 21c. The python-oracledb Thin mode returns timestamps in a string representation. Without a type handler, the python-oracledb Thin mode gives an error that ``DB_TYPE_JSON`` is not supported. A type handler is not needed when fetching from the Oracle Database 19c JSON type, since this is represented as VARCHAR2 or LOB. See `json_type.py `__ for a runnable example. Using the Oracle 12c JSON type in python-oracledb ================================================= When using Oracle Database 12c or later with JSON using BLOB storage to insert JSON strings, use: .. code-block:: python data = dict(name="Rod", dept="Sales", location="Germany") inssql = "insert into CustomersAsBlob values (:1, :2)" cursor.execute(inssql, [1, json.dumps(data)]) To fetch JSON strings, use: .. code-block:: python import json sql = "SELECT c.json_data FROM CustomersAsBlob c" for j, in cursor.execute(sql): print(json.loads(j.read())) See `json_blob.py `__ for a runnable example. IN Bind Type Mapping ==================== When binding to a JSON value, the ``type`` parameter for the variable must be specified as :data:`oracledb.DB_TYPE_JSON`. Python values are converted to JSON values as shown in the following table. The 'SQL Equivalent' syntax can be used in SQL INSERT and UPDATE statements if specific attribute types are needed but there is no direct mapping from Python. .. list-table-with-summary:: :header-rows: 1 :class: wy-table-responsive :widths: 1 1 1 :summary: The first column is the Python Type or Value. The second column is the equivalent JSON Attribute Type or Value. The third column is the SQL Equivalent syntax. :align: left * - Python Type or Value - JSON Attribute Type or Value - SQL Equivalent Example * - None - null - NULL * - True - true - n/a * - False - false - n/a * - int - NUMBER - json_scalar(1) * - float - NUMBER - json_scalar(1) * - decimal.Decimal - NUMBER - json_scalar(1) * - str - VARCHAR2 - json_scalar('String') * - datetime.date - TIMESTAMP - json_scalar(to_timestamp('2020-03-10', 'YYYY-MM-DD')) * - datetime.datetime - TIMESTAMP - json_scalar(to_timestamp('2020-03-10', 'YYYY-MM-DD')) * - bytes - RAW - json_scalar(utl_raw.cast_to_raw('A raw value')) * - list - Array - json_array(1, 2, 3 returning json) * - dict - Object - json_object(key 'Fred' value json_scalar(5), key 'George' value json_scalar('A string') returning json) * - n/a - CLOB - json_scalar(to_clob('A short CLOB')) * - n/a - BLOB - json_scalar(to_blob(utl_raw.cast_to_raw('A short BLOB'))) * - n/a - DATE - json_scalar(to_date('2020-03-10', 'YYYY-MM-DD')) * - n/a - INTERVAL YEAR TO MONTH - json_scalar(to_yminterval('+5-9')) * - n/a - INTERVAL DAY TO SECOND - json_scalar(to_dsinterval('P25DT8H25M')) * - n/a - BINARY_DOUBLE - json_scalar(to_binary_double(25)) * - n/a - BINARY_FLOAT - json_scalar(to_binary_float(15.5)) An example of creating a CLOB attribute with key ``mydocument`` in a JSON column using SQL is: .. code-block:: python cursor.execute(""" insert into mytab (myjsoncol) values (json_object(key 'mydocument' value json_scalar(to_clob(:b)) returning json))""", ['A short CLOB']) When `mytab` is queried in python-oracledb, the CLOB data will be returned as a Python string, as shown by the following table. Output might be like:: {mydocument: 'A short CLOB'} Query and OUT Bind Type Mapping =============================== When getting Oracle Database 21 JSON values from the database, the following attribute mapping occurs: .. list-table-with-summary:: :header-rows: 1 :class: wy-table-responsive :widths: 1 1 :align: left :summary: The first column is the Database JSON Attribute Type or Value. The second column is the corresponding Python Type or Value mapped. * - Database JSON Attribute Type or Value - Python Type or Value * - null - None * - false - False * - true - True * - NUMBER - decimal.Decimal * - VARCHAR2 - str * - RAW - bytes * - CLOB - str * - BLOB - bytes * - DATE - datetime.datetime * - TIMESTAMP - datetime.datetime * - INTERVAL YEAR TO MONTH - not supported * - INTERVAL DAY TO SECOND - datetime.timedelta * - BINARY_DOUBLE - float * - BINARY_FLOAT - float * - Arrays - list * - Objects - dict SQL/JSON Path Expressions ========================= Oracle Database provides SQL access to JSON data using SQL/JSON path expressions. A path expression selects zero or more JSON values that match, or satisfy, it. Path expressions can use wildcards and array ranges. A simple path expression is ``$.friends`` which is the value of the JSON field ``friends``. For example, the previously created ``customers`` table with JSON column ``json_data`` can be queried like: .. code-block:: sql select c.json_data.location FROM customers c With the JSON ``'{"name":"Rod","dept":"Sales","location":"Germany"}'`` stored in the table, the queried value would be ``Germany``. The JSON_EXISTS functions tests for the existence of a particular value within some JSON data. To look for JSON entries that have a ``location`` field: .. code-block:: python for blob, in cursor.execute(""" select json_data from customers where json_exists(json_data, '$.location')"""): data = json.loads(blob.read()) print(data) This query might display:: {'name': 'Rod', 'dept': 'Sales', 'location': 'Germany'} The SQL/JSON functions ``JSON_VALUE`` and ``JSON_QUERY`` can also be used. Note that the default error-handling behavior for these functions is ``NULL ON ERROR``, which means that no value is returned if an error occurs. To ensure that an error is raised, use ``ERROR ON ERROR``. For more information, see `SQL/JSON Path Expressions `__ in the Oracle JSON Developer's Guide. Accessing Relational Data as JSON ================================= In Oracle Database 12.2 or later, the `JSON_OBJECT `__ function is a great way to convert relational table data to JSON: .. code-block:: python cursor.execute(""" select json_object('deptId' is d.department_id, 'name' is d.department_name) department from departments d where department_id < :did order by d.department_id""", [50]); for row in cursor: print(row) This produces:: ('{"deptId":10,"name":"Administration"}',) ('{"deptId":20,"name":"Marketing"}',) ('{"deptId":30,"name":"Purchasing"}',) ('{"deptId":40,"name":"Human Resources"}',) python-oracledb-1.2.1/doc/src/user_guide/lob_data.rst000066400000000000000000000165601434177474600226240ustar00rootroot00000000000000.. _lobdata: ************************ Using CLOB and BLOB Data ************************ Oracle Database uses :ref:`LOB objects ` to store large data such as text, images, videos, and other multimedia formats. The maximum size of a LOB is limited to the size of the tablespace storing it. There are four types of LOB (large object): * BLOB - Binary Large Object, used for storing binary data. python-oracledb uses the type :attr:`oracledb.DB_TYPE_BLOB`. * CLOB - Character Large Object, used for string strings in the database character set format. python-oracledb uses the type :attr:`oracledb.DB_TYPE_CLOB`. * NCLOB - National Character Large Object, used for string strings in the national character set format. python-oracledb uses the type :attr:`oracledb.DB_TYPE_NCLOB`. * BFILE - External Binary File, used for referencing a file stored on the host operating system outside of the database. python-oracledb uses the type :attr:`oracledb.DB_TYPE_BFILE`. LOBs can be streamed to, and from, Oracle Database. LOBs up to 1 GB in length can be also be handled directly as strings or bytes in python-oracledb. This makes LOBs easy to work with, and has significant performance benefits over streaming. However, it requires the entire LOB data to be present in Python memory, which may not be possible. See `GitHub `__ for LOB examples. Simple Insertion of LOBs ------------------------ Consider a table with CLOB and BLOB columns: .. code-block:: sql CREATE TABLE lob_tbl ( id NUMBER, c CLOB, b BLOB ); With python-oracledb, LOB data can be inserted in the table by binding strings or bytes as needed: .. code-block:: python with open('example.txt', 'r') as f: text_data = f.read() with open('image.png', 'rb') as f: img_data = f.read() cursor.execute(""" insert into lob_tbl (id, c, b) values (:lobid, :clobdata, :blobdata)""", lobid=10, clobdata=text_data, blobdata=img_data) Note that with this approach, LOB data is limited to 1 GB in size. .. _directlobs: Fetching LOBs as Strings and Bytes ---------------------------------- CLOBs and BLOBs smaller than 1 GB can queried from the database directly as strings and bytes. This can be much faster than streaming a :ref:`LOB Object `. Support is enabled by setting the :ref:`Defaults Object `. .. code-block:: python import oracledb # returns strings or bytes instead of a locator oracledb.defaults.fetch_lobs = False . . . id_val = 1 text_data = "The quick brown fox jumps over the lazy dog" binary_data = b"Some binary data" cursor.execute("insert into lob_tbl (id, c, b) values (:1, :2, :3)", [id_val, text_data, binary_data]) cursor.execute("select c, b from lob_tbl where id = :1", [id_val]) clob_data, blob_data = cursor.fetchone() print("CLOB length:", len(clob_data)) print("CLOB data:", clob_data) print("BLOB length:", len(blob_data)) print("BLOB data:", blob_data) This displays:: CLOB length: 43 CLOB data: The quick brown fox jumps over the lazy dog BLOB length: 16 BLOB data: b'Some binary data' An older alternative to using ``oracledb.defaults.fetch_lobs`` is to use a type handler: .. code-block:: python def output_type_handler(cursor, name, default_type, size, precision, scale): if default_type == oracledb.DB_TYPE_CLOB: return cursor.var(oracledb.DB_TYPE_LONG, arraysize=cursor.arraysize) if default_type == oracledb.DB_TYPE_BLOB: return cursor.var(oracledb.DB_TYPE_LONG_RAW, arraysize=cursor.arraysize) if default_type == oracledb.DB_TYPE_NCLOB: return cursor.var(oracledb.DB_TYPE_LONG_NVARCHAR, arraysize=cursor.arraysize) connection.outputtypehandler = output_type_handler Streaming LOBs (Read) --------------------- Without setting ``oracledb.defaults.fetch_lobs`` to False, or without using an output type handler, the CLOB and BLOB values are fetched as :ref:`LOB objects`. The size of the LOB object can be obtained by calling :meth:`LOB.size()` and the data can be read by calling :meth:`LOB.read()`: .. code-block:: python id_val = 1 text_data = "The quick brown fox jumps over the lazy dog" binary_data = b"Some binary data" cursor.execute("insert into lob_tbl (id, c, b) values (:1, :2, :3)", [id_val, text_data, binary_data]) cursor.execute("select b, c from lob_tbl where id = :1", [id_val]) b, c = cursor.fetchone() print("CLOB length:", c.size()) print("CLOB data:", c.read()) print("BLOB length:", b.size()) print("BLOB data:", b.read()) This approach produces the same results as the previous example but it will perform more slowly because it requires more :ref:`round-trips ` to Oracle Database and has higher overhead. It is needed, however, if the LOB data cannot be fetched as one block of data from the server. To stream the BLOB column, the :meth:`LOB.read()` method can be called repeatedly until all of the data has been read, as shown below: .. code-block:: python cursor.execute("select b from lob_tbl where id = :1", [10]) blob, = cursor.fetchone() offset = 1 num_bytes_in_chunk = 65536 with open("image.png", "wb") as f: while True: data = blob.read(offset, num_bytes_in_chunk) if data: f.write(data) if len(data) < num_bytes_in_chunk: break offset += len(data) Streaming LOBs (Write) ---------------------- If a row containing a LOB is being inserted or updated, and the quantity of data that is to be inserted or updated cannot fit in a single block of data, the data can be streamed using the method :meth:`LOB.write()` instead as shown in the following code: .. code-block:: python id_val = 9 lob_var = cursor.var(oracledb.DB_TYPE_BLOB) cursor.execute(""" insert into lob_tbl (id, b) values (:1, empty_blob()) returning b into :2""", [id_val, lob_var]) blob, = lobVar.getvalue() offset = 1 num_bytes_in_chunk = 65536 with open("image.png", "rb") as f: while True: data = f.read(num_bytes_in_chunk) if data: blob.write(data, offset) if len(data) < num_bytes_in_chunk: break offset += len(data) connection.commit() Temporary LOBs -------------- All of the examples shown thus far have made use of permanent LOBs. These are LOBs that are stored in the database. Oracle also supports temporary LOBs that are not stored in the database but can be used to pass large quantities of data. These LOBs use space in the temporary tablespace until all variables referencing them go out of scope or the connection in which they are created is explicitly closed. When calling PL/SQL procedures with data that exceeds 32,767 bytes in length, python-oracledb automatically creates a temporary LOB internally and passes that value through to the procedure. If the data that is to be passed to the procedure exceeds that which can fit in a single block of data, however, you can use the method :meth:`Connection.createlob()` to create a temporary LOB. This LOB can then be read and written just like in the examples shown above for persistent LOBs. python-oracledb-1.2.1/doc/src/user_guide/plsql_execution.rst000066400000000000000000000265221434177474600242740ustar00rootroot00000000000000.. _plsqlexecution: **************** Executing PL/SQL **************** PL/SQL stored procedures, functions and anonymous blocks can be called from python-oracledb. .. _plsqlproc: PL/SQL Stored Procedures ------------------------ The :meth:`Cursor.callproc()` method is used to call PL/SQL procedures. If a procedure with the following definition exists: .. code-block:: sql create or replace procedure myproc ( a_Value1 number, a_Value2 out number ) as begin a_Value2 := a_Value1 * 2; end; then the following Python code can be used to call it: .. code-block:: python out_val = cursor.var(int) cursor.callproc('myproc', [123, out_val]) print(out_val.getvalue()) # will print 246 Calling :meth:`Cursor.callproc()` actually generates an anonymous PL/SQL block as shown below, which is then executed: .. code-block:: python cursor.execute("begin myproc(:1,:2); end;", [123, out_val]) See :ref:`bind` for information on binding. .. _plsqlfunc: PL/SQL Stored Functions ----------------------- The :meth:`Cursor.callfunc()` method is used to call PL/SQL functions. The ``returnType`` parameter for :meth:`~Cursor.callfunc()` is expected to be a Python type, one of the :ref:`oracledb types ` or an :ref:`Object Type `. If a function with the following definition exists: .. code-block:: sql create or replace function myfunc ( a_StrVal varchar2, a_NumVal number ) return number as begin return length(a_StrVal) + a_NumVal * 2; end; then the following Python code can be used to call it: .. code-block:: python return_val = cursor.callfunc("myfunc", int, ["a string", 15]) print(return_val) # will print 38 A more complex example that returns a spatial (SDO) object can be seen below. First, the SQL statements necessary to set up the example: .. code-block:: sql create table MyPoints ( id number(9) not null, point sdo_point_type not null ); insert into MyPoints values (1, sdo_point_type(125, 375, 0)); create or replace function spatial_queryfn ( a_Id number ) return sdo_point_type is t_Result sdo_point_type; begin select point into t_Result from MyPoints where Id = a_Id; return t_Result; end; / The Python code that will call this procedure looks as follows: .. code-block:: python obj_type = connection.gettype("SDO_POINT_TYPE") cursor = connection.cursor() return_val = cursor.callfunc("spatial_queryfn", obj_type, [1]) print(f"({return_val.X}, {return_val.Y}, {return_val.Z})") # will print (125, 375, 0) See :ref:`bind` for information on binding. Anonymous PL/SQL Blocks ----------------------- An anonymous PL/SQL block can be called as shown: .. code-block:: python var = cursor.var(int) cursor.execute(""" begin :out_val := length(:in_val); end;""", in_val="A sample string", out_val=var) print(var.getvalue()) # will print 15 See :ref:`bind` for information on binding. Creating Stored Procedures and Packages --------------------------------------- To create PL/SQL stored procedures and packages, use :meth:`Cursor.execute()` with a SQL CREATE command. Creation warning messages can be found from database views like USER_ERRORS. For example, creating a procedure with an error could be like: .. code-block:: python with connection.cursor() as cursor: cursor.execute(""" create or replace procedure badproc (a in number) as begin WRONG WRONG WRONG end;""") cursor.execute(""" select line, position, text from user_errors where name = 'BADPROC' and type = 'PROCEDURE' order by name, type, line, position""") errors = cursor.fetchall() if errors: for info in errors: print("Error at line {} position {}:\n{}".format(*info)) else: print("Created successfully") The output would be:: PLS-00103: Encountered the symbol "WRONG" when expecting one of the following: := . ( @ % ; Using DBMS_OUTPUT ----------------- The standard way to print output from PL/SQL is with the package `DBMS_OUTPUT `__. Note, PL/SQL code that uses ``DBMS_OUTPUT`` runs to completion before any output is available to the user. Also, other database connections cannot access the buffer. To use DBMS_OUTPUT: * Call the PL/SQL procedure ``DBMS_OUTPUT.ENABLE()`` to enable output to be buffered for the connection. * Execute some PL/SQL that calls ``DBMS_OUTPUT.PUT_LINE()`` to put text in the buffer. * Call ``DBMS_OUTPUT.GET_LINE()`` or ``DBMS_OUTPUT.GET_LINES()`` repeatedly to fetch the text from the buffer until there is no more output. For example: .. code-block:: python # enable DBMS_OUTPUT cursor.callproc("dbms_output.enable") # execute some PL/SQL that calls DBMS_OUTPUT.PUT_LINE cursor.execute(""" begin dbms_output.put_line('This is the python-oracledb manual'); dbms_output.put_line('Demonstrating how to use DBMS_OUTPUT'); end;""") # tune this size for your application chunk_size = 100 # create variables to hold the output lines_var = cursor.arrayvar(str, chunk_size) num_lines_var = cursor.var(int) num_lines_var.setvalue(0, chunk_size) # fetch the text that was added by PL/SQL while True: cursor.callproc("dbms_output.get_lines", (lines_var, num_lines_var)) num_lines = num_lines_var.getvalue() lines = lines_var.getvalue()[:num_lines] for line in lines: print(line or "") if num_lines < chunk_size: break This will produce the following output:: This is the python-oracledb manual Demonstrating use of DBMS_OUTPUT An alternative is to call ``DBMS_OUTPUT.GET_LINE()`` once per output line, which may be much slower: .. code-block:: python text_var = cursor.var(str) status_var = cursor.var(int) while True: cursor.callproc("dbms_output.get_line", (text_var, status_var)) if status_var.getvalue() != 0: break print(text_var.getvalue()) Implicit results ---------------- Implicit results permit a Python program to consume cursors returned by a PL/SQL block without the requirement to use OUT REF CURSOR parameters. The method :meth:`Cursor.getimplicitresults()` can be used for this purpose. It requires both the Oracle Client and Oracle Database to be 12.1 or higher. An example using implicit results is as shown: .. code-block:: python cursor.execute(""" declare cust_cur sys_refcursor; sales_cur sys_refcursor; begin open cust_cur for SELECT * FROM cust_table; dbms_sql.return_result(cust_cur); open sales_cur for SELECT * FROM sales_table; dbms_sql.return_result(sales_cur); end;""") for implicit_cursor in cursor.getimplicitresults(): for row in implicit_cursor: print(row) Data from both the result sets are returned:: (1, 'Tom') (2, 'Julia') (1000, 1, 'BOOKS') (2000, 2, 'FURNITURE') .. _ebr: Edition-Based Redefinition (EBR) -------------------------------- Oracle Database's `Edition-Based Redefinition `__ feature enables upgrading of the database component of an application while it is in use, thereby minimizing or eliminating down time. This feature allows multiple versions of views, synonyms, PL/SQL objects and SQL Translation profiles to be used concurrently. Different versions of the database objects are associated with an "edition". .. note:: Setting the Edition-Based Redefinition (EBR) edition at connection time is only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. In python-oracledb Thin mode, the edition can be changed with ALTER SESSION after connecting. The simplest way to set an edition is to pass the ``edition`` parameter to :meth:`oracledb.connect()` or :meth:`oracledb.create_pool()`: .. code-block:: python connection = oracledb.connect(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb", edition="newsales", encoding="UTF-8") The edition could also be set by setting the environment variable ``ORA_EDITION`` or by executing the SQL statement: .. code-block:: sql alter session set edition = ; Regardless of which method is used to set the edition, the value that is in use can be seen by examining the attribute :attr:`Connection.edition`. If no value has been set, the value will be None. This corresponds to the database default edition ``ORA$BASE``. Consider an example where one version of a PL/SQL function ``Discount`` is defined in the database default edition ``ORA$BASE`` and the other version of the same function is defined in a user created edition ``DEMO``. .. code-block:: sql connect / -- create function using the database default edition CREATE OR REPLACE FUNCTION Discount(price IN NUMBER) RETURN NUMBER IS BEGIN return price * 0.9; END; / A new edition named 'DEMO' is created and the user given permission to use editions. The use of ``FORCE`` is required if the user already contains one or more objects whose type is editionable and that also have non-editioned dependent objects. .. code-block:: sql connect system/ CREATE EDITION demo; ALTER USER ENABLE EDITIONS FORCE; GRANT USE ON EDITION demo to ; The ``Discount`` function for the demo edition is as follows: .. code-block:: sql connect / alter session set edition = demo; -- Function for the demo edition CREATE OR REPLACE FUNCTION Discount(price IN NUMBER) RETURN NUMBER IS BEGIN return price * 0.5; END; / The Python application can then call the required version of the PL/SQL function as shown: .. code-block:: python connection = oracledb.connect(user=user, password=password, dsn="dbhost.example.com/orclpdb", encoding="UTF-8") print("Edition is:", repr(connection.edition)) cursor = connection.cursor() discounted_price = cursor.callfunc("Discount", int, [100]) print("Price after discount is:", discounted_price) # Use the edition parameter for the connection connection = oracledb.connect(user=user, password=password, dsn="dbhost.example.com/orclpdb", edition="demo", encoding="UTF-8") print("Edition is:", repr(connection.edition)) cursor = connection.cursor() discounted_price = cursor.callfunc("Discount", int, [100]) print("Price after discount is:", discounted_price) The output of the function call for the default and demo edition is as shown:: Edition is: None Price after discount is: 90 Edition is: 'DEMO' Price after discount is: 50 python-oracledb-1.2.1/doc/src/user_guide/soda.rst000066400000000000000000000224331434177474600220010ustar00rootroot00000000000000.. _sodausermanual: ************************************************* Working with Simple Oracle Document Access (SODA) ************************************************* Oracle Database Simple Oracle Document Access (SODA) allows documents to be inserted, queried, and retrieved from Oracle Database using a set of NoSQL-style python-oracledb methods. Documents are generally JSON data, but they can be any data at all (including video, images, sounds, or other binary content). Documents can be fetched from the database by key lookup or by using query-by-example (QBE) pattern-matching. .. note:: SODA is only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. SODA uses a SQL schema to store documents, but you do not need to know SQL or how the documents are stored. However, access through SQL does allow use of advanced Oracle Database functionality such as analytics for reporting. Oracle SODA implementations are also available in `Node.js `__, `Java `__, `PL/SQL `__, `Oracle Call Interface `__ and through `REST `__. For general information on SODA, see the `SODA home page `__ and the Oracle Database `Introduction to Simple Oracle Document Access (SODA) `__ manual. For specific requirements, see the python-oracledb :ref:`SODA requirements `. Python-oracledb uses the following objects for SODA: * :ref:`SODA Database Object `: The top-level object for python-oracledb SODA operations. This is acquired from an Oracle Database connection. A 'SODA database' is an abstraction, allowing access to SODA collections in that 'SODA database', which then allow access to documents in those collections. A SODA database is analogous to an Oracle Database user or schema, a collection is analogous to a table, and a document is analogous to a table row with one column for a unique document key, a column for the document content, and other columns for various document attributes. * :ref:`SODA Collection Object `: Represents a collection of SODA documents. By default, collections allow JSON documents to be stored. This is recommended for most SODA users. However, optional metadata can set various details about a collection, such as its database storage, whether it should track version and time stamp document components, how such components are generated, and what document types are supported. By default, the name of the Oracle Database table storing a collection is the same as the collection name. Note: do not use SQL to drop the database table, since SODA metadata will not be correctly removed. Use the :meth:`SodaCollection.drop()` method instead. * :ref:`SODA Document Object `: Represents a document. Typically the document content will be JSON. The document has properties including the content, a key, timestamps, and the media type. By default, document keys are automatically generated. See :ref:`SODA Document objects ` for the forms of SodaDoc. * :ref:`SODA Document Cursor `: A cursor object representing the result of the :meth:`SodaOperation.getCursor()` method from a :meth:`SodaCollection.find()` operation. It can be iterated over to access each SodaDoc. * :ref:`SODA Operation Object `: An internal object used with :meth:`SodaCollection.find()` to perform read and write operations on documents. Chained methods set properties on a SodaOperation object which is then used by a terminal method to find, count, replace, or remove documents. This is an internal object that should not be directly accessed. SODA Examples ============= Creating and adding documents to a collection can be done as follows: .. code-block:: python soda = connection.getSodaDatabase() # create a new SODA collection; this will open an existing collection, if # the name is already in use collection = soda.createCollection("mycollection") # insert a document into the collection; for the common case of a JSON # document, the content can be a simple Python dictionary which will # internally be converted to a JSON document content = {'name': 'Matilda', 'address': {'city': 'Melbourne'}} returned_doc = collection.insertOneAndGet(content) key = returned_doc.key print('The key of the new SODA document is: ', key) By default, a system generated key is created when documents are inserted. With a known key, you can retrieve a document: .. code-block:: python # this will return a dictionary (as was inserted in the previous code) content = collection.find().key(key).getOne().getContent() print(content) You can also search for documents using query-by-example syntax: .. code-block:: python # Find all documents with names like 'Ma%' print("Names matching 'Ma%'") qbe = {'name': {'$like': 'Ma%'}} for doc in collection.find().filter(qbe).getDocuments(): content = doc.getContent() print(content["name"]) See the `samples directory `__ for runnable SODA examples. .. _sodametadatacache: Using the SODA Metadata Cache ============================= SODA metadata can be cached to improve the performance of :meth:`SodaDatabase.createCollection()` and :meth:`SodaDatabase.openCollection()` by reducing :ref:`round-trips ` to the database. Caching is available with Oracle Client 21.3 (or later). The feature is also available in Oracle Client 19 from 19.11 onwards. The metadata cache can be turned on when creating a connection pool with :meth:`oracledb.create_pool()`. Each pool has its own cache: .. code-block:: python # Create the connection pool pool = oracledb.create_pool(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb", soda_metadata_cache=True) The cache is not available for standalone connections. Applications using these should retain and reuse the :ref:`collection ` returned from ``createCollection()`` or ``openCollection()`` wherever possible, instead of making repeated calls to those methods. The cache is not used by ``createCollection()`` when explicitly passing metadata. In this case, instead of using only ``createCollection()`` and relying on its behavior of opening an existing collection like: .. code-block:: python mymetadata = { . . . } # open an existing collection, or create a new collection collection = soda.createCollection("mycollection", mymetadata) collection.insertOne(mycontent) you will find it more efficient to use logic similar to: .. code-block:: python collection = soda.openCollection("mycollection") if collection is None: mymetadata = { . . . } collection = soda.createCollection("mycollection", mymetadata) collection.insertOne(mycontent) If collection metadata changes are made externally, the cache can become invalid. If this happens, the cache can be cleared by calling :meth:`ConnectionPool.reconfigure()` with ``soda_metadata_cache`` set to False, or by setting the attribute :attr:`ConnectionPool.soda_metadata_cache` to False. Use a second call to ``reconfigure()`` or set ``soda_metadata_cache`` to re-enable the cache. Committing SODA Work ==================== The general recommendation for SODA applications is to turn on :attr:`~Connection.autocommit` globally: .. code-block:: python connection.autocommit = True If your SODA document write operations are mostly independent of each other, this removes the overhead of application transaction management and the need for explicit :meth:`Connection.commit()` calls. When deciding how to commit transactions, beware of transactional consistency and performance requirements. If you are using individual SODA calls to insert or update a large number of documents with individual calls, you should turn :attr:`~Connection.autocommit` off and issue a single, explicit :meth:`~Connection.commit()` after all documents have been processed. Also consider using :meth:`SodaCollection.insertMany()` or :meth:`SodaCollection.insertManyAndGet()` which have performance benefits. If you are not autocommitting, and one of the SODA operations in your transaction fails, then previous uncommitted operations will not be rolled back. Your application should explicitly roll back the transaction with :meth:`Connection.rollback()` to prevent any later commits from committing a partial transaction. Note: - SODA DDL operations do not commit an open transaction the way that SQL always does for DDL statements. - When :attr:`~Connection.autocommit` is True, most SODA methods will issue a commit before successful return. - SODA provides optimistic locking. See :meth:`SodaOperation.version()`. - When mixing SODA and relational access, any commit or rollback on the connection will affect all work. python-oracledb-1.2.1/doc/src/user_guide/sql_execution.rst000066400000000000000000000665061434177474600237460ustar00rootroot00000000000000.. _sqlexecution: ************* Executing SQL ************* Executing SQL statements is the primary way in which a Python application communicates with Oracle Database. Statements are executed using the methods :meth:`Cursor.execute()` or :meth:`Cursor.executemany()`. Statements include queries, Data Manipulation Language (DML), and Data Definition Language (DDL). A few other `specialty statements `__ can also be executed. PL/SQL statements are discussed in :ref:`plsqlexecution`. Other chapters contain information on specific data types and features. See :ref:`batchstmnt`, :ref:`lobdata`, :ref:`jsondatatype`, and :ref:`xmldatatype`. Python-oracledb can be used to execute individual statements, one at a time. It does not read SQL*Plus ".sql" files. To read SQL files, use a technique like the one in ``run_sql_script()`` in `samples/sample_env.py `__ SQL statements should not contain a trailing semicolon (";") or forward slash ("/"). This will fail: .. code-block:: python cur.execute("select * from MyTable;") This is correct: .. code-block:: python cur.execute("select * from MyTable") SQL Queries =========== Queries (statements beginning with SELECT or WITH) can only be executed using the method :meth:`Cursor.execute()`. Rows can then be iterated over, or can be fetched using one of the methods :meth:`Cursor.fetchone()`, :meth:`Cursor.fetchmany()` or :meth:`Cursor.fetchall()`. There is a :ref:`default type mapping ` to Python types that can be optionally :ref:`overridden `. .. IMPORTANT:: Interpolating or concatenating user data with SQL statements, for example ``cur.execute("SELECT * FROM mytab WHERE mycol = '" + myvar + "'")``, is a security risk and impacts performance. Use :ref:`bind variables ` instead. For example, ``cur.execute("SELECT * FROM mytab WHERE mycol = :mybv", mybv=myvar)``. .. _fetching: Fetch Methods ------------- After :meth:`Cursor.execute()`, the cursor is returned as a convenience. This allows code to iterate over rows like: .. code-block:: python cur = connection.cursor() for row in cur.execute("select * from MyTable"): print(row) Rows can also be fetched one at a time using the method :meth:`Cursor.fetchone()`: .. code-block:: python cur = connection.cursor() cur.execute("select * from MyTable") while True: row = cur.fetchone() if row is None: break print(row) If rows need to be processed in batches, the method :meth:`Cursor.fetchmany()` can be used. The size of the batch is controlled by the ``numRows`` parameter, which defaults to the value of :attr:`Cursor.arraysize`. .. code-block:: python cur = connection.cursor() cur.execute("select * from MyTable") num_rows = 10 while True: rows = cur.fetchmany(num_rows) if not rows: break for row in rows: print(row) If all of the rows need to be fetched and can be contained in memory, the method :meth:`Cursor.fetchall()` can be used. .. code-block:: python cur = connection.cursor() cur.execute("select * from MyTable") rows = cur.fetchall() for row in rows: print(row) The fetch methods return data as tuples. To return results as dictionaries, see :ref:`rowfactories`. Closing Cursors --------------- A cursor may be used to execute multiple statements. Once it is no longer needed, it should be closed by calling :meth:`~Cursor.close()` in order to reclaim resources in the database. It will be closed automatically when the variable referencing it goes out of scope (and no further references are retained). One other way to control the lifetime of a cursor is to use a "with" block, which ensures that a cursor is closed once the block is completed. For example: .. code-block:: python with connection.cursor() as cursor: for row in cursor.execute("select * from MyTable"): print(row) This code ensures that once the block is completed, the cursor is closed and resources have been reclaimed by the database. In addition, any attempt to use the variable ``cursor`` outside of the block will simply fail. .. _querymetadata: Query Column Metadata --------------------- After executing a query, the column metadata such as column names and data types can be obtained using :attr:`Cursor.description`: .. code-block:: python cur = connection.cursor() cur.execute("select * from MyTable") for column in cur.description: print(column) This could result in metadata like:: ('ID', , 39, None, 38, 0, 0) ('NAME', , 20, 20, None, None, 1) .. _defaultfetchtypes: Fetch Data Types ---------------- The following table provides a list of all of the data types that python-oracledb knows how to fetch. The middle column gives the type that is returned in the :ref:`query metadata `. The last column gives the type of Python object that is returned by default. Python types can be changed with :ref:`Output Type Handlers `. .. list-table-with-summary:: :header-rows: 1 :class: wy-table-responsive :widths: 1 1 1 :align: left :summary: The first column is the Oracle Database Type. The second column is the oracledb Database Type that is returned in the query metadata. The third column is the type of Python object that is returned by default. * - Oracle Database Type - oracledb Database Type - Default Python type * - BFILE - :attr:`oracledb.DB_TYPE_BFILE` - :ref:`oracledb.LOB ` * - BINARY_DOUBLE - :attr:`oracledb.DB_TYPE_BINARY_DOUBLE` - float * - BINARY_FLOAT - :attr:`oracledb.DB_TYPE_BINARY_FLOAT` - float * - BLOB - :attr:`oracledb.DB_TYPE_BLOB` - :ref:`oracledb.LOB ` * - CHAR - :attr:`oracledb.DB_TYPE_CHAR` - str * - CLOB - :attr:`oracledb.DB_TYPE_CLOB` - :ref:`oracledb.LOB ` * - CURSOR - :attr:`oracledb.DB_TYPE_CURSOR` - :ref:`oracledb.Cursor ` * - DATE - :attr:`oracledb.DB_TYPE_DATE` - datetime.datetime * - INTERVAL DAY TO SECOND - :attr:`oracledb.DB_TYPE_INTERVAL_DS` - datetime.timedelta * - JSON - :attr:`oracledb.DB_TYPE_JSON` - dict, list or a scalar value [4]_ * - LONG - :attr:`oracledb.DB_TYPE_LONG` - str * - LONG RAW - :attr:`oracledb.DB_TYPE_LONG_RAW` - bytes * - NCHAR - :attr:`oracledb.DB_TYPE_NCHAR` - str * - NCLOB - :attr:`oracledb.DB_TYPE_NCLOB` - :ref:`oracledb.LOB ` * - NUMBER - :attr:`oracledb.DB_TYPE_NUMBER` - float or int [1]_ * - NVARCHAR2 - :attr:`oracledb.DB_TYPE_NVARCHAR` - str * - OBJECT [3]_ - :attr:`oracledb.DB_TYPE_OBJECT` - :ref:`oracledb.Object ` * - RAW - :attr:`oracledb.DB_TYPE_RAW` - bytes * - ROWID - :attr:`oracledb.DB_TYPE_ROWID` - str * - TIMESTAMP - :attr:`oracledb.DB_TYPE_TIMESTAMP` - datetime.datetime * - TIMESTAMP WITH LOCAL TIME ZONE - :attr:`oracledb.DB_TYPE_TIMESTAMP_LTZ` - datetime.datetime [2]_ * - TIMESTAMP WITH TIME ZONE - :attr:`oracledb.DB_TYPE_TIMESTAMP_TZ` - datetime.datetime [2]_ * - UROWID - :attr:`oracledb.DB_TYPE_ROWID`, :attr:`oracledb.DB_TYPE_UROWID` - str * - VARCHAR2 - :attr:`oracledb.DB_TYPE_VARCHAR` - str .. [1] If the precision and scale obtained from query column metadata indicate that the value can be expressed as an integer, the value will be returned as an int. If the column is unconstrained (no precision and scale specified), the value will be returned as a float or an int depending on whether the value itself is an integer. In all other cases the value is returned as a float. .. [2] The timestamps returned are naive timestamps without any time zone information present. .. [3] These include all user-defined types such as VARRAY, NESTED TABLE, etc. .. [4] If the JSON is an object, then a dict is returned. If it is an array, then a list is returned. If it is a scalar value, then that particular scalar value is returned. .. _outputtypehandlers: Changing Fetched Data Types with Output Type Handlers ----------------------------------------------------- Sometimes the default conversion from an Oracle Database type to a Python type must be changed in order to prevent data loss or to fit the purposes of the Python application. In such cases, an output type handler can be specified for queries. Output type handlers do not affect values returned from :meth:`Cursor.callfunc()` or :meth:`Cursor.callproc()`. Output type handlers can be specified on the :attr:`connection ` or on the :attr:`cursor `. If specified on the cursor, fetch type handling is only changed on that particular cursor. If specified on the connection, all cursors created by that connection will have their fetch type handling changed. The output type handler is expected to be a function with the following signature:: handler(cursor, name, defaultType, size, precision, scale) The parameters are the same information as the query column metadata found in :attr:`Cursor.description`. The function is called once for each column that is going to be fetched. The function is expected to return a :ref:`variable object ` (generally by a call to :func:`Cursor.var()`) or the value ``None``. The value ``None`` indicates that the default type should be used. Examples of output handlers are shown in :ref:`numberprecision`, :ref:`directlobs` and :ref:`fetching-raw-data`. Also see samples such as `samples/type_handlers.py `__ .. _numberprecision: Fetched Number Precision ------------------------ One reason for using an output type handler is to ensure that numeric precision is not lost when fetching certain numbers. Oracle Database uses decimal numbers and these cannot be converted seamlessly to binary number representations like Python floats. In addition, the range of Oracle numbers exceeds that of floating point numbers. Python has decimal objects which do not have these limitations and python-oracledb knows how to perform the conversion between Oracle numbers and Python decimal values if directed to do so. The following code sample demonstrates the issue: .. code-block:: python cur = connection.cursor() cur.execute("create table test_float (X number(5, 3))") cur.execute("insert into test_float values (7.1)") connection.commit() cur.execute("select * from test_float") val, = cur.fetchone() print(val, "* 3 =", val * 3) This displays ``7.1 * 3 = 21.299999999999997`` Using Python decimal objects, however, there is no loss of precision: .. code-block:: python import decimal def number_to_decimal(cursor, name, default_type, size, precision, scale): if default_type == oracledb.DB_TYPE_NUMBER: return cursor.var(decimal.Decimal, arraysize=cursor.arraysize) cur = connection.cursor() cur.outputtypehandler = number_to_decimal cur.execute("select * from test_float") val, = cur.fetchone() print(val, "* 3 =", val * 3) This displays ``7.1 * 3 = 21.3`` The Python ``decimal.Decimal`` converter gets called with the string representation of the Oracle number. The output from ``decimal.Decimal`` is returned in the output tuple. See `samples/return_numbers_as_decimals.py `__ .. _outconverters: Changing Query Results with Outconverters ----------------------------------------- Python-oracledb "outconverters" can be used with :ref:`output type handlers ` to change returned data. For example, to make queries return empty strings instead of NULLs: .. code-block:: python def out_converter(value): if value is None: return '' return value def output_type_handler(cursor, name, default_type, size, precision, scale): if default_type in (oracledb.DB_TYPE_VARCHAR, oracledb.DB_TYPE_CHAR): return cursor.var(str, size, arraysize=cur.arraysize, outconverter=out_converter) connection.outputtypehandler = output_type_handler .. _rowfactories: Changing Query Results with Rowfactories ---------------------------------------- Python-oracledb "rowfactories" are methods called for each row that is retrieved from the database. The :meth:`Cursor.rowfactory` method is called with the tuple that would normally be returned from the database. The method can convert the tuple to a different value and return it to the application in place of the tuple. For example, to fetch each row of a query as a dictionary: .. code-block:: python cursor.execute("select * from locations where location_id = 1000") columns = [col[0] for col in cursor.description] cursor.rowfactory = lambda *args: dict(zip(columns, args)) data = cursor.fetchone() print(data) The output is:: {'LOCATION_ID': 1000, 'STREET_ADDRESS': '1297 Via Cola di Rie', 'POSTAL_CODE': '00989', 'CITY': 'Roma', 'STATE_PROVINCE': None, 'COUNTRY_ID': 'IT'} If you join tables where the same column name occurs in both tables with different meanings or values, then use a column alias in the query. Otherwise, only one of the similarly named columns will be included in the dictionary: .. code-block:: sql select cat_name, cats.color as cat_color, dog_name, dogs.color from cats, dogs .. _scrollablecursors: Scrollable Cursors ------------------ Scrollable cursors enable applications to move backwards, forwards, to skip rows, and to move to a particular row in a query result set. The result set is cached on the database server until the cursor is closed. In contrast, regular cursors are restricted to moving forward. .. note:: Scrollable cursors are only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. A scrollable cursor is created by setting the parameter ``scrollable=True`` when creating the cursor. The method :meth:`Cursor.scroll()` is used to move to different locations in the result set. Examples are: .. code-block:: python cursor = connection.cursor(scrollable=True) cursor.execute("select * from ChildTable order by ChildId") cursor.scroll(mode="last") print("LAST ROW:", cursor.fetchone()) cursor.scroll(mode="first") print("FIRST ROW:", cursor.fetchone()) cursor.scroll(8, mode="absolute") print("ROW 8:", cursor.fetchone()) cursor.scroll(6) print("SKIP 6 ROWS:", cursor.fetchone()) cursor.scroll(-4) print("SKIP BACK 4 ROWS:", cursor.fetchone()) .. _fetchobjects: Fetching Oracle Database Objects and Collections ------------------------------------------------ Oracle Database named object types and user-defined types can be fetched directly in queries. Each item is represented as a :ref:`Python object ` corresponding to the Oracle Database object. This Python object can be traversed to access its elements. Attributes including :attr:`DbObjectType.name` and :attr:`DbObjectType.iscollection`, and methods including :meth:`DbObject.aslist` and :meth:`DbObject.asdict` are available. For example, if a table ``mygeometrytab`` contains a column ``geometry`` of Oracle's predefined Spatial object type `SDO_GEOMETRY `__, then it can be queried and printed: .. code-block:: python cur.execute("select geometry from mygeometrytab") for obj, in cur: dumpobject(obj) Where ``dumpobject()`` is defined as: .. code-block:: python def dumpobject(obj, prefix = ""): if obj.type.iscollection: print(prefix, "[") for value in obj.aslist(): if isinstance(value, oracledb.Object): dumpobject(value, prefix + " ") else: print(prefix + " ", repr(value)) print(prefix, "]") else: print(prefix, "{") for attr in obj.type.attributes: value = getattr(obj, attr.name) if isinstance(value, oracledb.Object): print(prefix + " " + attr.name + ":") dumpobject(value, prefix + " ") else: print(prefix + " " + attr.name + ":", repr(value)) print(prefix, "}") This might produce output like:: { SDO_GTYPE: 2003 SDO_SRID: None SDO_POINT: { X: 1 Y: 2 Z: 3 } SDO_ELEM_INFO: [ 1 1003 3 ] SDO_ORDINATES: [ 1 1 5 7 ] } Other information on using Oracle objects is in :ref:`Using Bind Variables `. Performance-sensitive applications should consider using scalar types instead of objects. If you do use objects, avoid calling :meth:`Connection.gettype()` unnecessarily, and avoid objects with large numbers of attributes. .. _rowlimit: Limiting Rows ------------- Query data is commonly broken into one or more sets: - To give an upper bound on the number of rows that a query has to process, which can help improve database scalability. - To perform 'Web pagination' that allows moving from one set of rows to a next, or previous, set on demand. - For fetching of all data in consecutive small sets for batch processing. This happens because the number of records is too large for Python to handle at one time. The latter can be handled by calling :meth:`Cursor.fetchmany()` with one execution of the SQL query. 'Web pagination' and limiting the maximum number of rows are detailed in this section. For each 'page' of results, a SQL query is executed to get the appropriate set of rows from a table. Since the query may be executed more than once, ensure to use :ref:`bind variables ` for row numbers and row limits. Oracle Database 12c SQL introduced an ``OFFSET`` / ``FETCH`` clause which is similar to the ``LIMIT`` keyword of MySQL. In Python, you can fetch a set of rows using: .. code-block:: python myoffset = 0 // do not skip any rows (start at row 1) mymaxnumrows = 20 // get 20 rows sql = """SELECT last_name FROM employees ORDER BY last_name OFFSET :offset ROWS FETCH NEXT :maxnumrows ROWS ONLY""" cur = connection.cursor() for row in cur.execute(sql, offset=myoffset, maxnumrows=mymaxnumrows): print(row) In applications where the SQL query is not known in advance, this method sometimes involves appending the ``OFFSET`` clause to the 'real' user query. Be very careful to avoid SQL injection security issues. For Oracle Database 11g and earlier there are several alternative ways to limit the number of rows returned. The old, canonical paging query is:: SELECT * FROM (SELECT a.*, ROWNUM AS rnum FROM (YOUR_QUERY_GOES_HERE -- including the order by) a WHERE ROWNUM <= MAX_ROW) WHERE rnum >= MIN_ROW Here, ``MIN_ROW`` is the row number of first row and ``MAX_ROW`` is the row number of the last row to return. For example:: SELECT * FROM (SELECT a.*, ROWNUM AS rnum FROM (SELECT last_name FROM employees ORDER BY last_name) a WHERE ROWNUM <= 20) WHERE rnum >= 1 This always has an 'extra' column, here called RNUM. An alternative and preferred query syntax for Oracle Database 11g uses the analytic ``ROW_NUMBER()`` function. For example, to get the 1st to 20th names the query is:: SELECT last_name FROM (SELECT last_name, ROW_NUMBER() OVER (ORDER BY last_name) AS myr FROM employees) WHERE myr BETWEEN 1 and 20 Ensure to use :ref:`bind variables ` for the upper and lower limit values. .. _crc: Client Result Cache ------------------- Python-oracledb applications can use Oracle Database's `Client Result Cache `__ The CRC enables client-side caching of SQL query (SELECT statement) results in client memory for immediate use when the same query is re-executed. This is useful for reducing the cost of queries for small, mostly static, lookup tables, such as for postal codes. CRC reduces network :ref:`round-trips `, and also reduces database server CPU usage. .. note:: Client Result Caching is only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. The cache is at the application process level. Access and invalidation is managed by the Oracle Client libraries. This removes the need for extra application logic, or external utilities, to implement a cache. CRC can be enabled by setting the `database parameters `__ ``CLIENT_RESULT_CACHE_SIZE`` and ``CLIENT_RESULT_CACHE_LAG``, and then restarting the database. For example, to set the parameters: .. code-block:: sql SQL> ALTER SYSTEM SET CLIENT_RESULT_CACHE_LAG = 3000 SCOPE=SPFILE; SQL> ALTER SYSTEM SET CLIENT_RESULT_CACHE_SIZE = 64K SCOPE=SPFILE; CRC can alternatively be configured in an :ref:`oraaccess.xml ` or :ref:`sqlnet.ora ` file on the Python host, see `Client Configuration Parameters `__. Tables can then be created, or altered, so repeated queries use CRC. This allows existing applications to use CRC without needing modification. For example: .. code-block:: sql SQL> CREATE TABLE cities (id number, name varchar2(40)) RESULT_CACHE (MODE FORCE); SQL> ALTER TABLE locations RESULT_CACHE (MODE FORCE); Alternatively, hints can be used in SQL statements. For example: .. code-block:: sql SELECT /*+ result_cache */ postal_code FROM locations .. _fetching-raw-data: Fetching Raw Data ----------------- Sometimes python-oracledb may have problems converting data stored in the database to Python strings. This can occur if the data stored in the database does not match the character set defined by the database. The ``encoding_errors`` parameter to :meth:`Cursor.var()` permits the data to be returned with some invalid data replaced, but for additional control the parameter ``bypass_decode`` can be set to True and python-oracledb will bypass the decode step and return `bytes` instead of `str` for data stored in the database as strings. The data can then be examined and corrected as required. This approach should only be used for troubleshooting and correcting invalid data, not for general use! The following sample demonstrates how to use this feature: .. code-block:: python # define output type handler def return_strings_as_bytes(cursor, name, default_type, size, precision, scale): if default_type == oracledb.DB_TYPE_VARCHAR: return cursor.var(str, arraysize=cursor.arraysize, bypass_decode=True) # set output type handler on cursor before fetching data with connection.cursor() as cursor: cursor.outputtypehandler = return_strings_as_bytes cursor.execute("select content, charset from SomeTable") data = cursor.fetchall() This will produce output as:: [(b'Fianc\xc3\xa9', b'UTF-8')] Note that last ``\xc3\xa9`` is é in UTF-8. Since this is valid UTF-8 you can then perform a decode on the data (the part that was bypassed): .. code-block:: python value = data[0][0].decode("UTF-8") This will return the value "Fiancé". If you want to save ``b'Fianc\xc3\xa9'`` into the database directly without using a Python string, you will need to create a variable using :meth:`Cursor.var()` that specifies the type as :data:`~oracledb.DB_TYPE_VARCHAR` (otherwise the value will be treated as :data:`~oracledb.DB_TYPE_RAW`). The following sample demonstrates this: .. code-block:: python with oracledb.connect(user="hr", password=userpwd, dsn="dbhost.example.com/orclpdb") as conn: with conn.cursor() cursor: var = cursor.var(oracledb.DB_TYPE_VARCHAR) var.setvalue(0, b"Fianc\xc4\x9b") cursor.execute(""" update SomeTable set SomeColumn = :param where id = 1""", param=var) .. warning:: The database will assume that the bytes provided are in the character set expected by the database so only use this for troubleshooting or as directed. .. _codecerror: Querying Corrupt Data --------------------- If queries fail with the error "codec can't decode byte" when you select data, then: * Check if your :ref:`character set ` is correct. Review the :ref:`database character sets `. Check with :ref:`fetching-raw-data`. Note that the encoding used for all character data in python-oracledb is "UTF-8". * Check for corrupt data in the database. If data really is corrupt, you can pass options to the internal `decode() `__ used by python-oracledb to allow it to be selected and prevent the whole query failing. Do this by creating an :ref:`outputtypehandler ` and setting ``encoding_errors``. For example to replace corrupt characters in character columns: .. code-block:: python def output_type_handler(cursor, name, default_type, size, precision, scale): if default_type == oracledb.DB_TYPE_VARCHAR: return cursor.var(default_type, size, arraysize=cursor.arraysize, encoding_errors="replace") cursor.outputtypehandler = output_type_handler cursor.execute("select column1, column2 from SomeTableWithBadData") Other codec behaviors can be chosen for ``encoding_errors``, see `Error Handlers `__. .. _dml: INSERT and UPDATE Statements ============================ SQL Data Manipulation Language statements (DML) such as INSERT and UPDATE can easily be executed with python-oracledb. For example: .. code-block:: python cur = connection.cursor() cur.execute("insert into MyTable values (:idbv, :nmbv)", [1, "Fredico"]) Do not concatenate or interpolate user data into SQL statements. See :ref:`bind` instead. See :ref:`txnmgmnt` for best practices on committing and rolling back data changes. When handling multiple data values, use :meth:`~Cursor.executemany()` for performance. See :ref:`batchstmnt` Inserting NULLs --------------- Oracle requires a type, even for null values. When you pass the value None, then python-oracledb assumes the type is STRING. If this is not the desired type, you can explicitly set it. For example, to insert a null :ref:`Oracle Spatial SDO_GEOMETRY ` object: .. code-block:: python type_obj = connection.gettype("SDO_GEOMETRY") cur = connection.cursor() cur.setinputsizes(type_obj) cur.execute("insert into sometable values (:1)", [None]) python-oracledb-1.2.1/doc/src/user_guide/startup.rst000066400000000000000000000045421434177474600225560ustar00rootroot00000000000000.. _startup: ************************************* Starting and Stopping Oracle Database ************************************* This chapter covers how to start up and shut down Oracle Database using python-oracledb. .. note:: Database start up and shut down functionality is only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. =========================== Starting Oracle Database Up =========================== Python-oracledb can start up a database instance. A privileged connection is required. This example shows a script that could be run as the 'oracle' operating system user who administers a local database installation on Linux. It assumes that the environment variable ``ORACLE_SID`` has been set to the SID of the database that should be started: .. code-block:: python # the connection must be in PRELIM_AUTH mode to perform startup connection = oracledb.connect(mode=oracledb.SYSDBA | oracledb.PRELIM_AUTH) connection.startup() # the following statements must be issued in normal SYSDBA mode connection = oracledb.connect(mode=oracledb.SYSDBA, encoding="UTF-8") cursor = connection.cursor() cursor.execute("alter database mount") cursor.execute("alter database open") To start up a remote database, you may need to configure the Oracle Net listener to use `static service registration `_ by adding a ``SID_LIST_LISTENER`` entry to the database `listener.ora` file. ============================= Shutting Oracle Database Down ============================= Python-oracledb has the ability to shut down the database using a privileged connection. This example also assumes that the environment variable ``ORACLE_SID`` has been set: .. code-block:: python # need to connect as SYSDBA or SYSOPER connection = oracledb.connect(mode=oracledb.SYSDBA) # first shutdown() call must specify the mode, if DBSHUTDOWN_ABORT is used, # there is no need for any of the other steps connection.shutdown(mode=oracledb.DBSHUTDOWN_IMMEDIATE) # now close and dismount the database cursor = connection.cursor() cursor.execute("alter database close normal") cursor.execute("alter database dismount") # perform the final shutdown call connection.shutdown(mode=oracledb.DBSHUTDOWN_FINAL) python-oracledb-1.2.1/doc/src/user_guide/tracing.rst000066400000000000000000000306731434177474600225070ustar00rootroot00000000000000.. _tracingsql: *********************** Tracing python-oracledb *********************** .. _applntracing: Application Tracing =================== There are multiple approaches for application tracing and monitoring: - :ref:`End-to-end database tracing ` attributes such as :attr:`Connection.action` and :attr:`Connection.module` are supported in the python-oracledb Thin and Thick modes. Using these attributes is recommended since they aid application monitoring and troubleshooting. - You can :ref:`subclass python-oracledb classes ` and implement your own driver API call tracing and logging. Also, the standard `Python tracing capabilities `__ can be used. - The Java Debug Wire Protocol (JDWP) for debugging PL/SQL can be used. See :ref:`jdwp`. - Python-oracledb in Thick mode can dump a trace of SQL statements executed. See :ref:`lowlevelsqltrace`. .. _endtoendtracing: Oracle Database End-to-End Tracing ---------------------------------- Oracle Database end-to-end application tracing simplifies diagnosing application code flow and performance problems in multi-tier or multi-user environments. The connection attributes, :attr:`~Connection.client_identifier`, :attr:`~Connection.clientinfo`, :attr:`~Connection.dbop`, :attr:`~Connection.module` and :attr:`~Connection.action`, set the metadata for end-to-end tracing. You can query data dictionary and dynamic performance views to monitor applications, or you can use tracing utilities. After attributes are set, the values are sent to the database when the next :ref:`round-trip ` to the database occurs, for example when the next SQL statement is executed. The attribute values will remain set in connections released back to a connection pool. When the application re-acquires a connection from the pool, it should initialize the values to a desired state before using that connection. The example below shows setting the action, module, and client identifier attributes on a connection object, and then querying a view to see the recorded values. The example both sets and queries the values, but typically monitoring is done externally to the application. .. code-block:: python # Set the tracing metadata connection.client_identifier = "pythonuser" connection.action = "Query Session tracing parameters" connection.module = "End-to-end Demo" for row in cursor.execute(""" SELECT username, client_identifier, module, action FROM V$SESSION WHERE SID = SYS_CONTEXT('USERENV', 'SID')"""): print(row) The output will be like:: ('SYSTEM', 'pythonuser', 'End-to-end Demo', 'Query Session tracing parameters') The values can also be manually set as shown by calling `DBMS_APPLICATION_INFO procedures `__ or `DBMS_SESSION.SET_IDENTIFIER `__. These incur round-trips to the database; however, reducing scalability. .. code-block:: sql BEGIN DBMS_SESSION.SET_IDENTIFIER('pythonuser'); DBMS_APPLICATION_INFO.set_module('End-to-End Demo'); DBMS_APPLICATION_INFO.set_action(action_name => 'Query Session tracing parameters'); END; The value of :attr:`Connection.dbop` will be shown in the ``DBOP_NAME`` column of the ``V$SQL_MONITOR`` table: .. code-block:: python connection.dbop = "my op" for row in cursor.execute(""" SELECT dbop_name FROM v$sql_monitor WHERE SID = SYS_CONTEXT('USERENV', 'SID')"""): print(row) .. _subclassconn: Subclassing Connections ----------------------- Subclassing enables applications to add "hooks" for connection and statement execution. This can be used to alter or log connection and execution parameters, and to extend python-oracledb functionality. The example below demonstrates subclassing a connection to log SQL execution to a file. This example also shows how connection credentials can be embedded in the custom subclass, so application code does not need to supply them. .. code-block:: python class Connection(oracledb.Connection): log_file_name = "log.txt" def __init__(self): connect_string = "hr/hr_password@dbhost.example.com/orclpdb" self._log("Connect to the database") return super(Connection, self).__init__(connect_string) def _log(self, message): with open(self.log_file_name, "a") as f: print(message, file=f) def execute(self, sql, parameters): self._log(sql) cursor = self.cursor() try: return cursor.execute(sql, parameters) except oracledb.Error as e: error_obj, = e.args self._log(error_obj.message) raise connection = Connection() connection.execute(""" select department_name from departments where department_id = :id""", dict(id=270)) The messages logged in ``log.txt`` are:: Connect to the database select department_name from departments where department_id = :id If an error occurs, perhaps due to a missing table, the log file would contain instead:: Connect to the database select department_name from departments where department_id = :id ORA-00942: table or view does not exist In production applications, be careful not to log sensitive information. See `Subclassing.py `__ for an example. .. _jdwp: Debugging PL/SQL with the Java Debug Wire Protocol -------------------------------------------------- The Java Debug Wire Protocol (JDWP) for debugging PL/SQL can be used with python-oracledb. Python-oracledb applications that call PL/SQL can step through that PL/SQL code using JDWP in a debugger. This allows Python and PL/SQL code to be debugged in the same debugger environment. You can enable PL/SQL debugging in the python-oracledb modes as follows: - If you are using python-oracledb Thick mode, set the ``ORA_DEBUG_JDWP`` environment variable to `host=hostname;port=portnum` indicating where the PL/SQL debugger is running. Then run the application. - In the python-oracledb Thin mode, you can additionally set the connection parameter ``debug_jdwp`` during connection. This variable defaults to the value of the ``ORA_DEBUG_JDWP`` environment variable. See `DBMS_DEBUG_JDWP `_ and `Debugging PL/SQL from ASP.NET and Visual Studio `_. .. _lowlevelsqltrace: Low Level SQL Tracing --------------------- The Thick mode of python-oracledb is implemented using the `ODPI-C `__ wrapper on top of the Oracle Client libraries. The ODPI-C tracing capability can be used to log executed python-oracledb statements to the standard error stream. Before executing Python, set the environment variable ``DPI_DEBUG_LEVEL`` to 16. At a Windows command prompt, this could be done with:: set DPI_DEBUG_LEVEL=16 On Linux, you might use:: export DPI_DEBUG_LEVEL=16 After setting the variable, run the Python Script, for example on Linux:: python end-to-endtracing.py 2> log.txt For an application that does a single query, the log file might contain a tracing line consisting of the prefix 'ODPI', a thread identifier, a timestamp, and the SQL statement executed:: ODPI [26188] 2019-03-26 09:09:03.909: ODPI-C 3.1.1 ODPI [26188] 2019-03-26 09:09:03.909: debugging messages initialized at level 16 ODPI [26188] 2019-03-26 09:09:09.917: SQL SELECT * FROM jobss Traceback (most recent call last): File "end-to-endtracing.py", line 14, in cursor.execute("select * from jobss") oracledb.DatabaseError: ORA-00942: table or view does not exist See `ODPI-C Debugging `__ for documentation on ``DPI_DEBUG_LEVEL``. .. _vsessconinfo: Finding the Python-oracledb Mode ================================ The boolean attributes :attr:`Connection.thin` and :attr:`ConnectionPool.thin` can be used to show the current mode of a python-oracledb connection or pool, respectively. The python-oracledb version can be shown with :data:`oracledb.__version__`. The information can also be seen in the Oracle Database data dictionary table ``V$SESSION_CONNECT_INFO``: .. code-block:: python with connection.cursor() as cursor: sql = """SELECT UNIQUE CLIENT_DRIVER FROM V$SESSION_CONNECT_INFO WHERE SID = SYS_CONTEXT('USERENV', 'SID')""" for r, in cursor.execute(sql): print(r) In the python-oracledb Thin mode, the output will be:: python-oracledb thn : 1.0.0 In the python-oracledb Thick mode, the output will be:: python-oracledb thk : 1.0.0 The ``CLIENT_DRIVER`` values is configurable in the python-oracledb Thick mode with a call like ``init_oracle_client(driver_name='myapp : 2.0.0')``. See :ref:`otherinit`. .. _dbviews: Database Views ============== This section shows some sample column values for database views. Other views also contain useful information, such as the DRCP views discussed in :ref:`monitoringdrcp`. ``V$SESSION_CONNECT_INFO`` -------------------------- The following table lists sample values for some `V$SESSION_CONNECT_INFO `__ columns: .. list-table-with-summary:: Sample V$SESSION_CONNECT_INFO column values :header-rows: 1 :class: wy-table-responsive :widths: 15 10 10 :name: V$SESSION_CONNECT_INFO :summary: The first column is the name of V$SESSION_CONNECT_INFO view's column. The second column lists a sample python-oracledb Thick mode value. The third column list a sample python-oracledb Thin mode value. * - Column - Thick value - Thin value * - CLIENT_OCI_LIBRARY - The Oracle Client or Instant Client type, such as "Full Instant Client" - "Unknown" * - CLIENT_VERSION - The Oracle Client library version number - "1.0.0.0.0" (the python-oracledb version number with an extra .0.0) * - CLIENT_DRIVER - "python-oracledb thk : 1.0.0" - "python-oracledb thn : 1.0.0" ``V$SESSION`` ------------- The following table list sample values for columns with differences in `V$SESSION `__. .. list-table-with-summary:: Sample V$SESSION column values :header-rows: 1 :class: wy-table-responsive :widths: 15 10 10 :name: V$SESSION_COLUMN_VALUES :summary: The first column is the name of the column. The second column lists a sample python-oracledb Thick mode value. The third column lists a sample python-oracledb Thin mode value. * - Column - Thick value - Thin value * - TERMINAL - similar to `ttys001` - the string "unknown" * - PROGRAM - similar to `python@myuser-mac2 (TNS V1-V3)` - the contents of Python's ``sys.executable``, such as `/Users/myuser/.pyenv/versions/3.9.6/bin/python` * - MODULE - similar to `python@myuser-mac2 (TNS V1-V3)` - the contents of Python's ``sys.executable``, such as `/Users/myuser/.pyenv/versions/3.9.6/bin/python` The ``MODULE`` column value can be set as shown in :ref:`endtoendtracing`. Low Level Python-oracledb Driver Tracing ======================================== Low level tracing is mostly useful to maintainers of python-oracledb. - For the python-oracledb Thin mode, packets can be traced by setting the environment variable:: PYO_DEBUG_PACKETS=1 Output goes to stdout. The logging is similar to an Oracle Net trace of level 16. - The python-oracledb Thick mode can be traced using: - dpi_debug_level as documented in `ODPI-C Debugging `__. - Oracle Call Interface (OCI) tracing as directed by Oracle Support. - Oracle Net services tracing as documented in `Oracle Net Services Tracing Parameters `__ python-oracledb-1.2.1/doc/src/user_guide/tuning.rst000066400000000000000000000516421434177474600223630ustar00rootroot00000000000000.. _tuning: *********************** Tuning python-oracledb *********************** Some general tuning tips are: * Tune your application architecture. A general application goal is to reduce the number of :ref:`round-trips ` between python-oracledb and the database. For multi-user applications, make use of connection pooling. Create the pool once during application initialization. Do not oversize the pool, see :ref:`connpooling` . Use a session callback function to set session state, see :ref:`Session CallBacks for Setting Pooled Connection State `. Make use of efficient python-oracledb functions. For example, to insert multiple rows use :meth:`Cursor.executemany()` instead of :meth:`Cursor.execute()`. * Tune your SQL statements. See the `SQL Tuning Guide `__. Use :ref:`bind variables ` to avoid statement reparsing. Tune :attr:`Cursor.arraysize` and :attr:`Cursor.prefetchrows` for each query, see :ref:`Tuning Fetch Performance `. Do simple optimizations like :ref:`limiting the number of rows ` and avoiding selecting columns not used in the application. It may be faster to work with simple scalar relational values than to use Oracle Database object types. Make good use of PL/SQL to avoid executing many individual statements from python-oracledb. Tune the :ref:`Statement Cache `. Enable :ref:`Client Result Caching ` for small lookup tables. * Tune your database. See the `Database Performance Tuning Guide `__. * Tune your network. For example, when inserting or retrieving a large number of rows (or for large data), or when using a slow network, then tune the Oracle Network Session Data Unit (SDU) and socket buffer sizes, see `Oracle Net Services: Best Practices for Database Performance and High Availability `__. * Do not commit or rollback unnecessarily. Use :attr:`Connection.autocommit` on the last of a sequence of DML statements. .. _tuningfetch: Tuning Fetch Performance ======================== To tune queries, you can adjust python-oracledb's internal buffer sizes to improve the speed of fetching rows across the network from the database, and to optimize memory usage. Regardless of which :ref:`python-oracledb method ` is used to get query results, internally all rows are fetched in batches from the database and buffered before being returned to the application. The internal buffer sizes can have a significant performance impact. The buffer sizes do not affect how or when rows are returned to your application. They do not affect the minimum or maximum number of rows returned by a query. For best performance, tune "array fetching" with :attr:`Cursor.arraysize` and "row prefetching" with :attr:`Cursor.prefetchrows` before calling :meth:`Cursor.execute()`. Queries that return LOBs and similar types will never prefetch rows, so the ``prefetchrows`` value is ignored in those cases. The common query tuning scenario is for SELECT statements that return a large number of rows over a slow network. Increasing ``arraysize`` can improve performance by reducing the number of :ref:`round-trips ` to the database. However increasing this value increases the amount of memory required. Adjusting ``prefetchrows`` will also affect performance and memory usage. Row prefetching and array fetching are both internal buffering techniques to reduce :ref:`round-trips ` to the database. The difference is when the buffering occurs. Internally python-oracledb performs separate "execute SQL statement" and "fetch data" steps. Prefetching allows query results to be returned to the application when the acknowledgment of successful statement execution is returned from the database. This means that the subsequent internal "fetch data" operation does not always need to make a round-trip to the database because rows are already buffered in python-oracledb or in the Oracle Client libraries. Reducing round-trips helps performance and scalability. An overhead of prefetching when using the python-oracledb Thick mode is the need for additional data copies from Oracle Client's prefetch buffer. Choosing values for ``arraysize`` and ``prefetchrows`` ------------------------------------------------------ The best :attr:`Cursor.arraysize` and :attr:`Cursor.prefetchrows` values can be found by experimenting with your application under the expected load of normal application use. This is because the cost of the extra memory copy from the prefetch buffer when fetching a large quantity of rows or very "wide" rows may outweigh the cost of a round-trip for a single python-oracledb user on a fast network. However under production application load, the reduction of round-trips may help performance and overall system scalability. The documentation in :ref:`round-trips ` shows how to measure round-trips. Here are some suggestions for tuning: * To tune queries that return an unknown number of rows, estimate the number of rows returned and start with an appropriate :attr:`Cursor.arraysize` value. The default is 100. Then set :attr:`Cursor.prefetchrows` to the ``arraysize`` value. For example: .. code-block:: python cur = connection.cursor() cur.prefetchrows = 1000 cur.arraysize = 1000 for row in cur.execute("SELECT * FROM very_big_table"): print(row) Adjust the values as needed for performance, memory and round-trip usage. Do not make the sizes unnecessarily large. For a large quantity of rows or very "wide" rows on fast networks you may prefer to leave ``prefetchrows`` at its default value of 2. Keep ``arraysize`` as big, or bigger than, ``prefetchrows``. * If you are fetching a fixed number of rows, set ``arraysize`` to the number of expected rows, and set ``prefetchrows`` to one greater than this value. Adding one removes the need for a round-trip to check for end-of-fetch. For example, if you are querying 20 rows, perhaps to :ref:`display a page ` of data, then set ``prefetchrows`` to 21 and ``arraysize`` to 20: .. code-block:: python cur = connection.cursor() cur.prefetchrows = 21 cur.arraysize = 20 for row in cur.execute(""" SELECT last_name FROM employees ORDER BY last_name OFFSET 0 ROWS FETCH NEXT 20 ROWS ONLY"""): print(row) This will return all rows for the query in one round-trip. * If you know that a query returns just one row then set :attr:`Cursor.arraysize` to 1 to minimize memory usage. The default prefetch value of 2 allows minimal round-trips for single-row queries: .. code-block:: python cur = connection.cursor() cur.arraysize = 1 cur.execute("select * from MyTable where id = 1"): row = cur.fetchone() print(row) Application Default Prefetchrows and Arraysize Values +++++++++++++++++++++++++++++++++++++++++++++++++++++ Application-wide defaults can be set using :attr:`defaults.prefetchrows` and :attr:`defaults.arraysize`, for example: .. code-block:: python import oracledb oracledb.defaults.prefetchrows = 1000 oracledb.defaults.arraysize = 1000 When using python-oracledb in the Thick mode, prefetching can also be tuned in an external :ref:`oraaccess.xml ` file, which may be useful for tuning an application when modifying its code is not feasible. Setting the sizes with ``oracledb.defaults`` attributes or with ``oraaccess.xml`` will affect the whole application, so it should not be the first tuning choice. Changing Prefetchrows and Arraysize for Re-executed Statements ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ In python-oracledb, the ``arraysize`` and ``prefetchrows`` values are only examined when a statement is executed the first time. To change the values for a re-executed statement, create a new cursor. For example, to change ``arraysize``: .. code-block:: python array_sizes = (10, 100, 1000) for size in array_sizes: cursor = connection.cursor() cursor.arraysize = size start = time.time() cursor.execute(sql).fetchall() elapsed = time.time() - start print("Time for", size, elapsed, "seconds") Avoiding Premature Prefetching ++++++++++++++++++++++++++++++ There are two cases that will benefit from setting ``prefetchrows`` to 0: * When passing REF CURSORS into PL/SQL packages. Setting ``prefetchrows`` to 0 can stop rows being prematurely (and silently) fetched into the python-oracledb or Oracle Client (in python-oracledb Thick mode) internal buffer, making those rows unavailable to the PL/SQL code that receives the REF CURSOR. * When querying a PL/SQL function that uses PIPE ROW to emit rows at intermittent intervals. By default, several rows needs to be emitted by the function before python-oracledb can return them to the application. Setting ``prefetchrows`` to 0 helps give a consistent flow of data to the application. Tuning Data Copies between Databases ------------------------------------ One place where increasing ``arraysize`` is particularly useful is in copying data from one database to another: .. code-block:: python # setup cursors source_cursor = source_connection.cursor() source_cursor.arraysize = 1000 target_cursor = target_connection.cursor() # perform fetch and bulk insertion source_cursor.execute("select * from MyTable") while True: rows = source_cursor.fetchmany() if not rows: break target_cursor.executemany("insert into MyTable values (:1, :2)", rows) target_connection.commit() Note that it may be preferable to use database links between the databases and use an INSERT INTO SELECT statement so that data is not copied to, and from, Python. Tuning Fetching from REF CURSORS -------------------------------- In python-oracledb, REF CURSORS can also be tuned by setting the values of ``arraysize`` and ``prefetchrows``. The ``prefetchrows`` value must be set before calling the PL/SQL procedure as the REF CURSOR is executed on the server. For example: .. code-block:: python # Set the arraysize and prefetch rows of the REF cursor ref_cursor = connection.cursor() ref_cursor.prefetchrows = 1000 ref_cursor.arraysize = 1000 # Perform the tuned fetch sum_rows = 0 cursor.callproc("myrefcursorproc", [ref_cursor]) print("Sum of IntCol for", num_rows, "rows:") for row in ref_cursor: sum_rows += row[0] print(sum_rows) .. _roundtrips: Database Round-trips ==================== A round-trip is defined as the trip from the Oracle Client libraries (used by python-oracledb) to the database and back. Calling each python-oracledb function, or accessing each attribute, will require zero or more round-trips. Along with tuning an application's architecture and `tuning its SQL statements `__, a general performance and scalability goal is to minimize `round-trips `__. Some general tips for reducing round-trips are: * Tune :attr:`Cursor.arraysize` and :attr:`Cursor.prefetchrows` for each query. * Use :meth:`Cursor.executemany()` for optimal DML execution. * Only commit when necessary. Use :attr:`Connection.autocommit` on the last statement of a transaction. * For connection pools, use a callback to set connection state, see :ref:`Session CallBacks for Setting Pooled Connection State `. * Make use of PL/SQL procedures which execute multiple SQL statements instead of executing them individually from python-oracledb. * Use scalar types instead of Oracle Database object types. * Avoid overuse of :meth:`Connection.ping()`. * Avoid setting :attr:`ConnectionPool.ping_interval` to 0 or a small value. * When using :ref:`SODA `, use pooled connections and enable the :ref:`SODA metadata cache `. Finding the Number of Round-Trips ---------------------------------- Oracle's `Automatic Workload Repository `__ (AWR) reports show 'SQL*Net roundtrips to/from client' and are useful for finding the overall behavior of a system. Sometimes you may wish to find the number of round-trips used for a specific application. Snapshots of the ``V$SESSTAT`` view taken before and after doing some work can be used for this: .. code-block:: sql SELECT ss.value, sn.display_name FROM v$sesstat ss, v$statname sn WHERE ss.sid = SYS_CONTEXT('USERENV','SID') AND ss.statistic# = sn.statistic# AND sn.name LIKE '%roundtrip%client%'; .. _stmtcache: Statement Caching ================= Python-oracledb's :meth:`Cursor.execute()` and :meth:`Cursor.executemany()` functions use the `Oracle Call Interface statement cache `__ for efficient re-execution of statements. Statement caching lets Oracle Database cursors be used without re-parsing the statement. Statement caching also reduces metadata transfer costs between python-oracledb and the database. Performance and scalability are improved. Each standalone or pooled connection has its own cache of statements with a default size of 20. The default size of the statement cache can be changed using the :attr:`defaults.stmtcachesize` attribute. The size can be set when creating connection pools or standalone connections. In general, set the statement cache size to the size of the working set of statements being executed by the application. To manually tune the cache, monitor the general application load and the `Automatic Workload Repository `__ (AWR) "bytes sent via SQL*Net to client" values. The latter statistic should benefit from not shipping statement metadata to python-oracledb. Adjust the statement cache size to your satisfaction. With Oracle Database 12c, or later, the statement cache size can be automatically tuned using an :ref:`oraaccess.xml ` file. Setting the Statement Cache --------------------------- The statement cache size can be set globally with :attr:`defaults.stmtcachesize`: .. code-block:: python import oracledb oracledb.defaults.stmtcachesize = 40 The value can be overridden in an :meth:`oracledb.connect()` call, or when creating a pool with :meth:`oracledb.create_pool()`. For example: .. code-block:: python oracledb.create_pool(user="scott", password=userpwd, dsn="dbhost.example.com/orclpb", min=2, max=5, increment=1, stmtcachesize=50) When using Oracle Client 21 (or later), changing the cache size with :meth:`ConnectionPool.reconfigure()` does not immediately affect connections previously acquired and currently in use. When those connections are subsequently released to the pool and re-acquired, they will then use the new value. When using Oracle Client prior to version 21, changing the pool's statement cache size has no effect on connections that already exist in the pool but will affect new connections that are subsequently created, for example when the pool grows. Tuning the Statement Cache -------------------------- In general, set the statement cache to the size of the working set of statements being executed by the application. :ref:`SODA ` internally makes SQL calls, so tuning the cache is also beneficial for SODA applications. With Oracle Client Libraries 12c, or later, the statement cache size can be automatically tuned with the Oracle Client Configuration oraaccess.xml file. For manual tuning use views like V$SYSSTAT: .. code-block:: sql SELECT value FROM V$SYSSTAT WHERE name = 'parse count (total)' Find the value before and after running application load to give the number of statement parses during the load test. Alter the statement cache size and repeat the test until you find a minimal number of parses. If you have Automatic Workload Repository (AWR) reports you can monitor general application load and the "bytes sent via SQL*Net to client" values. The latter statistic should benefit from not shipping statement metadata to python-oracledb. Adjust the statement cache size and re-run the test to find the best cache size. Disabling the Statement Cache ----------------------------- Statement caching can be disabled by setting the cache size to 0: .. code-block:: python oracledb.stmtCacheSize = 0 Disabling the cache may be beneficial when the quantity or order of statements causes cache entries to be flushed before they get a chance to be reused. For example if there are more distinct statements than cache slots, and the order of statement execution causes older statements to be flushed from the cache before the statements are re-executed. Disabling the statement cache may also be helpful in test and development environments. The statement cache can become invalid if connections remain open and database schema objects are recreated. This can also happen when a connection uses identical query text with different ``fetchAsString`` or ``fetchInfo`` data types. Applications can receive errors such as ORA-3106. After a statement execution error is returned once to the application, python-oracledb automatically drops that statement from the cache. This lets subsequent re-executions of the statement on that connection to succeed. When it is inconvenient to pass statement text through an application, the :meth:`Cursor.prepare()` call can be used to avoid statement re-parsing. If the ``cache_statement`` parameter in the :meth:`Cursor.prepare()` method is True and the statement cache size is greater than 0, then the statements will be added to the cache, if not already present. If the ``cache_statement`` parameter in the :meth:`Cursor.prepare()` method is False and the statement cache size is greater than 0, then the statement will be removed from the statement cache (if present) or will not be cached (if not present). The subsequent ``execute()`` calls use the value None instead of the SQL text. This feature can prevent a rarely executed statement from flushing a potential more frequently executed one from a full cache. For example, if a statement will only ever be executed once: .. code-block:: python cursor.prepare("select user from dual", cache_statement = False) cursor.execute(None) Alternatively, .. code-block:: python sql = "select user from dual" cursor.prepare(sql, cache_statement=Fasle) cursor.execute(sql) Statements passed to :meth:`~Cursor.prepare()` are also stored in the statement cache. .. _clientresultcache: Client Result Caching (CRC) =========================== Python-oracledb applications can use Oracle Database's `Client Result Cache `__. The CRC enables client-side caching of SQL query (SELECT statement) results in client memory for immediate use when the same query is re-executed. This is useful for reducing the cost of queries for small, mostly static, lookup tables, such as for postal codes. CRC reduces network :ref:`round-trips `, and also reduces database server CPU usage. .. note:: Client Result Caching is only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. The cache is at the application process level. Access and invalidation is managed by the Oracle Client libraries. This removes the need for extra application logic, or external utilities, to implement a cache. CRC can be enabled by setting the `database parameters `__ ``CLIENT_RESULT_CACHE_SIZE`` and ``CLIENT_RESULT_CACHE_LAG``, and then restarting the database, for example: .. code-block:: sql SQL> ALTER SYSTEM SET CLIENT_RESULT_CACHE_LAG = 3000 SCOPE=SPFILE; SQL> ALTER SYSTEM SET CLIENT_RESULT_CACHE_SIZE = 64K SCOPE=SPFILE; SQL> STARTUP FORCE CRC can alternatively be configured in an :ref:`oraaccess.xml ` or :ref:`sqlnet.ora ` file on the Python host, see `Client Configuration Parameters `__. Tables can then be created, or altered, so repeated queries use CRC. This allows existing applications to use CRC without needing modification. For example: .. code-block:: sql SQL> CREATE TABLE cities (id number, name varchar2(40)) RESULT_CACHE (MODE FORCE); SQL> ALTER TABLE locations RESULT_CACHE (MODE FORCE); Alternatively, hints can be used in SQL statements. For example: .. code-block:: sql SELECT /*+ result_cache */ postal_code FROM locations python-oracledb-1.2.1/doc/src/user_guide/two_phase_commit.rst000066400000000000000000000130501434177474600244070ustar00rootroot00000000000000.. _tcp: ***************************** Using Two-Phase Commits (TPC) ***************************** The python-oracledb functions :meth:`~Connection.tpc_begin()` and :meth:`~Connection.tpc_end()` support distributed transactions. See `Two-Phase Commit Mechanism `_ in the Oracle Database documentation. .. note:: The two-phase commit (TPC) functionality is only supported in the python-oracledb Thick mode. See :ref:`enablingthick`. The method :meth:`Connection.tpc_begin()` can be used to start a TPC transaction. Distributed transaction protocols attempt to keep multiple data sources consistent with one another by ensuring updates to the data sources participating in a distributed transaction are all performed, or none of them performed. These data sources, also called participants or resource managers, may be traditional database systems, messaging systems, and other systems that store state such as caches. A common class of distributed transaction protocols are referred to as two-phase commit protocols. These protocols split the commitment of a distributed transaction into two distinct, separate phases. During the first phase, the participants (data sources) are polled or asked to vote on the outcome of the distributed transaction. This phase, called the prepare phase, ensures agreement or consensus on the ability for each participant to commit their portion of the transaction. When asked to prepare, the participants respond positively if they can commit their portion of the distributed transaction when requested or respond that there were no changes, so they have no need to be committed. Once all participants have responded to the first phase, the second phase of the protocol can begin. The method :meth:`Connection.tpc_prepare()` can be used to prepare a global transaction for commit. During the second phase of the protocol, called the commit phase, all of the participants that indicated they needed to be committed are asked to either commit their prepared changes or roll them back. If the decision on the outcome of the distributed transaction was to commit the transaction, each participant is asked to commit their changes. If the decision was to abort or rollback the distributed transaction, each participant is asked to rollback their changes. The methods :meth:`Connection.tpc_commit()` and :meth:`Connection.tpc_rollback()` can be used to commit or rollback a transaction respectively. While applications can coordinate these activities, it takes on additional responsibilities to ensure the correct outcome of all participants, even in the face of failures. These failures could be of the application itself, one of the participants in the transaction, of communication links, etc. In order to assure the atomic characteristics of a distributed transaction, once the decision has been made to commit the distributed transaction, this decision needs to be durably recorded in case of failure. The application, as part of its steps for recovery from a failure, now needs to check the durable log and notify the participants of the outcome. Failures may be nested such that not only might the application fail, one or more participants or connections to participants might fail. All these scenarios require careful consideration and remediation to ensure that all participants either committed or rolled back their local updates. As a result, most applications rely upon the services provided by a transaction manager (TM), also called a transaction coordinator. The purpose of having a transaction manager perform this coordination is to eliminate having to have each application perform these transaction management functions. The application asks the transaction manager to start a transaction. As additional participants or resource managers join the transaction, they register with the transaction manager as participants. When the original application decides the transaction is to be committed or rolled back, it asks the transaction manager to commit or rollback the transaction. If the application asked the transaction to be rolled back, the transaction coordinator notifies all participants to roll back. Otherwise, the transaction manager then starts the two-phase commit protocol. The following example shows how to perform an application level two-phase commit: .. code-block:: python import oracledb oracledb.init_oracle_client() # connect to first database and begin transaction conn1 = oracledb.connect(DSN1) xid1 = conn1.xid(1000, "txn1", "branch1") conn1.tpc_begin(xid1) with conn1.cursor() as cursor: cursor.execute("insert into SomeTable values (1, 'Some value')") # connect to second database and begin transaction conn2 = oracledb.connect(DSN2) xid2 = conn1.xid(1000, "txn1", "branch2") conn2.tpc_begin(xid2) with conn2.cursor() as cursor: cursor.execute("insert into SomeOtherTable values (2, 'Some value')") # prepare both transactions and commit commit_needed1 = conn1.tpc_prepare() commit_needed2 = conn2.tpc_prepare() if commit_needed1: conn1.tpc_commit() if commit_needed2: conn2.tpc_commit() The following example shows how to perform recovery. .. code-block:: python import oracledb oracledb.init_oracle_client() with oracledb.connect(DSN, mode=oracledb.SYSDBA) as conn: for xid in conn.tpc_recover(): print("Recovering xid by rolling it back:", xid) conn.tpc_rollback(xid) python-oracledb-1.2.1/doc/src/user_guide/txn_management.rst000066400000000000000000000056051434177474600240620ustar00rootroot00000000000000.. _txnmgmnt: ********************* Managing Transactions ********************* A database transaction is a grouping of SQL statements that make a logical data change to the database. When :meth:`Cursor.execute()` executes a SQL statement, a transaction is started or continued. By default, python-oracledb does not commit this transaction to the database. The methods :meth:`Connection.commit()` and :meth:`Connection.rollback()` methods can be used to explicitly commit or rollback a transaction: .. code-block:: python cursor.execute("INSERT INTO mytab (name) VALUES ('John')") connection.commit() When a database connection is closed, such as with :meth:`Connection.close()`, or when variables referencing the connection go out of scope, any uncommitted transaction will be rolled back. Autocommitting ============== An alternative way to commit is to set the attribute :attr:`~Connection.autocommit` of the connection to ``True``. This ensures all :ref:`DML ` statements (INSERT, UPDATE, and so on) are committed as they are executed. Unlike :meth:`Connection.commit()`, this does not require an additional :ref:`round-trip ` to the database so it is more efficient when used appropriately. Note that irrespective of the autocommit value, Oracle Database will always commit an open transaction when a DDL statement is executed. When executing multiple DML statements that constitute a single transaction, it is recommended to use autocommit mode only for the last DML statement in the sequence of operations. Unnecessarily committing causes extra database load, and can destroy transactional consistency. The example below shows a new customer being added to the table ``CUST_TABLE``. The corresponding ``SALES`` table is updated with a purchase of 3000 pens from the customer. The final insert uses autocommit mode to commit both new records: .. code-block:: python # Add a new customer id_var = cursor.var(int) connection.autocommit = False # make sure any previous value is off cursor.execute(""" INSERT INTO cust_table (name) VALUES ('John') RETURNING id INTO :bvid""", bvid=id_var) # Add sales data for the new customer and commit all new values id_val = id_var.getvalue()[0] connection.autocommit = True cursor.execute("INSERT INTO sales_table VALUES (:bvid, 'pens', 3000)", bvid=id_val) Explicit Transactions ===================== The method :meth:`Connection.begin()` can be used to explicitly start a local or global transaction. Without parameters, this explicitly begins a local transaction; otherwise, this explicitly begins a distributed (global) transaction with the given parameters. See the Oracle documentation for more details. Note that in order to make use of global (distributed) transactions, the attributes :attr:`Connection.internal_name` and :attr:`Connection.external_name` attributes must be set. python-oracledb-1.2.1/doc/src/user_guide/xml_data_type.rst000066400000000000000000000041011434177474600236750ustar00rootroot00000000000000.. _xmldatatype: ****************** Using XMLTYPE Data ****************** Oracle XMLType columns are fetched as strings by default in Thin and Thick mode. Note that in Thick mode you may need to use ``XMLTYPE.GETCLOBVAL()`` as discussed below. The examples below demonstrate using XMLType data with python-oracledb. The following table will be used in these examples: .. code-block:: sql CREATE TABLE xml_table ( id NUMBER, xml_data SYS.XMLTYPE ); Inserting into the table can be done by simply binding a string: .. code-block:: python xml_data = """ John Smith 43 Professor Mathematics """ cursor.execute("insert into xml_table values (:id, :xml)", id=1, xml=xml_data) This approach works with XML strings up to 1 GB in size. For longer strings, a temporary CLOB must be created using :meth:`Connection.createlob()` and cast when bound: .. code-block:: python clob = connection.createlob(oracledb.DB_TYPE_CLOB) clob.write(xml_data) cursor.execute("insert into xml_table values (:id, sys.xmltype(:xml))", id=2, xml=clob) Fetching XML data can be done directly in Thin mode. This also works in Thick mode for values that are shorter than the length of a VARCHAR2 column: .. code-block:: python cursor.execute("select xml_data from xml_table where id = :id", id=1) xml_data, = cursor.fetchone() print(xml_data) In Thick mode, for values that exceed the length of a VARCHAR2 column, a CLOB must be returned by using the function ``XMLTYPE.GETCLOBVAL()``: .. code-block:: python cursor.execute(""" select xmltype.getclobval(xml_data) from xml_table where id = :id""", id=1) clob, = cursor.fetchone() print(clob.read()) The LOB that is returned can be streamed, as shown. Alternatively a string can be returned. See :ref:`lobdata` for more information. python-oracledb-1.2.1/pyproject.toml000066400000000000000000000001561434177474600175440ustar00rootroot00000000000000[build-system] requires = ["setuptools >= 40.6.0", "wheel", "cython"] build-backend = "setuptools.build_meta" python-oracledb-1.2.1/samples/000077500000000000000000000000001434177474600162725ustar00rootroot00000000000000python-oracledb-1.2.1/samples/README.md000066400000000000000000000024531434177474600175550ustar00rootroot00000000000000This directory contains samples for python-oracledb. 1. The schemas and SQL objects that are referenced in the samples can be created by running the Python script [create_schema.py][1]. The script requires SYSDBA privileges and will prompt for these credentials as well as the names of the schemas and edition that will be created, unless a number of environment variables are set as documented in the Python script [sample_env.py][2]. Run the script using the following command: python create_schema.py 2. Run a Python script, for example: python query.py 3. After running python-oracledb samples, the schemas and SQL objects can be dropped by running the Python script [drop_schema.py][3]. The script requires SYSDBA privileges and will prompt for these credentials as well as the names of the schemas and edition that will be dropped, unless a number of environment variables are set as documented in the Python script [sample_env.py][2]. Run the script using the following command: python drop_schema.py [1]: https://github.com/oracle/python-oracledb/blob/main/samples/create_schema.py [2]: https://github.com/oracle/python-oracledb/blob/main/samples/sample_env.py [3]: https://github.com/oracle/python-oracledb/blob/main/samples/drop_schema.py python-oracledb-1.2.1/samples/app_context.py000066400000000000000000000050571434177474600211770ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. # # Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, # Canada. All rights reserved. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # app_context.py # # Demonstrates the use of application context. Application context is available # within logon triggers and can be retrieved by using the function # sys_context(). #------------------------------------------------------------------------------ import oracledb import sample_env # this script is currently only supported in python-oracledb thick mode oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) # client context attributes to be set APP_CTX_NAMESPACE = "CLIENTCONTEXT" APP_CTX_ENTRIES = [ ( APP_CTX_NAMESPACE, "ATTR1", "VALUE1" ), ( APP_CTX_NAMESPACE, "ATTR2", "VALUE2" ), ( APP_CTX_NAMESPACE, "ATTR3", "VALUE3" ) ] connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string(), appcontext=APP_CTX_ENTRIES) with connection.cursor() as cursor: for namespace, name, value in APP_CTX_ENTRIES: cursor.execute("select sys_context(:1, :2) from dual", (namespace, name)) value, = cursor.fetchone() print("Value of context key", name, "is", value) python-oracledb-1.2.1/samples/aq_notification.py000066400000000000000000000053501434177474600220160ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2018, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # aq_notification.py # # Demonstrates using advanced queuing notification. Once this script is # running, run object_aq.py in another terminal to enqueue a few messages to # the "DEMO_BOOK_QUEUE" queue. #------------------------------------------------------------------------------ import time import oracledb import sample_env # this script is currently only supported in python-oracledb thick mode oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) registered = True def process_messages(message): global registered print("Message type:", message.type) if message.type == oracledb.EVENT_DEREG: print("Deregistration has taken place...") registered = False return print("Queue name:", message.queue_name) print("Consumer name:", message.consumer_name) print("Message id:", message.msgid) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string(), events=True) sub = connection.subscribe(namespace=oracledb.SUBSCR_NAMESPACE_AQ, name="DEMO_BOOK_QUEUE", callback=process_messages, timeout=300) print("Subscription:", sub) print("--> Connection:", sub.connection) print("--> Callback:", sub.callback) print("--> Namespace:", sub.namespace) print("--> Protocol:", sub.protocol) print("--> Timeout:", sub.timeout) while registered: print("Waiting for notifications....") time.sleep(5) python-oracledb-1.2.1/samples/array_dml_rowcounts.py000066400000000000000000000060331434177474600227430ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # array_dml_rowcounts.py # # Demonstrates the use of the 12.1 feature that allows cursor.executemany() # to return the number of rows affected by each individual execution as a list. # The parameter "arraydmlrowcounts" must be set to True in the call to # cursor.executemany() after which cursor.getarraydmlrowcounts() can be called. #------------------------------------------------------------------------------ import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) with connection.cursor() as cursor: # show the number of rows for each parent ID as a means of verifying the # output from the delete statement for parent_id, count in cursor.execute(""" select ParentId, count(*) from ChildTable group by ParentId order by ParentId"""): print("Parent ID:", parent_id, "has", int(count), "rows.") print() # delete the following parent IDs only parent_ids_to_delete = [20, 30, 50] print("Deleting Parent IDs:", parent_ids_to_delete) print() # enable array DML row counts for each iteration executed in executemany() cursor.executemany(""" delete from ChildTable where ParentId = :1""", [(i,) for i in parent_ids_to_delete], arraydmlrowcounts = True) # display the number of rows deleted for each parent ID row_counts = cursor.getarraydmlrowcounts() for parent_id, count in zip(parent_ids_to_delete, row_counts): print("Parent ID:", parent_id, "deleted", count, "rows.") python-oracledb-1.2.1/samples/batch_errors.py000066400000000000000000000113131434177474600213200ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # batch_errors.py # # Demonstrates the use of the Oracle Database 12.1 feature that allows # cursor.executemany() to complete successfully, even if errors take # place during the execution of one or more of the individual # executions. The parameter "batcherrors" must be set to True in the # call to cursor.executemany() after which cursor.getbatcherrors() can # be called, which will return a list of error objects. #------------------------------------------------------------------------------ import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) with connection.cursor() as cursor: # retrieve the number of rows in the table cursor.execute(""" select count(*) from ChildTable""") count, = cursor.fetchone() print("Number of rows in child table:", int(count)) # define data to insert data_to_insert = [ (1016, 10, 'Child B of Parent 10'), (1017, 10, 'Child C of Parent 10'), (1018, 20, 'Child D of Parent 20'), (1018, 20, 'Child D of Parent 20'), # duplicate key (1019, 30, 'Child C of Parent 30'), (1020, 30, 'Child D of Parent 40'), (1021, 60, 'Child A of Parent 60'), # parent does not exist (1022, 40, 'Child F of Parent 40'), ] print("Number of rows to insert:", len(data_to_insert)) # old method: executemany() with data errors results in stoppage after the # first error takes place; the row count is updated to show how many rows # actually succeeded try: cursor.executemany("insert into ChildTable values (:1, :2, :3)", data_to_insert) except oracledb.DatabaseError as e: error, = e.args print("Failure with error:", error.message) print("Number of rows successfully inserted:", cursor.rowcount) # demonstrate that the row count is accurate cursor.execute(""" select count(*) from ChildTable""") count, = cursor.fetchone() print("Number of rows in child table after failed insert:", int(count)) # roll back so we can perform the same work using the new method connection.rollback() # new method: executemany() with batch errors enabled (and array DML row # counts also enabled) results in no immediate error being raised cursor.executemany("insert into ChildTable values (:1, :2, :3)", data_to_insert, batcherrors=True, arraydmlrowcounts=True) # display the errors that have taken place errors = cursor.getbatcherrors() print("Number of rows with bad values:", len(errors)) for error in errors: print("Error", error.message.rstrip(), "at row offset", error.offset) # arraydmlrowcounts also shows rows with invalid data: they have a row # count of 0; otherwise 1 is shown row_counts = cursor.getarraydmlrowcounts() print("Array DML row counts:", row_counts) # demonstrate that all of the rows without errors have been successfully # inserted cursor.execute(""" select count(*) from ChildTable""") count, = cursor.fetchone() print("Number of rows in child table after insert with batcherrors " "enabled:", int(count)) python-oracledb-1.2.1/samples/bind_insert.py000066400000000000000000000072521434177474600211520ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # bind_insert.py # # Demonstrates how to insert rows into a table using bind variables. #------------------------------------------------------------------------------ import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) #------------------------------------------------------------------------------ # "Bind by position" #------------------------------------------------------------------------------ rows = [ (1, "First"), (2, "Second"), (3, "Third"), (4, "Fourth"), (5, None), # Insert a NULL value (6, "Sixth"), (7, "Seventh") ] with connection.cursor() as cursor: # predefine the maximum string size to avoid data scans and memory # reallocations. The value 'None' indicates that the default processing # can take place cursor.setinputsizes(None, 20) cursor.executemany("insert into mytab(id, data) values (:1, :2)", rows) #------------------------------------------------------------------------------ # "Bind by name" #------------------------------------------------------------------------------ rows = [ {"d": "Eighth", "i": 8}, {"d": "Ninth", "i": 9}, {"d": "Tenth", "i": 10}, {"i": 11} # Insert a NULL value ] with connection.cursor() as cursor: # Predefine maximum string size to avoid data scans and memory # reallocations cursor.setinputsizes(d=20) cursor.executemany("insert into mytab(id, data) values (:i, :d)", rows) #------------------------------------------------------------------------------ # Inserting a single bind still needs tuples #------------------------------------------------------------------------------ rows = [ ("Eleventh",), ("Twelth",) ] with connection.cursor() as cursor: cursor.executemany("insert into mytab(id, data) values (12, :1)", rows) # Don't commit - this lets the demo be run multiple times # connection.commit() #------------------------------------------------------------------------------ # Now query the results back #------------------------------------------------------------------------------ with connection.cursor() as cursor: for row in cursor.execute("select * from mytab order by id"): print(row) python-oracledb-1.2.1/samples/bind_query.py000066400000000000000000000133201434177474600210040ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # bind_query.py # # Demonstrates the use of bind variables in queries. Binding is important for # scalability and security. Since the text of a query that is re-executed is # unchanged, no additional parsing is required, thereby reducing overhead and # increasing performance. It also permits data to be bound without having to be # concerned about escaping special characters, or be concerned about SQL # injection attacks. ##------------------------------------------------------------------------------ import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) # Bind by position with lists with connection.cursor() as cursor: print("1. Bind by position: single value list") sql = 'select * from SampleQueryTab where id = :bvid' for row in cursor.execute(sql, [1]): print(row) print() print("2. Bind by position: multiple values") sql = 'select * from SampleQueryTab where id = :bvid and 123 = :otherbind' for row in cursor.execute(sql, [2, 123]): print(row) print() # With bind-by-position, the order of the data in the bind list matches the # order of the placeholders used in the SQL statement. The bind list data # order is not associated by the name of the bind variable placeholders in # the SQL statement, even though those names are ":1" and ":2". print("3. Bind by position: multiple values with numeric placeholder names") sql = 'select * from SampleQueryTab where id = :2 and 456 = :1' for row in cursor.execute(sql, [3, 456]): print(row) print() # With bind-by-position, repeated use of bind placeholder names in the SQL # statement requires the input list data to be repeated. print("4. Bind by position: multiple values with a repeated placeholder") sql = 'select * from SampleQueryTab where id = :2 and 3 = :2' for row in cursor.execute(sql, [3, 3]): print(row) print() # Bind by position with tuples with connection.cursor() as cursor: print("5. Bind by position with single value tuple") sql = 'select * from SampleQueryTab where id = :bvid' for row in cursor.execute(sql, (4,)): print(row) print() print("6. Bind by position with a multiple value tuple") sql = 'select * from SampleQueryTab where id = :bvid and 789 = :otherbind' for row in cursor.execute(sql, (4,789)): print(row) print() # Bind by name with a dictionary with connection.cursor() as cursor: print("7. Bind by name with a dictionary") sql = 'select * from SampleQueryTab where id = :bvid' for row in cursor.execute(sql, {"bvid": 4}): print(row) print() # With bind-by-name, repeated use of bind placeholder names in the SQL # statement lets you supply the data once. print("8. Bind by name with multiple value dict and repeated placeholders") sql = 'select * from SampleQueryTab where id = :bvid and 4 = :bvid' for row in cursor.execute(sql, {"bvid": 4}): print(row) print() # Bind by name with parameters. The execute() parameter names match the bind # variable placeholder names. with connection.cursor() as cursor: print("9. Bind by name using parameters") sql = 'select * from SampleQueryTab where id = :bvid' for row in cursor.execute(sql, bvid=5): print(row) print() print("10. Bind by name using multiple parameters") sql = 'select * from SampleQueryTab where id = :bvid and 101 = :otherbind' for row in cursor.execute(sql, bvid=5, otherbind=101): print(row) print() # With bind-by-name, repeated use of bind placeholder names in the SQL # statement lets you supply the data once. print("11. Bind by name: multiple values with repeated placeholder names") sql = 'select * from SampleQueryTab where id = :bvid and 6 = :bvid' for row in cursor.execute(sql, bvid=6): print(row) print() # Rexcuting a query with different data values with connection.cursor() as cursor: sql = 'select * from SampleQueryTab where id = :bvid' print("12. Query results with id = 7") for row in cursor.execute(sql, [4]): print(row) print() print("13. Rexcuted query results with id = 1") for row in cursor.execute(sql, [1]): print(row) print() python-oracledb-1.2.1/samples/bulk_aq.py000066400000000000000000000067541434177474600202760ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2019, 2022, Oracle and/or its affiliates. # # Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. # # Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, # Canada. All rights reserved. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # bulk_aq.py # # Demonstrates how to use bulk enqueuing and dequeuing of messages with # advanced queuing. It makes use of a RAW queue created in the sample setup. #------------------------------------------------------------------------------ import oracledb import sample_env QUEUE_NAME = "DEMO_RAW_QUEUE" PAYLOAD_DATA = [ "The first message", "The second message", "The third message", "The fourth message", "The fifth message", "The sixth message", "The seventh message", "The eighth message", "The ninth message", "The tenth message", "The eleventh message", "The twelfth and final message" ] # this script is currently only supported in python-oracledb thick mode oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) # connect to database connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) # create a queue with connection.cursor() as cursor: queue = connection.queue(QUEUE_NAME) queue.deqoptions.wait = oracledb.DEQ_NO_WAIT queue.deqoptions.navigation = oracledb.DEQ_FIRST_MSG # dequeue all existing messages to ensure the queue is empty, just so that # the results are consistent while queue.deqone(): pass # enqueue a few messages with connection.cursor() as cursor: print("Enqueuing messages...") batch_size = 6 data_to_enqueue = PAYLOAD_DATA while data_to_enqueue: batch_data = data_to_enqueue[:batch_size] data_to_enqueue = data_to_enqueue[batch_size:] messages = [connection.msgproperties(payload=d) for d in batch_data] for data in batch_data: print(data) queue.enqmany(messages) connection.commit() # dequeue the messages with connection.cursor() as cursor: print("\nDequeuing messages...") batch_size = 8 while True: messages = queue.deqmany(batch_size) if not messages: break for props in messages: print(props.payload.decode()) connection.commit() print("\nDone.") python-oracledb-1.2.1/samples/call_timeout.py000066400000000000000000000053651434177474600213360ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2019, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # call_timeout.py # # Demonstrates the use of the Oracle Client 18c feature that enables round # trips to the database to time out if a specified amount of time # (in milliseconds) has passed without a response from the database. # # This script requires Oracle Client 18.1 and higher when using thick mode. #------------------------------------------------------------------------------ import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) connection.call_timeout = 2000 print("Call timeout set at", connection.call_timeout, "milliseconds...") with connection.cursor() as cursor: cursor.execute("select sysdate from dual") today, = cursor.fetchone() print("Fetch of current date before timeout:", today) # dbms_session.sleep() replaces dbms_lock.sleep() from Oracle Database 18c sleep_proc_name = "dbms_session.sleep" \ if int(connection.version.split(".")[0]) >= 18 \ else "dbms_lock.sleep" print("Sleeping...should time out...") try: cursor.callproc(sleep_proc_name, (3,)) except oracledb.DatabaseError as e: print("ERROR:", e) cursor.execute("select sysdate from dual") today, = cursor.fetchone() print("Fetch of current date after timeout:", today) python-oracledb-1.2.1/samples/connection_pool.py000066400000000000000000000145221434177474600220400ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # connection_pool.py # # Demonstrates the use of connection pooling using a Flask web application. # # Connection Pools can significantly reduce connection times for long running # applications that repeatedly open and close connections. Connection pools # allow multiple, concurrent web requests to be efficiently handled. Internal # features help protect against dead connections, and also aid use of Oracle # Database features such as FAN and Application Continuity. # # To run this sample: # # 1. Install Flask, for example like: # # python -m pip install Flask # # 2. (Optional) Set environment variables referenced in sample_env.py # # 3. Run: # # python connection_pool.py # # 4. In a browser load a URL as shown below. # # The default route will display a welcome message: # http://127.0.0.1:8080/ # # To find a username you can pass an id, for example 1: # http://127.0.0.1:8080/user/1 # # To insert new a user 'fred' you can call: # http://127.0.0.1:8080/post/fred # #------------------------------------------------------------------------------ import os import sys from flask import Flask import oracledb import sample_env # Port to listen on port = int(os.environ.get('PORT', '8080')) # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) #------------------------------------------------------------------------------ # start_pool(): starts the connection pool def start_pool(): # Generally a fixed-size pool is recommended, i.e. pool_min=pool_max. Here # the pool contains 4 connections, which will allow 4 concurrent users. pool_min = 4 pool_max = 4 pool_inc = 0 pool = oracledb.create_pool(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string(), min=pool_min, max=pool_max, increment=pool_inc, session_callback=init_session) return pool # init_session(): a 'session callback' to efficiently set any initial state # that each connection should have. # # This particular demo doesn't use dates, so sessionCallback could be omitted, # but it does show the kinds of settings many apps would use. # # If you have multiple SQL statements, then call them all in a PL/SQL anonymous # block with BEGIN/END so you only use execute() once. This is shown later in # create_schema(). # def init_session(connection, requestedTag_ignored): with connection.cursor() as cursor: cursor.execute(""" alter session set time_zone = 'UTC' nls_date_format = 'YYYY-MM-DD HH24:MI'""") #------------------------------------------------------------------------------ # create_schema(): drop and create the demo table, and add a row def create_schema(): with pool.acquire() as connection: with connection.cursor() as cursor: cursor.execute(""" begin begin execute immediate 'drop table demo'; exception when others then if sqlcode <> -942 then raise; end if; end; execute immediate 'create table demo ( id number generated by default as identity, username varchar2(40))'; execute immediate 'insert into demo (username) values (''chris'')'; commit; end;""") #------------------------------------------------------------------------------ app = Flask(__name__) # Display a welcome message on the 'home' page @app.route('/') def index(): return "Welcome to the demo app" # Add a new username # # The new user's id is generated by the database and returned in the OUT bind # variable 'idbv'. @app.route('/post/') def post(username): with pool.acquire() as connection: with connection.cursor() as cursor: connection.autocommit = True idbv = cursor.var(int) cursor.execute(""" insert into demo (username) values (:unbv) returning id into :idbv""", [username, idbv]) return f'Inserted {username} with id {idbv.getvalue()[0]}' # Show the username for a given id @app.route('/user/') def show_username(id): with pool.acquire() as connection: with connection.cursor() as cursor: cursor.execute("select username from demo where id = :idbv", [id]) r = cursor.fetchone() return r[0] if r is not None else "Unknown user id" #------------------------------------------------------------------------------ if __name__ == '__main__': # Start a pool of connections pool = start_pool() # Create a demo table create_schema() m = f"\nTry loading http://127.0.0.1:{port}/user/1 in a browser\n" sys.modules['flask.cli'].show_server_banner = lambda *x: print(m) # Start a webserver app.run(port=port) python-oracledb-1.2.1/samples/cqn.py000066400000000000000000000074021434177474600174300ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. # # Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, # Canada. All rights reserved. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # cqn.py # # Demonstrates using continuous query notification in Python, a feature that is # available in Oracle 11g and later. Once this script is running, use another # session to insert, update or delete rows from the table TestTempTable and you # will see the notification of that change. #------------------------------------------------------------------------------ import time import oracledb import sample_env # this script is currently only supported in python-oracledb thick mode oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) registered = True def callback(message): global registered print("Message type:", message.type) if not message.registered: print("Deregistration has taken place...") registered = False return print("Message database name:", message.dbname) print("Message tranasction id:", message.txid) print("Message queries:") for query in message.queries: print("--> Query ID:", query.id) print("--> Query Operation:", query.operation) for table in query.tables: print("--> --> Table Name:", table.name) print("--> --> Table Operation:", table.operation) if table.rows is not None: print("--> --> Table Rows:") for row in table.rows: print("--> --> --> Row RowId:", row.rowid) print("--> --> --> Row Operation:", row.operation) print("-" * 60) print("=" * 60) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string(), events=True) qos = oracledb.SUBSCR_QOS_QUERY | oracledb.SUBSCR_QOS_ROWIDS sub = connection.subscribe(callback=callback, timeout=1800, qos=qos, client_initiated=True) print("Subscription:", sub) print("--> Connection:", sub.connection) print("--> Callback:", sub.callback) print("--> Namespace:", sub.namespace) print("--> Protocol:", sub.protocol) print("--> Timeout:", sub.timeout) print("--> Operations:", sub.operations) print("--> Rowids?:", bool(sub.qos & oracledb.SUBSCR_QOS_ROWIDS)) query_id = sub.registerquery("select * from TestTempTable") print("Registered query:", query_id) while registered: print("Waiting for notifications....") time.sleep(5) python-oracledb-1.2.1/samples/cqn_pool.py000066400000000000000000000100151434177474600204530ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # cqn_pool.py # # Demonstrates using continuous query notification in Python, a feature that is # available in Oracle Database 11g and later. Once this script is running, use # another session to insert, update or delete rows from the table TestTempTable # and you will see the notification of that change. # # This script differs from cqn.py in that it shows how a connection can be # acquired from a session pool and used to query the changes that have been # made. #------------------------------------------------------------------------------ import time import oracledb import sample_env # this script is currently only supported in python-oracledb thick mode oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) registered = True def callback(message): global registered if not message.registered: print("Deregistration has taken place...") registered = False return connection = pool.acquire() for query in message.queries: for table in query.tables: if table.rows is None: print("Too many row changes detected in table", table.name) continue num_rows_deleted = 0 print(len(table.rows), "row changes detected in table", table.name) for row in table.rows: if row.operation & oracledb.OPCODE_DELETE: num_rows_deleted += 1 continue ops = [] if row.operation & oracledb.OPCODE_INSERT: ops.append("inserted") if row.operation & oracledb.OPCODE_UPDATE: ops.append("updated") cursor = connection.cursor() cursor.execute(""" select IntCol from TestTempTable where rowid = :rid""", rid=row.rowid) int_col, = cursor.fetchone() print(" Row with IntCol", int_col, "was", " and ".join(ops)) if num_rows_deleted > 0: print(" ", num_rows_deleted, "rows deleted") print("=" * 60) pool = oracledb.create_pool(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string(), min=1, max=4, increment=1, events=True) with pool.acquire() as connection: qos = oracledb.SUBSCR_QOS_QUERY | oracledb.SUBSCR_QOS_ROWIDS sub = connection.subscribe(callback=callback, timeout=1800, qos=qos, client_initiated=True) print("Subscription created with ID:", sub.id) query_id = sub.registerquery("select * from TestTempTable") print("Registered query with ID:", query_id) while registered: print("Waiting for notifications....") time.sleep(5) python-oracledb-1.2.1/samples/create_schema.py000066400000000000000000000043401434177474600214300ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # create_schema.py # # Creates users and populates their schemas with the tables and packages # necessary for running the python-oracledb sample scripts. An edition is also # created for the demonstration of PL/SQL editioning. #------------------------------------------------------------------------------ import oracledb import drop_schema import sample_env # connect as administrative user (usually SYSTEM or ADMIN) conn = oracledb.connect(sample_env.get_admin_connect_string()) # drop existing users and editions, if applicable drop_schema.drop_schema(conn) # create sample schema and edition print("Creating sample schemas and edition...") sample_env.run_sql_script(conn, "create_schema", main_user=sample_env.get_main_user(), main_password=sample_env.get_main_password(), edition_user=sample_env.get_edition_user(), edition_password=sample_env.get_edition_password(), edition_name=sample_env.get_edition_name()) print("Done.") python-oracledb-1.2.1/samples/data/000077500000000000000000000000001434177474600172035ustar00rootroot00000000000000python-oracledb-1.2.1/samples/data/load_csv.csv000066400000000000000000000032731434177474600215170ustar00rootroot000000000000001,Biologist 2,Doctor 3,Call Center Representative 4,Executive Director 5,Laboratory Technician 6,Cashier 7,Global Logistics Supervisor 8,Investment Advisor 9,HR Coordinator 10,HR Specialist 11,Investment Advisor 12,Baker 13,Baker 14,Cashier 15,CNC Operator 16,Software Engineer 17,Call Center Representative 18,Auditor 19,Accountant 20,Auditor 21,Loan Officer 22,Bellman 23,Cashier 24,Baker 25,HR Coordinator 26,Operator 27,Service Supervisor 28,Lecturer 29,IT Support Staff 30,Staffing Consultant 31,Paramedic 32,Associate Professor 33,HR Coordinator 34,HR Coordinator 35,Restaurant Manager 36,Webmaster 37,Global Logistics Supervisor 38,Bellman 39,Design Engineer 40,Cashier 41,Global Logistics Supervisor 42,Steward 43,Fabricator 44,Inspector 45,Pharmacist 46,Loan Officer 47,Staffing Consultant 48,Chef Manager 49,Biologist 50,Food Technologist 51,Biologist 52,Global Logistics Supervisor 53,Laboratory Technician 54,Design Engineer 55,Fabricator 56,Lecturer 57,Loan Officer 58,Investment Advisor 59,Treasurer 60,Cash Manager 61,Audiologist 62,Fabricator 63,Systems Administrator 64,Health Educator 65,Fabricator 66,HR Specialist 67,Physician 68,Staffing Consultant 69,Fabricator 70,Physician 71,Auditor 72,Biologist 73,Budget Analyst 74,Bellman 75,Restaurant Manager 76,Stockbroker 77,Paramedic 78,Front Desk Coordinator 79,Cashier 80,CNC Operator 81,Design Engineer 82,Audiologist 83,CNC Operator 84,IT Support Staff 85,Paramedic 86,Fabricator 87,Laboratory Technician 88,Mobile Developer 89,Global Logistics Supervisor 90,Budget Analyst 91,Lecturer 92,Physician 93,Retail Trainee 94,Pharmacist 95,Service Supervisor 96,Production Painter 97,Cash Manager 98,Global Logistics Supervisor 99,Treasurer 100,Pharmacist python-oracledb-1.2.1/samples/database_change_notification.py000066400000000000000000000070401434177474600244640ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. # # Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, # Canada. All rights reserved. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # database_change_notification.py # # Demonstrates using database change notification in Python, a feature that is # available in Oracle 10g Release 2. Once this script is running, use another # session to insert, update or delete rows from the table TestTempTable and you # will see the notification of that change. #------------------------------------------------------------------------------ import time import oracledb import sample_env # this script is currently only supported in python-oracledb thick mode oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) registered = True def callback(message): global registered print("Message type:", message.type) if not message.registered: print("Deregistration has taken place...") registered = False return print("Message database name:", message.dbname) print("Message tranasction id:", message.txid) print("Message tables:") for table in message.tables: print("--> Table Name:", table.name) print("--> Table Operation:", table.operation) if table.rows is not None: print("--> Table Rows:") for row in table.rows: print("--> --> Row RowId:", row.rowid) print("--> --> Row Operation:", row.operation) print("-" * 60) print("=" * 60) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string(), events=True) sub = connection.subscribe(callback=callback, timeout=1800, qos=oracledb.SUBSCR_QOS_ROWIDS, client_initiated=True) print("Subscription:", sub) print("--> Connection:", sub.connection) print("--> ID:", sub.id) print("--> Callback:", sub.callback) print("--> Namespace:", sub.namespace) print("--> Protocol:", sub.protocol) print("--> Timeout:", sub.timeout) print("--> Operations:", sub.operations) print("--> Rowids?:", bool(sub.qos & oracledb.SUBSCR_QOS_ROWIDS)) sub.registerquery("select * from TestTempTable") while registered: print("Waiting for notifications....") time.sleep(5) python-oracledb-1.2.1/samples/database_shutdown.py000066400000000000000000000044221434177474600223450ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. # # Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, # Canada. All rights reserved. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # database_shutdown.py # # Demonstrates shutting down a database using Python. The connection used # assumes that the environment variable ORACLE_SID has been set. #------------------------------------------------------------------------------ import oracledb import sample_env # this script is currently only supported in python-oracledb thick mode oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) # need to connect as SYSDBA or SYSOPER connection = oracledb.connect(mode=oracledb.SYSDBA) # first shutdown() call must specify the mode, if DBSHUTDOWN_ABORT is used, # there is no need for any of the other steps connection.shutdown(mode=oracledb.DBSHUTDOWN_IMMEDIATE) # now close and dismount the database cursor = connection.cursor() cursor.execute("alter database close normal") cursor.execute("alter database dismount") # perform the final shutdown call connection.shutdown(mode=oracledb.DBSHUTDOWN_FINAL) python-oracledb-1.2.1/samples/database_startup.py000066400000000000000000000041721434177474600221760ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. # # Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, # Canada. All rights reserved. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # database_startup.py # # Demonstrates starting up a database using Python. The connection used # assumes that the environment variable ORACLE_SID has been set. #------------------------------------------------------------------------------ import oracledb import sample_env # this script is currently only supported in python-oracledb thick mode oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) # the connection must be in PRELIM_AUTH mode connection = oracledb.connect(mode=oracledb.SYSDBA | oracledb.PRELIM_AUTH) connection.startup() # the following statements must be issued in normal SYSDBA mode connection = oracledb.connect(mode=oracledb.SYSDBA) cursor = connection.cursor() cursor.execute("alter database mount") cursor.execute("alter database open") python-oracledb-1.2.1/samples/dbms_output.py000066400000000000000000000053521434177474600212160ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # dbms_output.py # # Demonstrates one method of fetching the lines produced by the DBMS_OUTPUT # package. #------------------------------------------------------------------------------ import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) with connection.cursor() as cursor: # enable DBMS_OUTPUT cursor.callproc("dbms_output.enable") # execute some PL/SQL that generates output with DBMS_OUTPUT.PUT_LINE cursor.execute(""" begin dbms_output.put_line('This is some text'); dbms_output.put_line(''); dbms_output.put_line('Demonstrating use of DBMS_OUTPUT'); end;""") # tune this size for your application chunk_size = 10 # create variables to hold the output lines_var = cursor.arrayvar(str, chunk_size) num_lines_var = cursor.var(int) num_lines_var.setvalue(0, chunk_size) # fetch the text that was added by PL/SQL while True: cursor.callproc("dbms_output.get_lines", (lines_var, num_lines_var)) num_lines = num_lines_var.getvalue() lines = lines_var.getvalue()[:num_lines] for line in lines: print(line or "") if num_lines < chunk_size: break python-oracledb-1.2.1/samples/dml_returning_multiple_rows.py000066400000000000000000000055761434177474600245170ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. # # Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, # Canada. All rights reserved. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # dml_returning_multiple_rows.py # # Demonstrates the use of DML returning with multiple rows being returned at # once. #------------------------------------------------------------------------------ import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) with connection.cursor() as cursor: # truncate table first so that script can be rerun print("Truncating table...") cursor.execute("truncate table TestTempTable") # populate table with a few rows for i in range(5): data = (i + 1, "Test String #%d" % (i + 1)) print("Adding row", data) cursor.execute("insert into TestTempTable values (:1, :2)", data) # now delete them and use DML returning to return the data that was # deleted int_col = cursor.var(int) string_col = cursor.var(str) print("Deleting data with DML returning...") cursor.execute(""" delete from TestTempTable returning IntCol, StringCol into :int_col, :string_col""", int_col=int_col, string_col=string_col) print("Data returned:") for int_val, string_val in zip(int_col.getvalue(), string_col.getvalue()): print(tuple([int_val, string_val])) python-oracledb-1.2.1/samples/drcp_pool.py000066400000000000000000000201261434177474600206260ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # drcp_pool.py # # Demonstrates the use of Database Resident Connection Pooling (DRCP) # connection pooling using a Flask web application. This sample is similar to # connection_pool.py # # DRCP can be used with standalone connections from connect(), but it is often # used together with a python-oracledb connection pool created with # create_pool(), as shown here. # # DRCP provides a connection pool in the database server. The pool reduces the # cost of creating and tearing down database server processs. This pool of # server processes can be shared across application processs, allowing for # resource sharing. # # There is no difference in how a connection is used once it has been # established. # # To use DRCP, the connection string should request the database use a pooled # server. For example, "localhost/orclpdb:pooled". It is best practice for # connections to specify a connection class and server purity when creating # a pool # # For on premise databases, the DRCP pool can be started and stopped in the # database by issuing the following commands in SQL*Plus: # # exec dbms_connection_pool.start_pool() # exec dbms_connection_pool.stop_pool() # # For multitenant databases, DRCP management needs to be done the root ("CDB") # database unless the database initialization parameter ENABLE_PER_PDB_DRCP is # TRUE. # # Oracle Autonomous Databases already have DRCP enabled. # # Statistics on DRCP usage are recorded in various data dictionary views, for # example in V$CPOOL_CC_STATS. # # See the python-oracledb documentation for more information on DRCP. # # To run this sample: # # 1. Install Flask, for example like: # # python -m pip install Flask # # 2. (Optional) Set environment variables referenced in sample_env.py # # 3. Run: # # python drcp_pool.py # # 4. In a browser load a URL as shown below. # # The default route will display a welcome message: # http://127.0.0.1:8080/ # # To find a username you can pass an id, for example 1: # http://127.0.0.1:8080/user/1 # # To insert new a user 'fred' you can call: # http://127.0.0.1:8080/post/fred # # Multi-user load can be simulated with a testing tool such as 'ab': # # ab -n 1000 -c 4 http://127.0.0.1:8080/user/1 # # Then you can query the data dictionary: # # select cclass_name, num_requests, num_hits, # num_misses, num_waits, num_authentications # from v$cpool_cc_stats; # # Output will be like: # # CCLASS_NAME NUM_REQUESTS NUM_HITS NUM_MISSES NUM_WAITS NUM_AUTHENTICATIONS # ---------------- ------------ -------- ---------- --------- ------------------- # PYTHONDEMO.MYAPP 1001 997 4 0 4 # # With ADB-S databases, query V$CPOOL_CONN_INFO instead. # #------------------------------------------------------------------------------ import os import sys from flask import Flask import oracledb import sample_env # Port to listen on port = int(os.environ.get('PORT', '8080')) # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) #------------------------------------------------------------------------------ # start_pool(): starts the connection pool def start_pool(): # Generally a fixed-size pool is recommended, i.e. pool_min=pool_max. Here # the pool contains 4 connections, which will allow 4 concurrent users. pool_min = 4 pool_max = 4 pool_inc = 0 pool = oracledb.create_pool(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_drcp_connect_string(), min=pool_min, max=pool_max, increment=pool_inc, session_callback=init_session, cclass="MYAPP", purity=oracledb.ATTR_PURITY_SELF) return pool # init_session(): a 'session callback' to efficiently set any initial state # that each connection should have. # # This particular demo doesn't use dates, so sessionCallback could be omitted, # but it does show the kinds of settings many apps would use. # # If you have multiple SQL statements, then call them all in a PL/SQL anonymous # block with BEGIN/END so you only use execute() once. This is shown later in # create_schema(). # def init_session(connection, requestedTag_ignored): with connection.cursor() as cursor: cursor.execute(""" alter session set time_zone = 'UTC' nls_date_format = 'YYYY-MM-DD HH24:MI'""") #------------------------------------------------------------------------------ # create_schema(): drop and create the demo table, and add a row def create_schema(): with pool.acquire() as connection: with connection.cursor() as cursor: cursor.execute(""" begin begin execute immediate 'drop table demo'; exception when others then if sqlcode <> -942 then raise; end if; end; execute immediate 'create table demo ( id number generated by default as identity, username varchar2(40))'; execute immediate 'insert into demo (username) values (''chris'')'; commit; end;""") #------------------------------------------------------------------------------ app = Flask(__name__) # Display a welcome message on the 'home' page @app.route('/') def index(): return "Welcome to the demo app" # Add a new username # # The new user's id is generated by the database and returned in the OUT bind # variable 'idbv'. @app.route('/post/') def post(username): with pool.acquire() as connection: with connection.cursor() as cursor: connection.autocommit = True idbv = cursor.var(int) cursor.execute(""" insert into demo (username) values (:unbv) returning id into :idbv""", [username, idbv]) return f'Inserted {username} with id {idbv.getvalue()[0]}' # Show the username for a given id @app.route('/user/') def show_username(id): with pool.acquire() as connection: with connection.cursor() as cursor: cursor.execute("select username from demo where id = :idbv", [id]) r = cursor.fetchone() return r[0] if r is not None else "Unknown user id" #------------------------------------------------------------------------------ if __name__ == '__main__': # Start a pool of connections pool = start_pool() # Create a demo table create_schema() m = f"\nTry loading http://127.0.0.1:{port}/user/1 in a browser\n" sys.modules['flask.cli'].show_server_banner = lambda *x: print(m) # Start a webserver app.run(port=port) python-oracledb-1.2.1/samples/drop_schema.py000066400000000000000000000040231434177474600211270ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # drop_schema.py # # Drops the database objects used by the python-oracledb samples. # # This script is also executed by the Python script sample_setup.py for # dropping the existing users and editions, if applicable, before creating the # sample schemas and editions. #------------------------------------------------------------------------------ import oracledb import sample_env def drop_schema(conn): print("Dropping sample schemas and edition...") sample_env.run_sql_script(conn, "drop_schema", main_user=sample_env.get_main_user(), edition_user=sample_env.get_edition_user(), edition_name=sample_env.get_edition_name()) if __name__ == "__main__": conn = oracledb.connect(sample_env.get_admin_connect_string()) drop_schema(conn) print("Done.") python-oracledb-1.2.1/samples/editioning.py000066400000000000000000000101121434177474600207700ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. # # Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, # Canada. All rights reserved. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # editioning.py # # Demonstrates the use of Edition-Based Redefinition, a feature that is # available in Oracle Database 11.2 and higher. See the Oracle documentation on # the subject for additional information. Adjust the contents at the top of the # script for your own database as needed. #------------------------------------------------------------------------------ import os import oracledb import sample_env # this script is currently only supported in python-oracledb thick mode oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) # connect to the editions user and create a procedure edition_connect_string = sample_env.get_edition_connect_string() edition_name = sample_env.get_edition_name() connection = oracledb.connect(edition_connect_string) print("Edition should be None, actual value is:", repr(connection.edition)) cursor = connection.cursor() cursor.execute(""" create or replace function TestEditions return varchar2 as begin return 'Base Procedure'; end;""") result = cursor.callfunc("TestEditions", str) print("Function should return 'Base Procedure', actually returns:", repr(result)) # next, change the edition and recreate the procedure in the new edition cursor.execute("alter session set edition = %s" % edition_name) print("Edition should be", repr(edition_name.upper()), "actual value is:", repr(connection.edition)) cursor.execute(""" create or replace function TestEditions return varchar2 as begin return 'Edition 1 Procedure'; end;""") result = cursor.callfunc("TestEditions", str) print("Function should return 'Edition 1 Procedure', actually returns:", repr(result)) # next, change the edition back to the base edition and demonstrate that the # original function is being called cursor.execute("alter session set edition = ORA$BASE") result = cursor.callfunc("TestEditions", str) print("Function should return 'Base Procedure', actually returns:", repr(result)) # the edition can be set upon connection connection = oracledb.connect(edition_connect_string, edition=edition_name.upper()) cursor = connection.cursor() result = cursor.callfunc("TestEditions", str) print("Function should return 'Edition 1 Procedure', actually returns:", repr(result)) # it can also be set via the environment variable ORA_EDITION os.environ["ORA_EDITION"] = edition_name.upper() connection = oracledb.connect(edition_connect_string) print("Edition should be", repr(edition_name.upper()), "actual value is:", repr(connection.edition)) cursor = connection.cursor() result = cursor.callfunc("TestEditions", str) print("Function should return 'Edition 1 Procedure', actually returns:", repr(result)) python-oracledb-1.2.1/samples/generic_row_factory.py000066400000000000000000000054011434177474600226760ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # generic_row_factory.py # # Demonstrates the ability to return named tuples for all queries using a # subclassed cursor and row factory. #------------------------------------------------------------------------------ import collections import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) class Connection(oracledb.Connection): def cursor(self): return Cursor(self) class Cursor(oracledb.Cursor): def execute(self, statement, args=None): prepare_needed = (self.statement != statement) result = super().execute(statement, args or []) if prepare_needed: description = self.description if description is not None: names = [d[0] for d in description] self.rowfactory = collections.namedtuple("GenericQuery", names) return result # create a new subclassed connection and cursor connection = Connection(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) with connection.cursor() as cursor: # the names are now available directly for each query executed for row in cursor.execute("select ParentId, Description from ParentTable"): print(row.PARENTID, "->", row.DESCRIPTION) print() for row in cursor.execute("select ChildId, Description from ChildTable"): print(row.CHILDID, "->", row.DESCRIPTION) print() python-oracledb-1.2.1/samples/implicit_results.py000066400000000000000000000052771434177474600222520ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. # # Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, # Canada. All rights reserved. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # implicit_results.py # # Demonstrates the use of the Oracle Database 12.1 feature that allows PL/SQL # procedures to return result sets implicitly, without having to explicitly # define them. #------------------------------------------------------------------------------ import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) with connection.cursor() as cursor: # A PL/SQL block that returns two cursors cursor.execute(""" declare c1 sys_refcursor; c2 sys_refcursor; begin open c1 for select * from TestNumbers; dbms_sql.return_result(c1); open c2 for select * from TestStrings; dbms_sql.return_result(c2); end;""") # display results for ix, result_set in enumerate(cursor.getimplicitresults()): print("Result Set #" + str(ix + 1)) for row in result_set: print(row) print() python-oracledb-1.2.1/samples/insert_geometry.py000066400000000000000000000061531434177474600220700ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. # # Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, # Canada. All rights reserved. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # insert_geometry.py # # Demonstrates the ability to create Oracle objects (this example uses # SDO_GEOMETRY) and insert them into a table. #------------------------------------------------------------------------------ import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) # create and populate Oracle objects type_obj = connection.gettype("MDSYS.SDO_GEOMETRY") element_info_type_obj = connection.gettype("MDSYS.SDO_ELEM_INFO_ARRAY") ordinate_type_obj = connection.gettype("MDSYS.SDO_ORDINATE_ARRAY") obj = type_obj.newobject() obj.SDO_GTYPE = 2003 obj.SDO_ELEM_INFO = element_info_type_obj.newobject() obj.SDO_ELEM_INFO.extend([1, 1003, 3]) obj.SDO_ORDINATES = ordinate_type_obj.newobject() obj.SDO_ORDINATES.extend([1, 1, 5, 7]) print("Created object", obj) with connection.cursor() as cursor: # create sample table cursor.execute(""" begin begin execute immediate 'drop table TestGeometry'; exception when others then if sqlcode <> -942 then raise; end if; end; execute immediate 'create table TestGeometry ( IntCol number(9) not null, Geometry MDSYS.SDO_GEOMETRY)'; end;""") print("Adding row to table...") cursor.execute("insert into TestGeometry values (1, :objbv)", objbv=obj) connection.commit() print("Success!") python-oracledb-1.2.1/samples/json_blob.py000066400000000000000000000073651434177474600206260ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # json_blob.py # # Demonstrates how to use a BLOB as a JSON column store. # # Note: Oracle Database 12c lets JSON be stored in VARCHAR2 or LOB columns. # With Oracle Database 21c using the new JSON type is recommended # instead, see json_direct.py # # Documentation: # python-oracledb: https://oracledb.readthedocs.io/en/latest/user_guide/json_data_type.html # Oracle Database: https://www.oracle.com/pls/topic/lookup?ctx=dblatest&id=ADJSN #------------------------------------------------------------------------------ import json import sys import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) if not connection.thin: client_version = oracledb.clientversion()[0] db_version = int(connection.version.split(".")[0]) # Minimum database vesion is 12 if db_version < 12: sys.exit("This example requires Oracle Database 12.1.0.2 or later") # Insert JSON data with connection.cursor() as cursor: data = dict(name="Rod", dept="Sales", location="Germany") inssql = "insert into CustomersAsBlob values (:1, :2)" if not connection.thin and client_version >= 21 and db_version >= 21: # Take advantage of direct binding cursor.setinputsizes(None, oracledb.DB_TYPE_JSON) cursor.execute(inssql, [1, data]) else: # Insert the data as a JSON string cursor.execute(inssql, [1, json.dumps(data)]) # Select JSON data with connection.cursor() as cursor: sql = "SELECT c.json_data FROM CustomersAsBlob c" for j, in cursor.execute(sql): print(json.loads(j.read())) # Using JSON_VALUE to extract a value from a JSON column sql = """SELECT JSON_VALUE(json_data, '$.location') FROM CustomersAsBlob OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY""" for r in cursor.execute(sql): print(r) # Using dot-notation to extract a value from a JSON (BLOB storage) column sql = """SELECT c.json_data.location FROM CustomersAsBlob c OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY""" for j, in cursor.execute(sql): print(j) # Using JSON_OBJECT to extract relational data as JSON sql = """SELECT JSON_OBJECT('key' IS d.dummy) dummy FROM dual d""" for r in cursor.execute(sql): print(r) python-oracledb-1.2.1/samples/json_direct.py000066400000000000000000000074271434177474600211610ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # json_direct.py # # Demonstrates the use of some JSON features with the JSON type that is # available in Oracle Database 21c and higher. # # See https://www.oracle.com/pls/topic/lookup?ctx=dblatest&id=ADJSN # # For JSON with older databases see json_blob.py # # Note: To use the JSON type in python-oracledb thin mode see json_type.py #------------------------------------------------------------------------------ import json import sys import oracledb import sample_env # this script is currently only supported in python-oracledb thick mode oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) client_version = oracledb.clientversion()[0] db_version = int(connection.version.split(".")[0]) # this script only works with Oracle Database 21 if db_version < 21: sys.exit("This example requires Oracle Database 21.1 or later. " "Try json_blob.py") # Insert JSON data with connection.cursor() as cursor: data = dict(name="Rod", dept="Sales", location="Germany") inssql = "insert into CustomersAsJson values (:1, :2)" if client_version >= 21: # Take advantage of direct binding cursor.setinputsizes(None, oracledb.DB_TYPE_JSON) cursor.execute(inssql, [1, data]) else: # Insert the data as a JSON string cursor.execute(inssql, [1, json.dumps(data)]) # Select JSON data with connection.cursor() as cursor: sql = "select c.json_data from CustomersAsJson c" if client_version >= 21: for j, in cursor.execute(sql): print(j) else: for j, in cursor.execute(sql): print(json.loads(j.read())) # Using JSON_VALUE to extract a value from a JSON column sql = """select json_value(json_data, '$.location') from CustomersAsJson offset 0 rows fetch next 1 rows only""" for r in cursor.execute(sql): print(r) # Using dot-notation to extract a value from a JSON column sql = """select c.json_data.location from CustomersAsJson c offset 0 rows fetch next 1 rows only""" if client_version >= 21: for j, in cursor.execute(sql): print(j) else: for j, in cursor.execute(sql): print(json.loads(j.read())) # Using JSON_OBJECT to extract relational data as JSON sql = """select json_object('key' is d.dummy) dummy from dual d""" for r in cursor.execute(sql): print(r) python-oracledb-1.2.1/samples/json_type.py000066400000000000000000000100201434177474600206470ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # json_type.py # # Demonstrates storing and fetching the JSON data into/from a Oracle Database # 21c JSON type column. # # In order to use the JSON type in python-oracledb thin mode a type handler is # needed to fetch the 21c JSON datatype. # # Note: The type handler is not needed when using python-oracledb thick mode # and Oracle Client 21.1 or higher. However, if a type handler is used # the behavior is the same in python-oracledb thin and thick modes. # # This script requires Oracle Database 21.1 or higher. #------------------------------------------------------------------------------ import json import sys import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) if not connection.thin: client_version = oracledb.clientversion()[0] db_version = int(connection.version.split(".")[0]) # Minimum database vesion is 21 if db_version < 21: sys.exit("This example requires Oracle Database 21.1 or later.") def type_handler(cursor, name, default_type, size, precision, scale): # to fetch the 21c JSON datatype when using python-oracledb thin mode if default_type == oracledb.DB_TYPE_JSON: return cursor.var(str, arraysize=cursor.arraysize, outconverter=json.loads) # if using Oracle Client version < 21, then the database returns the # BLOB data type instead of the JSON data type elif default_type == oracledb.DB_TYPE_BLOB: return cursor.var(default_type, arraysize=cursor.arraysize, outconverter=lambda v: json.loads(v.read())) # Insert JSON data into a JSON column with connection.cursor() as cursor: data = [ (1, dict(name="Rod", dept="Sales", location="Germany")), (2, dict(name="George", dept="Marketing", location="Bangalore")), (3, dict(name="Sam", dept="Sales", location="Mumbai")), (4, dict(name="Jill", dept="Marketing", location="Germany")) ] insert_sql = "insert into CustomersAsJson values (:1, :2)" if not connection.thin and client_version >= 21: # Take advantage of direct binding cursor.setinputsizes(None, oracledb.DB_TYPE_JSON) cursor.executemany(insert_sql, data) else: # Insert the data as a JSON string cursor.executemany(insert_sql, [(i, json.dumps(j)) for i, j in data]) # Select JSON data from a JSON column with connection.cursor() as cursor: if connection.thin or client_version < 21: cursor.outputtypehandler = type_handler for row in cursor.execute("select * from CustomersAsJson"): print(row) python-oracledb-1.2.1/samples/last_rowid.py000066400000000000000000000063221434177474600210160ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2019, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # last_rowid.py # # Demonstrates the use of the cursor.lastrowid attribute. #------------------------------------------------------------------------------ import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) with connection.cursor() as cursor: # insert a couple of rows and retain the rowid of each row1 = [1, "First"] row2 = [2, "Second"] cursor.execute("insert into mytab (id, data) values (:1, :2)", row1) rowid1 = cursor.lastrowid print("Row 1:", row1) print("Rowid 1:", rowid1) print() cursor.execute("insert into mytab (id, data) values (:1, :2)", row2) rowid2 = cursor.lastrowid print("Row 2:", row2) print("Rowid 2:", rowid2) print() # the row can be fetched with the rowid that was returned cursor.execute("select id, data from mytab where rowid = :1", [rowid1]) print("Row 1:", cursor.fetchone()) cursor.execute("select id, data from mytab where rowid = :1", [rowid2]) print("Row 2:", cursor.fetchone()) print() # updating multiple rows only returns the rowid of the last updated row cursor.execute("update mytab set data = data || ' (Modified)'") cursor.execute("select id, data from mytab where rowid = :1", [cursor.lastrowid]) print("Last updated row:", cursor.fetchone()) # deleting multiple rows only returns the rowid of the last deleted row cursor.execute("delete from mytab") print("Rowid of last deleted row:", cursor.lastrowid) # deleting no rows results in a value of None cursor.execute("delete from mytab") print("Rowid when no rows are deleted:", cursor.lastrowid) # Don't commit - this lets us run the demo multiple times #connection.commit() python-oracledb-1.2.1/samples/load_csv.py000066400000000000000000000075751434177474600204540ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # load_csv.py # # A sample showing how to load CSV data. # ------------------------------------------------------------------------------ import csv import os import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) # CSV file. This sample file has both valid rows and some rows with data too # large to insert. FILE_NAME = os.path.join('data', 'load_csv.csv') # Adjust the number of rows to be inserted in each iteration to meet your # memory and performance requirements. Typically this is a large-ish value to # reduce the number of calls to executemany() to a reasonable size. For this # demo with a small CSV file a smaller number is used to show the looping # behavior of the code. BATCH_SIZE = 19 connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) def process_batch(batch_number, cursor, data): print("processing batch", batch_number + 1) cursor.executemany(sql, data, batcherrors=True) for error in cursor.getbatcherrors(): line_num = (batch_number * BATCH_SIZE) + error.offset + 1 print("Error", error.message, "at line", line_num) with connection.cursor() as cursor: # Clean up the table for demonstration purposes cursor.execute('truncate table LoadCsvTab'); # Predefine the memory areas to match the table definition. # This can improve performance by avoiding memory reallocations. # Here, one parameter is passed for each of the columns. # "None" is used for the ID column, since the size of NUMBER isn't # variable. The "25" matches the maximum expected data size for the # NAME column cursor.setinputsizes(None, 25) # Loop over the data and insert it in batches with open(FILE_NAME, 'r') as csv_file: csv_reader = csv.reader(csv_file, delimiter=',') sql = "insert into LoadCsvTab (id, name) values (:1, :2)" data = [] batch_number = 0 for line in csv_reader: data.append((line[0], line[1])) if len(data) % BATCH_SIZE == 0: process_batch(batch_number, cursor, data) data = [] batch_number += 1 if data: process_batch(batch_number, cursor, data) # In a production system you might choose to fix any invalid rows, # re-insert them, and then commit. Or you could rollback everything. # In this sample we simply commit and ignore the invalid rows that # couldn't be inserted. connection.commit() python-oracledb-1.2.1/samples/multi_consumer_aq.py000066400000000000000000000062761434177474600224050ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. # # Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, # Canada. All rights reserved. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # multi_consumer_aq.py # # Demonstrates how to use multi-consumer advanced queuing. It makes use of a # RAW queue created in the sample setup. #------------------------------------------------------------------------------ import oracledb import sample_env # this script is currently only supported in python-oracledb thick mode oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) QUEUE_NAME = "DEMO_RAW_QUEUE_MULTI" PAYLOAD_DATA = [ "The first message", "The second message", "The third message", "The fourth and final message" ] # connect to database connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) # create a queue queue = connection.queue(QUEUE_NAME) queue.deqoptions.wait = oracledb.DEQ_NO_WAIT queue.deqoptions.navigation = oracledb.DEQ_FIRST_MSG # enqueue a few messages with connection.cursor() as cursor: print("Enqueuing messages...") for data in PAYLOAD_DATA: print(data) queue.enqone(connection.msgproperties(payload=data)) connection.commit() print() # dequeue the messages for consumer A with connection.cursor() as cursor: print("Dequeuing the messages for consumer A...") queue.deqoptions.consumername = "SUBSCRIBER_A" while True: props = queue.deqone() if not props: break print(props.payload.decode()) connection.commit() print() # dequeue the message for consumer B with connection.cursor() as cursor: print("Dequeuing the messages for consumer B...") queue.deqoptions.consumername = "SUBSCRIBER_B" while True: props = queue.deqone() if not props: break print(props.payload.decode()) connection.commit() print("\nDone.") python-oracledb-1.2.1/samples/object_aq.py000066400000000000000000000061251434177474600205770ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. # # Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, # Canada. All rights reserved. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # object_aq.py # # Demonstrates how to use advanced queuing with objects. It makes use of a # simple type and queue created in the sample setup. #------------------------------------------------------------------------------ import decimal import oracledb import sample_env # this script is currently only supported in python-oracledb thick mode oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) BOOK_TYPE_NAME = "UDT_BOOK" QUEUE_NAME = "DEMO_BOOK_QUEUE" BOOK_DATA = [ ("The Fellowship of the Ring", "Tolkien, J.R.R.", decimal.Decimal("10.99")), ("Harry Potter and the Philosopher's Stone", "Rowling, J.K.", decimal.Decimal("7.99")) ] # connect to database connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) # create a queue books_type = connection.gettype(BOOK_TYPE_NAME) queue = connection.queue(QUEUE_NAME, payload_type=books_type) queue.deqoptions.wait = oracledb.DEQ_NO_WAIT queue.deqoptions.navigation = oracledb.DEQ_FIRST_MSG # dequeue all existing messages to ensure the queue is empty, just so that # the results are consistent while queue.deqone(): pass # enqueue a few messages print("Enqueuing messages...") for title, authors, price in BOOK_DATA: book = books_type.newobject() book.TITLE = title book.AUTHORS = authors book.PRICE = price print(title) queue.enqone(connection.msgproperties(payload=book)) connection.commit() # dequeue the messages print("\nDequeuing messages...") while True: props = queue.deqone() if not props: break print(props.payload.TITLE) connection.commit() print("\nDone.") python-oracledb-1.2.1/samples/oracledb_upgrade.py000066400000000000000000000220031434177474600221230ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # oracledb_upgrade.py # # Example module to assist upgrading large applications from cx_Oracle 8 to # python-oracledb (the renamed major new release of cx_Oracle). # # An environment variable ORA_PYTHON_DRIVER_TYPE can be set to determine # whether to use cx_Oracle, python-oracledb Thin mode, or python-oracledb Thick # mode. # # NOTE # # Most users DO NOT need this file. Instead you can do: # # - Install python-oracledb: # # python -m pip install oracledb # # - Change "import cx_Oracle" to: # # import oracledb as cx_Oracle # # - Remove any call to init_oracle_client() if you want to use # python-oracledb Thin mode. Conversely add a call if you want to use # Thick mode. # # - Use named parameters in calls to connect(), Connection() and # SessionPool(): # # c = oracledb.connect(user="un", password="pw", dsn="cs") # # Other updates noted in the upgrade documentation may also need to be # made. # # However, if you want to toggle which driver to use or have code where # changes are not easy, then this file may help. # # USAGE # # - Install python-oracledb: # # python -m pip install oracledb # # - Change your code's first "import cx_Oracle" to: # # import oracledb_upgrade as cx_Oracle # # This needs to be imported before cx_Oracle is ever imported for it to # take effect. Subsequent imports do not need to be changed but may be, # if desired. # # - Remove any call to init_oracle_client() from your existing code base. # # - Set lib_dir (in the module code below) to your Oracle Client library # directory if you are calling init_oracle_client() with that parameter in # your existing cx_Oracle code on Windows or macOS, and you intend to use # python-oracledb Thick mode or cx_Oracle. # # - Review python-oracledb documentation for additional changes that may be # needed in your code. # # - Set the environment variable ORA_PYTHON_DRIVER_TYPE to "cx", "thin", or # "thick" to choose which module to use: # # thin -> python-oracledb Thin mode (the default) # # thick -> python-oracledb Thick mode # # cx -> cx_Oracle # # - Run your application # # An example application showing this module in use is: # # import oracledb_upgrade as cx_Oracle # import os # # un = os.environ.get("PYTHON_USERNAME") # pw = os.environ.get("PYTHON_PASSWORD") # cs = os.environ.get("PYTHON_CONNECTSTRING") # # connection = cx_Oracle.connect(user=un, password=pw, dsn=cs) # with connection.cursor() as cursor: # sql = """SELECT UNIQUE CLIENT_DRIVER # FROM V$SESSION_CONNECT_INFO # WHERE SID = SYS_CONTEXT('USERENV', 'SID')""" # for r, in cursor.execute(sql): # print(r) # #------------------------------------------------------------------------------ import os import sys import platform import cx_Oracle import oracledb # Set True to print which driver and mode is being used MODE_TRACE = False # Enable a 'shim' if your cx_Oracle code makes connect(), Connection() or # SessionPool() calls that use positional parameters e.g. like: # cx_Oracle.connect(username, password, connect_string) ALLOW_POSITIONAL_CONNECT_ARGS = True # On macOS and Windows set lib_dir to your Instant Client path if you are # currently calling init_oracle_client() in your application. On Linux do not # set lib_dir; instead set LD_LIBRARY_PATH or configure ldconfig before running # Python. lib_dir = None if platform.system() == "Darwin": lib_dir = os.environ.get("HOME")+"/Downloads/instantclient_19_8" elif platform.system() == "Windows": lib_dir = r"C:\oracle\instantclient_19_14" # Determine which module and mode to use. # The default is python-oracledb Thin mode. driver_type = os.environ.get("ORA_PYTHON_DRIVER_TYPE", "thin") if driver_type.lower() == "cx": if MODE_TRACE: print("Using cx_Oracle") from cx_Oracle import * sys.modules["oracledb"] = cx_Oracle sys.modules["cx_Oracle"] = cx_Oracle oracledb.init_oracle_client(lib_dir=lib_dir) else: from oracledb import * sys.modules["oracledb"] = oracledb sys.modules["cx_Oracle"] = oracledb if driver_type.lower() == "thick": if MODE_TRACE: print("Using python-oracledb thick") # For python-oracledb Thick mode, init_oracle_client() MUST be called # on all operating systems. Whether to use a lib_dir value depends on # how your system library search path is configured. oracledb.init_oracle_client(lib_dir=lib_dir) else: if MODE_TRACE: print("Using python-oracledb thin") # If your existing cx_Oracle code never used positional arguments for # connection and pool creation calls then inject_connect_shim() is not # necessary and you can set ALLOW_POSITIONAL_CONNECT_ARGS to False def inject_connect_shim(): """ Allow python-oracledb to use positional arguments in connect(), Connection() and SessionPool() signatures as allowed by cx_Oracle. """ class ShimConnection(oracledb.Connection): def __init__(self, user=None, password=None, dsn=None, mode=oracledb.DEFAULT_AUTH, handle=0, pool=None, threaded=False, events=False, cclass=None, purity=oracledb.ATTR_PURITY_DEFAULT, newpassword=None, encoding=None, nencoding=None, edition=None, appcontext=[], tag=None, matchanytag=False, shardingkey=[], supershardingkey=[], stmtcachesize=20): if dsn is None and password is None: dsn = user user = None super().__init__(dsn=dsn, user=user, password=password, mode=mode, handle=handle, pool=pool, threaded=threaded, events=events, cclass=cclass, purity=purity, newpassword=newpassword, edition=edition, appcontext=appcontext, tag=tag, matchanytag=matchanytag, shardingkey=shardingkey, supershardingkey=supershardingkey, stmtcachesize=stmtcachesize) class ShimPool(oracledb.SessionPool): def __init__(self, user=None, password=None, dsn=None, min=1, max=2, increment=1, connectiontype=oracledb.Connection, threaded=True, getmode=oracledb.SPOOL_ATTRVAL_NOWAIT, events=False, homogeneous=True, externalauth=False, encoding=None, nencoding=None, edition=None, timeout=0, wait_timeout=0, max_lifetime_session=0, session_callback=None, max_sessions_per_shard=0, soda_metadata_cache=False, stmtcachesize=20, ping_interval=60): super().__init__(dsn=dsn, user=user, password=password, min=min, max=max, increment=increment, connectiontype=connectiontype, threaded=threaded, getmode=getmode, events=events, homogeneous=homogeneous, externalauth=externalauth, encoding=encoding, nencoding=nencoding, edition=edition, timeout=timeout, wait_timeout=wait_timeout, max_lifetime_session=max_lifetime_session, session_callback=session_callback, max_sessions_per_shard=max_sessions_per_shard, soda_metadata_cache=soda_metadata_cache, stmtcachesize=stmtcachesize, ping_interval=ping_interval) global connect connect = ShimConnection global Connection Connection = ShimConnection global SessionPool SessionPool = ShimPool if ALLOW_POSITIONAL_CONNECT_ARGS and driver_type.lower() != "cx": inject_connect_shim() python-oracledb-1.2.1/samples/plsql_collection.py000066400000000000000000000052231434177474600222140ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # plsql_collection.py # # Demonstrates how to get the value of a PL/SQL collection from a stored # procedure. # # This feature is only available in Oracle Database 12.1 and higher. #------------------------------------------------------------------------------ import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) # create a new empty object of the correct type. # note the use of a PL/SQL type that is defined in a package type_obj = connection.gettype("PKG_DEMO.UDT_STRINGLIST") obj = type_obj.newobject() # call the stored procedure which will populate the object with connection.cursor() as cursor: cursor.callproc("pkg_Demo.DemoCollectionOut", (obj,)) # show the indexes that are used by the collection print("Indexes and values of collection:") ix = obj.first() while ix is not None: print(ix, "->", obj.getelement(ix)) ix = obj.next(ix) print() # show the values as a simple list print("Values of collection as list:") print(obj.aslist()) print() # show the values as a simple dictionary print("Values of collection as dictionary:") print(obj.asdict()) print() python-oracledb-1.2.1/samples/plsql_function.py000066400000000000000000000036621434177474600217130ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # plsql_function.py # # Demonstrates how to call a PL/SQL function and get its return value. #------------------------------------------------------------------------------ import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) with connection.cursor() as cursor: # The second parameter is the expected return type of the PL/SQL function res = cursor.callfunc('myfunc', int, ('abc', 2)) print(res) python-oracledb-1.2.1/samples/plsql_procedure.py000066400000000000000000000036351434177474600220560ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # plsql_procedure.py # # Demonstrates how to call a PL/SQL stored procedure and get the results of an # OUT variable. #------------------------------------------------------------------------------ import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) with connection.cursor() as cursor: myvar = cursor.var(int) cursor.callproc('myproc', (123, myvar)) print(myvar.getvalue()) python-oracledb-1.2.1/samples/plsql_record.py000066400000000000000000000053611434177474600213420ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # plsql_record.py # # Demonstrates how to bind (IN and OUT) a PL/SQL record. # # This feature is only available in Oracle Database 12.1 and higher. #------------------------------------------------------------------------------ import datetime import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) # create new object of the correct type # note the use of a PL/SQL record defined in a package # a table record identified by TABLE%ROWTYPE can also be used type_obj = connection.gettype("PKG_DEMO.UDT_DEMORECORD") obj = type_obj.newobject() obj.NUMBERVALUE = 6 obj.STRINGVALUE = "Test String" obj.DATEVALUE = datetime.datetime(2016, 5, 28) obj.BOOLEANVALUE = False # show the original values print("NUMBERVALUE ->", obj.NUMBERVALUE) print("STRINGVALUE ->", obj.STRINGVALUE) print("DATEVALUE ->", obj.DATEVALUE) print("BOOLEANVALUE ->", obj.BOOLEANVALUE) print() with connection.cursor() as cursor: # call the stored procedure which will modify the object cursor.callproc("pkg_Demo.DemoRecordsInOut", (obj,)) # show the modified values print("NUMBERVALUE ->", obj.NUMBERVALUE) print("STRINGVALUE ->", obj.STRINGVALUE) print("DATEVALUE ->", obj.DATEVALUE) print("BOOLEANVALUE ->", obj.BOOLEANVALUE) print() python-oracledb-1.2.1/samples/query.py000066400000000000000000000050641434177474600200160ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # query.py # # Demonstrates different ways of fetching rows from a query. #------------------------------------------------------------------------------ import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) sql = """select * from SampleQueryTab where id < 6 order by id""" with connection.cursor() as cursor: print("Get all rows via an iterator") for result in cursor.execute(sql): print(result) print() print("Query one row at a time") cursor.execute(sql) row = cursor.fetchone() print(row) row = cursor.fetchone() print(row) print() print("Fetch many rows") cursor.execute(sql) res = cursor.fetchmany(3) print(res) print() print("Fetch all rows") cursor.execute(sql) res = cursor.fetchall() print(res) print() print("Fetch each row as a Dictionary") cursor.execute(sql) columns = [col[0] for col in cursor.description] cursor.rowfactory = lambda *args: dict(zip(columns, args)) for row in cursor: print(row) python-oracledb-1.2.1/samples/query_arraysize.py000066400000000000000000000061511434177474600221050ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # query_arraysize.py # # Demonstrates how to alter the array size and prefetch rows values in order to # tune the performance of fetching data from the database. Increasing these # values can reduce the number of network round trips and overhead required to # fetch all of the rows from a large table. The value affect internal buffers # and do not affect how, or when, rows are returned to your application. # # The best values need to be determined by tuning in your production # environment. #------------------------------------------------------------------------------ import time import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) # Global values can be set to override the defaults used when a cursor is # created oracledb.defaults.prefetchrows = 200 # default is 2 oracledb.defaults.arraysize = 200 # default is 100 with connection.cursor() as cursor: # Example 1 start = time.time() cursor.execute('select * from bigtab') res = cursor.fetchall() elapsed = (time.time() - start) print("Prefetchrows:", cursor.prefetchrows, "Arraysize:", cursor.arraysize) print("Retrieved", len(res), "rows in", elapsed, "seconds") # Example 2 start = time.time() # values can be set per-cursor cursor.prefetchrows = 1000 cursor.arraysize = 1000 cursor.execute('select * from bigtab') res = cursor.fetchall() # print(res) # uncomment to display the query results elapsed = (time.time() - start) print("Prefetchrows:", cursor.prefetchrows, "Arraysize:", cursor.arraysize) print("Retrieved", len(res), "rows in", elapsed, "seconds") python-oracledb-1.2.1/samples/query_strings_as_bytes.py000066400000000000000000000060451434177474600234600ustar00rootroot00000000000000# ----------------------------------------------------------------------------- # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # query_strings_as_bytes.py # # Demonstrates how to query strings as bytes (bypassing decoding of the bytes # into a Python string). This can be useful when attempting to fetch data that # was stored in the database in the wrong encoding. #------------------------------------------------------------------------------ import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) STRING_VAL = 'I bought a cafetière on the Champs-Élysées' def return_strings_as_bytes(cursor, name, default_type, size, precision, scale): if default_type == oracledb.DB_TYPE_VARCHAR: return cursor.var(str, arraysize=cursor.arraysize, bypass_decode=True) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) # truncate table and populate with our data of choice with connection.cursor() as cursor: cursor.execute("truncate table TestTempTable") cursor.execute("insert into TestTempTable values (1, :val)", val=STRING_VAL) connection.commit() # fetch the data normally and show that it is returned as a string with connection.cursor() as cursor: cursor.execute("select IntCol, StringCol from TestTempTable") print("Data fetched using normal technique:") for row in cursor: print(row) print() # fetch the data, bypassing the decode and show that it is returned as # bytes with connection.cursor() as cursor: cursor.outputtypehandler = return_strings_as_bytes cursor.execute("select IntCol, StringCol from TestTempTable") print("Data fetched using bypass decode technique:") for row in cursor: print(row) python-oracledb-1.2.1/samples/raw_aq.py000066400000000000000000000056511434177474600201250ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2019, 2022, Oracle and/or its affiliates. # # Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. # # Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, # Canada. All rights reserved. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # raw_aq.py # # Demonstrates how to use advanced queuing with RAW data. It makes use of a # RAW queue created in the sample setup. #------------------------------------------------------------------------------ import oracledb import sample_env # this script is currently only supported in python-oracledb thick mode oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) QUEUE_NAME = "DEMO_RAW_QUEUE" PAYLOAD_DATA = [ "The first message", "The second message", "The third message", "The fourth and final message" ] connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) # create a queue with connection.cursor() as cursor: queue = connection.queue(QUEUE_NAME) queue.deqoptions.wait = oracledb.DEQ_NO_WAIT queue.deqoptions.navigation = oracledb.DEQ_FIRST_MSG # dequeue all existing messages to ensure the queue is empty, just so that # the results are consistent while queue.deqone(): pass # enqueue a few messages print("Enqueuing messages...") with connection.cursor() as cursor: for data in PAYLOAD_DATA: print(data) queue.enqone(connection.msgproperties(payload=data)) connection.commit() # dequeue the messages print("\nDequeuing messages...") with connection.cursor() as cursor: while True: props = queue.deqone() if not props: break print(props.payload.decode()) connection.commit() print("\nDone.") python-oracledb-1.2.1/samples/ref_cursor.py000066400000000000000000000076141434177474600210250ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2018, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # ref_cursor.py # # Demonstrates the use of REF CURSORS. #------------------------------------------------------------------------------ import time import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) with connection.cursor() as cursor: ref_cursor = connection.cursor() cursor.callproc("myrefcursorproc", (2, 6, ref_cursor)) print("Rows between 2 and 6:") for row in ref_cursor: print(row) print() ref_cursor = connection.cursor() cursor.callproc("myrefcursorproc", (8, 9, ref_cursor)) print("Rows between 8 and 9:") for row in ref_cursor: print(row) print() #-------------------------------------------------------------------------- # Setting prefetchrows and arraysize of a REF CURSOR can improve # performance when fetching a large number of rows by reducing network # round-trips. #-------------------------------------------------------------------------- # Truncate the table used for this demo cursor.execute("truncate table TestTempTable") # Populate the table with a large number of rows num_rows = 50000 sql = "insert into TestTempTable (IntCol) values (:1)" data = [(n + 1,) for n in range(num_rows)] cursor.executemany(sql, data) # Perform an untuned fetch ref_cursor = connection.cursor() print("ref_cursor.prefetchrows =", ref_cursor.prefetchrows, "ref_cursor.arraysize =", ref_cursor.arraysize) start = time.time() sum_rows = 0 cursor.callproc("myrefcursorproc2", [ref_cursor]) for row in ref_cursor: sum_rows += row[0] elapsed = (time.time() - start) print("Sum of IntCol for", num_rows, "rows is ", sum_rows, "in", elapsed, "seconds") print() # Repeat the call but increase the internal arraysize and prefetch row # buffers for the REF CURSOR to tune the number of round-trips to the # database ref_cursor = connection.cursor() ref_cursor.prefetchrows = 1000 ref_cursor.arraysize = 1000 print("ref_cursor.prefetchrows =", ref_cursor.prefetchrows, "ref_cursor.arraysize =", ref_cursor.arraysize) start = time.time() sum_rows = 0 cursor.callproc("myrefcursorproc2", [ref_cursor]) for row in ref_cursor: sum_rows += row[0] elapsed = (time.time() - start) print("Sum of IntCol for", num_rows, "rows is ", sum_rows, "in", elapsed, "seconds") python-oracledb-1.2.1/samples/return_lobs_as_strings.py000066400000000000000000000067501434177474600234460ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. # # Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, # Canada. All rights reserved. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # return_lobs_as_strings.py # # Returns all CLOB values as strings and BLOB values as bytes. The # performance of this technique is significantly better than fetching the LOBs # and then reading the contents of the LOBs as it avoids round-trips to the # database. Be aware, however, that this method requires contiguous memory so # is not suitable for very large LOBs. #------------------------------------------------------------------------------ import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) # indicate that LOBS should not be fetched oracledb.defaults.fetch_lobs = False connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) with connection.cursor() as cursor: # add some data to the tables print("Populating tables with data...") cursor.execute("truncate table TestClobs") cursor.execute("truncate table TestBlobs") long_string = "" for i in range(10): char = chr(ord('A') + i) long_string += char * 25000 cursor.execute("insert into TestClobs values (:1, :2)", (i + 1, "STRING " + long_string)) cursor.execute("insert into TestBlobs values (:1, :2)", (i + 1, long_string.encode("ascii"))) connection.commit() # fetch the data and show the results print("CLOBS returned as strings") cursor.execute(""" select IntCol, ClobCol from TestClobs order by IntCol""") for int_col, value in cursor: print("Row:", int_col, "string of length", len(value)) print() print("BLOBS returned as bytes") cursor.execute(""" select IntCol, BlobCol from TestBlobs order by IntCol""") for int_col, value in cursor: print("Row:", int_col, "string of length", value and len(value) or 0) python-oracledb-1.2.1/samples/return_numbers_as_decimals.py000066400000000000000000000044311434177474600242440ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # return_numbers_as_decimals.py # # Returns all numbers as decimals. This is needed if the full decimal # precision of Oracle numbers is required by the application. See this article # (http://blog.reverberate.org/2016/02/06/floating-point-demystified-part2.html) # for an explanation of why decimal numbers (like Oracle numbers) cannot be # represented exactly by floating point numbers. #------------------------------------------------------------------------------ import decimal import oracledb import sample_env # indicate that numbers should be fetched as decimals oracledb.defaults.fetch_decimals = True # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) with connection.cursor() as cursor: cursor.execute("select * from TestNumbers") for row in cursor: print("Row:", row) python-oracledb-1.2.1/samples/rows_as_instance.py000066400000000000000000000061671434177474600222170ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. # # Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, # Canada. All rights reserved. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # rows_as_instance.py # # Returns rows as instances instead of tuples. See the ceDatabase.Row class # in the cx_PyGenLib project (http://cx-pygenlib.sourceforge.net) for a more # advanced example. #------------------------------------------------------------------------------ import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) class Test: def __init__(self, a, b, c): self.a = a self.b = b self.c = c connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) with connection.cursor() as cursor: # create sample data cursor.execute(""" begin begin execute immediate 'drop table TestInstances'; exception when others then if sqlcode <> -942 then raise; end if; end; execute immediate 'create table TestInstances ( a varchar2(60) not null, b number(9) not null, c date not null)'; execute immediate 'insert into TestInstances values (''First'', 5, sysdate)'; execute immediate 'insert into TestInstances values (''Second'', 25, sysdate)'; commit; end;""") # retrieve the data and display it cursor.execute("select * from TestInstances") cursor.rowfactory = Test print("Rows:") for row in cursor: print("a = %s, b = %s, c = %s" % (row.a, row.b, row.c)) python-oracledb-1.2.1/samples/sample_container/000077500000000000000000000000001434177474600216155ustar00rootroot00000000000000python-oracledb-1.2.1/samples/sample_container/Dockerfile000066400000000000000000000047531434177474600236200ustar00rootroot00000000000000# NAME # # Dockerfile # # PURPOSE # # Creates a container with the Python python-oracledb samples and a running # Oracle Database so python-oracledb can be evaluated. # # Python-oracledb is the Python database driver for Oracle Database. See # https://oracle.github.io/python-oracledb/ # # USAGE # # Get an Oracle Database container (see # https://hub.docker.com/r/gvenzl/oracle-xe): # # podman pull docker.io/gvenzl/oracle-xe:21-slim # # Create a container with the database, Python, python-oracledb and the # samples. Choose a password for the sample schemas and pass it as an # argument: # # podman build -t pyo --build-arg PYO_PASSWORD=a_secret . # # Start the container, which creates the database. Choose a password for the # privileged database users and pass it as a variable: # # podman run -d --name pyo -p 1521:1521 -it -e ORACLE_PASSWORD=a_secret pyo # # Log into the container: # # podman exec -it pyo bash # # At the first login, create the sample schema: # # python setup.py # # Run samples like: # # python bind_insert.py # # The database will persist across container shutdowns, but will be deleted # when the container is deleted. FROM docker.io/gvenzl/oracle-xe:21-slim USER root RUN microdnf module disable python36 && \ microdnf module enable python39 && \ microdnf install python39 python39-pip python39-setuptools python39-wheel vim vi httpd-tools && \ microdnf clean all WORKDIR /samples/ COPY setup.py setup.py RUN curl -LO https://github.com/oracle/python-oracledb/archive/refs/heads/main.zip && \ unzip main.zip && mv python-oracledb-main/samples/* . && \ /bin/rm -rf python-oracledb-main samples main.zip && \ cat create_schema.py >> /samples/setup.py && chown -R oracle.oinstall /samples/ USER oracle RUN python3 -m pip install oracledb Flask --user --no-warn-script-location ARG PYO_PASSWORD ENV PYO_SAMPLES_MAIN_USER=pythondemo ENV PYO_SAMPLES_MAIN_PASSWORD=${PYO_PASSWORD} ENV PYO_SAMPLES_EDITION_USER=pythoneditions ENV PYO_SAMPLES_EDITION_PASSWORD=${PYO_PASSWORD} ENV PYO_SAMPLES_EDITION_NAME=python_e1 ENV PYO_SAMPLES_CONNECT_STRING="localhost/xepdb1" ENV PYO_SAMPLES_DRCP_CONNECT_STRING="localhost/xepdb1:pooled" ENV PYO_SAMPLES_ADMIN_USER=system # Run the samples using the default python-oracledb 'Thin' mode, if possible ENV PYO_SAMPLES_DRIVER_MODE="thin" # The privileged user password is set in setup.py from the "podman run" # environment variable ORACLE_PASSWORD #ENV PYO_SAMPLES_ADMIN_PASSWORD= python-oracledb-1.2.1/samples/sample_container/README.md000066400000000000000000000023771434177474600231050ustar00rootroot00000000000000# python-oracledb Samples in a Container This Dockerfile creates a container with python-oracledb samples and a running Oracle Database. It has been tested in an Oracle Linux 8 environment using 'podman', but 'docker' should work too. ## Usage - Get an Oracle Database container (see https://hub.docker.com/r/gvenzl/oracle-xe): ``` podman pull docker.io/gvenzl/oracle-xe:21-slim ``` - Create a container with the database, Python, python-oracledb and the samples. Choose a password for the sample schemas and pass it as an argument: ``` podman build -t pyo --build-arg PYO_PASSWORD=a_secret . ``` - Start the container, which creates the database. Choose a password for the privileged database users and pass it as a variable: ``` podman run -d --name pyo -p 1521:1521 -it -e ORACLE_PASSWORD=a_secret_password pyo ``` - Log into the container: ``` podman exec -it pyo bash ``` - At the first login, create the sample schema: ``` python setup.py ``` The schema used can be seen in `sql/create_schema.sql` - In the container, run samples like: ``` python bind_insert.py ``` Use `vim` to edit files, if required. The database will persist across container shutdowns, but will be deleted when the container is deleted. python-oracledb-1.2.1/samples/sample_container/setup.py000066400000000000000000000030521434177474600233270ustar00rootroot00000000000000#! /usr/bin/env python3.9 # # NAME # # setup.py # # PURPOSE # # Creates the python-oracledb sample schema after waiting for the database to # open. # # USAGE # # ./setup.py import oracledb import os import time oracledb.init_oracle_client() pw = os.environ.get("ORACLE_PASSWORD") os.environ["PYO_SAMPLES_ADMIN_PASSWORD"] = pw c = None for i in range(30): try: c = oracledb.connect(user="system", password=pw, dsn="localhost/xepdb1", tcp_connect_timeout=5) break except (OSError, oracledb.Error) as e: print("Waiting for database to open") time.sleep(5) if c: print("PDB is open") else: print("PDB did not open in allocated time") print("Try again in a few minutes") exit() print("Enabling per-PDB DRCP") c = oracledb.connect(mode=oracledb.SYSDBA) cursor = c.cursor() cursor.execute("alter pluggable database all close") cursor.execute("alter system set enable_per_pdb_drcp=true scope=spfile sid='*'") c = oracledb.connect(mode=oracledb.SYSDBA | oracledb.PRELIM_AUTH) c.startup(force=True) c = oracledb.connect(mode=oracledb.SYSDBA) cursor = c.cursor() cursor.execute("alter database mount") cursor.execute("alter database open") c = oracledb.connect(user="sys", password=pw, dsn="localhost/xepdb1", mode=oracledb.SYSDBA) cursor = c.cursor() cursor.callproc("dbms_connection_pool.start_pool") # create_schema.py will be appended here by the Dockerfile python-oracledb-1.2.1/samples/sample_env.py000066400000000000000000000206551434177474600210050ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # Sets the environment used by the python-oracledb sample scripts. Production # applications should consider using External Authentication to avoid hard # coded credentials. # # The samples will prompt for credentials and schema information unless the # following environment variables are set: # # PYO_SAMPLES_ORACLE_CLIENT_PATH: Oracle Client or Instant Client library dir # PYO_SAMPLES_ADMIN_USER: privileged administrative user for setting up samples # PYO_SAMPLES_ADMIN_PASSWORD: password of PYO_SAMPLES_ADMIN_USER # PYO_SAMPLES_CONNECT_STRING: database connection string # PYO_SAMPLES_DRCP_CONNECT_STRING: database connection string for DRCP # PYO_SAMPLES_MAIN_USER: user to be created. Used for most samples # PYO_SAMPLES_MAIN_PASSWORD: password for PYO_SAMPLES_MAIN_USER # PYO_SAMPLES_EDITION_USER: user to be created for editiong samples # PYO_SAMPLES_EDITION_PASSWORD: password of PYO_SAMPLES_EDITION_USER # PYO_SAMPLES_EDITION_NAME: name of edition for editioning samples # PYO_SAMPLES_DRIVER_MODE: python-oracledb mode (Thick or thin) to use # # - On Windows set PYO_SAMPLES_ORACLE_CLIENT_PATH if Oracle libraries are not # in PATH. On macOS set the variable to the Instant Client directory. On # Linux do not set the variable; instead set LD_LIBRARY_PATH or configure # ldconfig before running Python. # # - PYO_SAMPLES_ADMIN_USER should be the administrative user ADMIN for cloud # databases and SYSTEM for on premises databases. # # - PYO_SAMPLES_CONNECT_STRING is the connection string for your database. It # can be set to an Easy Connect string or to a Net Service Name from a # tnsnames.ora file or external naming service. # # The Easy Connect string is generally easiest. The basic syntax is: # # host_name[:port][/service_name][:server_type] # # Commonly just the host_name and service_name are needed # e.g. "localhost/orclpdb" or "localhost/XEPDB1". # # If PYO_SAMPLES_CONNECT_STRING is an aliases from tnsnames.ora file, then # set the TNS_ADMIN environment variable and put the file in # $TNS_ADMIN/tnsnames.ora. Alternatively for python-oracledb Thick mode, the # file will be automatically used if it is in a location such as # instantclient_XX_Y/network/admin/tnsnames.ora, # $ORACLE_HOME/network/admin/tnsnames.ora or /etc/tnsnames.ora. # # - PYO_SAMPLES_DRCP_CONNECT_STRING should be a connect string requesting a # pooled server, for example "localhost/orclpdb:pooled". # # - PYO_SAMPLES_MAIN_USER and PYO_SAMPLES_EDITION_USER are names of users that # will be created and used for running samples. Choose names that do not # exist because the schemas will be dropped and then recreated. # # - PYO_SAMPLES_EDITION_NAME can be set to a new name to be used for Edition # Based Redefinition examples. # # - PYO_SAMPLES_DRIVER_MODE should be "thin" or "thick". It is used by samples # that can run in both python-oracledb modes. # #------------------------------------------------------------------------------ import getpass import os import sys # default values DEFAULT_MAIN_USER = "pythondemo" DEFAULT_EDITION_USER = "pythoneditions" DEFAULT_EDITION_NAME = "python_e1" DEFAULT_CONNECT_STRING = "localhost/orclpdb1" DEFAULT_DRCP_CONNECT_STRING = "localhost/orclpdb1:pooled" # dictionary containing all parameters; these are acquired as needed by the # methods below (which should be used instead of consulting this dictionary # directly) and then stored so that a value is not requested more than once PARAMETERS = {} def get_value(name, label, default_value=None, password=False): value = PARAMETERS.get(name) if value is not None: return value value = os.environ.get(name) if value is None: if default_value is not None: label += " [%s]" % default_value label += ": " if not password: value = input(label).strip() else: value = getpass.getpass(label) if not value: value = default_value PARAMETERS[name] = value return value def get_main_user(): return get_value("PYO_SAMPLES_MAIN_USER", "Main User Name", DEFAULT_MAIN_USER) def get_main_password(): return get_value("PYO_SAMPLES_MAIN_PASSWORD", f"Password for {get_main_user()}", password=True) def get_edition_user(): return get_value("PYO_SAMPLES_EDITION_USER", "Edition User Name", DEFAULT_EDITION_USER) def get_edition_password(): return get_value("PYO_SAMPLES_EDITION_PASSWORD", f"Password for {get_edition_user()}", password=True) def get_edition_name(): return get_value("PYO_SAMPLES_EDITION_NAME", "Edition Name", DEFAULT_EDITION_NAME) def get_connect_string(): return get_value("PYO_SAMPLES_CONNECT_STRING", "Connect String", DEFAULT_CONNECT_STRING) def get_drcp_connect_string(): return get_value("PYO_SAMPLES_DRCP_CONNECT_STRING", "DRCP Connect String", DEFAULT_DRCP_CONNECT_STRING) def get_driver_mode(): return get_value("PYO_SAMPLES_DRIVER_MODE", "Driver mode (thin|thick)", "thin") def get_is_thin(): return get_driver_mode() == "thin" def get_edition_connect_string(): return "%s/%s@%s" % \ (get_edition_user(), get_edition_password(), get_connect_string()) def get_admin_connect_string(): admin_user = get_value("PYO_SAMPLES_ADMIN_USER", "Administrative user", "admin") admin_password = get_value("PYO_SAMPLES_ADMIN_PASSWORD", f"Password for {admin_user}", password=True) return "%s/%s@%s" % (admin_user, admin_password, get_connect_string()) def get_oracle_client(): if sys.platform in ("darwin", "win32"): return get_value("PYO_SAMPLES_ORACLE_CLIENT_PATH", "Oracle Instant Client Path") def run_sql_script(conn, script_name, **kwargs): statement_parts = [] cursor = conn.cursor() replace_values = [("&" + k + ".", v) for k, v in kwargs.items()] + \ [("&" + k, v) for k, v in kwargs.items()] script_dir = os.path.dirname(os.path.abspath(sys.argv[0])) file_name = os.path.join(script_dir, "sql", script_name + ".sql") for line in open(file_name): if line.strip() == "/": statement = "".join(statement_parts).strip() if statement: for search_value, replace_value in replace_values: statement = statement.replace(search_value, replace_value) try: cursor.execute(statement) except: print("Failed to execute SQL:", statement) raise statement_parts = [] else: statement_parts.append(line) cursor.execute(""" select name, type, line, position, text from dba_errors where owner = upper(:owner) order by name, type, line, position""", owner = get_main_user()) prev_name = prev_obj_type = None for name, obj_type, line_num, position, text in cursor: if name != prev_name or obj_type != prev_obj_type: print("%s (%s)" % (name, obj_type)) prev_name = name prev_obj_type = obj_type print(" %s/%s %s" % (line_num, position, text)) python-oracledb-1.2.1/samples/scrollable_cursors.py000066400000000000000000000066061434177474600225560ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. # # Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, # Canada. All rights reserved. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # scrollable_cursors.py # # Demonstrates how to use scrollable cursors. These allow moving forward and # backward in the result set but incur additional overhead on the server to # retain this information. #------------------------------------------------------------------------------ import oracledb import sample_env # this script is currently only supported in python-oracledb thick mode oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) # show all of the rows available in the table with connection.cursor() as cursor: cursor.execute("select * from TestStrings order by IntCol") print("ALL ROWS") for row in cursor: print(row) print() # create a scrollable cursor with connection.cursor(scrollable = True) as cursor: # set array size smaller than the default (100) to force scrolling by the # database; otherwise, scrolling occurs directly within the buffers cursor.arraysize = 3 cursor.execute("select * from TestStrings order by IntCol") # scroll to last row in the result set; the first parameter is not needed # and is ignored) cursor.scroll(mode = "last") print("LAST ROW") print(cursor.fetchone()) print() # scroll to the first row in the result set; the first parameter not needed # and is ignored cursor.scroll(mode = "first") print("FIRST ROW") print(cursor.fetchone()) print() # scroll to an absolute row number cursor.scroll(5, mode = "absolute") print("ROW 5") print(cursor.fetchone()) print() # scroll forward six rows (the mode parameter defaults to relative) cursor.scroll(3) print("SKIP 3 ROWS") print(cursor.fetchone()) print() # scroll backward four rows (the mode parameter defaults to relative) cursor.scroll(-4) print("SKIP BACK 4 ROWS") print(cursor.fetchone()) print() python-oracledb-1.2.1/samples/session_callback.py000066400000000000000000000155471434177474600221570ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2019, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # session_callback.py # # Demonstrates how to use a connection pool session callback written in # Python. The callback is invoked whenever a newly created session is acquired # from the pool, or when the requested tag does not match the tag that is # associated with the session. It is generally used to set session state, so # that the application can count on known session state, which allows the # application to reduce the number of round trips made to the database. # If all your connections should have the same session state, you can simplify # the session callback by removing the tagging logic. # # Also see session_callback_plsql.py #------------------------------------------------------------------------------ import oracledb import sample_env # this script is currently only supported in python-oracledb thick mode oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) # define a dictionary of NLS_DATE_FORMAT formats supported by this sample SUPPORTED_FORMATS = { "SIMPLE" : "'YYYY-MM-DD HH24:MI'", "FULL" : "'YYYY-MM-DD HH24:MI:SS'" } # define a dictionary of TIME_ZONE values supported by this sample SUPPORTED_TIME_ZONES = { "UTC" : "'UTC'", "MST" : "'-07:00'" } # define a dictionary of keys that are supported by this sample SUPPORTED_KEYS = { "NLS_DATE_FORMAT" : SUPPORTED_FORMATS, "TIME_ZONE" : SUPPORTED_TIME_ZONES } # define session callback def init_session(conn, requested_tag): # display the requested and actual tags print("init_session(): requested tag=%r, actual tag=%r" % \ (requested_tag, conn.tag)) # tags are expected to be in the form "key1=value1;key2=value2" # in this example, they are used to set NLS parameters and the tag is # parsed to validate it if requested_tag is not None: state_parts = [] for directive in requested_tag.split(";"): parts = directive.split("=") if len(parts) != 2: raise ValueError("Tag must contain key=value pairs") key, value = parts value_dict = SUPPORTED_KEYS.get(key) if value_dict is None: raise ValueError("Tag only supports keys: %s" % \ (", ".join(SUPPORTED_KEYS))) actual_value = value_dict.get(value) if actual_value is None: raise ValueError("Key %s only supports values: %s" % \ (key, ", ".join(value_dict))) state_parts.append("%s = %s" % (key, actual_value)) sql = "alter session set %s" % " ".join(state_parts) cursor = conn.cursor() cursor.execute(sql) # assign the requested tag to the connection so that when the connection # is closed, it will automatically be retagged; note that if the requested # tag is None (no tag was requested) this has no effect conn.tag = requested_tag # create pool with session callback defined pool = oracledb.create_pool(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string(), min=2, max=5, increment=1, session_callback=init_session) # acquire session without specifying a tag; since the session returned is # newly created, the callback will be invoked but since there is no tag # specified, no session state will be changed print("(1) acquire session without tag") with pool.acquire() as conn: cursor = conn.cursor() cursor.execute("select to_char(current_date) from dual") result, = cursor.fetchone() print("main(): result is", repr(result)) # acquire session, specifying a tag; since the session returned has no tag, # the callback will be invoked; session state will be changed and the tag will # be saved when the connection is closed print("(2) acquire session with tag") with pool.acquire(tag="NLS_DATE_FORMAT=SIMPLE") as conn: cursor = conn.cursor() cursor.execute("select to_char(current_date) from dual") result, = cursor.fetchone() print("main(): result is", repr(result)) # acquire session, specifying the same tag; since a session exists in the pool # with this tag, it will be returned and the callback will not be invoked but # the connection will still have the session state defined previously print("(3) acquire session with same tag") with pool.acquire(tag="NLS_DATE_FORMAT=SIMPLE") as conn: cursor = conn.cursor() cursor.execute("select to_char(current_date) from dual") result, = cursor.fetchone() print("main(): result is", repr(result)) # acquire session, specifying a different tag; since no session exists in the # pool with this tag, a new session will be returned and the callback will be # invoked; session state will be changed and the tag will be saved when the # connection is closed print("(4) acquire session with different tag") with pool.acquire(tag="NLS_DATE_FORMAT=FULL;TIME_ZONE=UTC") as conn: cursor = conn.cursor() cursor.execute("select to_char(current_date) from dual") result, = cursor.fetchone() print("main(): result is", repr(result)) # acquire session, specifying a different tag but also specifying that a # session with any tag can be acquired from the pool; a session with one of the # previously set tags will be returned and the callback will be invoked; # session state will be changed and the tag will be saved when the connection # is closed print("(4) acquire session with different tag but match any also specified") with pool.acquire(tag="NLS_DATE_FORMAT=FULL;TIME_ZONE=MST", matchanytag=True) \ as conn: cursor = conn.cursor() cursor.execute("select to_char(current_date) from dual") result, = cursor.fetchone() print("main(): result is", repr(result)) python-oracledb-1.2.1/samples/session_callback_plsql.py000066400000000000000000000127521434177474600233650ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2019, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # session_callback_plsql.py # # Demonstrates how to use a connection pool session callback written in # PL/SQL. The callback is invoked whenever the tag requested by the application # does not match the tag associated with the session in the pool. It should be # used to set session state, so that the application can count on known session # state, which allows the application to reduce the number of round trips to the # database. # # The primary advantage to this approach over the equivalent approach shown in # session_callback.py is when DRCP is used, as the callback is invoked on the # server and no round trip is required to set state. # # Also see session_callback.py #------------------------------------------------------------------------------ import oracledb import sample_env # this script is currently only supported in python-oracledb thick mode oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) # create pool with session callback defined pool = oracledb.create_pool(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string(), min=2, max=5, increment=1, session_callback="pkg_SessionCallback.TheCallback") # truncate table logging calls to PL/SQL session callback with pool.acquire() as conn: cursor = conn.cursor() cursor.execute("truncate table PLSQLSessionCallbacks") # acquire session without specifying a tag; the callback will not be invoked as # a result and no session state will be changed print("(1) acquire session without tag") with pool.acquire() as conn: cursor = conn.cursor() cursor.execute("select to_char(current_date) from dual") result, = cursor.fetchone() print("main(): result is", repr(result)) # acquire session, specifying a tag; since the session returned has no tag, # the callback will be invoked; session state will be changed and the tag will # be saved when the connection is closed print("(2) acquire session with tag") with pool.acquire(tag="NLS_DATE_FORMAT=SIMPLE") as conn: cursor = conn.cursor() cursor.execute("select to_char(current_date) from dual") result, = cursor.fetchone() print("main(): result is", repr(result)) # acquire session, specifying the same tag; since a session exists in the pool # with this tag, it will be returned and the callback will not be invoked but # the connection will still have the session state defined previously print("(3) acquire session with same tag") with pool.acquire(tag="NLS_DATE_FORMAT=SIMPLE") as conn: cursor = conn.cursor() cursor.execute("select to_char(current_date) from dual") result, = cursor.fetchone() print("main(): result is", repr(result)) # acquire session, specifying a different tag; since no session exists in the # pool with this tag, a new session will be returned and the callback will be # invoked; session state will be changed and the tag will be saved when the # connection is closed print("(4) acquire session with different tag") with pool.acquire(tag="NLS_DATE_FORMAT=FULL;TIME_ZONE=UTC") as conn: cursor = conn.cursor() cursor.execute("select to_char(current_date) from dual") result, = cursor.fetchone() print("main(): result is", repr(result)) # acquire session, specifying a different tag but also specifying that a # session with any tag can be acquired from the pool; a session with one of the # previously set tags will be returned and the callback will be invoked; # session state will be changed and the tag will be saved when the connection # is closed print("(4) acquire session with different tag but match any also specified") with pool.acquire(tag="NLS_DATE_FORMAT=FULL;TIME_ZONE=MST", matchanytag=True) \ as conn: cursor = conn.cursor() cursor.execute("select to_char(current_date) from dual") result, = cursor.fetchone() print("main(): result is", repr(result)) # acquire session and display results from PL/SQL session logs with pool.acquire() as conn: cursor = conn.cursor() cursor.execute(""" select RequestedTag, ActualTag from PLSQLSessionCallbacks order by FixupTimestamp""") print("(5) PL/SQL session callbacks") for requestedTag, actualTag in cursor: print("Requested:", requestedTag, "Actual:", actualTag) python-oracledb-1.2.1/samples/sharding_number_key.py000066400000000000000000000046471434177474600226760ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # sharding_number_key.py # # Demonstrates how to use sharding keys with a sharded database. # The sample schema provided does not include support for running this demo. A # sharded database must first be created. Information on how to create a # sharded database can be found in the documentation: # https://www.oracle.com/pls/topic/lookup?ctx=dblatest&id=SHARD #------------------------------------------------------------------------------ import oracledb import sample_env # this script is currently only supported in python-oracledb thick mode oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) pool = oracledb.create_pool(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string(), min=1, max=5, increment=1) def connect_and_display(sharding_key): print("Connecting with sharding key:", sharding_key) with pool.acquire(shardingkey=[sharding_key]) as conn: cursor = conn.cursor() cursor.execute("select sys_context('userenv', 'db_name') from dual") name, = cursor.fetchone() print("--> connected to database", name) connect_and_display(100) connect_and_display(167) python-oracledb-1.2.1/samples/soda_basic.py000066400000000000000000000113551434177474600207400ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2018, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # soda_basic.py # # A basic Simple Oracle Document Access (SODA) example. # # Oracle Client must be at 18.3 or higher. # Oracle Database must be at 18.1 or higher. # The user must have been granted the SODA_APP privilege. #------------------------------------------------------------------------------ import oracledb import sample_env # this script is currently only supported in python-oracledb thick mode oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) # The general recommendation for simple SODA usage is to enable autocommit connection.autocommit = True # Create the parent object for SODA soda = connection.getSodaDatabase() # drop the collection if it already exists in order to ensure that the sample # runs smoothly each time collection = soda.openCollection("mycollection") if collection is not None: collection.drop() # Explicit metadata is used for maximum version portability. # Refer to the documentation. metadata = { "keyColumn": { "name": "ID" }, "contentColumn": { "name": "JSON_DOCUMENT", "sqlType": "BLOB" }, "versionColumn": { "name": "VERSION", "method": "UUID" }, "lastModifiedColumn": { "name": "LAST_MODIFIED" }, "creationTimeColumn": { "name": "CREATED_ON" } } # Create a new SODA collection and index # This will open an existing collection, if the name is already in use. collection = soda.createCollection("mycollection", metadata) index_spec = { 'name': 'CITY_IDX', 'fields': [ { 'path': 'address.city', 'datatype': 'string', 'order': 'asc' } ] } collection.createIndex(index_spec) # Insert a document. # A system generated key is created by default. content = {'name': 'Matilda', 'address': {'city': 'Melbourne'}} doc = collection.insertOneAndGet(content) key = doc.key print('The key of the new SODA document is: ', key) # Fetch the document back doc = collection.find().key(key).getOne() # A SodaDocument content = doc.getContent() # A JavaScript object print('Retrieved SODA document dictionary is:') print(content) content = doc.getContentAsString() # A JSON string print('Retrieved SODA document string is:') print(content) # Replace document contents content = {'name': 'Matilda', 'address': {'city': 'Sydney'}} collection.find().key(key).replaceOne(content) # Insert some more documents without caring about their keys content = {'name': 'Venkat', 'address': {'city': 'Bengaluru'}} collection.insertOne(content) content = {'name': 'May', 'address': {'city': 'London'}} collection.insertOne(content) content = {'name': 'Sally-Ann', 'address': {'city': 'San Francisco'}} collection.insertOne(content) # Find all documents with names like 'Ma%' print("Names matching 'Ma%'") documents = collection.find().filter({'name': {'$like': 'Ma%'}}).getDocuments() for d in documents: content = d.getContent() print(content["name"]) # Count all documents c = collection.find().count() print('Collection has', c, 'documents') # Remove documents with cities containing 'o' print('Removing documents') c = collection.find().filter({'address.city': {'$regex': '.*o.*'}}).remove() print('Dropped', c, 'documents') # Count all documents c = collection.find().count() print('Collection has', c, 'documents') # Drop the collection if collection.drop(): print('Collection was dropped') python-oracledb-1.2.1/samples/soda_bulk_insert.py000066400000000000000000000067301434177474600222010ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2019, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # soda_bulk_insert.py # # Demonstrates the use of SODA bulk insert. # # Oracle Client must be at 18.5 or higher. # Oracle Database must be at 18.1 or higher. # The user must have been granted the SODA_APP privilege. #------------------------------------------------------------------------------ import oracledb import sample_env # this script is currently only supported in python-oracledb thick mode oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) # the general recommendation for simple SODA usage is to enable autocommit connection.autocommit = True # create the parent object for all SODA work soda = connection.getSodaDatabase() # drop the collection if it already exists in order to ensure that the sample # runs smoothly each time collection = soda.openCollection("SodaBulkInsert") if collection is not None: collection.drop() # Explicit metadata is used for maximum version portability. # Refer to the documentation. metadata = { "keyColumn": { "name": "ID" }, "contentColumn": { "name": "JSON_DOCUMENT", "sqlType": "BLOB" }, "versionColumn": { "name": "VERSION", "method": "UUID" }, "lastModifiedColumn": { "name": "LAST_MODIFIED" }, "creationTimeColumn": { "name": "CREATED_ON" } } # create a new (or open an existing) SODA collection collection = soda.createCollection("SodaBulkInsert", metadata) # remove all documents from the collection collection.find().remove() # define some documents that will be stored in_docs = [ dict(name="Sam", age=8), dict(name="George", age=46), dict(name="Bill", age=35), dict(name="Sally", age=43), dict(name="Jill", age=28), dict(name="Cynthia", age=12) ] # perform bulk insert result_docs = collection.insertManyAndGet(in_docs) for doc in result_docs: print("Inserted SODA document with key", doc.key) print() # perform search of all persons under the age of 40 print("Persons under the age of 40:") for doc in collection.find().filter({'age': {'$lt': 40}}).getDocuments(): print(doc.getContent()["name"] + ",", "key", doc.key) python-oracledb-1.2.1/samples/spatial_to_geopandas.py000066400000000000000000000257701434177474600230370ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2018, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # spatial_to_geopandas.py # # GeoPandas is a popular python library for working with geospatial data. # GeoPandas extends the Pandas data analysis library with geospatial support # using the Shapely library for geometry object support. # # See http://geopandas.org, https://pandas.pydata.org, # and https://github.com/shapely/shapely. # # This example shows how to bring geometries from Oracle Spatial (SDO_GEOMETRY # data type) into GeoPandas and perform a simple spatial operation. While the # spatial operation we perform in Python could have been performed in the # Oracle database, this example targets use cases where Python with GeoPandas # is being used to combine and work with geospatial data from numerous # additional sources such as files and web services. # # This script requires GeoPandas and its dependencies (see # https://geopandas.org/en/stable/getting_started/install.html). #------------------------------------------------------------------------------ from shapely.wkb import loads import geopandas as gpd import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) # create Oracle connection and cursor objects connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) cursor = connection.cursor() # enable autocommit to avoid the additional round trip to the database to # perform a commit; this should not be used if multiple statements must be # executed for a single transaction connection.autocommit = True # define output type handler to fetch LOBs, avoiding the second round trip to # the database to read the LOB contents def output_type_handler(cursor, name, default_type, size, precision, scale): if default_type == oracledb.BLOB: return cursor.var(oracledb.LONG_BINARY, arraysize=cursor.arraysize) connection.outputtypehandler = output_type_handler # drop and create table print("Dropping and creating table...") cursor.execute(""" begin execute immediate 'drop table TestStates'; exception when others then if sqlcode <> -942 then raise; end if; end;""") cursor.execute(""" create table TestStates ( state VARCHAR2(30) not null, geometry SDO_GEOMETRY not null )""") # acquire types used for creating SDO_GEOMETRY objects type_obj = connection.gettype("MDSYS.SDO_GEOMETRY") element_info_type_obj = connection.gettype("MDSYS.SDO_ELEM_INFO_ARRAY") ordinate_type_obj = connection.gettype("MDSYS.SDO_ORDINATE_ARRAY") # define function for creating an SDO_GEOMETRY object def create_geometry_obj(*ordinates): geometry = type_obj.newobject() geometry.SDO_GTYPE = 2003 geometry.SDO_SRID = 8307 geometry.SDO_ELEM_INFO = element_info_type_obj.newobject() geometry.SDO_ELEM_INFO.extend([1, 1003, 1]) geometry.SDO_ORDINATES = ordinate_type_obj.newobject() geometry.SDO_ORDINATES.extend(ordinates) return geometry # create SDO_GEOMETRY objects for three adjacent states in the USA geometry_nevada = create_geometry_obj(-114.052025, 37.103989, -114.049797, 37.000423, -113.484375, 37, -112.898598, 37.000401,-112.539604, 37.000683, -112, 37.000977, -111.412048, 37.001514, -111.133018, 37.00079,-110.75, 37.003201, -110.5, 37.004265, -110.469505, 36.998001, -110, 36.997967, -109.044571,36.999088, -109.045143, 37.375, -109.042824, 37.484692, -109.040848, 37.881176, -109.041405,38.153027, -109.041107, 38.1647, -109.059402, 38.275501, -109.059296, 38.5, -109.058868, 38.719906,-109.051765, 39, -109.050095, 39.366699, -109.050697, 39.4977, -109.050499, 39.6605, -109.050156,40.222694, -109.047577, 40.653641, -109.0494, 41.000702, -109.2313, 41.002102, -109.534233,40.998184, -110, 40.997398, -110.047768, 40.997696, -110.5, 40.994801, -111.045982, 40.998013,-111.045815, 41.251774, -111.045097, 41.579899, -111.045944, 42.001633, -111.506493, 41.999588,-112.108742, 41.997677, -112.16317, 41.996784, -112.172562, 41.996643, -112.192184, 42.001244,-113, 41.998314, -113.875, 41.988091, -114.040871, 41.993805, -114.038803, 41.884899, -114.041306,41, -114.04586, 40.116997, -114.046295, 39.906101, -114.046898, 39.542801, -114.049026, 38.67741, -114.049339, 38.572968, -114.049095, 38.14864, -114.0476, 37.80946,-114.05098, 37.746284, -114.051666, 37.604805, -114.052025, 37.103989) geometry_wyoming = create_geometry_obj(-111.045815, 41.251774, -111.045982, 40.998013, -110.5, 40.994801, -110.047768, 40.997696, -110, 40.997398, -109.534233, 40.998184, -109.2313, 41.002102, -109.0494, 41.000702, -108.525368, 40.999634, -107.917793, 41.002071, -107.317177, 41.002956, -106.857178, 41.002697, -106.455704, 41.002167, -106.320587, 40.999153, -106.189987, 40.997604, -105.729874, 40.996906, -105.276604, 40.998188, -104.942848, 40.998226, -104.625, 41.00145, -104.052742, 41.001423, -104.051781, 41.39333, -104.052032, 41.564301, -104.052185, 41.697983, -104.052109, 42.001736, -104.052277, 42.611626, -104.052643, 43.000614, -104.054337, 43.47784, -104.054298, 43.503101, -104.055, 43.8535, -104.054108, 44.141102, -104.054001, 44.180401, -104.055458, 44.570877, -104.057205, 44.997444, -104.664658, 44.998631, -105.037872, 45.000359, -105.088867, 45.000462, -105.912819, 45.000957, -105.927612, 44.99366, -106.024239, 44.993591, -106.263, 44.993801, -107.054871, 44.996384, -107.133545, 45.000141, -107.911095, 45.001343, -108.248672, 44.999504, -108.620628, 45.000328, -109.082314, 44.999664, -109.102745, 45.005955, -109.797951, 45.002247, -110.000771, 45.003502, -110.10936, 45.003967, -110.198761, 44.99625, -110.286026, 44.99691, -110.361946, 45.000656, -110.402176, 44.993874, -110.5, 44.992355, -110.704506, 44.99239, -110.784241, 45.003021, -111.05442, 45.001392, -111.054558, 44.666336, -111.048203, 44.474144, -111.046272, 43.983456, -111.044724, 43.501213, -111.043846, 43.3158, -111.043381, 43.02013, -111.042786, 42.719578, -111.045967, 42.513187, -111.045944, 42.001633, -111.045097, 41.579899, -111.045815, 41.251774) geometry_colorado = create_geometry_obj(-109.045143, 37.375, -109.044571, 36.999088, -108.378571, 36.999516, -107.481133, 37, -107.420311, 37, -106.876701, 37.00013, -106.869209, 36.992416, -106.475639, 36.993748, -106.006058, 36.995327, -105.717834, 36.995823, -105.220055, 36.995144, -105.154488, 36.995239, -105.028671, 36.992702, -104.407616, 36.993446, -104.007324, 36.996216, -103.085617, 37.000244, -103.001709, 37.000084, -102.986488, 36.998505, -102.759384, 37, -102.69767, 36.995132, -102.041794, 36.993061, -102.041191, 37.389172, -102.04113, 37.644268, -102.041695, 37.738529, -102.043938, 38.262466, -102.044113, 38.268803, -102.04483, 38.615234, -102.044762, 38.697556, -102.046112, 39.047035, -102.046707, 39.133144, -102.049301, 39.568176, -102.049347, 39.574062, -102.051277, 40.00309, -102.051117, 40.34922, -102.051003, 40.440018, -102.050873, 40.697556, -102.050835, 40.749596, -102.051155, 41.002384, -102.620567, 41.002609, -102.652992, 41.002342, -103.382011, 41.00227, -103.574036, 41.001736, -104.052742, 41.001423, -104.625, 41.00145, -104.942848, 40.998226, -105.276604, 40.998188, -105.729874, 40.996906, -106.189987, 40.997604, -106.320587, 40.999153, -106.455704, 41.002167, -106.857178, 41.002697, -107.317177, 41.002956, -107.917793, 41.002071, -108.525368, 40.999634, -109.0494, 41.000702, -109.047577, 40.653641, -109.050156, 40.222694, -109.050499, 39.6605, -109.050697, 39.4977, -109.050095, 39.366699, -109.051765, 39, -109.058868, 38.719906, -109.059296, 38.5, -109.059402, 38.275501, -109.041107, 38.1647, -109.041405, 38.153027, -109.040848, 37.881176, -109.042824, 37.484692, -109.045143, 37.375) # Insert rows for test states. If we were analyzing these geometries in Oracle # we would also add Spatial metadata and indexes. However in this example we # are only storing the geometries so that we load them back into Python, so we # will skip the metadata and indexes. print("Adding rows to table...") data = [ ('Nevada', geometry_nevada), ('Colorado', geometry_colorado), ('Wyoming', geometry_wyoming) ] cursor.executemany('insert into TestStates values (:state, :obj)', data) # We now have test geometries in Oracle Spatial (SDO_GEOMETRY) and will next # bring them back into Python to analyze with GeoPandas. GeoPandas is able to # consume geometries in the Well Known Text (WKT) and Well Known Binary (WKB) # formats. Oracle database includes utility functions to return SDO_GEOMETRY as # both WKT and WKB. Therefore we use that utility function in the query below # to provide results in a format readily consumable by GeoPandas. These utility # functions were introduced in Oracle 10g. We use WKB here; however the same # process applies for WKT. cursor.execute(""" SELECT state, sdo_util.to_wkbgeometry(geometry) FROM TestStates""") gdf = gpd.GeoDataFrame(cursor.fetchall(), columns=['state', 'wkbgeometry']) # create GeoSeries to replace the WKB geometry column gdf['geometry'] = gpd.GeoSeries(gdf['wkbgeometry'].apply(lambda x: loads(x))) del gdf['wkbgeometry'] # display the GeoDataFrame print() print(gdf) # perform a basic GeoPandas operation (unary_union) # to combine the 3 adjacent states into 1 geometry print() print("GeoPandas combining the 3 geometries into a single geometry...") print(gdf.unary_union) python-oracledb-1.2.1/samples/sql/000077500000000000000000000000001434177474600170715ustar00rootroot00000000000000python-oracledb-1.2.1/samples/sql/create_schema.sql000066400000000000000000000434511434177474600224040ustar00rootroot00000000000000/*----------------------------------------------------------------------------- * Copyright 2017, 2022, Oracle and/or its affiliates. * * This software is dual-licensed to you under the Universal Permissive License * (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License * 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose * either license.* * * If you elect to accept the software under the Apache License, Version 2.0, * the following applies: * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *---------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------- * create_schema.sql * * Performs the actual work of creating and populating the schemas with the * database objects used by the python-oracledb samples. An edition is also * created for the demonstration of PL/SQL editioning. It is executed by the * Python script create_schema.py. *---------------------------------------------------------------------------*/ alter session set nls_date_format = 'YYYY-MM-DD HH24:MI:SS' / alter session set nls_numeric_characters='.,' / create user &main_user identified by "&main_password" / grant create session, create table, create procedure, create type, create sequence, select any dictionary, change notification, unlimited tablespace to &main_user / grant aq_administrator_role to &main_user / begin execute immediate 'begin dbms_session.sleep(0); end;'; exception when others then begin execute immediate 'grant execute on dbms_lock to &main_user'; exception when others then raise_application_error(-20000, 'Ensure the following grant is made: ' || 'grant execute on dbms_lock to ' || user || ' with grant option'); end; end; / begin for r in ( select role from dba_roles where role in ('SODA_APP') ) loop execute immediate 'grant ' || r.role || ' to &main_user'; end loop; end; / create user &edition_user identified by "&edition_password" / grant create session, create procedure to &edition_user / alter user &edition_user enable editions / create edition &edition_name / grant use on edition &edition_name to &edition_user / -- create types create type &main_user..udt_SubObject as object ( SubNumberValue number, SubStringValue varchar2(60) ); / create or replace type &main_user..udt_Building as object ( BuildingId number(9), NumFloors number(3), Description varchar2(60), DateBuilt date ); / create or replace type &main_user..udt_Book as object ( Title varchar2(100), Authors varchar2(100), Price number(5,2) ); / -- create tables create table &main_user..TestNumbers ( IntCol number(9) not null, NumberCol number(9, 2) not null, FloatCol float not null, UnconstrainedCol number not null, NullableCol number(38) ) / create table &main_user..TestStrings ( IntCol number(9) not null, StringCol varchar2(20) not null, RawCol raw(30) not null, FixedCharCol char(40) not null, NullableCol varchar2(50) ) / create table &main_user..TestCLOBs ( IntCol number(9) not null, CLOBCol clob not null ) / create table &main_user..TestBLOBs ( IntCol number(9) not null, BLOBCol blob not null ) / create table &main_user..TestTempTable ( IntCol number(9) not null, StringCol varchar2(400), constraint TestTempTable_pk primary key (IntCol) ) / create table &main_user..TestUniversalRowids ( IntCol number(9) not null, StringCol varchar2(250) not null, DateCol date not null, constraint TestUniversalRowids_pk primary key (IntCol, StringCol, DateCol) ) organization index / create table &main_user..BuildingsAsObjects ( BuildingId number(9) not null, BuildingObj &main_user..udt_Building not null ) / create table &main_user..BuildingsAsJsonStrings ( BuildingId number(9) not null, BuildingStr varchar2(400) not null, constraint BuildingsAsJsonStrings_pk primary key (BuildingId) ) / create table &main_user..BigTab ( mycol varchar2(20) ) / create table &main_user..SampleQueryTab ( id number not null, name varchar2(20) not null ) / create table &main_user..MyTab ( id number, data varchar2(20) ) / create table &main_user..ParentTable ( ParentId number(9) not null, Description varchar2(60) not null, constraint ParentTable_pk primary key (ParentId) ) / create table &main_user..ChildTable ( ChildId number(9) not null, ParentId number(9) not null, Description varchar2(60) not null, constraint ChildTable_pk primary key (ChildId), constraint ChildTable_fk foreign key (ParentId) references &main_user..ParentTable ) / create table &main_user..Ptab ( myid number, mydata varchar(20) ) / create table &main_user..PlsqlSessionCallbacks ( RequestedTag varchar2(250), ActualTag varchar2(250), FixupTimestamp timestamp ) / create table &main_user..CustomersAsBlob ( id number(9) not null, json_data blob check (json_data is json), constraint CustomersAsBlob_pk primary key (id) ) / create table &main_user..LoadCsvTab ( id number not null, name varchar2(25), constraint LoadCsvTab_pk primary key (id) ) / declare t_Version number; begin select to_number(substr(version, 1, instr(version, '.') - 1)) into t_Version from product_component_version where product like 'Oracle Database%'; if t_Version >= 21 then execute immediate 'create table &main_user..CustomersAsJson (' || ' id number(9) not null primary key,' || ' json_data json' || ')'; end if; end; / -- create queue table, queues and subscribers for demonstrating Advanced Queuing begin dbms_aqadm.create_queue_table('&main_user..BOOK_QUEUE_TAB', '&main_user..UDT_BOOK'); dbms_aqadm.create_queue('&main_user..DEMO_BOOK_QUEUE', '&main_user..BOOK_QUEUE_TAB'); dbms_aqadm.start_queue('&main_user..DEMO_BOOK_QUEUE'); dbms_aqadm.create_queue_table('&main_user..RAW_QUEUE_TAB', 'RAW'); dbms_aqadm.create_queue('&main_user..DEMO_RAW_QUEUE', '&main_user..RAW_QUEUE_TAB'); dbms_aqadm.start_queue('&main_user..DEMO_RAW_QUEUE'); dbms_aqadm.create_queue_table('&main_user..RAW_QUEUE_MULTI_TAB', 'RAW', multiple_consumers => true); dbms_aqadm.create_queue('&main_user..DEMO_RAW_QUEUE_MULTI', '&main_user..RAW_QUEUE_MULTI_TAB'); dbms_aqadm.start_queue('&main_user..DEMO_RAW_QUEUE_MULTI'); dbms_aqadm.add_subscriber('&main_user..DEMO_RAW_QUEUE_MULTI', sys.aq$_agent('SUBSCRIBER_A', null, null)); dbms_aqadm.add_subscriber('&main_user..DEMO_RAW_QUEUE_MULTI', sys.aq$_agent('SUBSCRIBER_B', null, null)); end; / -- populate tables begin for i in 1..20000 loop insert into &main_user..BigTab (mycol) values (dbms_random.string('A', 20)); end loop; end; / begin for i in 1..10 loop insert into &main_user..TestNumbers values (i, i + i * 0.25, i + i * .75, i * i * i + i *.5, decode(mod(i, 2), 0, null, power(143, i))); end loop; end; / declare t_RawValue raw(30); function ConvertHexDigit(a_Value number) return varchar2 is begin if a_Value between 0 and 9 then return to_char(a_Value); end if; return chr(ascii('A') + a_Value - 10); end; function ConvertToHex(a_Value varchar2) return varchar2 is t_HexValue varchar2(60); t_Digit number; begin for i in 1..length(a_Value) loop t_Digit := ascii(substr(a_Value, i, 1)); t_HexValue := t_HexValue || ConvertHexDigit(trunc(t_Digit / 16)) || ConvertHexDigit(mod(t_Digit, 16)); end loop; return t_HexValue; end; begin for i in 1..10 loop t_RawValue := hextoraw(ConvertToHex('Raw ' || to_char(i))); insert into &main_user..TestStrings values (i, 'String ' || to_char(i), t_RawValue, 'Fixed Char ' || to_char(i), decode(mod(i, 2), 0, null, 'Nullable ' || to_char(i))); end loop; end; / insert into &main_user..ParentTable values (10, 'Parent 10') / insert into &main_user..ParentTable values (20, 'Parent 20') / insert into &main_user..ParentTable values (30, 'Parent 30') / insert into &main_user..ParentTable values (40, 'Parent 40') / insert into &main_user..ParentTable values (50, 'Parent 50') / insert into &main_user..ChildTable values (1001, 10, 'Child A of Parent 10') / insert into &main_user..ChildTable values (1002, 20, 'Child A of Parent 20') / insert into &main_user..ChildTable values (1003, 20, 'Child B of Parent 20') / insert into &main_user..ChildTable values (1004, 20, 'Child C of Parent 20') / insert into &main_user..ChildTable values (1005, 30, 'Child A of Parent 30') / insert into &main_user..ChildTable values (1006, 30, 'Child B of Parent 30') / insert into &main_user..ChildTable values (1007, 40, 'Child A of Parent 40') / insert into &main_user..ChildTable values (1008, 40, 'Child B of Parent 40') / insert into &main_user..ChildTable values (1009, 40, 'Child C of Parent 40') / insert into &main_user..ChildTable values (1010, 40, 'Child D of Parent 40') / insert into &main_user..ChildTable values (1011, 40, 'Child E of Parent 40') / insert into &main_user..ChildTable values (1012, 50, 'Child A of Parent 50') / insert into &main_user..ChildTable values (1013, 50, 'Child B of Parent 50') / insert into &main_user..ChildTable values (1014, 50, 'Child C of Parent 50') / insert into &main_user..ChildTable values (1015, 50, 'Child D of Parent 50') / insert into &main_user..SampleQueryTab values (1, 'Anthony') / insert into &main_user..SampleQueryTab values (2, 'Barbie') / insert into &main_user..SampleQueryTab values (3, 'Chris') / insert into &main_user..SampleQueryTab values (4, 'Dazza') / insert into &main_user..SampleQueryTab values (5, 'Erin') / insert into &main_user..SampleQueryTab values (6, 'Frankie') / insert into &main_user..SampleQueryTab values (7, 'Gerri') / commit / -- -- For PL/SQL Examples -- create or replace function &main_user..myfunc ( a_Data varchar2, a_Id number ) return number as begin insert into &main_user..ptab (mydata, myid) values (a_Data, a_Id); return (a_Id * 2); end; / create or replace procedure &main_user..myproc ( a_Value1 number, a_Value2 out number ) as begin a_Value2 := a_Value1 * 2; end; / create or replace procedure &main_user..myrefcursorproc ( a_StartingValue number, a_EndingValue number, a_RefCursor out sys_refcursor ) as begin open a_RefCursor for select * from TestStrings where IntCol between a_StartingValue and a_EndingValue; end; / create procedure &main_user..myrefcursorproc2 ( a_RefCursor out sys_refcursor ) as begin open a_RefCursor for select * from TestTempTable; end; / -- -- Create package for demoing PL/SQL collections and records. -- create or replace package &main_user..pkg_Demo as type udt_StringList is table of varchar2(100) index by binary_integer; type udt_DemoRecord is record ( NumberValue number, StringValue varchar2(30), DateValue date, BooleanValue boolean ); procedure DemoCollectionOut ( a_Value out nocopy udt_StringList ); procedure DemoRecordsInOut ( a_Value in out nocopy udt_DemoRecord ); end; / create or replace package body &main_user..pkg_Demo as procedure DemoCollectionOut ( a_Value out nocopy udt_StringList ) is begin a_Value(-1048576) := 'First element'; a_Value(-576) := 'Second element'; a_Value(284) := 'Third element'; a_Value(8388608) := 'Fourth element'; end; procedure DemoRecordsInOut ( a_Value in out nocopy udt_DemoRecord ) is begin a_Value.NumberValue := a_Value.NumberValue * 2; a_Value.StringValue := a_Value.StringValue || ' (Modified)'; a_Value.DateValue := a_Value.DateValue + 5; a_Value.BooleanValue := not a_Value.BooleanValue; end; end; / -- -- Create package for demoing PL/SQL session callback -- create or replace package &main_user..pkg_SessionCallback as procedure TheCallback ( a_RequestedTag varchar2, a_ActualTag varchar2 ); end; / create or replace package body &main_user..pkg_SessionCallback as type udt_Properties is table of varchar2(64) index by varchar2(64); procedure LogCall ( a_RequestedTag varchar2, a_ActualTag varchar2 ) is pragma autonomous_transaction; begin insert into PlsqlSessionCallbacks values (a_RequestedTag, a_ActualTag, systimestamp); commit; end; procedure ParseProperty ( a_Property varchar2, a_Name out nocopy varchar2, a_Value out nocopy varchar2 ) is t_Pos number; begin t_Pos := instr(a_Property, '='); if t_Pos = 0 then raise_application_error(-20000, 'Tag must contain key=value pairs'); end if; a_Name := substr(a_Property, 1, t_Pos - 1); a_Value := substr(a_Property, t_Pos + 1); end; procedure SetProperty ( a_Name varchar2, a_Value varchar2 ) is t_ValidValues udt_Properties; begin if a_Name = 'TIME_ZONE' then t_ValidValues('UTC') := 'UTC'; t_ValidValues('MST') := '-07:00'; elsif a_Name = 'NLS_DATE_FORMAT' then t_ValidValues('SIMPLE') := 'YYYY-MM-DD HH24:MI'; t_ValidValues('FULL') := 'YYYY-MM-DD HH24:MI:SS'; else raise_application_error(-20000, 'Unsupported session setting'); end if; if not t_ValidValues.exists(a_Value) then raise_application_error(-20000, 'Unsupported session setting'); end if; execute immediate 'ALTER SESSION SET ' || a_Name || '=''' || t_ValidValues(a_Value) || ''''; end; procedure ParseTag ( a_Tag varchar2, a_Properties out nocopy udt_Properties ) is t_PropertyName varchar2(64); t_PropertyValue varchar2(64); t_StartPos number; t_EndPos number; begin t_StartPos := 1; while t_StartPos < length(a_Tag) loop t_EndPos := instr(a_Tag, ';', t_StartPos); if t_EndPos = 0 then t_EndPos := length(a_Tag) + 1; end if; ParseProperty(substr(a_Tag, t_StartPos, t_EndPos - t_StartPos), t_PropertyName, t_PropertyValue); a_Properties(t_PropertyName) := t_PropertyValue; t_StartPos := t_EndPos + 1; end loop; end; procedure TheCallback ( a_RequestedTag varchar2, a_ActualTag varchar2 ) is t_RequestedProps udt_Properties; t_ActualProps udt_Properties; t_PropertyName varchar2(64); begin LogCall(a_RequestedTag, a_ActualTag); ParseTag(a_RequestedTag, t_RequestedProps); ParseTag(a_ActualTag, t_ActualProps); t_PropertyName := t_RequestedProps.first; while t_PropertyName is not null loop if not t_ActualProps.exists(t_PropertyName) or t_ActualProps(t_PropertyName) != t_RequestedProps(t_PropertyName) then SetProperty(t_PropertyName, t_RequestedProps(t_PropertyName)); end if; t_PropertyName := t_RequestedProps.next(t_PropertyName); end loop; end; end; / python-oracledb-1.2.1/samples/sql/drop_schema.sql000066400000000000000000000037301434177474600221010ustar00rootroot00000000000000/*----------------------------------------------------------------------------- * Copyright 2017, 2022, Oracle and/or its affiliates. * * This software is dual-licensed to you under the Universal Permissive License * (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License * 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose * either license.* * * If you elect to accept the software under the Apache License, Version 2.0, * the following applies: * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *---------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------- * drop_schema.sql * * Performs the actual work of dropping the database schemas and edition used * by the python-oracledb samples. It is executed by the Python script * drop_schema.py. *---------------------------------------------------------------------------*/ begin for r in ( select username from dba_users where username in (upper('&main_user'), upper('&edition_user')) ) loop execute immediate 'drop user ' || r.username || ' cascade'; end loop; for r in ( select edition_name from dba_editions where edition_name in (upper('&edition_name')) ) loop execute immediate 'drop edition ' || r.edition_name || ' cascade'; end loop; end; / python-oracledb-1.2.1/samples/sqlp.py000077500000000000000000000244251434177474600176350ustar00rootroot00000000000000#! /usr/bin/env python #------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # sqlp.py # # USAGE # python sqlp.py [username@password/connect_string] # # DESCRIPTION # Example interactive command line SQL executor for ad hoc statements. # # This is modelled on SQL*Plus but has MANY differences. Some are: # - It always reads from the keyboard and doesn't directly read SQL files # - It doesn't execute SQL*Plus-specific commands like SET or DESC # - It doesn't support "&" substitution or bind variables # - It doesn't display all data types, e.g. object types aren't supported # - It doesn't do smart sizing or wrapping of query columns # - Statements like "CREATE OR REPLACE" must have all keywords on the same # (first) line # - it has very limited error handling #------------------------------------------------------------------------------ import oracledb import getpass import sys import re import os import signal # Statement types STMT_TYPE_UNKNOWN = 0 STMT_TYPE_SQL = 1 # Like SELECT or INSERT STMT_TYPE_PLSQL = 2 # Like BEGIN or CREATE FUNCTION STMT_TYPE_SQLPLUS = 3 # Like SET or DESC # Simple regexps for statement type identification SQL_PATTERN = re.compile( r'^(administer|alter|analyze|associate|audit|call|comment|commit|create' '|delete|disassociate|drop|explain|flashback|grant|insert|lock|merge' '|noaudit|purge|rename|revoke|rollback|savepoint|select|truncate|update' '|with|set\s+constraint[s*]|set\s+role|set\s+transaction)(\s|$|;)', re.IGNORECASE) PLSQL_PATTERN = re.compile( r'^(begin|declare|create\s+or\s+replace|create\s+function' '|create\s+procedure|create\s+package|create\s+type)(\s|$)', re.IGNORECASE) SQLPLUS_PATTERN = re.compile( r'^(@|@@|(acc(e?|ep?|ept?))|(a(p?|pp?|ppe?|ppen?|ppend?))|(archive\s+log)' '|(attr(i?|ib?|ibu?|ibut?|ibute?))|(bre(a?|ak?))|(bti(t?|tl?|tle?))' '|(c(h?|ha?|han?|hang?|hange?))|(cl(e?|ea?|ear?))|(col(u?|um?|umn?))' '|(comp(u?|ut?|ute?))|(conn(e?|ec?|ect?))|copy|(def(i?|in?|ine?))|del' '|(desc(r?|ri?|rib?|ribe?))|(disc(o?|on?|onn?|onne?|onnec?|onnect?))' '|(ed(i?|it?))|(exec(u?|ut?|ute?))|exit|get|help|history|host' '|(i(n?|np?|npu?|nput?))|(l(i?|is?|ist?))|(passw(o?|or?|ord?))' '|(pau(s?|se?))|print|(pro(m?|mp?|mpt?))|quit|recover|(rem(a?|ar?|ark?))' '|(repf(o?|oo?|oot?|oote?|ooter?))|(reph(e?|ea?|ead?|eade?|eader?))' '|(r(u?|un?))|(sav(e?))|set|(sho(w?))|shutdown|(spo(o?|ol?))|(sta(r?|rt?))' '|startup|store|(timi(n?|ng?))|(tti(t?|tl?|tle?))|(undef(i?|in?|ine?))' '|(var(i?|ia?|iab?|iabl?|iable?))|whenever|xquery|--.*)(\s|$)', re.IGNORECASE) QUERY_PATTERN = re.compile(r'(select|with)\s*', re.IGNORECASE) # Look up the first keywords to find the statement type def detect_statement_type(s): if PLSQL_PATTERN.match(s): return STMT_TYPE_PLSQL elif SQL_PATTERN.match(s): return STMT_TYPE_SQL elif SQLPLUS_PATTERN.match(s): return STMT_TYPE_SQLPLUS else: return STMT_TYPE_UNKNOWN # Read text until the expected end-of-statement terminator is seen. # # - SQL*Plus commands like SET or DESC can have an optional semi-colon # statement terminator. # # - SQL commands like SELECT and INSERT can end with a semi-colon or with a # slash on a new line. # # - PL/SQL commands like BEGIN or CREATE PROCEDURE must end with a slash on a # new line. # def read_statement(): statement = '' statement_type = STMT_TYPE_UNKNOWN line_number = 1 print('SQLP> ', end='') while True: try: line = input().strip() except EOFError: sys.exit(0) line_number += 1 if len(line) == 0 and statement_type != STMT_TYPE_PLSQL: statement = '' break if statement_type == STMT_TYPE_UNKNOWN: statement_type = detect_statement_type(line) if statement_type == STMT_TYPE_UNKNOWN: return(line, STMT_TYPE_UNKNOWN) elif (line == '/' and (statement_type == STMT_TYPE_SQL or statement_type == STMT_TYPE_PLSQL)): break elif ((statement_type == STMT_TYPE_SQL or statement_type == STMT_TYPE_SQLPLUS) and line[-1] == ';'): statement = f'{statement} {line[:-1]}' if statement else line[:-1] break elif statement_type == STMT_TYPE_SQLPLUS: statement = line break else: statement = f'{statement} {line}' if statement else line print('{0:3} '.format(line_number), end='') return(statement, statement_type) # Execute a statement that needs to be sent to the database def execute_db_statement(connection, statement, statement_type): if not connection: print('Not connected') else: with connection.cursor() as cursor: try: cursor.execute(statement) if (statement_type == STMT_TYPE_SQL and QUERY_PATTERN.match(statement)): fetch_rows(cursor) except oracledb.Error as e: error, = e.args print(statement) print('*'.rjust(error.offset+1, ' ')) print(error.message) # Handle "local" SQL*Plus commands def execute_sqlplus_statement(connection, statement): if re.match(r'(conn(e?|ec?|ect?))(\s|$)', statement): a = re.split(r'\s+', statement) dsn = None if len(a) <= 1 else a[1] connection = get_connection(dsn) elif (statement.lower().strip() == 'exit' or statement.lower().strip() == 'quit'): sys.exit(0) elif (re.match(r'(rem(a?|ar?|ark?))(\s|$)', statement) or statement[:2] == '--'): return connection #elif ... # This is where you can extend keyword support else: print('Unsupported SQL*Plus command "{}"'. format(re.split(r'\s+', statement)[0])) return connection # Fetch and display query rows def fetch_rows(cursor): try: rows = cursor.fetchmany() if not rows: print('no rows selected') else: col_formats = get_col_formats(cursor.description) print_headings(col_formats) while rows: for row in rows: print_row(col_formats, row) rows = cursor.fetchmany() except oracledb.Error as e: error, = e.args print(error.message) # Naive logic to choose column display widths def get_col_formats(description): col_formats = [] for col in description: if col[2] == None: # no width, e.g. a LOB w = len(col[0]) # use heading length elif col[1] == oracledb.DB_TYPE_NUMBER: w = max(40, len(col[0])) else: w = max(col[2], len(col[0])) col_formats.append({'heading': col[0], 'type': col[1], 'width': w}) return col_formats # Print query column headings and separator def print_headings(col_formats): for col in col_formats: print('{h:{w}s}'.format(h=col['heading'], w=col['width']), end=' ') print() for col in col_formats: print('-'.rjust(col['width'], '-'), end=' ') print() # Print a row of query data # No column wrapping occurs def print_row(col_formats, row): for i, v in enumerate(row): v = ' ' if v == None else v print('{v:{w}s}'.format(v=str(v), w=col_formats[i]['width']), end=' ') print() # Connect def get_connection(dsn=None): connection = None try: if dsn: connection = oracledb.connect(dsn=dsn) else: un = get_user() pw = get_password() cs = get_connect_string() if un and pw and cs: connection = oracledb.connect(user=un, password=pw, dsn=cs) else: raise ValueError('Invalid credentials entered') except ValueError as e: print(e) except oracledb.Error as e: error, = e.args print('Failed to connect') print(error.message) finally: return connection # Signal handler for graceful interrupts def signal_handler(sig, frame): print() sys.exit(0) # Connection helper functions def get_user(): return input('Enter username: ').strip() def get_password(): return getpass.getpass('Enter password: ') def get_connect_string(): return input('Enter connection string: ') # Main body if __name__ == '__main__': # Allow graceful interrupts signal.signal(signal.SIGINT, signal_handler) # Fetch LOBs directly as strings or bytes oracledb.defaults.fetch_lobs = False # Fetch numbers as decimal.Decimal oracledb.defaults.fetch_decimals = True # Connect connection = None if len(sys.argv) <= 1 else get_connection(sys.argv[1]) # Loop to read statements and execute them while True: (statement, statement_type) = read_statement() if len(statement) == 0: continue elif statement_type == STMT_TYPE_UNKNOWN: print('Unknown command "{}"'. format(re.split(r'\s+', statement)[0])) elif statement_type == STMT_TYPE_SQLPLUS: connection = execute_sqlplus_statement(connection, statement) else: execute_db_statement(connection, statement, statement_type) python-oracledb-1.2.1/samples/subclassing.py000066400000000000000000000060271434177474600211660ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # subclassing.py # # Demonstrates how to subclass connections and cursors in order to add # additional functionality (like logging) or create specialized interfaces for # particular applications. #------------------------------------------------------------------------------ import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) # sample subclassed Connection which overrides the constructor (so no # parameters are required) and the cursor() method (so that the subclassed # cursor is returned instead of the default cursor implementation) class Connection(oracledb.Connection): def __init__(self): print("CONNECT", sample_env.get_connect_string()) super().__init__(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) def cursor(self): return Cursor(self) # sample subclassed cursor which overrides the execute() and fetchone() # methods in order to perform some simple logging class Cursor(oracledb.Cursor): def execute(self, statement, args): print("EXECUTE", statement) print("ARGS:") for arg_index, arg in enumerate(args): print(" ", arg_index + 1, "=>", repr(arg)) return super().execute(statement, args) def fetchone(self): print("FETCHONE") return super().fetchone() # create instances of the subclassed Connection and cursor connection = Connection() with connection.cursor() as cursor: # demonstrate that the subclassed connection and cursor are being used cursor.execute("select count(*) from ChildTable where ParentId = :1", (30,)) count, = cursor.fetchone() print("COUNT:", int(count)) python-oracledb-1.2.1/samples/transaction_guard.py000066400000000000000000000074701434177474600223630ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. # # Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, # Canada. All rights reserved. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # transaction_guard.py # # Demonstrates the use of Transaction Guard to verify if a transaction has # completed, ensuring that a duplicate transaction is not created or attempted # if the application chooses to handle the error. This # feature is only available in Oracle Database 12.1. It follows loosely the # OCI sample provided by Oracle in its documentation about OCI and Transaction # Guard. # # Run the following as SYSDBA to set up Transaction Guard # # grant execute on dbms_app_cont to pythondemo; # # declare # t_Params dbms_service.svc_parameter_array; # begin # t_Params('COMMIT_OUTCOME') := 'true'; # t_Params('RETENTION_TIMEOUT') := 604800; # dbms_service.create_service('orcl-tg', 'orcl-tg', t_Params); # dbms_service.start_service('orcl-tg'); # end; # / #------------------------------------------------------------------------------ import sys import oracledb import sample_env # this script is currently only supported in python-oracledb thick mode oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) # constants CONNECT_STRING = "localhost/orcl-tg" # create transaction and generate a recoverable error pool = oracledb.create_pool(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=CONNECT_STRING, min=1, max=9, increment=2) connection = pool.acquire() cursor = connection.cursor() cursor.execute(""" delete from TestTempTable where IntCol = 1""") cursor.execute(""" insert into TestTempTable values (1, null)""") input("Please kill %s session now. Press ENTER when complete." % \ sample_env.get_main_user()) try: connection.commit() # this should fail sys.exit("Session was not killed. Terminating.") except oracledb.DatabaseError as e: error_obj, = e.args if not error_obj.isrecoverable: sys.exit("Session is not recoverable. Terminating.") ltxid = connection.ltxid if not ltxid: sys.exit("Logical transaction not available. Terminating.") pool.drop(connection) # check if previous transaction completed connection = pool.acquire() cursor = connection.cursor() args = (oracledb.Binary(ltxid), cursor.var(bool), cursor.var(bool)) _, committed, completed = cursor.callproc("dbms_app_cont.get_ltxid_outcome", args) print("Failed transaction was committed:", committed) print("Failed call was completed:", completed) python-oracledb-1.2.1/samples/tutorial/000077500000000000000000000000001434177474600201355ustar00rootroot00000000000000python-oracledb-1.2.1/samples/tutorial/Python-and-Oracle-Database-The-New-Wave-of-Scripting.html000066400000000000000000003243671434177474600324770ustar00rootroot00000000000000 Python and Oracle Database Tutorial: The New Wave of Scripting

Contents

Overview

This tutorial is a primary guide on using Python with Oracle Database. It contains both beginner and advanced materials. Choose the content that interests you and your skill level. The tutorial has scripts to run and modify, and has suggested solutions.

Python is a popular general purpose dynamic scripting language. The python-oracledb driver provides Python APIs to access Oracle Database. It is an upgrade for the hugely popular cx_Oracle interface.

If you are new to Python, review the Appendix: Python Primer to gain an understanding of the language.

When you have finished this tutorial, we recommend reviewing the python-oracledb documentation.

The original copy of these instructions that you are reading is here.

Python-oracledb Architecture

The python-oracledb driver enables access to Oracle Database using either one of two modes. Both modes have comprehensive functionality supporting the Python Database API v2.0 Specification. By default, python-oracledb runs in a "thin" mode, which connects directly to Oracle Database. This mode does not need Oracle Client libraries. However, some additional features are available when python-oracledb uses them. Python-oracledb applications that load the Oracle Client libraries via an application script runtime option are said to be in "thick" mode. This tutorial has examples in both modes.

Python python-oracledb architecture

The database can be on the same machine as Python, or it can be remote.

Setup

  • Install software

    Install Python 3 if not already available. It can be obtained from your operating system package library or from python.org. On Windows, use Python 3.7 or later. On macOS, use Python 3.7 or later. On Linux, use Python 3.6 or later.

    Install python-oracledb with a command like pip install oracledb --upgrade

    Ensure you can access an Oracle Database.

  • Download the tutorial scripts

    The Python scripts used in this example are in the python-oracledb GitHub repository.

    Download a zip file of the repository from here and unzip it. Alternatively you can use 'git' to clone the repository.

    git clone https://github.com/oracle/python-oracledb.git

    The samples/tutorial directory has scripts to run and modify. The samples/tutorial/solutions directory has scripts with suggested code changes. The samples/tutorial/sql directory has all the SQL scripts used by the Python files to create database tables and other objects.

  • Review the privileged database credentials used for creating the schema

    Review db_config_sys.py in the tutorial directory. This file is included in other Python files for creating and dropping the tutorial user.

    Edit db_config_sys.py file and change the default values to match the system connection information for your environment. Alternatively, you can set the given environment variables in your terminal window. For example, the default username is "SYSTEM" unless the environment variable "PYTHON_SYSUSER" contains a different username. The default system connection string is for the "orclpdb" database service on the same machine as Python. In Python Database API terminology, the connection string parameter is called the "data source name", or "dsn". Using environment variables is convenient because you will not be asked to re-enter the password when you run scripts:

    user = os.environ.get("PYTHON_SYSUSER", "SYSTEM")
    
    dsn = os.environ.get("PYTHON_SYS_CONNECT_STRING", "localhost/orclpdb")
    
    pw = os.environ.get("PYTHON_SYSPASSWORD")
    if pw is None:
        pw = getpass.getpass("Enter password for %s: " % user)
    

    Substitute the admin values for your environment. If you are using Oracle Autonomous Database (ADB), use the ADMIN user instead of SYSTEM. The tutorial instructions may need adjusting, depending on how you have set up your environment.

  • Create a database user

    If you have an existing user, you may be able to use it for most examples (some examples may require extra permissions).

    If you need to create a new user for this tutorial, review the grants created in samples/tutorial/sql/create_user.sql by opening it in your favorite text editor. Then open a terminal window and run create_user.py to execute the create_user.sql script and create the sample user. This tutorial uses the name pythondemo:

    python create_user.py

    The example above connects as the SYSTEM (or ADMIN for ADB) user using db_config_sys file discussed in the earlier section. The connection string is "localhost/orclpdb", meaning use the database service "orclpdb" running on localhost (the computer you are running your Python scripts on).

    If it runs successfully, you will see something similar below:

    Enter password for SYSTEM:
    Enter password for pythondemo:
    Creating user...
    SQL File Name:  D:\python-oracledb\samples\tutorial\sql\create_user.sql
    Done.

    The new user pythondemo is created.

    When the tutorial is finished, ensure that all the database sessions connected to the tutorial user pythondemo are closed and then run drop_user.py to remove the tutorial user.

  • Install the tables and other database objects for the tutorial

    Once you have a database user, then you can create the key tutorial tables and database objects for the tutorial by running setup_tutorial.py (the environment setup file), using your values for the tutorial username, password and connection string:

    python setup_tutorial.py

    On successful completion of the run, You will see something like:

    Setting up the sample tables and other DB objects for the tutorial...
    SQL File Name:  D:\python-oracledb\samples\tutorial\sql\setup_tutorial.sql
    Done.

    This will call the setup_tutorial.sql file from tutorials/sql directory to setup some sample tables and database objects required for running the examples in the tutorial.

  • Review the connection credentials used by the tutorial scripts

    Review db_config.py (thin mode), and db_config.sql files in the tutorial and tutorial/sql directories respectively. These are included in other Python and SQL files for setting up the database connection.

    Edit db_config.py file and change the default values to match the connection information for your environment. Alternatively, you can set the given environment variables in your terminal window. For example, the default username is "pythondemo" unless the environment variable "PYTHON_USER" contains a different username. The default connection string is for the 'orclpdb' database service on the same machine as Python. In Python Database API terminology, the connection string parameter is called the "data source name", or "dsn". Using environment variables is convenient because you will not be asked to re-enter the password when you run scripts:

    user = os.environ.get("PYTHON_USER", "pythondemo")
    
    dsn = os.environ.get("PYTHON_CONNECT_STRING", "localhost/orclpdb")
    
    pw = os.environ.get("PYTHON_PASSWORD")
    if pw is None:
        pw = getpass.getpass("Enter password for %s: " % user)
    

    Also, change the database username and connection string in the SQL configuration file db_config.sql based on your environment settings:

    -- Default database username
    def user = "pythondemo"
    
    -- Default database connection string
    def connect_string = "localhost/orclpdb"
    
    -- Prompt for the password
    accept pw char prompt 'Enter database password for &user: ' hide
    

    The tutorial instructions may need adjusting, depending on how you have set up your environment.

  • Runtime Naming

    At runtime, the module name of the python-oracledb package is oracledb:

    import oracledb
  • Python-oracledb defaults

    A singleton oracledb.defaults contains attributes that can be used to adjust the default behavior of python-oracledb. Attributes not supported in a mode (thin or thick) will be ignored in that mode.

    Open defaults.py in an editor. This will look like:

    import oracledb
    
    print("Default array size:", oracledb.defaults.arraysize)
    Run the script:
    python defaults.py
    It displays:
    Default array size: 100

    This gives the default array size tuning parameter that will be useful in Section 3.4 of this tutorial.

    The default values can also be edited using the defaults attribute. All the default values that can be set and read with defaults attribute are available in the python-oracledb documentation.

1. Connecting to Oracle

You can connect from Python to a local, remote or cloud Oracle Database. Documentation link for further reading: Connecting to Oracle Database.

  • 1.1 Creating a basic connection

    Review the code contained in connect.py :

    import oracledb
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    print("Database version:", con.version)
    

    The python-oracledb module is imported to provide the API for accessing the Oracle database. Many inbuilt and third-party modules can be included in Python scripts this way.

    The username, the password and the connection string that you configured in the db_config.py module is passed to the connect() method. By default, Oracle's Easy Connect connection string syntax is used. It consists of the hostname of your machine, localhost, and the database service name orclpdb. (In Python Database API terminology, the connection string parameter is called the "data source name", or "dsn").

    Open a command terminal and change to the tutorial directory:

    cd samples/tutorial

    Run the Python script:

    python connect.py

    The version number of the database should be displayed. An exception is raised if the connection fails. Adjust the username, password, or connection string parameters to invalid values to see the exception.

    Python-oracledb also supports "external authentication", which allows connections without needing usernames and passwords to be embedded in the code. Authentication would then be performed by, for example, LDAP or Oracle Wallets.

  • 1.2 Indentation indicates code structure

    In Python, there are no statement terminators, begin/end keywords, or braces to indicate code blocks.

    Open connect.py in an editor. Indent the print statement with some spaces:

    import oracledb
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
      print("Database version:", con.version)
    

    Save the script and run it again:

    python connect.py 

    This raises an exception about the indentation. The number of spaces or tabs must be consistent in each block; otherwise, the Python interpreter will either raise an exception or execute code unexpectedly.

    Python may not always be able to identify accidental from deliberate indentation. Check if your indentation is correct before running each example. Make sure to indent all statement blocks equally. Note that the sample files use spaces, not tabs.

  • 1.3 Executing a query

    Open query.py in an editor. It looks like:

    import oracledb
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    

    Edit the file and add the code shown in bold below:

    import oracledb
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    
    cur = con.cursor()
    cur.execute("select * from dept order by deptno")
    res = cur.fetchall()
    for row in res:
        print(row)
    

    Make sure the print(row) line is indented. This tutorial uses spaces, not tabs.

    The code executes a query and fetches all data.

    Save the file and run it:

    python query.py

    In each loop iteration, a new row is stored in row variable as a Python "tuple" and is displayed.

    Fetching data is described further in Section 3.

  • 1.4 Closing connections

    Connections and other resources used by python-oracledb will automatically be closed at the end of scope. This is a common programming style that takes care of the correct order of resource closure.

    Resources can also be explicitly closed to free up database resources if they are no longer needed. This is strongly recommended in blocks of code that remain active for some time.

    Open query.py in an editor and add calls to close the cursor and connection like:

    import oracledb
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    
    cur = con.cursor()
    cur.execute("select * from dept order by deptno")
    res = cur.fetchall()
    for row in res:
        print(row)
    
    cur.close()
    con.close()
    

    Running the script completes without error:

    python query.py

    If you swap the order of the two close() calls you will see an error.

  • 1.5 Checking versions

    Review the code contained in versions.py:

    import oracledb
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    
    print(oracledb.__version__) # two underscores before and after the version

    Run the script:

    python versions.py

    This gives the version of the python-oracledb interface.

    Edit the file to print the version of the database, and the Oracle client libraries used by python-oracledb:

    import oracledb
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    
    print(oracledb.__version__)
    print("Database version:", con.version)
    

    When the script is run, it will display:

    1.0.0
    Database version: 19.3.0.0.0

    Any python-oracledb installation can connect to older and newer Oracle Database versions. By checking the Oracle Database version numbers, the application can make use of the best Oracle features available.

  • 1.6 Using the ConnectParams builder class

    A connection property builder function oracledb.ConnectParams() has been added. It returns a new ConnectParams object. The object can be passed to oracledb.connect() or oracledb.create_pool().

    Open connect_params2.py in a text editor. It looks like:

    import oracledb
    import db_config
    
    params = oracledb.ConnectParams(host="localhost", port=1521, service_name="orclpdb")
    con = oracledb.connect(user=db_config.user, password=db_config.pw, params=params)
    print("Database version:", con.version)
    When the script is run (python connect_params2.py), it will display:
    Database version: 19.3.0.0.

    The use of ConnectParams() is optional. Users can continue to use previous approaches. The list of parameters for the ConnectParams class is available in the python-oracledb documentation.

    Notes:

    • If the params parameter is specified and keyword parameters are also specified, then the params parameter is updated with the values from the keyword parameters before being used to create the connection.
    • If the dsn parameter is specified and the params parameter is specified, then the params parameter is updated with the contents of the dsn parameter before being used to create the connection.
  • 1.7 Checking Connection Health

    The function Connection.is_healthy() checks the usability of a database connection locally. This function returns a boolean value indicating the health status of a connection.

    Connections may become unusable in several cases, such as if the network socket is broken, if an Oracle error indicates the connection is unusable or after receiving a planned down notification from the database. This function is best used before starting a new database request on an existing standalone connection. Pooled connections internally perform this check before returning a connection to the application. If this function returns False, the connection should be not be used by the application and a new connection should be established instead.

    Open connect_health.py in a text editor. It looks like:

    import oracledb
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    if con.is_healthy():
        print("Healthy connection!")
    else:
        print("Unusable connection. Please check the database and network settings.")

    When the script is run (python connect_health.py), it will display (when the connection is OK):

    Healthy Connection!

    To fully check a connection's health, use Connection.ping() which performs a round-trip to the database.

2. Connection Pooling

Connection pooling is important for performance when multi-threaded applications frequently connect and disconnect from the database. Pooling also gives the best support for Oracle's High Availability (HA) features. Documentation link for further reading: Connection Pooling.

  • 2.1 Connection pooling

    Review the code contained in connect_pool.py:

    import oracledb
    import threading
    import db_config
    
    pool = oracledb.create_pool(user=db_config.user, password=db_config.pw, dsn=db_config.dsn,
                                min=2, max=5, increment=1, getmode=oracledb.POOL_GETMODE_WAIT)
    
    def Query():
        con = pool.acquire()
        cur = con.cursor()
        for i in range(4):
            cur.execute("select myseq.nextval from dual")
            seqval, = cur.fetchone()
            print("Thread", threading.current_thread().name, "fetched sequence =", seqval)
    
    thread1 = threading.Thread(name='#1', target=Query)
    thread1.start()
    
    thread2 = threading.Thread(name='#2', target=Query)
    thread2.start()
    
    thread1.join()
    thread2.join()
    
    print("All done!")
    

    The create_pool() function creates a pool of Oracle connections for the user. Connections in the pool can be used by python-oracledb by calling pool.acquire(). The initial pool size is 2 connections. The maximum size is 5 connections. When the pool needs to grow, then a single new connection will be created at a time based on the increment parameter. The pool can shrink back to the minimum size of 2 when the connections are no longer in use.

    The def Query(): line creates a method that is called by each thread.

    In the Query method, the pool.acquire() call gets one connection from the pool (as long as less than 5 are already in use). This connection is used in a loop of 4 iterations to query the sequence myseq. At the end of the method, python-oracledb will automatically close the cursor and release the connection back to the pool for reuse.

    The seqval, = cur.fetchone() line fetches a row and puts the single value contained in the result tuple into the variable seqval. Without the comma, the value in seqval would be a tuple like "(1,)".

    Two threads are created, each invoking the Query() method.

    In a command terminal, run:

    python connect_pool.py

    The output shows the interleaved query results as each thread fetches values independently. The order of interleaving may vary from run to run.

  • 2.2 Connection pool experiments

    Review connect_pool2.py, which has a loop for the number of threads, each iteration invoking the Query() method:

    import oracledb
    import threading
    import db_config
    
    pool = oracledb.create_pool(user=db_config.user, password=db_config.pw, dsn=db_config.dsn,
                                min=2, max=5, increment=1, getmode=oracledb.POOL_GETMODE_WAIT)
    
    def Query():
        con = pool.acquire()
        cur = con.cursor()
        for i in range(4):
            cur.execute("select myseq.nextval from dual")
            seqval, = cur.fetchone()
            print("Thread", threading.current_thread().name, "fetched sequence =", seqval)
    
    numberOfThreads = 2
    threadArray = []
    
    for i in range(numberOfThreads):
        thread = threading.Thread(name='#' + str(i), target=Query)
        threadArray.append(thread)
        thread.start()
    
    for t in threadArray:
        t.join()
    
    print("All done!")
    

    In a command terminal, run:

    python connect_pool2.py

    Experiment with different values of the pool parameters and numberOfThreads. Larger initial pool sizes will make the pool creation slower, but the connections will be available immediately when needed.

    Try changing getmode to oracledb.POOL_GETMODE_WAIT. When numberOfThreads exceeds the maximum size of the pool, the acquire() call will now generate an error such as "ORA-24459: OCISessionGet() timed out waiting for pool to create new connections".

    Pool configurations where min is the same as max (and increment = 0) are often recommended as a best practice for the optimum performance. Pools with such configurations are referred to as "static pools". This configuration avoids connection storms on the database server.

  • 2.3 Creating a DRCP Connection

    Database Resident Connection Pooling allows multiple Python processes on multiple machines to share a small pool of database server processes.

    Below left is a diagram without DRCP. Every application standalone connection (or python-oracledb connection-pool connection) has its own database server process. Standalone application connect() and close calls require the expensive create and destroy of those database server processes. Python-oracledb connection pools reduce these costs by keeping database server processes open, but every python-oracledb connection pool will require its own set of database server processes, even if they are not doing database work: these idle server processes consume database host resources. Below right is a diagram with DRCP. Scripts and Python processes can share database servers from a pre-created pool of servers and return them when they are not in use.

    Picture of 3-tier application architecture without DRCP showing connections from multiple application processes each going to a server process in the database tier

    Without DRCP

    Picture of 3-tier application architecture with DRCP showing connections from multiple application processes going to a pool of server processes in the database tier

    With DRCP

    DRCP is useful when the database host machine does not have enough memory to handle the number of database server processes required. If DRCP is enabled, it is best used in conjunction with python-oracledb's connection pooling. However, the default 'dedicated' server process model is generally recommended if the database host memory is large enough. This can be with or without a python-oracledb connection pool, depending on the connection rate.

    Batch scripts doing long running jobs should generally use dedicated connections. Both dedicated and DRCP servers can be used together in the same application or database.

    Start the Database Resident Connection Pool (DRCP)

    If you are running a local or remote Oracle Database (that is not an ADB), start the DRCP pool. Note that the DRCP pool is started in an Oracle Autonomous Database by default.

    Run SQL*Plus with SYSDBA privileges, for example:

    sqlplus -l sys/syspassword@localhost/orclcdb as sysdba
    

    and execute the command:

    execute dbms_connection_pool.start_pool()
    

    Note: If you are using Oracle Database 21c,

    Run show parameter enable_per_pdb_drcp in SQL*Plus.

    If this shows TRUE,

    then you will need to run the execute command in a pluggable database, not a container database.

    Connect to the Oracle Database through DRCP

    Review the code contained in connect_drcp.py:

    import oracledb
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn + ":pooled",
                           cclass="PYTHONDEMO", purity=oracledb.PURITY_SELF)
    print("Database version:", con.version)
    

    This is similar to connect.py but ":pooled" is appended to the connection string, telling the database to use a pooled server. A Connection Class "PYTHONDEMO" is also passed into the connect() method to allow grouping of database servers to applications. Note that with Autonomous Database, the connection string has a different form, see the ADB documentation.

    The "purity" of the connection is defined as the PURITY_SELF constant, meaning the session state (such as the default date format) might be retained between connection calls, giving performance benefits. Session information will be discarded if a pooled server is later reused by an application with a different connection class name.

    Applications that should never share session information should use a different connection class and/or use PURITY_NEW to force creation of a new session. This reduces overall scalability but prevents applications from misusing the session information. The default purity for connections created with connect() is PURITY_NEW.

    Run connect_drcp.py in a terminal window.

    python connect_drcp.py

    The output is simply the version of the database.

  • 2.4 Connection pooling and DRCP

    DRCP works well with python-oracledb's connection pooling. The default purity for pooled connections is PURITY_SELF.

    Edit connect_pool2.py, reset any changed pool options, and modify it to use DRCP:

    import oracledb
    import threading
    import db_config
    
    pool = oracledb.create_pool(user=db_config.user, password=db_config.pw, dsn=db_config.dsn + ":pooled",
                                min=2, max=5, increment=1, getmode=oracledb.POOL_GETMODE_WAIT,
                                cclass="PYTHONDEMO", purity=oracledb.PURITY_SELF)
    
    def Query():
        con = pool.acquire()
        cur = conn.cursor()
        for i in range(4):
            cur.execute("select myseq.nextval from dual")
            seqval, = cur.fetchone()
            print("Thread", threading.current_thread().name, "fetched sequence =", seqval)
    
    numberOfThreads = 2
    threadArray = []
    
    for i in range(numberOfThreads):
        thread = threading.Thread(name='#' + str(i), target=Query)
        threadArray.append(thread)
        thread.start()
    
    for t in threadArray:
        t.join()
    
    print("All done!")
    

    The script logic does not need to be changed to benefit from DRCP connection pooling.

    Run the script:

    python connect_pool2.py

    Optionally, you can run drcp_query.py to check the DRCP pool statistics.

    python drcp_query.py

    This will prompt for the SYSTEM (or ADMIN user), the password, and the database connection string. For running the file, you will need to connect to the container database in Oracle Database v19 or lower. From Oracle Database 21c onwards, you can enable DRCP in pluggable databases.

    Note that with ADB, this view does not contain rows, so running this script is not useful. For other Oracle Databases, the script shows the number of connection requests made to the pool since the database was started ("NUM_REQUESTS"), how many of those reused a pooled server's session ("NUM_HITS"), and how many had to create new sessions ("NUM_MISSES"). Typically the goal is a low number of misses.

    If the file is run successfully, you should see something like

    Looking at DRCP Pool stats...
    
    (CCLASS_NAME, NUM_REQUESTS, NUM_HITS, NUM_MISSES)
    -------------------------------------------------
    ('PYTHONDEMO.SHARED', 5, 0, 5)
    ('PYTHONDEMO.PYTHONDEMO', 4, 2, 2)
    ('SYSTEM.SHARED', 11, 0, 11)
    Done.

    To see the pool configuration, you can query DBA_CPOOL_INFO.

  • 2.5 More DRCP investigation

    To further explore the behaviors of python-oracledb connection pooling and DRCP pooling, you could try changing the purity to oracledb.PURITY_NEW to see the effect on the DRCP NUM_MISSES statistic.

    Another experiement is to include the time module at the file top:

    import time

    and add calls to time.sleep(1) in the code, for example in the query loop. Then look at the way the threads execute. Use drcp_query.sql to monitor the pool's behavior.

3. Fetching Data

Executing SELECT queries is the primary way to get data from Oracle Database. Documentation link for further reading: SQL Queries.

  • 3.1 A simple query

    There are several functions you can use to query an Oracle database, but the basics of querying are always the same:

    1. Execute the statement.
    2. Bind data values (optional).
    3. Fetch the results from the database.

    Review the code contained in query2.py:

    import oracledb
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    
    cur = con.cursor()
    cur.execute("select * from dept order by deptno")
    for deptno, dname, loc in cur:
        print("Department number: ", deptno)
        print("Department name: ", dname)
        print("Department location:", loc)
    

    The cursor() method opens a cursor for statements to use.

    The execute() method parses and executes the statement.

    The loop fetches each row from the cursor and unpacks the returned tuple into the variables deptno, dname, loc, which are then printed.

    Run the script in a terminal window:

    python query2.py

    The output is:

    Department number:  10
    Department name:  ACCOUNTING
    Department location: NEW YORK
    Department number:  20
    Department name:  RESEARCH
    Department location: DALLAS
    Department number:  30
    Department name:  SALES
    Department location: CHICAGO
    Department number:  40
    Department name:  OPERATIONS
    Department location: BOSTON
  • 3.2 Using fetchone()

    When the number of rows is large, the fetchall() call may use too much memory.

    Review the code contained in query_one.py:

    import oracledb
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, password=db_config.dsn)
    cur = con.cursor()
    
    cur.execute("select * from dept order by deptno")
    row = cur.fetchone()
    print(row)
    
    row = cur.fetchone()
    print(row)
    

    This uses the fetchone() method to return just a single row as a tuple. When called multiple time, consecutive rows are returned:

    Run the script in a terminal window:

    python query_one.py

    The first two rows of the table are printed.

  • 3.3 Using fetchmany()

    Review the code contained in query_many.py:

    import oracledb
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    cur = con.cursor()
    
    cur.execute("select * from dept order by deptno")
    num_rows =  3
    res = cur.fetchmany(num_rows)
    print(res)
    

    The fetchmany() method returns a list of tuples. By default the maximum number of rows that can be returned is specified by the cursor attribute arraysize (which defaults to 100). Here the numRows parameter specifies that three rows should be returned.

    Run the script in a terminal window:

    python query_many.py

    The first three rows of the table are returned as a list (Python's name for an array) of tuples.

    You can access elements of the lists by position indexes. To see this, edit the file and add:

    print(res[0])    # first row
    print(res[0][1]) # second element of first row
    
  • 3.4 Tuning with arraysize and prefetchrows

    This section demonstrates a way to improve query performance by increasing the number of rows returned in each batch from Oracle to the Python program.

    Row prefetching and array fetching are internal buffering techniques to reduce round-trips to the database. The difference is the code layer that is doing the buffering, and when the buffering occurs.

    The environment setup file has already created the bigtab table with a large number of rows (to be used by the query_arraysize.py file) by internally running the sql script below:

    create table bigtab (mycol varchar2(20));
    
    begin
     for i in 1..20000
     loop
      insert into bigtab (mycol) values (dbms_random.string('A',20));
     end loop;
    end;

    The setup file has also inserted around 20000 string values in the bigtab table.

    Review the code contained in query_arraysize.py:

    import oracledb
    import time
    import db_config
    
    con = oracledb.connect(name=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    
    start = time.time()
    
    cur = con.cursor()
    cur.prefetchrows = 100
    cur.arraysize = 100
    cur.execute("select * from bigtab")
    res = cur.fetchall()
    # print(res)  # uncomment to display the query results
    
    elapsed = (time.time() - start)
    print(elapsed, "seconds")
    

    This uses the 'time' module to measure elapsed time of the query. The prefetchrows and arraysize values are 100. This causes batches of 100 records at a time to be returned from the database to a cache in Python. These values can be tuned to reduce the number of "round-trips" made to the database, often reducing network load and reducing the number of context switches on the database server. The fetchone(), fetchmany() and fetchall() methods will read from the cache before requesting more data from the database.

    In a terminal window, run:

    python query_arraysize.py

    Rerun a few times to see the average times.

    Experiment with different prefetchrows and arraysize values. For example, edit query_arraysize.py and change the arraysize to:

    cur.arraysize = 2000

    Rerun the script to compare the performance of different arraysize settings.

    In general, larger array sizes improve performance. Depending on how fast your system is, you may need to use different values than those given here to see a meaningful time difference.

    There is a time/space tradeoff for increasing the values. Larger values will require more memory in Python for buffering the records.

    If you know the query returns a fixed number of rows, for example, 20 rows, then set arraysize to 20 and prefetchrows to 21. The addition of one extra row for prefetchrows prevents a round-trip to check for end-of-fetch. The statement execution and fetch will take a total of one round-trip. This minimizes the load on the database.

    If you know a query only returns a few records, decrease the arraysize from the default to reduce memory usage.

4. Binding Data

Bind variables enable you to re-execute statements with new data values without the overhead of re-parsing the statement. Binding improves code reusability, improves application scalability, and can reduce the risk of SQL injection attacks. Using bind variables is strongly recommended. Documentation link for further reading: Using Bind Variables.

  • 4.1 Binding in queries

    Review the code contained in bind_query.py:

    import oracledb
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    cur = con.cursor()
    
    sql = "select * from dept where deptno = :id order by deptno"
    
    cur.execute(sql, id=20)
    res = cur.fetchall()
    print(res)
    
    cur.execute(sql, id=10)
    res = cur.fetchall()
    print(res)
    

    The statement contains a bind variable ":id" placeholder. The statement is executed twice with different values for the WHERE clause.

    From a terminal window, run:

    python bind_query.py

    The output shows the details for the two departments.

    An arbitrary number of named arguments can be used in an execute() call. Each argument name must match a bind variable name. Alternatively, instead of passing multiple arguments you could pass a second argument to execute() that is a sequence or a dictionary. Later examples show these syntaxes.

    To bind a database NULL, use the Python value None.

    python-oracledb uses a cache of executed statements. As long as the statement you pass to execute() is in that cache, you can use different bind values and still avoid a full statement parse. The statement cache size is configurable for each connection. To see the default statement cache size, edit bind_query.py and add a line at the end:

    print(con.stmtcachesize)
    

    Re-run the file.

    You would set the statement cache size to the number of unique statements commonly executed in your applications.

  • 4.2 Binding in inserts

    The environment setup file has already created the mytab table (to be used by the bind_insert.py file) by internally running the sql script below:

    create table mytab (id number, data varchar2(20), constraint my_pk primary key (id))

    Now, review the code contained in bind_insert.py:

    import oracledb
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    cur = con.cursor()
    
    rows = [ (1, "First" ), (2, "Second" ),
             (3, "Third" ), (4, "Fourth" ),
             (5, "Fifth" ), (6, "Sixth" ),
             (7, "Seventh" ) ]
    
    cur.executemany("insert into mytab(id, data) values (:1, :2)", rows)
    
    # Now query the results back
    
    cur2 = con.cursor()
    cur2.execute('select * from mytab')
    res = cur2.fetchall()
    print(res)

    The 'rows' array contains the data to be inserted into the mytab table created earlier.

    The executemany() call inserts all rows. This call uses "array binding", which is an efficient way to insert multiple records.

    The final part of the script queries the results back and displays them as a list of tuples.

    From a terminal window, run:

    python bind_insert.py

    The new results are automatically rolled back at the end of the script. So, re-running the script will always show the same number of rows in the table.

  • 4.3 Batcherrors

    The Batcherrors features allows invalid data to be identified while allowing valid data to be inserted.

    Edit the data values in bind_insert.py and create a row with a duplicate key:

    rows = [ (1, "First" ), (2, "Second" ),
             (3, "Third" ), (4, "Fourth" ),
             (5, "Fifth" ), (6, "Sixth" ),
             (6, "Duplicate" ),
             (7, "Seventh" ) ]
    

    From a terminal window, run:

    python bind_insert.py

    The duplicate generates the error "ORA-00001: unique constraint (PYTHONHOL.MY_PK) violated". The data is rolled back and the query returns no rows.

    Edit the file again and enable batcherrors like:

    import oracledb
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    cur = con.cursor()
    
    rows = [ (1, "First" ), (2, "Second" ),
             (3, "Third" ), (4, "Fourth" ),
             (5, "Fifth" ), (6, "Sixth" ),
             (6, "Duplicate" ),
             (7, "Seventh" ) ]
    
    cur.executemany("insert into mytab(id, data) values (:1, :2)", rows, batcherrors=True)
    
    for error in cur.getbatcherrors():
        print("Error", error.message.rstrip(), "at row offset", error.offset)
    
    # Now query the results back
    
    cur2 = con.cursor()
    cur2.execute('select * from mytab')
    res = cur2.fetchall()
    print(res)
    

    Run the file:

    python bind_insert.py

    The new code shows the offending duplicate row: "ORA-00001: unique constraint (PYTHONDEMO.MY_PK) violated at row offset 6". This indicates the 6th data value (counting from 0) had a problem.

    The other data gets inserted and is queried back.

    At the end of the script, python-oracledb will roll back an uncommitted transaction. If you want to commit results, you can use:

    con.commit()

    To force python-oracledb to roll back the transaction, use:

    con.rollback()

5. PL/SQL

PL/SQL is Oracle's procedural language extension to SQL. PL/SQL procedures and functions are stored and run in the database. Using PL/SQL lets all database applications reuse logic, no matter how the application accesses the database. Many data-related operations can be performed in PL/SQL faster than extracting the data into a program (for example, Python) and then processing it. Documentation link for further reading: PL/SQL Execution.

  • 5.1 PL/SQL function

    The environment setup file has already created the new table named ptab and a PL/SQL stored function myfunc to insert a row into ptab and return double the inserted value by internally running the sql script below:

    create table ptab (mydata varchar(20), myid number);
    
    create or replace function myfunc(d_p in varchar2, i_p in number) return number as
      begin
        insert into ptab (mydata, myid) values (d_p, i_p);
        return (i_p * 2);
      end;
    /

    The myfunc PL/SQL stored function will be used by the plsql_func.py file below.

    Review the code contained in plsql_func.py:

    import oracledb
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    cur = con.cursor()
    
    res = cur.callfunc('myfunc', int, ('abc', 2))
    print(res)
    

    This uses the callfunc() method to execute the function. The second parameter is the type of the returned value. It should be one of the types supported by python-oracledb or one of the type constants defined by python-oracledb (such as oracledb.NUMBER). The two PL/SQL function parameters are passed as a tuple, binding them to the function parameter arguments.

    From a terminal window, run:

    python plsql_func.py

    The output is a result of the PL/SQL function calculation.

  • 5.2 PL/SQL procedures

    The environment setup file has already created a PL/SQL stored procedure myproc to accept two parameters by internally running the sql script below:

    create or replace procedure myproc(v1_p in number, v2_p out number) as
    begin
      v2_p := v1_p * 2;
    end;
    /

    The second parameter contains an OUT return value.The myproc PL/SQL stored procedure will be used by the plsql_proc.py file below.

    Review the code contained in plsql_proc.py:

    import oracledb
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    cur = con.cursor()
    
    myvar = cur.var(int)
    cur.callproc('myproc', (123, myvar))
    print(myvar.getvalue())

    This creates an integer variable myvar to hold the value returned by the PL/SQL OUT parameter. The input number 123 and the output variable name are bound to the procedure call parameters using a tuple.

    To call the PL/SQL procedure, the callproc() method is used.

    In a terminal window, run:

    python plsql_proc.py

    The getvalue() method displays the returned value.

6. Type Handlers

Type handlers enable applications to alter data that is fetched from, or sent to, the database. Documentation links for further reading: Changing Fetched Data Types with Output Type Handlers and Changing Bind Data Types using an Input Type Handler.

  • 6.1 Basic output type handler

    Output type handlers enable applications to change how data is fetched from the database. For example, numbers can be returned as strings or decimal objects. LOBs can be returned as strings or bytes.

    A type handler is enabled by setting the outputtypehandler attribute on either a cursor or the connection. If set on a cursor, it only affects queries executed by that cursor. If set on a connection, it affects all queries executed on cursors created by that connection.

    Review the code contained in type_output.py:

    import oracledb
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    cur = con.cursor()
    
    print("Standard output...")
    for row in cur.execute("select * from dept"):
        print(row)
    

    In a terminal window, run:

    python type_output.py

    This shows the department number represented as digits like 10.

    Add an output type handler to the bottom of the file:

    def ReturnNumbersAsStrings(cursor, name, defaultType, size, precision, scale):
        if defaultType == oracledb.NUMBER:
            return cursor.var(str, 9, cursor.arraysize)
    
    print("Output type handler output...")
    cur = con.cursor()
    cur.outputtypehandler = ReturnNumbersAsStrings
    for row in cur.execute("select * from dept"):
        print(row)
    

    This type handler converts any number columns to strings with maximum size 9.

    Run the script again:

    python type_output.py

    The new output shows the department numbers are now strings within quotes like '10'.

  • 6.2 Output type handlers and variable converters

    When numbers are fetched from the database, the conversion from Oracle's decimal representation to Python's binary format may need careful handling. To avoid unexpected issues, the general recommendation is to do number operations in SQL or PL/SQL, or to use the decimal module in Python.

    Output type handlers can be combined with variable converters to change how data is fetched.

    Review type_converter.py:

    import oracledb
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    cur = con.cursor()
    
    for value, in cur.execute("select 0.1 from dual"):
        print("Value:", value, "* 3 =", value * 3)
    

    Run the file:

    python type_converter.py

    The output is like:

    Value: 0.1 * 3 = 0.30000000000000004

    Edit the file and add a type handler that uses a Python decimal converter:

    import oracledb
    import decimal
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    cur = con.cursor()
    
    def ReturnNumbersAsDecimal(cursor, name, defaultType, size, precision, scale):
        if defaultType == oracledb.NUMBER:
            return cursor.var(str, 9, cursor.arraysize, outconverter=decimal.Decimal)
    
    cur.outputtypehandler = ReturnNumbersAsDecimal
    
    for value, in cur.execute("select 0.1 from dual"):
        print("Value:", value, "* 3 =", value * 3)
    

    The Python decimal.Decimal converter gets called with the string representation of the Oracle number. The output from decimal.Decimal is returned in the output tuple.

    Run the file again:

    python type_converter.py

    Output is like:

    Value: 0.1 * 3 = 0.3

    The code above demonstrates the use of outconverter, but in this particular case, python-oracledb offers a simple convenience attribute to do the same conversion:

    import oracledb
    
    oracledb.defaults.fetch_decimals = True
    
  • 6.3 Input type handlers

    Input type handlers enable applications to change how data is bound to statements, or to enable new types to be bound directly without having to be converted individually.

    Review type_input.py, with the addition of a new class and converter (shown in bold):

    import oracledb
    import db_config
    import json
    
    con = oracledb.connect(user=db_config.user,
                           password=db_config.pw, dsn=db_config.dsn)
    cur = con.cursor()
    
    # Create table
    cur.execute("""begin
                     execute immediate 'drop table BuildingTable';
                     exception when others then
                       if sqlcode <> -942 then
                         raise;
                       end if;
                   end;""")
    cur.execute("""create table BuildingTable (
                   ID number(9) not null,
                   BuildingDetails varchar2(400),
                   constraint TestTempTable_pk primary key (ID))""")
    
    # Create a Python class for a Building
    class Building(object):
    
        def __init__(self, building_id, description, num_floors):
            self.building_id = building_id
            self.description = description
            self.num_floors = num_floors
    
        def __repr__(self):
            return "<Building %s: %s>" % (self.building_id, self.description)
    
        def __eq__(self, other):
            if isinstance(other, Building):
                return other.building_id == self.building_id \
                    and other.description == self.description \
                    and other.num_floors == self.num_floors
            return NotImplemented
    
        def to_json(self):
            return json.dumps(self.__dict__)
    
        @classmethod
        def from_json(cls, value):
            result = json.loads(value)
            return cls(**result)
    
    # Convert a Python building object to SQL JSON type that can be read as a string
    def building_in_converter(value):
        return value.to_json()
    
    
    def input_type_handler(cursor, value, num_elements):
        if isinstance(value, Building):
            return cursor.var(oracledb.STRING, arraysize=num_elements,
                              inconverter=building_in_converter)
    
    
    building = Building(1, "The First Building", 5)  # Python object
    cur.execute("truncate table BuildingTable")
    cur.inputtypehandler = input_type_handler
    cur.execute("insert into BuildingTable (ID, BuildingDetails) values (:1, :2)",
                (building.building_id, building))
    con.commit()
    
    # Query the row
    print("Querying the row just inserted...")
    cur.execute("select ID, BuildingDetails from BuildingTable")
    for (int_col, string_col) in cur:
        print("Building ID:", int_col)
        print("Building Details in JSON format:", string_col)
    

    In the new file, a Python class Building is defined, which holds basic information about a building. The Building class is used lower in the code to create a Python instance:

    building = Building(1, "The First Building", 5)

    which is then directly bound into the INSERT statement like

    cur.execute("insert into BuildingTable (ID, BuildingDetails) values (:1, :2)", (building.building_id, building))

    The mapping between Python and Oracle objects is handled in building_in_converter which creates an Oracle STRING object from the Building Python object in a JSON format. The building_in_converter method is called by the input type handler input_type_handler,whenever an instance of Building is inserted with the cursor.

    To confirm the behavior, run the file:

    python type_input.py

    You should see the following output:

    Querying the row just inserted...
    Building ID: 1
    Building Details in JSON format: {"building_id": 1, "description": "The First Building", "num_floors": 5}

7. LOBs

Oracle Database "LOB" long objects can be streamed using a LOB locator, or worked with directly as strings or bytes. Documentation link for further reading: Using CLOB and BLOB Data.

  • 7.1 Fetching a CLOB using a locator

    Review the code contained in clob.py:

    import oracledb
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    cur = con.cursor()
    
    print("Inserting data...")
    cur.execute("truncate table testclobs")
    long_string = ""
    for i in range(5):
        char = chr(ord('A') + i)
        long_string += char * 250
        cur.execute("insert into testclobs values (:1, :2)",
                       (i + 1, "String data " + longString + ' End of string'))
    con.commit()
    
    print("Querying data...")
    cur.execute("select * from testclobs where id = :id", {'id': 1})
    (id, clob) = cur.fetchone()
    print("CLOB length:", clob.size())
    clobdata = clob.read()
    print("CLOB data:", clobdata)
    

    This inserts some test string data and then fetches one record into clob, which is a python-oracledb character LOB Object. Methods on LOB include size() and read().

    To see the output, run the file:

    python clob.py

    Edit the file and experiment reading chunks of data by giving start character position and length, such as clob.read(1,10).

  • 7.2 Fetching a CLOB as a string

    For CLOBs small enough to fit in the application memory, it is much faster to fetch them directly as strings.

    Review the code contained in clob_string.py. The differences from clob.py are shown in bold:

    import oracledb
    import db_config
    
    oracledb.defaults.fetch_lobs = False
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    cur = con.cursor()
    
    print("Inserting data...")
    cur.execute("truncate table testclobs")
    long_string = ""
    for i in range(5):
        char = chr(ord('A') + i)
        long_string += char * 250
        cur.execute("insert into testclobs values (:1, :2)",
                    (i + 1, "String data " + long_string + ' End of string'))
    con.commit()
    
    print("Querying data...")
    cur.execute("select * from testclobs where id = :id", {'id': 1})
    (id, clobdata) = cur.fetchone()
    print("CLOB length:", len(clobdata))
    print("CLOB data:", clobdata)
    

    Setting oracledb.defaults.fetch_lobs to False causes python-oracledb to fetch the CLOB as a string. Standard Python string functions such as len() can be used on the result.

    The output is the same as for clob.py. To check, run the file:

    python clob_string.py

8. Rowfactory functions

Rowfactory functions enable queries to return objects other than tuples. They can be used to provide names for the various columns or to return custom objects.

  • 8.1 Rowfactory for mapping column names

    Review the code contained in rowfactory.py:

    import collections
    import oracledb
    import db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    cur = con.cursor()
    
    cur.execute("select deptno, dname from dept")
    rows = cur.fetchall()
    
    print('Array indexes:')
    for row in rows:
        print(row[0], "->", row[1])
    
    print('Loop target variables:')
    for c1, c2 in rows:
        print(c1, "->", c2)
    

    This shows two methods of accessing result set items from a data row. The first uses array indexes like row[0]. The second uses loop target variables that take each row tuple's values.

    Run the file:

    python rowfactory.py

    Both access methods gives the same results.

    To use a rowfactory function, edit rowfactory.py and add this code at the bottom:

    print('Rowfactory:')
    cur.execute("select deptno, dname from dept")
    cur.rowfactory = collections.namedtuple("MyClass", ["DeptNumber", "DeptName"])
    
    rows = cur.fetchall()
    for row in rows:
        print(row.DeptNumber, "->", row.DeptName)
    

    This uses the Python factory function namedtuple() to create a subclass of tuple that allows access to the elements via indexes or the given field names.

    The print() function shows the use of the new named tuple fields. This coding style can help reduce coding errors.

    Run the script again:

    python rowfactory.py

    The output results are the same.

9. Subclassing connections and cursors

Subclassing enables application to "hook" connection and cursor creation. This can be used to alter or log connection and execution parameters, and to extend python-oracledb functionality. Documentation link for further reading: Application Tracing.

  • 9.1 Subclassing connections

    Review the code contained in subclass.py:

    import oracledb
    import db_config
    
    class MyConnection(oracledb.Connection):
    
        def __init__(self):
            print("Connecting to database")
            return super(MyConnection, self).__init__(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    
    con = MyConnection()
    cur = con.cursor()
    
    cur.execute("select count(*) from emp where deptno = :bv", (10,))
    count, = cur.fetchone()
    print("Number of rows:", count)
    

    This creates a new class "MyConnection" that inherits from the python-oracledb Connection class. The __init__ method is invoked when an instance of the new class is created. It prints a message and calls the base class, passing the connection credentials.

    In the "normal" application, the application code:

    con = MyConnection()

    does not need to supply any credentials, as they are embedded in the custom subclass. All the python-oracledb methods such as cursor() are available, as shown by the query.

    Run the file:

    python subclass.py

    The query executes successfully.

  • 9.2 Subclassing cursors

    Edit subclass.py and extend the cursor() method with a new MyCursor class:

    import oracledb
    import db_config
    
    class MyConnection(oracledb.Connection):
    
        def __init__(self):
            print("Connecting to database")
            return super(MyConnection, self).__init__(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    
        def cursor(self):
            return MyCursor(self)
    
    class MyCursor(oracledb.Cursor):
    
       def execute(self, statement, args):
           print("Executing:", statement)
           print("Arguments:")
           for argIndex, arg in enumerate(args):
               print("  Bind", argIndex + 1, "has value", repr(arg))
               return super(MyCursor, self).execute(statement, args)
    
       def fetchone(self):
           print("Fetchone()")
           return super(MyCursor, self).fetchone()
    
    con = MyConnection()
    cur = con.cursor()
    
    cur.execute("select count(*) from emp where deptno = :bv", (10,))
    count, = cur.fetchone()
    print("Number of rows:", count)
    

    When the application gets a cursor from the MyConnection class, the new cursor() method returns an instance of our new MyCursor class.

    The "application" query code remains unchanged. The new execute() and fetchone() methods of the MyCursor class get invoked. They do some logging and invoke the parent methods to do the actual statement execution.

    To confirm this, run the file again:

    python subclass.py

10. Python-oracledb Thick mode

All the above examples use python-oracledb in thin mode, but there are certain features which are only available in the thick mode of the python-oracledb driver. The upcoming sections show some of these. Note that you can also run all the earlier examples in thick mode by just changing the import line in examples from import db_config to import db_config_thick as db_config.

The following sections assume you have installed the tutorial schema as shown at the tutorial start.

  • 10.1 Review the Oracle Client library path

    You additionally need to make Oracle Client libraries available. Follow the documentation on Installing python-oracledb.

    When you have installed Oracle Client libraries, review the library path settings in db_config_thick.py file. If python-oracledb cannot locate Oracle Client libraries, then your applications will fail with an error like "DPI-1047: Cannot locate a 64-bit Oracle Client library". For our examples, we are using Oracle Instant Client libraries.

    # On Linux, this must be None.
    # Instead, the Oracle environment must be set before Python starts.
    instant_client_dir = None
    
    # On Windows, if your database is on the same machine, comment these lines out
    # and let instant_client_dir be None.  Otherwise, set this to your Instant
    # Client directory.  Note the use of the raw string r"...", which allows backslashes to
    # be used as directory separators.
    if platform.system() == "Windows":
        instant_client_dir = r"C:\Oracle\instantclient_19_14"
    
    # On macOS (Intel x86) set the directory to your Instant Client directory
    if platform.system() == "Darwin":
        instant_client_dir = os.environ.get("HOME")+"/Downloads/instantclient_19_8"
    
    # You must always call init_oracle_client() to use thick mode
    oracledb.init_oracle_client(lib_dir=instant_client_dir)

    Important! Calling the init_oracle_client() function enables the thick mode of python-oracledb. Once python-oracledb is in thick mode, you cannot return to thin mode without removing calls to init_oracle_client() and restarting the application.

    Edit db_config_thick.py and set instant_client_dir to None or to a valid path according to the following notes:

    • If you are on macOS (Intel x86) or Windows, and you have installed Oracle Instant Client libraries because your database is on a remote machine, then set instant_client_dir to the path of the Instant Client libraries.

    • If you are on Windows and have a local database installed, then comment out the two Windows lines, so that instant_client_dir remains None.

    • In all other cases (including Linux with Oracle Instant Client), make sure that instant_client_dir is set to None. In these cases you must make sure that the Oracle libraries from Instant Client or your ORACLE_HOME are in your system library search path before you start Python. On Linux, the path can be configured with ldconfig or with the LD_LIBRARY_PATH environment variable.

  • 10.2 Review the configuration files for thick mode

    Review db_config_thick.py (thick mode), and db_config.sql files in the tutorial directory. These are included in other Python and SQL files for setting up the database connection.

    Edit db_config_thick.py file and change the default values to match the connection information for your environment. Alternatively, you can set the given environment variables in your terminal window. For example, the default username is "pythondemo" unless the environment variable "PYTHON_USER" contains a different username. The default connection string is for the 'orclpdb' database service on the same machine as Python. In Python Database API terminology, the connection string parameter is called the "data source name" or "dsn". Using environment variables is convenient because you will not be asked to re-enter the password when you run scripts:

    user = os.environ.get("PYTHON_USER", "pythondemo")
    
    dsn = os.environ.get("PYTHON_CONNECT_STRING", "localhost/orclpdb")
    
    pw = os.environ.get("PYTHON_PASSWORD")
    if pw is None:
        pw = getpass.getpass("Enter password for %s: " % user)
    

    Also, change the default username and connection string in the SQL configuration file db_config.sql:

    -- Default database username
    def user = "pythondemo"
    
    -- Default database connection string
    def connect_string = "localhost/orclpdb"
    
    -- Prompt for the password
    accept pw char prompt 'Enter database password for &user: ' hide
    

    The tutorial instructions may need adjusting, depending on how you have set up your environment.

The following sections are specific to the python-oracledb thick modes in this release of python-oracledb.

11. Scrollable cursors

Scrollable cursors enable python-oracledb thick mode applications to move backwards as well as forwards in query results. They can be used to skip rows as well as move to a particular row.

  • 11.1 Working with scrollable cursors

    Review the code contained in query_scroll.py:

    import oracledb
    import db_config_thick as db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    cur = con.cursor(scrollable=True)
    
    cur.execute("select * from dept order by deptno")
    
    cur.scroll(2, mode="absolute")  # go to second row
    print(cur.fetchone())
    
    cur.scroll(-1)                    # go back one row
    print(cur.fetchone())
    

    Run the script in a terminal window:

    python query_scroll.py

    Edit query_scroll.py and experiment with different scroll options and orders, such as:

    cur.scroll(1)  # go to next row
    print(cur.fetchone())
    
    cur.scroll(mode="first")  # go to first row
    print(cur.fetchone())

    Try some scroll options that go beyond the number of rows in the resultset.

12. Binding named objects

Python-oracledb's thick mode can fetch and bind named object types such as Oracle's Spatial Data Objects (SDO).

The SDO definition includes the following attributes:

 Name                                      Null?    Type
 ----------------------------------------- -------- ----------------------------
 SDO_GTYPE                                          NUMBER
 SDO_SRID                                           NUMBER
 SDO_POINT                                          MDSYS.SDO_POINT_TYPE
 SDO_ELEM_INFO                                      MDSYS.SDO_ELEM_INFO_ARRAY
 SDO_ORDINATES                                      MDSYS.SDO_ORDINATE_ARRAY
  • 12.1 How to bind named objects

    Review the code contained in bind_sdo.py:

    import oracledb
    import db_config_thick as db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    cur = con.cursor()
    
    # Create table
    cur.execute("""begin
                     execute immediate 'drop table testgeometry';
                     exception when others then
                       if sqlcode <> -942 then
                         raise;
                       end if;
                   end;""")
    cur.execute("""create table testgeometry (
                   id number(9) not null,
                   geometry MDSYS.SDO_GEOMETRY not null)""")
    
    # Create and populate Oracle objects
    type_obj = con.gettype("MDSYS.SDO_GEOMETRY")
    element_info_type_obj = con.gettype("MDSYS.SDO_ELEM_INFO_ARRAY")
    ordinate_type_obj = con.gettype("MDSYS.SDO_ORDINATE_ARRAY")
    obj = type_obj.newobject()
    obj.SDO_GTYPE = 2003
    obj.SDO_ELEM_INFO = element_info_type_obj.newobject()
    obj.SDO_ELEM_INFO.extend([1, 1003, 3])
    obj.SDO_ORDINATES = ordinate_type_obj.newobject()
    obj.SDO_ORDINATES.extend([1, 1, 5, 7])
    print("Created object", obj)
    
    # Add a new row
    print("Adding row to table...")
    cur.execute("insert into testgeometry values (1, :objbv)", objbv = obj)
    print("Row added!")
    
    # Query the row
    print("Querying row just inserted...")
    cur.execute("select id, geometry from testgeometry");
    for row in cur:
        print(row)

    This uses gettype() to get the database types of the SDO and its object attributes. The newobject() calls create Python representations of those objects. The python object atributes are then set. Oracle VARRAY types such as SDO_ELEM_INFO_ARRAY are set with extend().

    Run the file:

    python bind_sdo.py

    The new SDO is shown as an object, similar to

    (1, <oracledb.Object MDSYS.SDO_GEOMETRY at 0x104a76230>)

    To show the attribute values, edit the query code section at the end of the file. Add a new method that traverses the object. The file below the existing comment "# (Change below here)") should look like:

    # (Change below here)
    
    # Define a function to dump the contents of an Oracle object
    def dumpobject(obj, prefix = "  "):
        if obj.type.iscollection:
            print(prefix, "[")
            for value in obj.aslist():
                if isinstance(value, oracledb.Object):
                    dumpobject(value, prefix + "  ")
                else:
                    print(prefix + "  ", repr(value))
            print(prefix, "]")
        else:
            print(prefix, "{")
            for attr in obj.type.attributes:
                value = getattr(obj, attr.name)
                if isinstance(value, oracledb.Object):
                    print(prefix + "  " + attr.name + " :")
                    dumpobject(value, prefix + "    ")
                else:
                    print(prefix + "  " + attr.name + " :", repr(value))
            print(prefix, "}")
    
    # Query the row
    print("Querying row just inserted...")
    cur.execute("select id, geometry from testgeometry")
    for id, obj in cur:
        print("Id: ", id)
        dumpobject(obj)

    Run the file again:

    python bind_sdo.py

    This shows

    Querying row just inserted...
    Id:  1
       {
        SDO_GTYPE : 2003
        SDO_SRID : None
        SDO_POINT : None
        SDO_ELEM_INFO :
           [
             1
             1003
             3
           ]
        SDO_ORDINATES :
           [
             1
             1
             5
             7
           ]
       }
    

    To explore further, try setting the SDO attribute SDO_POINT, which is of type SDO_POINT_TYPE.

    The gettype() and newobject() methods can also be used to bind PL/SQL Records and Collections.

    Before deciding to use objects, review your performance goals because working with scalar values can be faster.

13. Input and Output Type Handlers with named objects

Named objects can only be used in python-oracledb's thick mode. Documentation links for further reading: Changing Fetched Data Types with Output Type Handlers and Changing Bind Data Types using an Input Type Handler.

  • 13.1 Input type handlers with named objects

    Input type handlers for named objects can enable applications to change how data is bound to the individual attributes of the named objects. Review the code contained in type_input_named_obj.py, which is similar to the final bind_sdo.py from section 12.1, with the addition of a new class and converter (shown in bold):

    import oracledb
    import db_config_thick as db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    cur = con.cursor()
    
    # Create table
    cur.execute("""begin
                     execute immediate 'drop table testgeometry';
                     exception when others then
                       if sqlcode <> -942 then
                         raise;
                       end if;
                   end;""")
    cur.execute("""create table testgeometry (
                   id number(9) not null,
                   geometry MDSYS.SDO_GEOMETRY not null)""")
    
    # Create a Python class for an SDO
    class mySDO(object):
        def __init__(self, gtype, elemInfo, ordinates):
            self.gtype = gtype
            self.elemInfo = elemInfo
            self.ordinates = ordinates
    
    # Get Oracle type information
    obj_type = con.gettype("MDSYS.SDO_GEOMETRY")
    element_info_type_obj = con.gettype("MDSYS.SDO_ELEM_INFO_ARRAY")
    ordinate_type_obj = con.gettype("MDSYS.SDO_ORDINATE_ARRAY")
    
    # Convert a Python object to MDSYS.SDO_GEOMETRY
    def SDOInConverter(value):
        obj = obj_type.newobject()
        obj.SDO_GTYPE = value.gtype
        obj.SDO_ELEM_INFO = element_info_type_obj.newobject()
        obj.SDO_ELEM_INFO.extend(value.elemInfo)
        obj.SDO_ORDINATES = ordinate_type_obj.newobject()
        obj.SDO_ORDINATES.extend(value.ordinates)
        return obj
    
    def SDOInputTypeHandler(cursor, value, numElements):
        if isinstance(value, mySDO):
            return cursor.var(oracledb.OBJECT, arraysize=numElements,
                    inconverter=SDOInConverter, typename=obj_type.name)
    
    sdo = mySDO(2003, [1, 1003, 3], [1, 1, 5, 7])  # Python object
    cur.inputtypehandler = SDOInputTypeHandler
    cur.execute("insert into testgeometry values (:1, :2)", (1, sdo))
    
    # Define a function to dump the contents of an Oracle object
    def dumpobject(obj, prefix = "  "):
        if obj.type.iscollection:
            print(prefix, "[")
            for value in obj.aslist():
                if isinstance(value, oracledb.Object):
                    dumpobject(value, prefix + "  ")
                else:
                    print(prefix + "  ", repr(value))
            print(prefix, "]")
        else:
            print(prefix, "{")
            for attr in obj.type.attributes:
                value = getattr(obj, attr.name)
                if isinstance(value, oracledb.Object):
                    print(prefix + "  " + attr.name + " :")
                    dumpobject(value, prefix + "    ")
                else:
                    print(prefix + "  " + attr.name + " :", repr(value))
            print(prefix, "}")
    
    # Query the row
    print("Querying row just inserted...")
    cur.execute("select id, geometry from testgeometry")
    for (id, obj) in cur:
        print("Id: ", id)
        dumpobject(obj)
    

    The mapping between Python and Oracle objects is handled in SDOInConverter which uses the python-oracledb newobject() and extend() methods to create an Oracle object from the Python object values. The SDOInConverter method is called by the input type handler SDOInputTypeHandler whenever an instance of mySDO is inserted with the cursor.

    To confirm the behavior, run the file:

    python type_input_named_obj.py

    This will show

    Querying row just inserted...
    Id:  1
       {
        SDO_GTYPE : 2003.0
        SDO_SRID : None
        SDO_POINT : None
        SDO_ELEM_INFO :
           [
             1.0
             1003.0
             3.0
           ]
        SDO_ORDINATES :
           [
             1.0
             1.0
             5.0
             7.0
           ]
       }
  • 13.2 Output type handlers with named objects

    Output type handlers enable applications to extract the data from database named objects into a user-defined Python object (defined by the mySDO class here). Review the code contained in type_output_named_obj.py with the output converter function shown in bold:

    import oracledb
    import db_config_thick as db_config
    
    con = oracledb.connect(user=db_config.user,
                           password=db_config.pw, dsn=db_config.dsn)
    cur = con.cursor()
    
    # Create table
    cur.execute("""begin
                     execute immediate 'drop table testgeometry';
                     exception when others then
                       if sqlcode <> -942 then
                         raise;
                       end if;
                   end;""")
    cur.execute("""create table testgeometry (
                   id number(9) not null,
                   geometry MDSYS.SDO_GEOMETRY not null)""")
    
    # Create a Python class for an SDO
    class mySDO(object):
        def __init__(self, gtype, elemInfo, ordinates):
            self.gtype = gtype
            self.elemInfo = elemInfo
            self.ordinates = ordinates
    
    # Get Oracle type information
    obj_type = con.gettype("MDSYS.SDO_GEOMETRY")
    element_info_type_obj = con.gettype("MDSYS.SDO_ELEM_INFO_ARRAY")
    ordinate_type_obj = con.gettype("MDSYS.SDO_ORDINATE_ARRAY")
    
    # Convert a Python object to MDSYS.SDO_GEOMETRY
    def SDOInConverter(value):
        obj = obj_type.newobject()
        obj.SDO_GTYPE = value.gtype
        obj.SDO_ELEM_INFO = element_info_type_obj.newobject()
        obj.SDO_ELEM_INFO.extend(value.elemInfo)
        obj.SDO_ORDINATES = ordinate_type_obj.newobject()
        obj.SDO_ORDINATES.extend(value.ordinates)
        return obj
    
    def SDOInputTypeHandler(cursor, value, numElements):
        if isinstance(value, mySDO):
            return cursor.var(oracledb.OBJECT, arraysize=numElements,
                              inconverter=SDOInConverter, typename=obj_type.name)
    
    # Convert a MDSYS.SDO_GEOMETRY DB Object to Python object
    def SDOOutConverter(DBobj):
        return mySDO(int(DBobj.SDO_GTYPE), DBobj.SDO_ELEM_INFO.aslist(),
                     DBobj.SDO_ORDINATES.aslist())
    
    def SDOOutputTypeHandler(cursor, name, default_type, size, precision,
                             scale):
        if default_type == oracledb.DB_TYPE_OBJECT:
            return cursor.var(obj_type, arraysize=cursor.arraysize,
                              outconverter=SDOOutConverter)
    
    sdo = mySDO(2003, [1, 1003, 3], [1, 1, 5, 7])  # Python object
    cur.inputtypehandler = SDOInputTypeHandler
    cur.execute("insert into testgeometry values (:1, :2)", (1, sdo))
    cur.outputtypehandler = SDOOutputTypeHandler
    
    # Query the SDO Table row
    print("Querying the Spatial Data Object(SDO) Table using the Output Type Handler...")
    print("----------------------------------------------------------------------------")
    cur.execute("select id, geometry from testgeometry")
    for (id, obj) in cur:
        print("SDO ID:", id)
        print("SDO GYTPE:", obj.gtype)
        print("SDO ELEMINFO:", obj.elemInfo)
        print("SDO_ORDINATES:", obj.ordinates)

    Note that the Input Type Handler and the InConverter functions are the same as the previous example.

    The mapping between the Python and Oracle objects is handled in SDOOutConverter. The SDOOutConverter method is called by the output type handler SDOOutputTypeHandler whenever data of the named object (MDSYS.SDOGEOMETRY in this case) is selected with the cursor and needs to be converted to a user-defined Python object (mySDO object in this case).

    To confirm the behavior, run the file:

    python type_output_named_obj.py

    This will show

    Querying the Spatial Data Object(SDO) Table using the Output Type Handler...
    ----------------------------------------------------------------------------
    SDO ID: 1
    SDO GYTPE: 2003
    SDO ELEMINFO: [1.0, 1003.0, 3.0]
    SDO_ORDINATES: [1.0, 1.0, 5.0, 7.0]

14. Advanced Queuing

Oracle Advanced Queuing (AQ) APIs usable in python-oracledb thick mode allow messages to be passed between applications. Documentation link for further reading: Oracle Advanced Queuing (AQ).

  • 14.1 Message passing with Oracle Advanced Queuing

    Review aq.py:

    import oracledb
    import decimal
    import db_config_thick as db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    cur = con.cursor()
    
    BOOK_TYPE_NAME = "UDT_BOOK"
    QUEUE_NAME = "BOOKS"
    QUEUE_TABLE_NAME = "BOOK_QUEUE_TABLE"
    
    # Cleanup
    cur.execute(
        """begin
             dbms_aqadm.stop_queue('""" + QUEUE_NAME + """');
             dbms_aqadm.drop_queue('""" + QUEUE_NAME + """');
             dbms_aqadm.drop_queue_table('""" + QUEUE_TABLE_NAME + """');
             execute immediate 'drop type """ + BOOK_TYPE_NAME + """';
             exception when others then
               if sqlcode <> -24010 then
                 raise;
               end if;
           end;""")
    
    # Create a type
    print("Creating books type UDT_BOOK...")
    cur.execute("""
            create type %s as object (
                title varchar2(100),
                authors varchar2(100),
                price number(5,2)
            );""" % BOOK_TYPE_NAME)
    
    # Create queue table and queue and start the queue
    print("Creating queue table...")
    cur.callproc("dbms_aqadm.create_queue_table",
            (QUEUE_TABLE_NAME, BOOK_TYPE_NAME))
    cur.callproc("dbms_aqadm.create_queue", (QUEUE_NAME, QUEUE_TABLE_NAME))
    cur.callproc("dbms_aqadm.start_queue", (QUEUE_NAME,))
    
    books_type = con.gettype(BOOK_TYPE_NAME)
    queue = con.queue(QUEUE_NAME, booksType)
    
    # Enqueue a few messages
    print("Enqueuing messages...")
    
    BOOK_DATA = [
        ("The Fellowship of the Ring", "Tolkien, J.R.R.", decimal.Decimal("10.99")),
        ("Harry Potter and the Philosopher's Stone", "Rowling, J.K.",
                decimal.Decimal("7.99"))
    ]
    
    for title, authors, price in BOOK_DATA:
        book = books_type.newobject()
        book.TITLE = title
        book.AUTHORS = authors
        book.PRICE = price
        print(title)
        queue.enqone(con.msgproperties(payload=book))
        con.commit()
    
    # Dequeue the messages
    print("\nDequeuing messages...")
    queue.deqoptions.wait = oracledb.DEQ_NO_WAIT
    while True:
        props = queue.deqone()
        if not props:
            break
        print(props.payload.TITLE)
        con.commit()
    
    print("\nDone.")
    

    This file sets up Advanced Queuing using Oracle's DBMS_AQADM package. The queue is used for passing Oracle UDT_BOOK objects. The file uses AQ interface features enhanced in python-oracledb v1.0.

    Run the file:

    python aq.py

    The output shows messages being queued and dequeued.

    To experiment, split the code into three files: one to create and start the queue and two other files to queue and dequeue messages. Experiment with running the queue and dequeue files concurrently in separate terminal windows.

    Try removing the commit() call in aq-dequeue.py. Now run aq-enqueue.py once and then aq-dequeue.py several times. The same messages will be available each time you try to dequeue them.

    Change aq-dequeue.py to commit in a separate transaction by changing the "visibility" setting:

    queue.deqoptions.visibility = oracledb.DEQ_IMMEDIATE
    

    This gives the same behavior as the original code.

    Now change the options of enqueued messages so that they expire from the queue if they have not been dequeued after four seconds:

    queue.enqone(con.msgproperties(payload=book, expiration=4))
    

    Now run aq-enqueue.py and wait four seconds before you run aq-dequeue.py. There should be no messages to dequeue.

    If you are stuck, please look in the solutions directory at the aq-dequeue.py, aq-enqueue.py and aq-queuestart.py files.

15. Simple Oracle Document Access (SODA)

Simple Oracle Document Access (SODA) is a set of NoSQL-style APIs. Documents can be inserted, queried, and retrieved from Oracle Database. By default, documents are JSON strings. SODA APIs exist in many languages. It is usable in python-oracledb's thick mode. Documentation link for further reading: Simple Oracle Document Access (SODA).

  • 15.1 Inserting JSON Documents

    Review soda.py :

    import oracledb
    import db_config_thick as db_config
    
    con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
    
    soda = con.getSodaDatabase()
    
    # Explicit metadata is used for maximum version portability
    metadata = {
                   "keyColumn": {
                       "name":"ID"
                   },
                   "contentColumn": {
                       "name": "JSON_DOCUMENT",
                       "sqlType": "BLOB"
                   },
                   "versionColumn": {
                       "name": "VERSION",
                       "method": "UUID"
                   },
                   "lastModifiedColumn": {
                       "name": "LAST_MODIFIED"
                   },
                   "creationTimeColumn": {
                       "name": "CREATED_ON"
                   }
               }
    
    collection = soda.createCollection("friends", metadata)
    
    content = {'name': 'Jared', 'age': 35, 'address': {'city': 'Melbourne'}}
    
    doc = collection.insertOneAndGet(content)
    key = doc.key
    
    doc = collection.find().key(key).getOne()
    content = doc.getContent()
    print('Retrieved SODA document dictionary is:')
    print(content)

    soda.createCollection() will create a new collection, or open an existing collection, if the name is already in use. (Due to a change in the default "sqlType" storage for Oracle Database 21c, the metadata is explicitly stated to use a BLOB column. This lets the example run with different client and database versions).

    insertOneAndGet() inserts the content of a document into the database and returns a SODA Document Object. This allows access to metadata such as the document key. By default, document keys are automatically generated.

    The find() method is used to begin an operation that will act upon documents in the collection.

    content is a dictionary. You can also get a JSON string by calling doc.getContentAsString().

    Run the file:

    python soda.py

    The output shows the content of the new document.

  • 15.2 Searching SODA Documents

    Extend soda.py to insert some more documents and perform a find filter operation:

    my_docs = [
        {'name': 'Gerald', 'age': 21, 'address': {'city': 'London'}},
        {'name': 'David', 'age': 28, 'address': {'city': 'Melbourne'}},
        {'name': 'Shawn', 'age': 20, 'address': {'city': 'San Francisco'}}
    ]
    collection.insertMany(my_docs)
    
    filter_spec = { "address.city": "Melbourne" }
    my_documents = collection.find().filter(filter_spec).getDocuments()
    
    print('Melbourne people:')
    for doc in my_documents:
        print(doc.getContent()["name"])
    

    Run the script again:

    python soda.py

    The find operation filters the collection and returns documents where the city is Melbourne. Note the insertMany() method is currently in preview.

    SODA supports query by example (QBE) with an extensive set of operators. Extend soda.py with a QBE to find documents where the age is less than 25:

    filter_spec = {'age': {'$lt': 25}}
    my_documents = collection.find().filter(filter_spec).getDocuments()
    
    print('Young people:')
    for doc in my_documents:
        print(doc.getContent()["name"])
    

    Running the script displays the names.

Summary

In this tutorial, you have learned how to:

  • Install the python-oracledb driver and use thin and thick modes
  • Create and work with connections
  • Use python-oracledb's connection pooling and Database Resident Connection Pooling
  • Execute queries and fetch data
  • Use bind variables
  • Use PL/SQL stored functions and procedures
  • Extend python-oracledb classes
  • Use scrollable cursors
  • Work with named objects
  • Use Oracle Advanced Queuing
  • Use the SODA document store API

For further reading, see the python-oracledb documentation.

Appendix: Python Primer

Python is a dynamically typed scripting language. It is most often used to run command-line scripts but is also used for web applications and web services.

Running Python

You can either:

  • Create a file of Python commands, such as myfile.py. This can be run with:

    python myfile.py
  • Alternatively run the Python interpreter by executing the python command in a terminal, and then interactively enter commands. Use Ctrl-D to exit back to the operating system prompt.

On some machines, you may need to run the python3 command instead of python.

When you run scripts, Python automatically creates bytecode versions of them in a folder called __pycache__. These improve the performance of scripts that are run multiple times. They are automatically recreated if the source file changes.

Indentation

Whitespace indentation is significant in Python. When copying examples, use the same column alignment as shown. The samples in this tutorial use spaces, not tabs.

The following indentation prints 'done' once after the loop has completed:

for i in range(5):
    print(i)
print('done')

But this indentation prints 'done' in each iteration:

for i in range(5):
    print(i)
    print('done')

Strings

Python strings can be enclosed in single or double quotes:

'A string constant'
"another constant"

Multi line strings use a triple-quote syntax:

"""
SELECT *
FROM EMP
"""

Variables

Variables do not need types declared:

count = 1
ename = 'Arnie'

Comments

Comments can be single line:

# a short comment

Or they can be multi-line using the triple-quote token to create a string that does nothing:

"""
a longer
comment
"""

Printing

Strings and variables can be displayed with a print() function:

print('Hello, World!')
print('Value:', count)

Data Structures

Associative arrays are called 'dictionaries':

a2 = {'PI':3.1415, 'E':2.7182}

Ordered arrays are called 'lists':

a3 = [101, 4, 67]

Lists can be accessed via indexes.

print(a3[0])
print(a3[-1])
print(a3[1:3])

Tuples are like lists but cannot be changed once they are created. They are created with parentheses:

a4 = (3, 7, 10)

Individual values in a tuple can be assigned to variables like:

v1, v2, v3 = a4

Now the variable v1 contains 3, the variable v2 contains 7 and the variable v3 contains 10.

The value in a single entry tuple like "(13,)"can be assigned to a variable by putting a comma after the variable name like:

v1, = (13,)

If the assignment is:

v1 = (13,)

then v1 will contain the whole tuple "(13,)"

Objects

Everything in Python is an object. As an example, given the of the list a3 above, the append() method can be used to add a value to the list.

a3.append(23)

Now a3 contains [101, 4, 67, 23]

Flow Control

Code flow can be controlled with tests and loops. The if/elif/else statements look like:

if v == 2 or v == 4:
    print('Even')
elif v == 1 or v == 3:
    print('Odd')
else:
    print('Unknown number')

This also shows how the clauses are delimited with colons, and each sub-block of code is indented.

Loops

A traditional loop is:

for i in range(10):
    print(i)

This prints the numbers from 0 to 9. The value of i is incremented in each iteration.

The 'for' command can also be used to iterate over lists and tuples:

a5 = ['Aa', 'Bb', 'Cc']
for v in a5:
    print(v)

This sets v to each element of the list a5 in turn.

Functions

A function may be defined as:

def myfunc(p1, p2):
    "Function documentation: add two numbers"
    print(p1, p2)
    return p1 + p2

Functions may or may not return values. This function could be called using:

v3 = myfunc(1, 3)

Function calls must appear after their function definition.

Functions are also objects and have attributes. The inbuilt __doc__ attribute can be used to find the function description:

print(myfunc.__doc__)

Modules

Sub-files can be included in Python scripts with an import statement.

import os
import sys

Many predefined modules exist, such as the os and the sys modules.

Resources


License

Copyright © 2017, 2022, Oracle and/or its affiliates.

This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.

If you elect to accept the software under the Apache License, Version 2.0, the following applies:

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

python-oracledb-1.2.1/samples/tutorial/aq.py000066400000000000000000000064431434177474600211170ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # aq.py (Section 14.1) # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ import oracledb import decimal import db_config_thick as db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() BOOK_TYPE_NAME = "UDT_BOOK" QUEUE_NAME = "BOOKS" QUEUE_TABLE_NAME = "BOOK_QUEUE_TABLE" # Cleanup cur.execute(f""" begin dbms_aqadm.stop_queue('{QUEUE_NAME}'); dbms_aqadm.drop_queue('{QUEUE_NAME}'); dbms_aqadm.drop_queue_table('{QUEUE_TABLE_NAME}'); execute immediate 'drop type {BOOK_TYPE_NAME}'; exception when others then if sqlcode <> -24010 then raise; end if; end;""") # Create a type print("Creating books type UDT_BOOK...") cur.execute(f""" create type {BOOK_TYPE_NAME} as object ( title varchar2(100), authors varchar2(100), price number(5,2) );""") # Create queue table and queue and start the queue print("Creating queue table...") cur.callproc("dbms_aqadm.create_queue_table", (QUEUE_TABLE_NAME, BOOK_TYPE_NAME)) cur.callproc("dbms_aqadm.create_queue", (QUEUE_NAME, QUEUE_TABLE_NAME)) cur.callproc("dbms_aqadm.start_queue", (QUEUE_NAME,)) books_type = con.gettype(BOOK_TYPE_NAME) queue = con.queue(QUEUE_NAME, books_type) # Enqueue a few messages print("Enqueuing messages...") BOOK_DATA = [ ("The Fellowship of the Ring", "Tolkien, J.R.R.", decimal.Decimal("10.99")), ("Harry Potter and the Philosopher's Stone", "Rowling, J.K.", decimal.Decimal("7.99")) ] for title, authors, price in BOOK_DATA: book = books_type.newobject() book.TITLE = title book.AUTHORS = authors book.PRICE = price print(title) queue.enqone(con.msgproperties(payload=book)) con.commit() # Dequeue the messages print("\nDequeuing messages...") queue.deqoptions.wait = oracledb.DEQ_NO_WAIT while True: props = queue.deqone() if not props: break print(props.payload.TITLE) con.commit() print("\nDone.") python-oracledb-1.2.1/samples/tutorial/bind_insert.py000066400000000000000000000034271434177474600230150ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # bind_insert.py (Section 4.2 and 4.3) # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() rows = [(1, "First"), (2, "Second"), (3, "Third"), (4, "Fourth"), (5, "Fifth"), (6, "Sixth"), (7, "Seventh")] cur.executemany("insert into mytab(id, data) values (:1, :2)", rows) # Now query the results back cur2 = con.cursor() cur2.execute('select * from mytab') res = cur2.fetchall() print(res) python-oracledb-1.2.1/samples/tutorial/bind_query.py000066400000000000000000000031721434177474600226530ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # bind_query.py (Section 4.1) # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() sql = "select * from dept where deptno = :id order by deptno" cur.execute(sql, id=20) res = cur.fetchall() print(res) cur.execute(sql, id=10) res = cur.fetchall() print(res) python-oracledb-1.2.1/samples/tutorial/bind_sdo.py000066400000000000000000000051571434177474600223000ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # bind_sdo.py (Section 12.1) # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ import oracledb import db_config_thick as db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() # Create table cur.execute("""begin execute immediate 'drop table testgeometry'; exception when others then if sqlcode <> -942 then raise; end if; end;""") cur.execute("""create table testgeometry ( id number(9) not null, geometry MDSYS.SDO_GEOMETRY not null)""") # Create and populate Oracle objects type_obj = con.gettype("MDSYS.SDO_GEOMETRY") element_info_type_obj = con.gettype("MDSYS.SDO_ELEM_INFO_ARRAY") ordinate_type_obj = con.gettype("MDSYS.SDO_ORDINATE_ARRAY") obj = type_obj.newobject() obj.SDO_GTYPE = 2003 obj.SDO_ELEM_INFO = element_info_type_obj.newobject() obj.SDO_ELEM_INFO.extend([1, 1003, 3]) obj.SDO_ORDINATES = ordinate_type_obj.newobject() obj.SDO_ORDINATES.extend([1, 1, 5, 7]) print("Created object", obj) # Add a new row print("Adding row to table...") cur.execute("insert into testgeometry values (1, :obj)", obj=obj) print("Row added!") # (Change below here) # Query the row print("Querying row just inserted...") cur.execute("select id, geometry from testgeometry") for row in cur: print(row) python-oracledb-1.2.1/samples/tutorial/clob.py000066400000000000000000000037061434177474600214340ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # clob.py (Section 7.1) # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() print("Inserting data...") cur.execute("truncate table testclobs") long_string = "" for i in range(5): char = chr(ord('A') + i) long_string += char * 250 cur.execute("insert into testclobs values (:1, :2)", (i + 1, "String data " + long_string + ' End of string')) con.commit() print("Querying data...") cur.execute("select * from testclobs where id = :id", {'id': 1}) (id, clob) = cur.fetchone() print("CLOB length:", clob.size()) clobdata = clob.read() print("CLOB data:", clobdata) python-oracledb-1.2.1/samples/tutorial/clob_string.py000066400000000000000000000037421434177474600230220ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # clob_string.py (Section 7.2) # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ import oracledb import db_config oracledb.defaults.fetch_lobs = False con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() print("Inserting data...") cur.execute("truncate table testclobs") long_string = "" for i in range(5): char = chr(ord('A') + i) long_string += char * 250 cur.execute("insert into testclobs values (:1, :2)", (i + 1, "String data " + long_string + ' End of string')) con.commit() print("Querying data...") cur.execute("select * from testclobs where id = :id", {'id': 1}) (id, clobdata) = cur.fetchone() print("CLOB length:", len(clobdata)) print("CLOB data:", clobdata) python-oracledb-1.2.1/samples/tutorial/connect.py000066400000000000000000000027431434177474600221460ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # connect.py (Section 1.1 and 1.2) # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) print("Database version:", con.version) python-oracledb-1.2.1/samples/tutorial/connect_drcp.py000066400000000000000000000030471434177474600231540ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # connect_drcp.py (Section 2.3 and 2.5) # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn + ":pooled", cclass="PYTHONDEMO", purity=oracledb.PURITY_SELF) print("Database version:", con.version) python-oracledb-1.2.1/samples/tutorial/connect_health.py000066400000000000000000000031101434177474600234600ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # connect_health.py (Section 1.7) # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) if con.is_healthy(): print("Healthy connection!") else: print("Unusable connection. Please check the database and network settings.") python-oracledb-1.2.1/samples/tutorial/connect_params2.py000066400000000000000000000030711434177474600235660ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # connect_params2.py (Section 1.6) # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ import oracledb import db_config params = oracledb.ConnectParams( host="localhost", port=1521, service_name="orclpdb") con = oracledb.connect(user=db_config.user, password=db_config.pw, params=params) print("Database version:", con.version) python-oracledb-1.2.1/samples/tutorial/connect_pool.py000066400000000000000000000037421434177474600231770ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # connect_pool.py (Section 2.1) # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ import oracledb import threading import db_config pool = oracledb.create_pool(user=db_config.user, password=db_config.pw, dsn=db_config.dsn, min=2, max=5, increment=1, getmode=oracledb.POOL_GETMODE_WAIT) def Query(): con = pool.acquire() cur = con.cursor() for i in range(4): cur.execute("select myseq.nextval from dual") seqval, = cur.fetchone() print("Thread", threading.current_thread().name, "fetched sequence =", seqval) thread1 = threading.Thread(name='#1', target=Query) thread1.start() thread2 = threading.Thread(name='#2', target=Query) thread2.start() thread1.join() thread2.join() print("All done!") python-oracledb-1.2.1/samples/tutorial/connect_pool2.py000066400000000000000000000040461434177474600232570ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # connect_pool2.py (Section 2.2 and 2.4) # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ import oracledb import threading import db_config pool = oracledb.create_pool(user=db_config.user, password=db_config.pw, dsn=db_config.dsn, min=2, max=5, increment=1, getmode=oracledb.POOL_GETMODE_WAIT) def Query(): con = pool.acquire() cur = con.cursor() for i in range(4): cur.execute("select myseq.nextval from dual") seqval, = cur.fetchone() print("Thread", threading.current_thread().name, "fetched sequence =", seqval) number_of_threads = 2 thread_array = [] for i in range(number_of_threads): thread = threading.Thread(name='#' + str(i), target=Query) thread_array.append(thread) thread.start() for t in thread_array: t.join() print("All done!") python-oracledb-1.2.1/samples/tutorial/create_user.py000066400000000000000000000054621434177474600230170ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # create_user.py (Setup Section) # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ import oracledb import db_config_sys import run_sql_script import getpass import os # default values PYTHON_USER = "pythondemo" PYTHON_CONNECT_STRING = "localhost/orclpdb" PYTHON_DRCP_CONNECT_STRING = "localhost/orclpdb:pooled" # dictionary containing all parameters; these are acquired as needed by the # methods below (which should be used instead of consulting this dictionary # directly) and then stored so that a value is not requested more than once PARAMETERS = {} def get_value(name, label, default_value=""): value = PARAMETERS.get(name) if value is not None: return value value = os.environ.get(name) if value is None: if default_value: label += " [%s]" % default_value label += ": " if default_value: value = input(label).strip() else: value = getpass.getpass(label) if not value: value = default_value PARAMETERS[name] = value return value def get_main_user(): return get_value("user", "Enter the User to be created", PYTHON_USER) def get_main_password(): return get_value("pw", "Enter the Password for %s" % get_main_user()) # Connect using the System User ID and password con = oracledb.connect(user=db_config_sys.sysuser, password=db_config_sys.syspw, dsn=db_config_sys.dsn) # create sample user and schema print("Creating user...") run_sql_script.run_sql_script(con, "create_user", user=get_main_user(), pw=get_main_password()) print("Done.") python-oracledb-1.2.1/samples/tutorial/db_config.py000066400000000000000000000027431434177474600224270ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ import oracledb import os import getpass # # Tutorial credentials and connection string. # Environment variable values are used, if they are defined. # user = os.environ.get("PYTHON_USER", "pythondemo") dsn = os.environ.get("PYTHON_CONNECT_STRING", "localhost/orclpdb") pw = os.environ.get("PYTHON_PASSWORD") if pw is None: pw = getpass.getpass("Enter password for %s: " % user) python-oracledb-1.2.1/samples/tutorial/db_config_sys.py000066400000000000000000000030241434177474600233160ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ import oracledb import os import getpass # # Tutorial credentials and connection string for the SYSTEM (ADMIN) user. # Environment variable values are used, if they are defined. # sysuser = os.environ.get("PYTHON_SYSUSER", "SYSTEM") dsn = os.environ.get("PYTHON_SYS_CONNECT_STRING", "localhost/orclpdb") syspw = os.environ.get("PYTHON_SYSPASSWORD") if syspw is None: syspw = getpass.getpass("Enter password for %s: " % sysuser) python-oracledb-1.2.1/samples/tutorial/db_config_thick.py000066400000000000000000000047311434177474600236100ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ import oracledb import platform import os import getpass ###################################################################### # # Oracle Client library configuration # # On Linux this must be None. # Instead, the Oracle environment must be set before Python starts. instant_client_dir = None # On Windows, if your database is on the same machine, comment these lines out # and let instant_client_dir be None. Otherwise, set this to your Instant # Client directory. Note the use of the raw string r"..." so backslashes can # be used as directory separators. if platform.system() == "Windows": instant_client_dir = r"C:\Oracle\instantclient_19_14" # On macOS (Intel x86) set the directory to your Instant Client directory if platform.system() == "Darwin": instant_client_dir = os.environ.get("HOME")+"/Downloads/instantclient_19_8" # You must always call init_oracle_client() to use thick mode in any platform oracledb.init_oracle_client(lib_dir=instant_client_dir) ###################################################################### # # Tutorial credentials and connection string. # Environment variable values are used, if they are defined. # user = os.environ.get("PYTHON_USER", "pythondemo") dsn = os.environ.get("PYTHON_CONNECT_STRING", "localhost/orclpdb") pw = os.environ.get("PYTHON_PASSWORD") if pw is None: pw = getpass.getpass("Enter password for %s: " % user) python-oracledb-1.2.1/samples/tutorial/defaults.py000066400000000000000000000025651434177474600223260ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # defaults.py (Overview Section) # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # ------------------------------------------------------------------------------ import oracledb print("Default array size:", oracledb.defaults.arraysize) python-oracledb-1.2.1/samples/tutorial/drcp_query.py000066400000000000000000000065451434177474600226760ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # drcp_query.py (Section 2.4 and 2.5) # Look at pool statistics of the DRCP Connection # ------------------------------------------------------------------------------ import oracledb import getpass import os # default values PYTHON_SYS_USER = "SYSTEM" PYTHON_USER = "pythondemo" PYTHON_CONNECT_STRING = "localhost/orclpdb" PYTHON_DRCP_CONNECT_STRING = "localhost/orclpdb:pooled" # dictionary containing all parameters; these are acquired as needed by the # methods below (which should be used instead of consulting this dictionary # directly) and then stored so that a value is not requested more than once PARAMETERS = {} def get_value(name, label, default_value=""): value = PARAMETERS.get(name) if value is not None: return value value = os.environ.get(name) if value is None: if default_value: label += " [%s]" % default_value label += ": " if default_value: value = input(label).strip() else: value = getpass.getpass(label) if not value: value = default_value PARAMETERS[name] = value return value def get_main_user(): return get_value("user", "Enter the DRCP User", PYTHON_SYS_USER) def get_main_password(): return get_value("pw", "Enter the Password for %s" % get_main_user()) def get_drcp_connect_string(): connect_string = get_value("DRCP_CONNECT_STRING", "Enter the DRCP Connect String", PYTHON_DRCP_CONNECT_STRING) return "%s/%s@%s" % (get_main_user(), get_main_password(), connect_string) drcp_user = get_main_user() drcp_password = get_main_password() drcp_connect_string = get_drcp_connect_string() con = oracledb.connect(user=drcp_user, password=drcp_password, dsn=drcp_connect_string) cur = con.cursor() # looking at the pool stats of the DRCP Connection print("\nLooking at DRCP Pool statistics...\n") cur.execute( "select cclass_name, num_requests, num_hits, num_misses from v$cpool_cc_stats") res = cur.fetchall() print('(CCLASS_NAME, NUM_REQUESTS, NUM_HITS, NUM_MISSES)') print('-------------------------------------------------') for row in res: print(row) print("Done.") python-oracledb-1.2.1/samples/tutorial/drop_user.py000066400000000000000000000032761434177474600225210ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # drop_user.py (Setup Section) # ------------------------------------------------------------------------------ import oracledb import db_config_sys import db_config import run_sql_script # Connect using the System User ID and password con = oracledb.connect(user=db_config_sys.sysuser, password=db_config_sys.syspw, dsn=db_config_sys.dsn) # drop the tutorial user and schema print("Dropping the tutorial user...") run_sql_script.run_sql_script(con, "drop_user", user=db_config.user) print("Done.") python-oracledb-1.2.1/samples/tutorial/plsql_func.py000066400000000000000000000030031434177474600226510ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # plsql_func.py (Section 5.1) # ------------------------------------------------------------------------------ import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() res = cur.callfunc('myfunc', int, ('abc', 2)) print(res) python-oracledb-1.2.1/samples/tutorial/plsql_proc.py000066400000000000000000000030341434177474600226650ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # plsql_proc.py (Section 5.2) # ------------------------------------------------------------------------------ import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() myvar = cur.var(int) cur.callproc('myproc', (123, myvar)) print(myvar.getvalue()) python-oracledb-1.2.1/samples/tutorial/query.py000066400000000000000000000026711434177474600216620ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # query.py (Section 1.3 and 1.4) # ------------------------------------------------------------------------------ import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) python-oracledb-1.2.1/samples/tutorial/query2.py000066400000000000000000000032151434177474600217370ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # query2.py (Section 3.1) # ------------------------------------------------------------------------------ import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() cur.execute('select * from dept order by deptno') for deptno, dname, loc in cur: print("Department number: ", deptno) print("Department name: ", dname) print("Department location:", loc) python-oracledb-1.2.1/samples/tutorial/query_arraysize.py000066400000000000000000000033061434177474600237470ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # query_arraysize.py (Section 3.5) # ------------------------------------------------------------------------------ import oracledb import time import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) start = time.time() cur = con.cursor() cur.prefetchrows = 100 cur.arraysize = 100 cur.execute("select * from bigtab") res = cur.fetchall() # print(res) # uncomment to display the query results elapsed = (time.time() - start) print(elapsed, "seconds") python-oracledb-1.2.1/samples/tutorial/query_many.py000066400000000000000000000030621434177474600227010ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # query_many.py (Section 3.3) # ------------------------------------------------------------------------------ import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() cur.execute("select * from dept order by deptno") num_rows = 3 res = cur.fetchmany(num_rows) print(res) python-oracledb-1.2.1/samples/tutorial/query_one.py000066400000000000000000000030741434177474600225210ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # query_one.py (Section 3.2) # ------------------------------------------------------------------------------ import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() cur.execute("select * from dept order by deptno") row = cur.fetchone() print(row) row = cur.fetchone() print(row) python-oracledb-1.2.1/samples/tutorial/query_scroll.py000066400000000000000000000032641434177474600232370ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # query_scroll.py (Section 11.1) # ------------------------------------------------------------------------------ import oracledb import db_config_thick as db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor(scrollable=True) cur.execute("select * from dept order by deptno") cur.scroll(2, mode="absolute") # go to second row print(cur.fetchone()) cur.scroll(-1) # go back one row print(cur.fetchone()) python-oracledb-1.2.1/samples/tutorial/resources/000077500000000000000000000000001434177474600221475ustar00rootroot00000000000000python-oracledb-1.2.1/samples/tutorial/resources/base.css000066400000000000000000001117241434177474600236010ustar00rootroot00000000000000* { -webkit-box-sizing: border-box; box-sizing: border-box; } html, body { margin: 5; padding: 5; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; font-size: 16px; line-height: 1.5; } a { color: #006ed0; text-decoration: none; } a:hover, a:focus { text-decoration: underline; } h1 { font-size: 24px; margin: 5; padding: 5; } h2, h3 { margin-bottom: 8px; } h4 { margin-bottom: 4px; } p { margin: 5 0 24px; } ul, ol { margin: 0 0 24px; } pre{ background:#E0E0E0; margin:1em 0px; } hr { margin: 24px 0; border: 0; border-top: 1px solid rgba(0, 0, 0, .15); padding: 0; background-color: transparent; } .container { padding-left: 0px; padding-right: 16px; margin-left: inherit; margin-right: inherit; } .oracleHeader { background-color: #fff; -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .1); box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .1); } .oracleLogo { width: 160px; height: 64px; padding-left: 160px; color: #fff; background-color: #c74634; background-repeat: no-repeat; background-size: contain; background-position: center center; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQMAAABRCAYAAADW4MODAAAACXBIWXMAABcRAAAXEQHKJvM/AAAH90lEQVR4nO3d/3GjOBQHcN3N/R86iDsIHcRXQbwVrFNBfBUsqeCcCtbpwKlg7QouqeBIB3YF3PDymDDiPfFLEjj3/cwwO2tsBAI9/QCR34qiMAAAv//vcwAACIIBABAEAwAgCAYAQBAMAIAgGAAAQTAAAIJgAAAEwQAACIIBABAEAwAgCAYAQBAMAIAgGAAAQTAAAIJgAAAEwQAACIIBABAEAwAgCAYAQBAMAIAgGAAAQTAAAIJgAAAEwQAACIIBAJA/ZpgNiTFmaYxJeUka3+jvZIx55eXA/+/qEPBYq/3a878hlHmYGWPujDFnPp4sYHqGz1+5LHjxJaudD+m8bAIfV5syr1d8zaYt3+1jx0tQc/pbi+XFszbGfG+s8e+ZL6y8w5ZjZdA7H790kQ+V8vaurN+feV2X4+9qwXm6EtLz4c0qYNJ5+TNw8JYkHITKc3ctrB/rzHnbpwIbZA7dhISj3q9IgcBwOv8aY7aeWh4+XHMebD1uc6cUzCvPNU3G+fldSc+HdaDtjrHmgPojUCAwnEbwQGBmEAxSbtbFCgK2B65J5hIQDO+Tj4BQ1lY3jU8/3fJ3xkj4/P3wsL8ujxM3/yVlMP0ZMPiVXrgLGcWU3QStCTuFN+6mSBF4qgwa0+RdcOFpy9sxTdCE988VcHx4V8YdpPMSq5uwi1CBhejKOU01gJhwxHNdrC98Yn3UCCkX9rvGmg83nFbXQZ9nT83sFTcDpXzYKYWgC617YKu6C6vGmnb7lkDwxt8ZWzijFYaOti2B4K02IDymeX+Kfuxly2CCZVfo9kVRLALtU7ndg5pyUWTCbyTS94YuSVEUr0IapXTANteNrXzIeZGshO24lkzYRuUwcL/7LpJl4DSXQpr1/A2dftBlikRdGboWvh9icV3MdiCS+AwGhtOU9E2nDCwnYTsF57uW9yf+rbTNPmnshO+HWiShC6MWtF975N9slykGELVBq79i3Etl5ej3U+PTz3Wxlc3Bo5Bm326C1j145ub6gbtftqsex71xpDHHEX9flkq3yDXedFFiB4NE6bcfPd9S62LDg1O2If1nH6T+YZ9goI2JnK0AvOHPbA+8jTZSgbfT+Iqk4zYxb/2FFjsYaBdb7EBQkWrDK8d+hiQNXkoBQpI4WlUb62LNleM2jm1UFsr99O1XKRAOUiXxPMNbnoPFvpsgXfDnmPdSLXu+V2xLIz/JpjVBu15omVJIj0oB33KNZqd5zdvSgoV0/oySxleyULpGywjXySpWoJ1DMJgysp64wNxan8d8CCl1BEPtc/v3D41PP2hNW8Pr/ml8+vEAkTZXQjp/7zO8/eeb1l27VoKwL48xW1yxg4FUyKSL7lIsWgpcm4XjnvWxYyHTauXHlt+/8iCqFEh2SsGXuNKA4d4dLbQg5jBrUYu6lyBvKdBjdBmQy4SmvuER7i4XUqY89HTD6U81lgMT3JmJPYAotQKmDgZj0y9P2n3j03Hulbyy91ubE9B1ZP/kuOgyIW+kJqvdxYLxniaYfRm9ZSA1KW/4opPWhZYqfb6+XZeqqb4Vatm+7h1NfylNW98Laa+Mm1SPKtfvrGj5sppwEDgGLT99PZZu0/I5qNjBQMvU9UQP+2g1qLafLrvay1OGBIQX3p8uQXEjFF5jvbykj1zZ3q1V0LV82XzxYGC462V3yZaOltXFmWLWYi7UxmfO2JgRMVVG049WbShl0KOjwKUcGKS+vOFjrffFcy5kfZ4pyJWA8632Ridp/RD2zMa98nDTt8gBQTovIWctlgHv78an7mvhskzwnPSm8WR3/Oe7U8fz9fb8CEnbnAHX5KOCJ/MMPdZ9Y2uf26y+45p7McS+tm3X/IYYE5Rc5yXk3ATXnIxYc2qCLlPMTdgpjwHfcI3m891xkqWjKf/mqQ944uN4bqz5cNtzynRlpdTKZ6u5mvGx+HJXay0dlHkUV7xOelLvKzg57q78dHQ5L8dEUUirXSq7ALXMsmXqdKGkKekzm3Ar/L5em3atzRLHFOSN8P208a1x8lprZuGoJQtupfSdFt13kYSetdilxbe+1BmMU77pKHPcGquTaqG+pMExmzaKL2VQ337iWnnsuaKlXbdVHhCyXxRat3Ksc1kpYx5PtRqw7ZgqPs5fpT44Kp2XN+X2pw+nWn5qLctQ+3KK0uKSIkTEpa2mjsXV55MMeZ/BqqU23Qq/qRZXS0pqzYxdtPcr2OlpL1IJJW05LyHVW1+uMacQQreyaJn6hajVAzvSlNoYzjwKHmOizZ773dqxPvB3pEe2tf17CnQHJufWj6S+LzsewdeOyacpX4pqT7F/5XPpc1xGc4x2l0aKEBMsiwlaCbuOfTvJmDcdLVr6na/W25a0OwN54L6pa4zCPv6kZWxkrFzYP9fr63xzvYYvC9hKOLWk7XWZSzCoF5Qs4Ik+cHOvTwZLxr72LGk5xuo2nWsQMEbT0dU9kfIw4fx1HdsQ0sBgGikgdDnXCXeZ9p4DgzQwHGyZ019Ukvj6M1Vj3lQrvegk9/T4tLTtehpGmB9QifXsuraPXfNU+31Xp5buge8/4WYbms8+/jRg1PkJcw8GABAJ/gozABAEAwAgCAYAQBAMAIAgGAAAQTAAAIJgAAAEwQAACIIBABAEAwAgCAYAQBAMAIAgGAAAQTAAAIJgAAAEwQAACIIBABAEAwAgCAYAQBAMAIAgGAAAQTAAAIJgAAAEwQAACIIBABhjjPkPtwj1Cxa1b78AAAAASUVORK5CYII='); white-space: nowrap; overflow: hidden; display: inline-block; vertical-align: top; -webkit-box-sizing: border-box; box-sizing: border-box; } .header { color: #FFFFFF; -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .1); box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .1); background-size: contain; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAdUAAAA/CAIAAAA5c8XwAAAF8HpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjarVZZruQ4Dvz3KfoI4iZKx9FCAXODPn4H5axXrxdM1wBjI9OZskRSEUFST/z+n/P8hovM9VHzVnutBZd27Tzwo5WfV3ye7xgVvd8/Lvp80/P9BfPnNWNE8JT3r+tnXD7jPwzVrycM/cMLsr8skC///N2xjy/H/KeIhhUr36/283PObufEu7uhFTDUd1Ovi+eHGUycQEnusorb8TH89nt33K2MskjLLqtM3Is6MUk5pLQfGnQoaOO5aCFG5WDHk3mx3LEmzp2XFCHRvOmwS5ctTVgWh4joI/wVC12//fpb1OB5E6YywRhhyX+9n3+b8Cv3OasAI6IEk/lihbiuCBBGMpffmAZC6Hx4swvwj/vrer4RK2DQLswNGxxlviam0U9tyRWAlORXy6sv8p2s8VWJwrchGBJQUCqJUaXizE6kwg0EDUTOojzBAJnxRpCsIhXcNOgIvrHG6c5l43ccqQJ+TKo4uOkyQJaqQT+uDRoaJqZmVs2tWbfxVKlardbqNXNuuLi6eXX35t1Hk6bNWm3eWuttdO6ClLReu/fWex8DPoc+wwZWD8wYY/KUqdNmnT7b7HMsyGfpslWXr7b6Gpu3bN226/bddt8jKCClJzQsani06DEOtHbk6LFTj592+hlfrH1Y/dv9P7BGH9b4MpXz/Is1jLqnoWuCss5YcgbGWAmMezIAQXNyVhqpcjKXnJXOyApjBGnJzaYyHqqgUIPYDn1x95O5X+btAdb/xhv/CnNPUvd/YI6fkL/w9g+s7ayE6zL2ZmFiWgTZd2QNYH80ngoPw5oXrTL70dEBvkXfc4TDCCbbPH1jYZV2Zqd+luf42CaEKIpH1/Vw8UOg/MwN1w4uqrA2irlv3RWgv5GsXU74AfyYE7ufqLUrLdezzgTfECTi6ssFmtjwWURWlXHC0ut2pCrt4Sh1x7BuWlnSI5xyzxnX2hAcnwdVFUPQXj8GKEAUS6tnRUd8bbRW0SuitNhbEEfyKtjiIEwGd2uLTj9okNV39IFgQ2bB2nf3CozYz41KDrpL4Qx9Lz2YdQJr24aQV8cw5vTzLGhhT8Lm6qqObUI2AN6hnVNXOwASljHTzkpiSjirMY8DNA7CqpPHLPsZC1UXZd64j927RNXTO4JBQ299IKs2LAsA2FGAr9npFbNVOhJ8pvpGnDofvhSXCDoB3NBNtEmkfkwmtSgA4RT0JPiODn0gn4bGbgOdHAEZUmeF0q1HPSbibwJyGn5O3dE2YsISzESpzSVb7ATgxIgccLK5O6njU0HheZJ0vET3ROR6KtrerN8g3xDd1N5klThboJpg9TCdEAFc15Jjw58j6QOCW+A+pUZnzTO2rMaKlFEfFTxZQItwgoODgQYAXj0GbGhqHht55m7u7AOEkfaDkKHyHhtIMfTXryKQuD1QZDa2Ar9tt6pXyaGJhcvZDySP0BAzj/wfBdzSybRC8kb6Q59vqf1Jr6oAGjCQvcLbcrhhRqN/FrSvEFF6Xopi1ZBr0CYSLBqGQBdAWiF7NrogZcYZeA3oL+HpqEFUHg3J/d64RhnuBiTGq9YD8VQYcjTGeWnAru8QwCDkwpabyh2ZjlyTzJhcZ1mIIH8khE4oYCgSe1P/qAV7Dq1pGKeYCa2OLA9MOIWeGc+A16zEu05CdZ44VaHoabLhKS/0gVjAvF+ESkUab9hA0mSLwolsKkOc5UGFW47Wgio/bfXRbGyctGJAOrEXfFYUXcSGgxj2hmoG2YKOxWm4SRlN99T2oEEYchO1JvMjy35KFflFWaBi4bAFUOgg0sAuYLeDcSCzR9/oOZFpgTXPPWmGtGGpyHZTwlJj6IsI6P5hiB6YceZwys3oPc9AFUYguANLnCHvFvPk41nMuGuiDo+1jeQeNZvuX3TFPL6jTNcbZb5ly8qRpp+sNqCo831RBk6a3vYbyC7vKPncLBD19QlX73uxGShxsIT4n3cMORgFjQkahkTQ3vyNH1mLc8WtCPkfzG2w9fwBwZEfYbSme7AAAAGDaUNDUElDQyBwcm9maWxlAAB4nH2RPUjDQBzFX1PFIhUFO4g4ZKhOFkRFdNMqFKFCqBVadTC59AuaNCQpLo6Ca8HBj8Wqg4uzrg6ugiD4AeLo5KToIiX+Lym0iPHguB/v7j3u3gFCvcw0q2MM0HTbTCXiYia7Kna9IogQgBn0ycwy5iQpCd/xdY8AX+9iPMv/3J+jR81ZDAiIxLPMMG3iDeKpTdvgvE8cYUVZJT4nHjXpgsSPXFc8fuNccFngmREznZonjhCLhTZW2pgVTY14kjiqajrlCxmPVc5bnLVylTXvyV8Yzukry1ynOYQEFrEECSIUVFFCGTZitOqkWEjRftzHP+j6JXIp5CqBkWMBFWiQXT/4H/zu1spPjHtJ4TjQ+eI4H8NA1y7QqDnO97HjNE6A4DNwpbf8lTow/Ul6raVFj4DebeDiuqUpe8DlDjDwZMim7EpBmkI+D7yf0Tdlgf5boHvN6625j9MHIE1dJW+Ag0NgpEDZ6z7vDrX39u+ZZn8/QOtyk7MGguQAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfmAwgAOgyiwpJSAAAgAElEQVR42qWd2XIb27G0AaKBxkQMJCWfiBO2z40v/BR+/0exRaAxz8N/8Um5c1c1IMk/LxQkBDS611CrKisrq/mvf/2r0Wjcbrf7/f7y8tL48XO9Xl9eXl5eXu73e8N+brfb7XbjnS8vL7fbrd1u3263y+XCm2+3W7PZ5A33+52P61K8odVqcbVms6kLFkVxu92u1+t4PO50Otfr9fPzsyzL6XR6Op2qqhqNRp1Op6qqRqMxGo0ajcZyuex2u4PB4Ha7rdfrRqMxHA7v9/tisSjL8vX19Xq9tlqt9Xp9uVxGo1Gz2dxsNsfjcTwet9vtZrO52+1Wq1W32x2NRrfbraqqfr8/GAzu9/tyubzdbtPptNlsNhqNqqpeXl50w4xYo9FotVrn87nX672+vjabzfv93mw2r9crv/C8jAY3w+tckyFiHHyQ9Vn+iy/VB19eXi6XC8PIK5ojbozXG3U/vIH/1f1ranR97v9+v3PDmizuJNwVH2dwePPtdmM2+eV6vfKpVqvFLwxFo9E4n89FUfD66XQqioIXe73efr+/3++DwWC/31+v17IsN5tNr9drNpuHw+H19XWxWHQ6ncFgcD6fdfEwttzkt2/fzudzWZZ8Ozfsg6DB8Y/zCm++Xq/NZrMoimazeTweT6cTf7KKNCZa8FoJPr/8y6XCFPC7bk/fzn/xEX5nWFqtVrvd9nf6/g1Lgt13uVwOh8P9fm+3291ul+/6xZ88MvmLfvqj9a9dczweed7hcMjA+jCG0cs7xd8Tlqje1mq1ME2PdoRG3pe6lgcmjm85nU5lWYbv5Q0YrkajcblcOp1Op9PRSpAF0Cs+462///3vXEXLyFeA7kZfpu/m/b7r9J6wAWSO8/W5QlmWw+GQJxmNRiws7Ol4PL7dbtvt9na7DYfD8/nMJux2u6fT6Xw+Yyvv9/vxeOx0OhwGbGAZu/1+//Ly0ul0Go3G8Xi83+9lWWKkrtfr6XRqt9udTudyuZxOp263yza7Xq/X67Xb7TKvh8Ph5eVlMpmUZXm5XM7nc6fT6fV6Oj9Op9PhcNDuYn9ievguH65gf33ANRG+ndzS+XSEq9V+XDPoxx5v4G6xL5o4Voyb3bDi9cPG1kfO5/PlcuGRT6cTE8QXYeUvl0tYIbqlRqOBLWOmrtcrk3K73TqdTqvVwgTzO5ZUb2DlYL65MiPAc+kkxvpkO4JJkr3zpa7XZUA1XIwnjx8sb61t4qndNZFz4y+6ueF1TZbbBY2qlt8j+5KvWTsIz3/8Tn73JzyjxtC3gDwzjbx/xG9DlstvLJwBfgV/Zz4L+UV7VnPkK5/XdW/cADaddSgDzfXxJ2pPBT2vZrOQHQz/kReEzkCZA30W2x8OTG1OeX/ykprNJlux2+32er2yLBmCsiwZu9VqdTgcML6Xy2W32w2Hw6Iottstdg0viQuy8xkO7aJ2u+2nQqfTkVvnPqP+ZWr9v9i6LHFsChZcbn673Z5MJlVVHQ6HbrdblmVVVZvNZrfb4RkVRdHtdtvttrvDYR0/8sL8RY8hTqfT5XLBVNWuckyV+7n6OZ1OrA88EU0lI+brWA6dW3nfKhxOjAyjoZ8wvM1ms9PpnE4n3Nj39/f9fr/ZbKbTaavVWq1W5/OZIIMDDFu53W57vR7mdbfbMYz3+329XnPsHY9HfwpZW9lETpRWq4Vd/vj46Ha7VVXJyPqpxgLmhGAMPeDQWZUtXdhavs9lYviU9qQ2lzYt364ZdHMvT1CzzPzqU+5rP7Kbsi84g8xasBG/6AW7dftFwx2WhH54ELmNrEx/Lrn/fJyncMvjDkF4kesrcNF60B70pa6wzGdBd36/33Ej3BOSUc67TFGmf0TLxp3X+/1e+E7zFZn3dniesCcZKX2x7sn/ZKMyytzE+Xy+Xq/r9XowGGCnjsfjfr+/XC7j8bgsy/V6fTgcms1mt9vlf93J5VJFUez3e93h6XTyDcAch2NTD8gvrVaLpRlCErw5BeOn02m5XPItACa+PVqt1mg0whPfbDbcBqdLURScGSwCX4I69hklX4h5kWH0w3wrQFY05Ls92wV3IhSXhANJN8AtnU4n4Qa++VlVfF1RFPrTAY1ut3s8HufzebPZfHt7AynCjQ23x/U93uLejsdjt9vFOO73e9lcvF3e3Ol0QDw0Fx73YYJfX1/LspzP54fDAWuOr83ThbBXMY3Mh8ZK83W9XjudTpgOvV+Oua8ot+m1W9R/12HgcStDxMJ2fy0f5CG6kj+UA9zfcmN/y2oHTy6cW0VRgORkCE4L25ExXxgZ7fH71CqSwfVYQTCpBxPZDSrL8n6/7/d7H1t9FocG++M3pj3ut+qhP3PH74VHNzoY81jo9XD6+bMFdz0PHN99Pp/xT9k/WpTH45EYv9PpjEYjbgnbOhqNut3uer2+Xq/D4bDT6Ww2m/P5PBwO5fgQKgJyAfXyjUywHtudR42L20GPPoByrtcr217oG/YUo8+Msp8ZoqIoCEw2m81+v9/v98CU+OC+P9frNUF09phqQWHFvH7qyunL0ESOsrOnIJzEI0SAmvP5zJIVbsDE8QZ8fI+sM+6vNTcYDLrdbqfTmc1m9/sdmJ4ZwXDzjRyE5/NZTqg/HRgFd8tXa/sRADGP2ZHk+ofDodPp/OUvf5nNZpvNhjMjgJtaq1hzQRBh74VdE0KZAMuGgOAR5hACc18MviZDMPorPmnAT3UqhMXgBj049RmJevQTgM4ndpnTpSgKfgkOQfAbAhysV3yd146bmyYHOnwSM44cImMdh+fzGXRUF8E++F3J9AWDG6Kf78ss5F5CHOoOs+cNwlkRjuvgwPud8YbX11e+xQG72+3GFpWV32636/W6LMvBYHC5XLbbbVmWQK7b7RbjCFJ8vV7JyB0Oh8vl4veG/XX3nBFkRBSSs4f5HTsOuIy5wRKNx2McQELpZrO53W55WOJld83AssGXj8fjcrkEvgTTkBfpTtwjx0TxV5gXdxiDm+yBZwCLaxMjHlLtdjuiezakUBTGzSMp9+wcjuCc47uAmEaj0eVyqarqfD6Px2MGismaTCaM8G63G4/H3DaG2GM9BQduZGXm8HAFZPE6mL4wGcaw1Wp9fHy02+2qqjyeyzk0/RkAvufGN5hyN8RyONwdq3UzHYPWrpFX7vbUna/n+TFhF0KKQtaBTCYj9rvQRMAowg3otnXMe6JekytHxB04LBK3HXx8LfKQNAt2SRY2eAkhuRVOylarRd6IEQ5Inawia9InnbXt36gPuof0Hf/1uLX2IJVFl2EKDxmOHY0aF9S/zWZTwbhOV4dCOEl4/XA4AER0u93hcNhut5fLJaBEq9UCxZtOp9iOw+HQ6/W4cw4oHXpKr/kM8afGRXh5gKe1bx3O5uNkcsbjMYhEURQ473hnSu61f/yUZXk4HA6Hw2636/f7/X4fD3o6nWoz1GbM5JA6LqTjxFeeZ1C16D0oCZ5UdluKojgcDuv1GmCn3W73+33l2QO9gR85yALytH/4FON2OBw2mw2rYjKZdLtd0N7lctnv99l1m82mKArWADFKCLfv9/vhcMhLlEmHFyFD6V5eoBwws+PxuCiKz89P8qhuiD1nLWJPTuzou3STwYbWhuHan7UhebDgoj2EdEu2KY/MZf4K1qoyrmG9yRN8BCg/8q9/egMhvebGF5RPJ5OvyZD81GdlizjJcJXkDzHafkCGwzXkSN3yymXMBDD3Ix0s8nBT3+tTo/vx7cMrhbYu31d7lnLIEFMrSNGaCOlyHiDAZK1WazAYYOYOh4NGn/uTQ3o4HIg9j8dju92eTqdlWTabzfV6vd1uX19fu93udrtdrVaj0Qg7vtlsGo1Gr9fD1cW96nQ6BKS4tP1+XzCFr93r9Xo+n/v9vqB0gZLcId4x9+ZBpQbhcrlwbk+nU/g0YtXg6pKyA2TodDr7/f5wOJxOp36/D7OilmYQYJAchofkhiP47gXzKWyZDFnA8gjDsYakN4fDIcekkwGwsMfjUViEUOBWq8VjyjVgV+hmTqdTr9fDvILyHw6Hqqo4XNk2h8Ph/f1dCSKcDqBnbOvlcjkej0yW+xGkbYNbEU4L7I6MC9PKgpzP56fTSXlt9+B8xAKw5q5G8Pi0zDwW0WgEID7MfrhzhZI5H6OURq25f5IKU7Cii+vjAJ2P0NhHptaNV3C3NVzuwYTFzFHNjBAOZvjIQ8MQWAsXvVwuJAkC/uPLQG6WH4EB3rnbT7iHWt9U06H1wJ34kezBU4h0iz+YaH/Oh/jgMnDEpIFe5vBFOOvCFO52O6yhM2d5wk6nMx6Pd7sdniwBL17k+XwGhRgOh+Px+Hw+a9/yLbvdju2H14y50bjv93tcOTYJtpJFwFbXZODp4Jr5PetUPJ1O8/kcP50xxdrqWa7XK8Q4MF8CaiG/zWYTm8tjbjabw+HQ7/cxLrVbyG1c7TbTatPK8MBNI1yLbIglcr1eV6vVbreDbDcYDKCRaA9wqmEWMWQMqUdhcgfc5MmKQTjlEXh2LCko/36/X6/Xr6+vQD2cXoDCu93ucrlwBpOGhW4opDjAsu7zBnJS4GPAn+l0Ol++fGGBOfoZeKCix4hNCGoZjs8A+fnNhHyDE5DFXNZh5lu03+/vdjvZsmwmPOip5ZxlcKAsS87RwWBAdOKIStj7P8V/nTxQ++YMIwhzc5KDog1PPgcAU+kKdq5nj1ilwaoGQ8QI4zvXcgcc51ECySc3wM1yPf1scJ81j1jYmEU+0wJ7N7OR/aCuDYf9PHdMWhktrUKnxJZlicOoz7IzcWnBXmez2cvLy3Q6FbG31Wr1+32CUBLlYlwDPry+vipFAwlf4K/sNbvxcrm4E1QUBd4Z9gJ3iaAbszifz/U4SuthLzgAwTfX63Wr1QJ7gWoOHLHf71erFXcojlqOB/MRms2x9pvD/MEFC6wpvhF6yfF4BAyR5SWYYJcCPTGwypUFDy7UpDhPAEwW3wqGWVEU4/EYyAhHWDUvsB2IJXFUoWPjCI9GI/hkOcMTeJMZE681EGARk8mk1+vJ/Q/v93yOny4CcEP62wnRgdUUmEWYHiGbwX3hv9br9aOHwp/w/IEndmrhfo94CP5qva7nFIj/4g1uRuTueAERD+tom8MsIUqQXXMDJT8ybJyQLXCUQ7MjQCOXz4QcDCUY4i8G5FbD6z6Z88Tzni2eMAf9CYWGBG5vPvxDsRY7FlYDv7TbbdY9dhBDoHMPIy72fqvVmk6n5N/+85//vLy8fPnyhe+CjQQFAitJBk/bAOsMbIL/LhS71Wodj8fj8Qi4fLvdoF6Iw6QkifYboC2U1c1m0+/3mQ8mYLlcwkvlanycYPlwOGy3W3jBwJ2E4YIjLpcL1CjNlqNUIQ4KE+R8RoI4JZ2ClXTQCloI9AzAE5BrPgLyzqLkibSk3BJx6sjyBghYHGrxVZj90WikFbzZbKAevr6+8hSLxYIpUyp1Op3iCHMGYHxVhlQbX4eVqRVb6wxyMOMI9/v95XJ5Op2CT5fNfUD9shny/a+Sjdo6C25b0I2bbCWsMl9FWVOPPh2qzk6VzoPAnXgCNfxiVdujvJ9Tbv0E8poFd6IdV2n++SfQfrPl8fIC5Z8CRJsxJbdvwdo62uDW1osVhQ1oLygA9XxjPoP/oNnUlkv64q6lqmjp+BdoZF9eXsBilIdxvFWmFkQPTIA4C0cMc1yWJQXH+LmLxeLl5eX9/V3jNZ/P4Utgr3e73evrKwkcpWtwsrAOYMryVna7XavVwl7jXkFKxaqeTqfxeKy4BieXP3Hl8NfghxLHHY/H1WqFc8FhI8SjLMvz+bxer5fLZbvdJsbH2e/3+4vFYjabDQYDKBzH4xHuIUtK4IlPkC+IUKDh2Jm29B/1NkUB0rder5vN5nQ65TDgRVgfeMdlWfLVHucyUCpy05YGDcAXc0q1vJu3tzdn15zPZyovXl9fCV/gsWBweRzKLhTp9/t98TRqE4khke0u4RN/TWf57Xbr9/tQzlerlVwqjz09V+NIgnxhz3H7+AdcLoTVmqNHzK1gfdwNyiSHHCoFPiyfIox7NJi/nnN7dAKFg8qTzJ48zPSSMIy1fl527XPuyo89PTtLWqhxiBueZ1B1eNQiXdyzIhL3rx0o8+/9g3+m6a/ljnjmJzMlg+2GLuaYOjgpVpXhEEWB1zG7bGDAPlxR7DUJK2qUtVtms1mr1VLZOO6n3oA7fL/fqXPHgVKSFDN6PB5lnVXPpv8FelbC/Xa7rVYrbP31egXS3W63ehy8Wgw3tkzernhmb29veMHz+VzWlsB/tVqx53H3mGmByyE1WnuAy9UCMQg1vkArXHO32+33+3a7jSvK8QaXmXIJzgzGX+aSH9xPocBefOEJN6eQy6kXdAMVhMhGYDoVjzw+GVepeRAkcQUP/TJE4AG4kmA6vYKxyLoZirfKsvz8/AxYpBI+XrCjFLxCJZkDWclQY0kWxEHD7JIrFnE4UriZG/RQYiAvO+Sy3Mq4TfxdqkNIfj5P0AXT6ZhkWLqCmGSOQugWUnw5kxlKJIJb6VBnePPzIpHwCrOPUxi42IGmImI7waiHGgIJX15eilpjH8qZc+m0Q7oaMsi5KgALyVbiSnlzWrJOy1X9MRWoeJfAhTwD+MBisQC2w1aCkSF/g6HByuCW8nUsetxS7hBbw3BITgVrzmcBLnD3+H273XIwgOrCvsIW8zuPwAmx2+222+1+vwf5ZRyoqd3v99vtFu8P2z2ZTPD4Go2G+LCu7RKgq9ogMagdebTFo3FLjUYDJgnblYQYpxfLiIQPXwdKg9klXCCgUbzsxGpuO1CyOp0OxDvfw9yALNpyuaQKGbsDOE4RjWJYXbDWUcjYl84AXtGfvhk8Wvf8da/X+9///d/5fK5jSaGxY1NsLa06Fp5vcu00bRNSkZzcoRbU7ZE717KtWV+CRaWyGl0hZxFdbcNPxOCd/dQQ/xYErPMmlAW53feAXZIsod7EhzHEIk4ICTFKGFj5p4FHlNN0ud7PT3EVQLk0UsiEBWKCVoUeXGnzolbDwk+wnBTK8cLLywu8BXYO/qPsqSJTVTBHELooEBsDCybkB+bDYInEdj6fF4tFo9F4e3sDR16tVipW1uGPfzccDlVFCk0CbxSryp8cUFR2sPkxuADWbC0hDP1+n6/Dv+aWgKFxGLHC3AO+Nr45xa+aGFJwx+OR1NNwOIQYC4JRVRXsVC/wzwThWt+E5QUezT3rqMPF7vf7oDS4vavV6na7vb6+9no94YlUDFdVhX+H2QW0dfI5I8l0u+KE+y+3220ymUgNg497Eo8jAWSJtUux8ng8xhN/lGx8UiDrLEPflo4FBzZeKOfn8Pv69etms5nNZlqZgfwuMydA1lmVBEac9PoulkrgtwiUdz2KACN4/K5lGbw2lx4UPTaQfIOiRSDDPpEleUQozrW8wWF079XjbPfHwxR49JAhYD9damUlnCPoSTwvPnamil/ZRXlc8yhUMDrTvN1uS4smDKZPqOAsvBP+LJ6UKuoXHQ7uOTudEMtIBN1ut8kmSVeQS8EkU4VFyAv1ej3CUiy1KjV0fbY6GWqZJ4zgaDTq9/tctigKSEvAiNztdrul5lWY7OVyobiAu8IXxpgCBCt2o6pCmACbjbqJxWKBK304HOA8SANBK2AwGBRFsVgsvn37hqllkYFXdLtdKBCTyYQQWylvCM5sM60quXK5RkMLBYspyjPOESJB4/GY+8E9h3jEV3PZy+UCMY4SDMhzQA1ai4yeY8GMiZgkoSoUy6uQTd4KaUmAXe6WRcKZBNr7nE2VS4fdLHq46o6k6NtO7QyUSoaRZfn169f1eo3/7lSHQOTUNLnnJQE8D1wAtYJOW/BAA8KoCwp+0VlYaxBzeO4ZUS9syVUYtdyPX/F5MzldcadPvavXet7MT6PMt3tk9LOn6CK3fqaG5GeGMkKiLMvdBD6Gwzg610Ppo1twlxbhRG+1WsUTZaPMifGzXUEc7CLMLsuCfUgg7yo24Xx22+F6KBJGUoQFnApPAG8RzxclX0BerAAVXJPJBNuKOT4ej1gZED3sjuQoMbjK41EcQZEbd07JkFL80I0VW4GNrFarxWJBtRucAUn2tNvt9/d39rCoDuJpF0VRVdVsNsOjHwwGjUZjs9ksFguyi5L7CVnaUO0WEDdEMnl9tVptt9vJZIIPLlYfWpryB3FFeUAyn5yFir6JS/B2uQ1CHIyvc9p9NXvanewrz3673Xq9HmET93C5XHhqdO+e54XytglVp76LGqY47LT8sCsyOozCzvv7e1EUhD6ysI5FZHOAE00g6GWvfmC4krLngtzpy1Of1XZCui+LwWYtxNr/0gYPOkHPU3DO/cqCErlQO9sTMUBUAetOcS7UfnRvXlrlt1SrzuEesZfVOJoqSo/fqkcV8ueCca9VHw3jTKxc5HKacOQ6zz+r5zFw4u2ztYhJg3OEKRHRz209+1wuEiYbPi/GFy9Jux0v6Xq9vr29iW6FqV2v16SVML6wDobDIdrBPDaZN8wx6TLhzhzX+IyQVWGq7XY7F7FFi13KO1CSiU8PhwMaadTv8i3X65VE/2KxIPmmzH6r1Xp/f5/P5/P5nNpcrCSUNVAUrGTAAQUg8lyhRpYD9nA4zGYzuBxwvBgi3F5RrcnIoaJJbOEuNiOm6hs4GwyF29zsSsiOYO4dOSVcUHKJWd5sNhLCfxQX58qgLM7i5BxZBz2RTIwCVS9jyVsaw0dR5Xq9hsWoWNJla9w7eeSte/Y8a7SH0uRscJ0G6kG9nwc+Ao+wcgn4KVvrgMyT6P6nZdaPdIfd5Q9V8u4dYx8kwPSoACTfnogltYVqgcfl/qnmLhTQqzjLFU6coeGetSc5czWKe5wK07/zHxoPxJW9ts9zuD6+4HrYRPABsuTIkEu3RblyBVBObhV7Tl7DZrPx1VaWpYgE6liB/oMLrKxWq/1+DxAsIiSkhcFgQGoV3BYyrywsKDMVVqTLcKiHwyF3hUFUfsyTGPf7fT6fE6gCVcNU22w2uOfdbhcqHoNJLw98YbHlXl5exuPxYrGgxweFYfArkFuTlkKWZXIXI6gAg2ufTqfpdNrv9/lztVqVZYnxVabUq71duAOyHUOnUaLU+5HBFfeAlcDoyUfWGRYKI1X3PBgMQj3CE8GKzDcKUbML/fnmF7tG+kcKxr06wPN+hAW9Xo8DzDVOnZEmZ8p1I2sfJ6TIsgvsZd86ERUX5joRkVV054+yBf5F7vizizP8+FMi2pOEhKf7VMfvtZqhwsVtnGRbfiX08VrwJ5oYAXwIsHuQCQ5hpdfdBfjCFT4Dqp4T43+ST8pSePyCu6qEQ8D+0TAjZgc0ZL/xflJb3p1FuwV/U3Rlvh0/ES9J/hFGB0uq0gyEAiiXcklmyXhjkbGS8Enf39+FTkC0ovkQ9oVvVwCOo63iK7oWgU7CYCN/BeCAeWW4uTHGjdvA9V4sFr1ejyoP5hjKF0bncDigK4+42na7rapqv99PJhOVe+x2u+VyqSq+HEhmGQHpOaAuhlHD6OPhStdc1BHhAFwKqJ2ivuFwqMr64LCIQcjO0ZzKneRT3W43M5YIPlCS6/V6ZAvz2v3dne+RnG8k4Qm8B6qZEu6yRFpRtVgqmAllICBOfjp60YFvYw8RHNj1QzQ8tTNkQ1WbbtJtejbZjxJororp1GOPnEJfm//iJ4hCBBUkBfUBx3eRB8+mZs83VDZrvpwlUitVEWIXHcmBgpZzdH5NsV9CKeNzNl4QdP8j/5bJv3hGOsw9jgC1hHQFUkl2i49jSeFUKaOCpZOoowpddNAJi5SkpAaR+JqMkDoAqXtVq9Xabrfb7RZ2rbj6EH53ux3Jd04FKr5wVBVWUwcBEIwogUrgEH8g8Y21ggIBHIzPSPWdaM7gp3Qtw6xDCl4ul3wvt4Ek42q1Wq/XONqQsYC21+v1bDYD1wZc5gEhw7opEbHMKSUAJvBJKPPlgERMDngB6WFoZ/insrwSj5ebHBwEV4TwvmTgNqwZZ0rIXmilyvKiT0/HqdwG4hHJ9/m2DzshN6cQARHsyPM/gWfimU+3fYhqAruRb1Cw7D2WAogXJOjc8cyKmsFgBSPucnRZZzZ8V27i56tFe80DhdCs4HfrMrKGkYf/QZQuSMTJx5Ry4a8AIBl9elQvGkBzP8CYX+X8g+nTqabyinyq5UPL/f2Qcmio/1BARoLmVsjCAVDiQIFRisDgYSa1bZhd9qp7bSrw1yuSWfG1Dj4gAXVg08aPpo2NRmO5XFLkhjiDoPHdbrfZbNSrjXFZLpfH4/Hr169iU5ANYwQw7qS/4I1xz0C9cnC22y1IKBwGTTYCb+DOFFmMRiPuarvdLhYLwA1QYAIIqZ9UVdXr9QBV+Xe5XFZV9fb2xmg7f05ADZsK8haayOzkw+EAEYUbIGiAYIfjr8pjYHGtEli6nMxIInh6miGS5SXK4bgN9i4k9KkM5PSSj8k9q6tQ0G984tj+NCOkOKN2c8qXgZvoIgOe9QrMMO/5JnEMUKPRaNTr9cCFJQmUM1EYQSWWg8HNKh+PRMgkXhMMihL9gZ5USyF1H1MNYmoLEH6rNCM7sy4/ndWBaysYdedBQ7HxoJmb07qD3xpq57wu0W1Rtn6uQyvvAa8CNqFeCQlwQVtPlJEDZFxkVqli2NwoYTgcEp5T4ICHCDgrEjhuEZG+/AI8O7jiVCcHOh7opPRAVZ0MFoG0ucYXWiUlUmgXUGLHxcnvCwgWKQ0FSwmXAPWK9nA8HpF3kQAx90kqAH4fziz0CfBivGBBFio+xuaqUg6rShdnZMY4FVRisNvtxEKjXyRXACuYTCaLxWK5XEKSDZxWbTCoHRhf1fWCRJPXYqBATgBSZJiWyyUaC1RGMCPCHOHYwR7hedVRKXc5yRQcN3/Olwj80NqKEi+R+vXI11XVfS+5tG6ON715kuMJteW2xG3NZhP0hkOOV9xpdZm8ziQAACAASURBVLRByX2XOss3X1uTlqtI3OXXMRZaJQX/KSuH/GEIfmSEfpr5/JUQxLspevPD55XEqhR1MNdrebIsTma81WqePKnGDt6DDm/Nu+I/R6u8xF9uijpiBFzY10zYF62///3vnpB1Gp37MsS/qilYr9eIw/OippnoFVou/hESi0TcUnfEisEkVe0ZHqLjyASnKpoQGQWOKriqgBgOgOVySW/HXq+nLCpVG+qyDM8JKEPWpyiK0WgEAM1X9Pt94ncOA7xFHoEKXUoYeBCvL8JSl2W53W6pQgbkJcxfrVbYaGycGpeBzBAECPklLACWoQk096PloowZQA2ZycFgQDHb4XCgSTt2HNQSRd3JZKLzr6oqZNhkfBlPgAsUlvHNGQF1Pve0CXQr/gWf4QxWcpIiDhfgFx6Vk3K/u/9dPStIB7iebEjg5DbPoQl0Tu6Fahd9EetcypauP1crgeblBlkI/Bfld11RIbDZXO7HQZVgXgVVq6ZLWbjfnYvQZixjEbXZuZyWdOKtcj+1ScuQt1Sn3UA9DOhTYE2EbEqoZHHSkR+cqoYPGH2g2OblF1osf+8/7zMnY69Ti4ZsuJNsSAwHTpacQVwAKh0GgwGunxxMOLxEvscfPxIWUKkbv4hOoFwBCWtQSzzHfr/vCx044ng8UnlBop+AfTabUTTBXFZVRQUd257kIS14GVxeAYsgjcbxw/CBxuJ3k6WUe+hMcipNGTESWXL8gYllxfB5MbigtDwvFlPvbLfb2+2WgCA0GVTNG3UlKsUGPPn4+ACzJsOJLywLMpvN6IyJZVcSf7PZACITqgN2u3NBgIKd5c4vP360TAEE3KMBi5BEMsTELCf4u55X8KBre7Lk7FNg9Th6oHEIXnxw8LVLCWgGgwHrVhlj98RzlzwW/C9a3vBcvlUDzC2xeb//rAStecxtw7JLriYJP52aLPmoUyfo0GevVkeCaOmZFygTnzNjAVgPusZBhyEkSB298cPATwsoPa0fP4+IEJkapBEIldNFrhPHSYGqha9EYQUbGHd1OByCD/I8CAk2Gg1wTOi07E9MpCBglHAllct5K1121z/28E0UC6QDsBRABAwQqXx0tsqyXK1WGEeYwtgOnAK6ftH8nINhu91SbAaCgTUZDocSCWM1fH5+4pZimhlQ6GUYSiw1Di9eJLxRXrzf76Clk8nk5eUFR5V2SgCC0N2AC8bjcaPRINqYz+fAAgz4brdzOpoWCsEvPj5TuVqt6PcO7MAxQG2FwLXFYsFZ4sRyvVkQrfpdetkbdtb5KqrI8vATDITJ0iwzO6RqQ/vC/87+1gbsEgwM/cCD2fUOiaHHTKAiKGIIXQj0pCxRVoXGCtxcMZMke/w8CEnOR0+XPcpQROCxbO1pVOtBi/PgB0bWSn7uBdfK/OdinIwFZ0qDN4rPh1A4OJ347DTe7IrWVt6KWyYV3Jy29eWkgsZQhpoNqRO0c+Tx3QD+7W9/y4Wb5L5OpxORLNgl3hCYAxaQBUdzIDa2bCsGEViTIiKcYhxbnaV4eTpqtEVVMgcRinQH6LMcczcWtL9EuYYoHu8YmTRM3v1+R+eFbsosd4rWXl9fMTHH43GxWOipCZDxaLTBJJ/GTWJKOHJU4w9GDCqH9QSL4Do4vMQEEAzYt36uqCZYzSWBdKjSBtt1wgMNfcUlwOTJapMSxPpLiAQBT4yvBL85SiUDxoy/vr6SbgX0ZzoAgoUvEQoQMLrxVTxOMKT3i5vojTUflbQ9YoDWwpq5fsEdW/WsVW7aBVQfNSQPPXdDm3oviFCpp0jT6iorwVLBuOx51+cN8Xv2FoOyZajKUQYlu3u1eIIbiEDMym+rnYhg4ELHs1wV/VM0I6uRydJ5UUZtw1PXd/cXvajPa3a8WUyogchiaaGPRICtassucv+KQDpu/fWvf3WHWVra0KRoVEyLGtYrgb/YuBjl6XSKjyx0FWYV+xNFSoIyvhjSqJiYUiQQTAHhTGLb2BFZZx3Ry+VyvV5zV/JweTw8X6BPycvCt4WSRTiPzhbnAWeMWF+cIqS/MBwAvmwnRd8q2hHxGRyc4FpymmVZUuKlvnAADjRFVwd4XOnj8ciL0r2V2wuSALNNeozk3AB5dfgxfbRxJSDA+DLdAOJU+jIFoMMA6+hAigOPcjHfy2wK7vRAzCuFVGqskjlP0mYtzdxg+BfRhrCfQ/fvWoSRgwRScyB1PWLLN+q6eOlTgTPrJApMIXEJrDW45+rn5NY27Hn3jjO7wJ16/6Bbn9wqIY9kUCP7/yf85tD7iZJD7fHpTOTGj2ZUQefaT0qPV9zU5jPMOdq1nVwcn3ViQ+PPjTkcJfclkSUggjBpKM4uXJgDcABAFgCXa2G5VAuLR4Mv2e122Yq8CAe22WziY6puR8kWFxBQRhibTqkVd4zsi7dxVG4Xt3S5XKowlI3EoGMvAJq73e5kMmFusCwI0PCk0LB4Iu5tvV5T2cGV1dZePdVpek/8jhEXmilaMbgBtb8cQmruMJ1OKXIDqKFSo6qq1Wr18fHBmVEUxWAwEMwNkNfr9dQbETVOBDeYbJAHOfUoCnEdCWK0Wi2sMzaxqiquo72Nv3y9XgUfN3604IaURsWwd8/TtHJNtaD3fgRaCRrDXGkZBAQa/+1P3niN1D1EKZ0gaR3K63kud3UfiY4HypqnvELRLeNJyhe0irSBK10IcQ5aOZ69UbwcqsU8IxoQz5+S9n5a7VarsFPb/tFlhdlooYDgSe4utIzzNp1BkChIGgVVGY94/Bm1/KRX55/VbWuJeuVeeIqgAelvA6GVz+7FNZ5r/VP9Bb4bIgk02pKiGD6aGL6wkZbLJZqBOFw4OxgCHEylCLG2woL9+JIbz34gyHVJWaE5IkXDkVA3e04Ibz2ABw2xVwYRfwc3loOEAjaJ3uKZQnoFlWZvANSCIUgZVj09lTrj+vwv4T8MLVqKYdQgbL2/vy8WCyQ0iUwnkwlqO2DQgAzX6xVgBOuP14n1xyfFw/34+AB7Ic5lAVG3htWDEYH6hGgkVVXBmmIuACjxbUmoahmB/BRFAZParY94DjpKRdvgST2DTN9S5ojYIrfzyd7Tb1GdnE4UEikeIbLwhD55lxqdr4PBQDRb72rjlsL3ZIA4c9Ma6IwgMMymtOWkc+KNRYLB9SSYtr0KOMPedi8PpMvLtBp1Pdi9W/AT++h5y1D0FXrL+9u8IeFznbZgoJWzVUGjM8By6WBoEBEw7lB1IsgrpxxDfFMb/Xie0O+50+kg1CU+X6jB8aH47k4p4dbv99lykK5YrGxvKXZLIRC5AMwBniO1ZLwogUGn6yMHAQgo0qJGzZOwfoKpsxmsCSZGhRiuLEfyippgqA7KDW42G7BpHU1oWb2/vwsPwYFV407cT/WjFGAiQQbwAYEJuJmo7nIqAFwAvFLgx2Z4e3tD5JeRZ0z+/e9/Xy6Xr1+/MhTQ9cB5eZDBYFBV1Xa7ZdPSRxn8hHQoEww6zMmERBwq7yQMCWVIuKnMBAQJv17MMAYNTh7QtlYnRynOMhwY4BedSSGoVDs1sm2qLApRtkxnrlvNrnEQc9BRXcswDSCpt9/2drzqZ6ydmZu8ZdXKwL3PpeG5pNUVHoRZSXdY2lVyHt2/lh/NvAfEI8MOPp45B5WFd5+EINmBFdwXdFADsTrY6ECrCLfnWURQUIXmtQTnLB8auIOhHK5WZ90pvV5Y6GiMGk15tbp7spzfas+cv53t7y58o9Fo/eMf/6As6nw+z+dz2A5ElN4uQT2G1SYH13W328mfIuDFXMJFY9dx1HMRcafEF1ba3eFq3Cu8XaWzAIKBpFmFqsvCA+U2JCaL/g5KNxggzCLOox4BmgfBOAMtZUuhKPBYVWchb0W3wWNSqKZWRryIfQdbIPCBawGGwCskErFo6gzPK0JXqCeUpL1IFACLLCPQcCaUvOhwOCSwoLL5fD57Dz0QEoBg7gQeG9VxwjRk3DmPCREIkqA9yGXDKcahxpQURQFCFRrHuh5TcL6ec4HVY0kVZYFflXMyoZxJoJY2MOMDSK04tLa2yvM27nNl6WH3ffzsCUeIhJx4mwYqMMYkr+Eq7x4jK+fpZig0/Qy+WODk5ZPjp5h7UEESY8StfEAG8gWzgLqIK2reQ/ytGlef4izm5RnOIP7Jm9X4xmGfJ/oqoaWmP7uTrxt/7vfqZdy5Zdx3Nx/DdL/fUavRUUy+BV6UEm7iD+DDUn8Mq4G4G7MCRAAC4AkHsmqgEI0fTQ8bjQaSDkTKjifgUrmmsJaUrqaibNJoeDTSg6dADh4uux1WBgE1/iyUYe6EMgeUGKnlpYCYPg5wKiStCQi+3+8hnOH+S1gH8wRtg5zYYrGAdAwjYjabrddrBhwAHad4PB7jTpZlCbiM3e/3+7DTSI4x/UIeKPxDj5g4CHIF6TvKizlU5CiJmEzhn+TcrtfreDwGgOJOsLz43eoqAk6Cp6b4wLvPqlEx9tqLvhTyU5unHlePlKuyUIDiEjfiQQcnHPZK6aiRVaA0cTV3xIK0SvYlvTjKa7WzD54JBsEeSVwCLI5xdv0Eyd4DU8jJCqXMPoxeyhUOFVf8Co7hIwjIzWXueRFoYa7vk7GU3NgtYOXsd/Bxz7o7lzwozDmkG/oShObfj7zjsMZc01J+EuCk55xFdwnahAGnhjUUFsb3xCKpfBBSmEa8G9iBDYw7Q3wNnQClcx0moKhoRJCXAxYQnwGWFV4nPqP65uL6scJAKtjVaqKjgAXrJhcAn8XjL1Ubg2gT1vF+aVRS4wBtC40ecQDW6zUflKPHv8ApNK1Q8g3vm5YN6D9gy+ihC8hLRk7C52q7OR6PUUbH0uHdw3OA+AUQCQ4AVA3lA86GmuMJQtnv9wAazAVX0AaoqgqgCZIjQhZsdRlfyqMlI8frlCZTiyhuGcAFzgjXF6nDXR6SisKgea6AwblmVRbQecJ/CCn7gAx63ZQzmRRpumiW6/wJ3nUD52VmHswG5ys3SPW65+A4Z/l8eVWsRoliqy+ULKb4QiRdMnGt8eeGoa4yE/p3aAOGMrNHCgbCOgUbBrwoVH7nXkqhFDDL3uIuqFUPS0W13bqOZs0lOzJY7/0pRIj0UoPa6bvbTyN1v/ZzKNDLnKwWiNihRuN7/QVwJ1sXW+xlozw/8mNKeeH5Ynwx/5hFMY0YBbLn0jhXpXKmTMoHFPoORKtktHTUBI+ywVQGqjlYLBacJa5gT+WFJNsBSYBrwWVw0jGaw+EQUwKywa1SQ4HqFcQyhoKCWh6T3COg7Xa7pVyFIwQzB6eC3/kKyAncKsgPapPSbieewLVXlyY8XDxTMGiEkODV8hEmlCFCKZHJgsEmyV0evNlsLhYL+jZxBouRBjjDEeJtTXBSEHXzVeWcAVWpCM30fJFo2qETbRavqdVqCSolXrf5KI/kuGEQVg2tHBxb9NYy/qRe4RoUmRt/7gDmkTJJCKCnIH8RhMm9dFVSc4ow3t7ekOVjOlRBoBZfEt323GOu4uMXipWIgGvVi3KRmycnaxU7Xf8sd6AIGsFBPtRV7d0Iesmfw6yuJFdbeJYhI68Fd0fV1Sf0Zmy9/IPstoch1bRSVwVen2H01j/+8Q8sHUYBNw2+EZ0IuC1HHtCRQZ8QQ8b2Vl0cCCkkKuhTvC7pJnfOa5v3Bd0cuBM4bqL6BheGay6Xy9PphJFVMwtMLUtWee3ZbIZPp6KXxWKBrcFNg0OGPyiuK5g1UKyaUQOzsBkIe4W6YLV5BZeBsw0HHFsJjU+gNuAy1h+015FiHme328HSw/4CH+G2U3zBhLKX+PPj44Nbgi98uVyg8TK/QN7S9sQBkYglxlf13wqQpZDtBQWAEgyRZIxEn6iVlfF1nAlqwaF70nwhy6nkIq4AEYS8uRxqVakFl8f7VgSxCEWjAXINBDXvSBTYCIE75bhHyFIKNcYlUmWTCvdVZaO8Yq6/CgIvqs3z7pOP3GG1mg3Cco9qXgLPzBs7BK/Q4V22gyZLpeqhyiakH90JVZYoFG07aczRJJ9N7zPkqh0uM1CLGntOSyBS7dJt/d///Z88WbWfIBuue+KMZY5xMBGvIVCCTUVQj43A9YOLBoIRRD0IkEnUQPphb0sXgv/FxqG9gF/JTXqspKwRfTEo3BJogPVZr9eDwWA8HotNQfWB2F1QBSDeqa5a3eNZmlKoIOSH4CECFpALBwanF2Hd/X4HCpDBJXIEUuAO0WbkwGAogJjJ+GFP5UVibaUeqVoPsBrocXCfQY2pGtehgqnF4kvRgqxa4L2gE8SLhDLL5RLtUJ1/TnIgTUq9InPHzuFoYbs+Ygh4sW8I5dwFCy1hQpgfCo1qY9tQI+vSKlntupbxFgxZ4DwxxX6F3FLIGZahAbMMt/8SigscCXGFNpkbsr5SAtDw6uzJNc15zOUN1MYfmZMX3DqP33PLjxzFe9t2d/zVUSLIQgZ5s9DdJ1SZ56ys13fkPF44HQOLXIeT+lq5PWX2vSOczo/aBVk48gDnATdWdVbYJngREJhQFeANVVVB6gTMpXm4CpcxVdgIPCN1z1VthehNHgtAU/PTKYymHAG2PYAODp3IbYC5ZJN4QOzIYrFA20yN8ObzORI8lJ9gBAFeOSGgLmB8gXpx7qBSIOdIorIoivl8PpvNOHjIouz3e2n7DgYDRCBBwIGS+QrmEp4cbgu+OcUpUsslAlKaWBVWYLtEOkoWQ3NGmJiKO2Bf0HnwBM4nhk7Ifq/Xk1g7uDkV5F5/qAYWPqGgTDwdJ9C3b9+azSa6SHIQxKrOThZwtoAR2QU6B5OfDNnkwIJ4JOHoNKPg6bhpUOLF9dezs+Pgg5a6EzkCxTXoS+T8Va3gr/a8mroGhiynLJB3MF4wTwjLGEB8mtyXvraHXq1EQ07l5xS/XmHhhdDe8Qrn8Lqigp8oToTw1ZK78Dn25WisF7YFjN7zeLlqrlZrzRkjIeEZYPQMLgVvoKDEiwthy8iTqIUawjpKWKGiC8cNOa63tzfeiWQthbDexQc/UckWDCub0911McBysyZJFPoqwSv3fr16A9ZTTRwEIeHoEa9pj8HigugqJin6k5hjb9lASgRvl3oNboNkFGjy29vbbDZD01JlZjTT5Kn7/T7ONVtoOBxyVHz9+pUb6Pf78DQgJ2Cywd8J7dlL6tIkcR/K2MCvVYuBm8zD8lkpEgA3o4jGe0D26UEHpFhV1fl8BtKBT6a6OI4HHH8XYpccJdUuuNUZ1XV41FtpeDcdd4tYVELiHFUgMvCkf45GvV4gZKW8tNTNt2MFmTzkBivstEcG1ztQiGtcG8nmaq6M4dZq6ebO04wMXgWbURRjh0RdTZF1Ao3yic5kLUz/KJ5QS0mhCg41hAo691JdHcHzYAGDDurJfp/yWIPRDEm2APeHepM8zp7e9DMmlzUHmvYf/i/bhqk6HA54eXIhZZEpploul6iugCHu93sIVTK+ovGrCRAYIkgrT6VJ9YUeFmjOOQJZkKvRghZxVR8numeaKXngaUklwVwWowMs5Xa7oWuuQZEqGH6lMs5MNpG4PES+BZ+ROgsKQObz+XK55H8nk0lVVaTaAE8l6SCCBNeEDghmT/oOf3wwGIBsYH263a6HFBISw7cl5AcKgE9Kfob6FEIT3Fi6kSK3BiNNxhdUgekj3MHaglCBsby+vsrsquMJvq16lMAUhqCmuNury2q79YTGM55f9e4DKlH1pE2g+ut1By6C4oFHuI7Z+VbMmnO5ippNJFdARQ1+BjzSAq9VCg8UsdzDAudAqzSXFHvLJQIjcp64FDqTHJMlkJWcvN7gEjG+N72Qweshg4ays5KdiRgG1s88ZVlVv+5d+4RWu61QslEDJbUvlW+EEm1nB2vKnEH8RFAiVKjnaX2kPPe9ZFRyxSADZPNZr8KCsRE4U5gqWFBQTXFyEXyBXsokIbmAvQYPlWcEn0HpS8hV1x8/DgO5vADcMnUYk8qqIEtBY/CR1cQBzAFXHQRWsTntgTldmEssMvW+TmbSpgKoBW4GtVB7i91uRxt54JrFYkEjIiTWPj8/gWIxwbi0ACPgLcj9kAEjRHABNkpaAMRHoxEGXf0CWItqYYeno07PzCYwCKGMsnBSRAO150iDdsb0fXx8aOXhHVP9KP1ilDQArKEtq8zEmZu5X4tMTIhsaimZavwjh861Bdw25RZwAavJmTqHUwM7orZ3Q7hD76/s7rActMAp9jSUKBCCO0KxbKZnOTrh9YR+V4rK1enZIRclRTm8lfGWuAfUFEnOuosjR1IdUnJxoI+Agy3u27pB9zRA1nbQQIU2d8FLC468q3EGTedMJHdHMCurZUKkbItD/0ESuhYKD+drIaYkbFalC8EB4QMgOEAxVbfblZwCtgN5LaXsSOItFgucQVLn2Gh10pQslgaF89ZRFT25VA1xtXIhNgCr2hDotJSkGSE5WSPZKUipJKYkQIyP/Pb2Rt2waqAFm2CIkfLq9/ur1erz8xMdDP53Pp9XVfX+/g6zArYATuhoNJLFpHKXEYYbAOZA0QTAmVx+cAbYDiiiMRrgP8jxYP6kk02ACReQo0VYhGfGSWkieqkun1AvSAygTom+PkiUmiWrFpmTD8dKZTI+laEdr7uoQJYs5dp+E0FWNfyvQjcnYNQmOrwW9onWT23Vf9bVDsGZ05kpeFGVXe4KEbAXb/3JODMvIfmjEoBAAgki66GVr55XBlTHjMiz8OGcWaUzg7x3VVUsY3fi3JcMKHAAWJ3t64SE0IbZk405Pg5iu4He6xZc/BBnHzs7LRRfBEYE/zL+cDSz8WV+8fBUGO2sj0eCn9kEf6+IRQSElg28FYOrJg5UatELkvyV1pkS5SxBwt5Go/HlyxeFRZRgcNNoUboENTdAnt2pJErpeL2/T6oQCewXuSyFyRSP0dwBzIHTHmYVAAWeL1YAmRjKc0mysUYhJMCyxH/EvybRPJ/P5/M5ihO01Vgul4vF4uPjQ44kF2T0GEl6JGtgSbtRQo7ypzT3FDehjiQShbKXgonEKiUKxovHEOBrw85WgyUCBZFeZFuZ4sFggJ4ynrJwc9VfMPjX65W8HEafgmZpVPMgVVUBlwd+aGAFBTzRa/Nlsr3fc+hVntWvPbQP1j9whvyzIaMdulc4cBHaP+tB3MY9aXfmeXZurCgKTmj1gQ0teAPyKC/Sfeec/Vd5XiZyBHmzQLcgZ4tMCgTzINkhlp6rfAWnMkhP6GwOZSm5fCM0JZIuLmWNXguXEXM1xAsCza7NpBM0lAKSlA4lPC45BEHew5EwEcHBfyIg1frnP/9JLguhFqytJGWpiBM5tN1uq0bWfWQoX6J2oZgl6QPACopuRUoNkqbKyKnaWKVKQc9YOT2gRpVCsEQYenBnOAMqxOCCtESTphevozeGpfYMj4ysZOPVQcdziTCuJIbb6XRwFcElOHhU1C+2mVo18xVsJ7Ii+KQir4jOCdlA/S8cbeAcgnOCBCUhJGOLgcbZ5zrIYoB1gAuD0dMUAxof+QBwiel0Kj1SXuFQIW9J308QQ1akko0cSwKLHlEmH4nD1lqrEC2GfukeV4ZrOjk36KjVdkgLzX6CIyOtItVPO+wYiGVeCODE0oAP4qAQkGXmlre3cLTXCWpPxtbjAF1B0bpX6DqFmeBGjftY4aj3CWLSofuoxecT7fxcAfio+i6U+fl1NKSi4oitmO17IK6FlqlBA/qRxnFoWhjGOfQ5rjXEzWbze5cq5bUxPexz4F1InQw3MJ9iUiwRCTeiSOmXS8NQ6jy49H5bEoIIWJ5Ad1HnpESHzQUyxvVzNwoVITgA4CGUPDDcFGuqcSf3LIyChJswfrhl2jkkygixXRgIztmXL1/wgslGknCTUR4OhwgoU6BMr1wcT5Yyt+Q9rolF5ENRlYQdBymiPpjxkVcIdixuPAPOnPLtjA9xA168JNMQa1dvOnKPGO7tdvv+/k4Sz3uI4KOBCAOqsEu1eznYqAQJhJZM2vV1L5Upr60KollyWHKThVCwnuVmHvVc8MsGbFdXC/zZXJrhZb65RCpQuPJN8sFQPO1s3MDUVPFRAKBdicZNc+73XDvCIQQRwz2rXHL2s9n5Rtjo8niC71/boCgQ10KTtCwNzOb1h/XxVwsSjV4u1cudkmuV2HSw6YR2neuAp7nshopyY8HFn6tAC9F31EkXw8ogSvkbwhY6ANKIIueG3lir1cLUwnMAV1LXYXUP4zGAKYRAQWhzQE3q1FIhkAy+io+xPhIa5p4Jw+V94y0CgAAgoMSmSVXvIpHwGEEAFrxR7D5aPKAck8lEKTgOpI+Pj2/fvi0WCwS2wRbwbfv9vkTQcdURgkDmnG51HBj4iWprT4QlLjCUWM09vAjKLkB7hb1q0NTmA5IDE0T6m7QbZxIiQeAYq9UKS81QLBYLn77ZbHa73T4+PqiEptqQKcZJd5lKBlxQZvAiFb/Xdtbxtt5BLTvkrzwdH8j5isc9OxQyewEPCTxW0SF8r3oui1Sqk94ClTg7Vt63Meu9BXw5kBlCOVkuPNFTByDYHR2l47w1Q5C4DPfAdvab9/fAL1R3czajCiBVDBZkc3Oyyzv1hfNJpaEhxxWqz73dn6f+PM8Zaixd1s4paIFPFoqY9RUuc1Hb5yIcwwG//p55I63PUxHae2BLgcZ6vcYwMRbL5fJ+vyvnht+E+gyzVVUVDC09Dz6jsmoYIBxGaK3sW4EyJNOVr3ctEiFr4pkqq6s7V1qfMmh8T5cBRJ2SgmYvE+QAwK9U7C+Di72moA78l4KU9/f32Wy2WCzwo2FQ4Kt6nbGEzQjoMGRA7YrTVfiH6ZGr2+v1pNWNV64yDTkg4DP4oTwU0sxKTvJdfB2HFilWkt1kC7Eyn5+fUN9YXuDjHx8fREJCoqj1EOcX6igWUx3tQFGkL5yzzNnXCCTQ3M9GWiKuE+aMIm8kLoBSLNRQlxGqnJ8IwHO0q2Oee1iOcgaVFrpczQAAFNBJREFUA+/vmXtJPEoDhh3rpALZEbdZejoXiwDGdYUaUcpC+cmjjiQZRs8V5F50ywbBH2Ll4xdnzWLdhn9jEL7RU+NWe3rNAW6ZjlrAyiuk3UCH5m/S86xt5xo08wJXJ1SN19bpBKe+4JhSqRLOL1l19ipeMNZT1CKKEUAe2GBQl6Q3uFqt1MkGfhJBNO6zuFze0g2Lo/YqQki9QFtJTOhr6mHuwqlyAMluiQMnLIIwHL1Nat48e6BjCrKBiMZAyYPBAB6YZIXxfKuqmk6n0H6p52ZzUmrc7Xahf7A3UE9n2PkTKpj7vIyGNhtGrSxLaCp41pxbnJdwinkblppDjlMNWJ9k436/VwUNlpreegwmFD1UgEHJpV7UaDQ+Pj7KskRpHoQHDJ38qviY0DOYDrp7ULCuZIX3ztGcukxfLosIWzSAfaENuESdxHp0em8tHPkEfX4kielOei1K6CYv9OP4aWZGhDkh/p5hyz0fXUw9qDR4SbQwX/Fnw4A8El9+BMu61+L10NyPNyeDpcPOdXzGxR5rs45KwkuxqDaI8ZRaaA7/SG3D87GhStgJbTkfm5sBasaZNby32modzzEW5G2kwUrgoLabCMF0u11cGBWtYlbUCw7XGCcLmWDRSMnVQJBQB0ksBTNBRQDzRAUHHdhcrdXZRZgbHED4UuzzgK/D+aUYjxUgiwbNC988MDS1pMTDVcINPtbr6yuJKQhz+/3+4+Pj/f3927dvJBhh5vIUUHolI7dYLMSdQMAMw8dJgzkGaGP+vBaZU4TRYxGDx1EAreiMPjcSgwcylhIFmTeVwGB/oSdzqFD6yPzSVpXpns1mGF906ZbL5XA4JLKBJ8PY6uM47BD4yrJ8e3uTA0KOFLp3pu661+a5aS9/yo6JS3yFNqDqhBRQ2swOrvVWHK3T60rihWM7x+yh7/oTgeNHNWZEM3JWciiAfmlG0l3gImT5tOB9TJzFETignujPlACHEYKihRf44Zxh9KVGK+k7TXoQz3Omh57FCYt/6iWcgJ1ggkMpWubJ6Kxym+Ahgqcog45oLnsLvLoA2nzHVAMdWtl5tTMgAMcbEraAtg4+FOkXqMG0kyDhhpsGRgz3SEqV0hdWdUpI7/rdC+7E7HKwUNMhZJDwlq/AXDouyTSzFeX2Sp0ybyHybwC+fJc8x9lshqrDeDxGOOLbt29vb2+TyYRjhopkogF0G2i1yyGBkhlGlvINBgHSgiNNkv30nBuZaFxapRwRMEKOgzcT9Ys5h86ONjN4CBw17CN8CRXjcbTI88UWY3xBYKCmqVMG80t4WFUV24NlR9gBguTUjkD1dRKr6y3IdsjoiPPvvq0aYTDpik91Kc/uPmnBW9vuIcSMIRp7ZDqD4oR7mq6T+cT5FWVV4nMZMAFwU4VYhoMDD8yxdTHMJCEdGM3cs07uQDLJrnHwYwJZTTNI3EZBEO4CdZhKBmS8220cmFvI0+qUDUB8pid62J1piy785oCVV3/8AR380OF6MvuuyBEOvO+MQ/XFYYecz2f1HMPY4Y6R8pZ0GQQjdVUgqUXMDlONeJZiVjXWRE2CKi/cLuUKiF6Jr9EAEr7mkJmIty4lp/5vfDtPRPsJlj5Ph+2goAtLFBRFg5wKLqpLuUNdwG5y/wAOq9Xq/f2dSgTeuVgs9vs9feqQvMEEO06t1z1hKo1wBl9+lnavkifYUEl6UssklQm58MhdyprTr0iCzqTaQJkUDQhKwl9GGk2gDW2bgYMF4FwuFx5ZhRiAQtR9gOSgH6SNIUanypd94yl5IuU8z3KonNQVIGVHcoCp7icyxD/to5wrl7ydWtAd5+zE9LurG3QksvzF805r0mDxlFrQTpNXVCsPlGXRQ0GEpAJEldWVlc59ohCUdXkC/BossqaVuWMpAlMoWeey975UHKpygbFMV8itBZVPCvUvQRLaD0hS6AAAuWBPlQGhC1zowRHwq+AF3263AtUY9SomvcB8UDBOZoYNz2GF1aCQAYcOgBg2ElApe5WW9SqKI2UHj1iZJWaFHJQ3N/N+LZL7cjUTDKukIFVxACgp34cDE4AVJ50n1Wfdw/KFAryLV8WxgWoaFUFQHdrt9vv7O6XAo9FoPp9j1OAVgDaoho1hEWcDJEHmGF6XpFLAedldIPKOmWJ9pC4o1pegc4YImEg4Mgcb+0pzzVEkV5Q9AImFKkHcf0IHtCBQT0ZTGOl35NygnaiDA8XrOloE6DMIfHVgd7m+oo7ecAaHxj+KFSTs4t2Og/fqxLIn3mv4YOgjl3lFWfQ9dKPxWhKtzCeCEh63Bk/ZHX91OfCKA0cwg4JwLirxLhg6PLz893ms8CgT9YjB7XPhVWeAaWw30ddcpcAJalmNKAvuhFM2iK6FQyXEKy6F6mViOn25c6fBOP8vNIHOgh66k/v9/r3+GFuuPKny5uwZimJ18CrYJ3cHEAGrFNYw5RuNRgMZF3a18jkqdwbJhRpF6SpfV5vxCFLWvCKeHKYEH9PrwUEhsTvQgeURA4BI0dHPTEweasJkz3q93mq1Ipv38fHx8fExn88J0rGelEJQ+oHB2mw2EIRxezmxMCuMwGAwmM/nRA84OJhpD3JbP368gB0tCJ7am7JICkfQG3w7rVfsu2o3IFAD7NA9iOBGFEMqwvmTgmyyc7PZ7HQ6ffnypdPpzGYzOtRBUyMTq0ZqMBchhKgRnG9aoXiZCByQB6GuAl5BPHwTOukqOHqhU2RtkO4dhgJAHJSuMnc407zkIgSp2Vrl4uc//iBCP92jrFVWlBnyPmnOIw78vwAX1JrgRzyNrPLlrquCm1rqt+dgKLjViOGBAv1hnXST2keZmZdnJDTpCLBsEAxiGajDdGYfC2sOVXMhOZztWB7tAhOjHid4hSIFi4oEMuhIlorc1aSdZgewJg6HA80mwJSrqpICuhrFY7kAExRQ12L/jsRrVnQEkelSwYI6aOAUY+nUQpRKvH6/LymWsGkF3OCuglaDeCLRMJvN3t7e3t7evn37tlqtgJg5vXq9Hn1LIRWQyYS658X1jDYkAbJw6tsmhhP8aHEeeF7niuPF48Zi/X2VAA1R3wFxGFcCZIByRxJoENp4CopfcX6hNPA6pxH1x5wrlG7PZrPdbjedTmmXR5oOsSEdNjKRLB5sh8g27t3kzRD6TXhjGDF5c+tJh/8CzTY4kk48Igj7/Pyk7zXTlDsX1OIGwZ9ydnCQ1nS7FlI08mzID7N9nF2gIFocBjY/+iG1WlzBkQ/tPp3K5vDoE2w6xwqO23o+iq5XFO88ydqFghqxI0R4IO1BTKmWZoG060fLI/6yE3XDfvcWy3RLcLxLEFloLMTaVkuO0IA508Z9xgUoFyIGyuDKPwIloKU50aJ6N7jfJCYplU74TbwOIrzZbGhXg1HAr6QWVgybwOfwjKSyZArACQrAPZRN0rQR+4MwUHLmuiRCG1Wg4aQTrWzp8A6HQ/Jpah7abrdXq9Xb21u/35/P58qzQcjD3QY/JdeHYpFgaMTpddrjwtNTzqWw3Atzs4sJU4yvUlQvQOIcxcGXf4RPqkpFXZY+cmVZQhPkAZloDkiyr0yZhO7AuDebzfv7+3A4JDjgPQys1C/BoNWESbVJbhpyP9onQg2q6ZKX7SFnwDRq7aYb1lDHTGlPJhiFQtInHAZ1PfCmyLKVOa0XOFKszNA1x+m3PjJeex0UHjJoUIsFPzpI8lmVpYfdcQnltnIjtERrVRpCNjJUsrkRIA/JKhoOh1hh72YdxDRyJjMMe+BO8HTeitBpwrV9m8KDh0PuEfMhjHMhaJUYwZlJ+Eoh+aNCFwlo8Qxs136/j/wu1lbV8YASKOeizUiMDy8YfVgyRVkTRIgE3hynAlkjEYxQrhH3kG/0GgS5V8721Vdoe3gdDgkBuBxlWc7nc0wz3CkIv/DDiBJwRSm1UEPP4/FIpo64HjMHqI/VE2lBaLhCEHE2XHxLHpDMqMuIcBE1VUIIWAxi0GSACJW9CEFCQh79MypHON7YQlC5VZ2MISbJOZ/Pd7sdbeKOxyN9PCVxRxkkeTloMIGvGhpP1Hbbrc2PeUIssOgDGSgQhGtFYLHm5GZVGe+oce1h4PevN8jOhv4dARhxvMvrOHD6WCRKQecQOBju4A/mEXPX25Ueg4p8rY57kOd25y7sWRlTjAOBpsPuQbbcRS/DN3IIiU0s3wg8cDweY6agA0ka25eBT4cKNGQ3vX0GN695DzU+fvCI0+3cjwz41uJd2RsohIz4YnJlPCeNY1bI85IUIjoAo4EoA49ViR3Cf+6YZA7aPYvFAk7rYDBwFQ8vZlcuLjSq4mok+iF1C7zGeGHuA8nfxU8hTkKQ8g5afKlKciFsLBaL8Xg8nU4h8BJZI4WBLLrE6Hh2iA2gIqTRPJ+mjFnYTlQeM/IkzTB8bk1YuM66w6SqGWjIvHv7bs45omzuHD4ZXy2uIbQ8Yj1x5kgJAunA/Pv8/Oz1etPpFNl4tO5msxkQtkjZ3D9cPYFXP804PZeIDMFy7dVCd5zQctFjycDiJOccklfu8bknW1sTnG87KDO4NeRP2hqwlZQJcHbdIxgx53YyGuPGNDBGfMzDfznrI/iqHh+IoOLdNmXcPa/7XH8nEGkD5ywMo5RLydpJg0LCLNoj7rEq6vIsmQ7I0Hw616/nHFoWhwq+be1hFn4KPxJ1r/KF2beAv9Ic4CTBvQLZYfNDgQKL5LIk7rDXwA5U+oL/giFyWZnswN8OQSL2FHUIwSaAzoS6HADqDhv6G3p6QZpVwVfy9qC4bFVVLZfLt7c3rC3QCp4s1dgw5ziExDN39YZAkHTyilSd3D9ygauAgcqrJTmGE41JDe2yBTQ7TdKFdxlJMm/cJ8k6JpcZgRrIpaQjAXz8/v4OuATt4T//+Q912HCJUFzjfFWM/CQWCzUOOYVdW55Q6+ipUCpEOV51Gsoo8gFQ24HG2Q6e639eSexCZSF0FRwUmifloygrIHvv+iw5JCWp3OCnNkj3zK0IncHtFR4avi4Yeg3gIxX5RxP3CNDPmm0KFmVSSb8TW0NgF0fQE/LB2XdDHJSjPazJ2s3aWSFoeNK/Nbcyut/vhXgnnGySEybGR2iRpLxT2eXWBUsvwRQYrGhaYg74ncQiCRyAVOJ3DHE+maWuxDL1hvM4iUEjGG8uJEPzZcV8FDzn1U3AvlDoYHTQukKqOgTpQAeyvyCSAOhipKr5oDSbBXlLUQgYXdGWKBBEQ36H8HIC2cUDHOACHFh1oJAb5cwY1dGRuMMYMaGMOTRwcpiMxvF4BFaC49Fqtb59+4ZwGtIQX758abfby+WSGhx2haN4T6q/Aq+L9MMjLzjUF2WhwmAovc4th4SPUIUc0Wf5nid77FFJRehzgcCuy2UFXaGAHshFCHfiTUZkGvghDCWf/MRDD3+KmuJDIR4k/hkUFLcAIaX5qBdq4Jw8upnQ3z4ch548kHtOPoa0DWRK9oLHPQ4gqOwNOK6221tIS9RKQ2TFjKz557VFf+ifaf5wrFR2TLZRy5Hf1a3EiXgCNJU9pOOk/CxyQcrMcFgtl0t6aig7H9ia2Fz1dEB+jDWhkmKvTK1VuxAVVGdM0EkRKCH0A7V1xdeIlsHnpaRYyXGva/DGU1hYmXjlnYgYSJQxGs7EYjVnCW3tNInNBwuiIBGUwBujqY2Tg8iaLPFevNiMHaU+GjBMQOfRcgMy5qkZmePxiHTy5+cnYQGKzIjksaxJz4K/P6k70C1JtaCWzlnbgc0pYq4JEKJCd82CFQviW/kNzkAKSpVPHOEAVgQ01tGMfD8BbyUB7sCgnzFBnSs0l3Seb22XEGHNWhIhDxx6HdVm1TxzWNuOL1vhfNRlCnZQdAsiGzKCuHqdTmc8HtNHUbiE+Je1x0BuZ1frngeIxmlUGQQL5tgN+vdATa4fYApmgroUuJ8OymCgOWTCqkLBlutwzdAYUeVJvEce8XA4hAucU4dAPEKBPbASB8CliTy4893osVjm4fviVgaMXsK0hgP0BFdSxKGJhM7FpyCBeBc/CcgBjMBBBhfLbV8BWwNW4MeDFpknjvgTfoUfsEJgGS4hztQH6nvV2Vo0cB6TQxFYX2QDkqj8L4AShYUY9Kqqjsfjx8eH+MI40aJa10qQZDOKxedh1bXkVxhRWUQmFGXpIAxMgAyVPvEN3S7nszDocyt95DQGn013z4WbBZA3UKRr8W6vGMQMaf0zX1yEtZfbmwZKmRS3H51wj9h4octRsDh5mnJP0rww9FzqU+csQ3fPvXiaqcfVU30dzKgAqavvwSMn3ZlkXoHpro+bb5/KcGKFNGyh/+OscLwff00f8+7fuOsaaBF7qYBgz1ORLIOraVa5HTEsF1TRcwDISUSo4jD0RhRakmH1R0JTmr8sZCWHmgwenppU06SYzm0zYiq7EqYBCKBCWx5c/Fyp4Qj+44RjkrRz8D11V7xHc6mplQQEz8W3q64PSCcQnL1wzhcQBlqAD/ML8ss8cuCJdOjVIgDHFDQDK1FXwpxKp8mTJ8/lFwCpKEuV0Oivx/vy+7JeYlbUzQmi2iqp5/Al3ERHsThKOXGDkq9gfQe+3SLXak7qqzV3OSbwRJk/LNkCkhZq/O535ZoJASkSZ9Fv7/nh50hIrgnMcEStpxk4yK1WSy1XwoHh8L3nNhRPEzUigtjr9T4/P1UH5FTXXETjmKQHUhoxFcKEYzjQGcJl/8RQplBNumXUUKi9vJaX+nqpYYTug/BTuLAKz3gPEShMCcmEQ4HyxqvSypMqEvvZ+5o4BOZHnxetisKdV0ne/F6U5a02uQ1cNmwillT1yi62i31RVw5cY7kbcNcw35TJ8aJqE4bDIX/iYMLM4+nUU44ybpoDiYLGeYClpsqDm0S0TCvDs5SCAnF/wBmch4RVhTrNmz0Vxs3w7eTr4C+LvMFQwGzh5uX9SUgoRGdPulEwDirW+PVMDvXQ3s/Yc5jOQ8hecOgM9tz05/7HOb3zCLx2yyI3whPFtUUTajgfMkXCynO8z+/qWcXiDK0fvMWMtwTlarPZDED/uSxybdlxeLpaJYp8GNdiI0TMLkUg2u+jqhA/9qAGlGX5P//zP7gUYU0Gb93Ji44de0gR7H6GQPMMhsPm/wHXVpvNkOd97QAAAABJRU5ErkJggg==') } .header > .container { display: -webkit-box; display: -ms-flexbox; display: flex; -webkit-box-align: center; -ms-flex-align: center; align-items: center; } @media (max-width: 767px) { .header > .container { -webkit-box-orient: vertical; -webkit-box-direction: normal; -ms-flex-direction: column; flex-direction: column; -webkit-box-align: start; -ms-flex-align: start; align-items: flex-start; } } .headerLogoContainer { -webkit-box-flex: 0; -ms-flex-positive: 0; flex-grow: 0; -ms-flex-negative: 0; flex-shrink: 0; -ms-flex-preferred-size: auto; flex-basis: auto; margin-right: 16px; } .headerLogo { max-width: 128px; display: block; height: auto; width: 100%; margin-right: auto; } @media (max-width: 767px) { .headerLogoContainer { margin-right: 0; } .headerLogo { max-width: 100%; } } .headerTitle { font-size: 40px; line-height: 1.2; font-weight: 700; margin: 16px 0; } .headerNav { margin: 16px 0; } .headerNav a { color: #FFFFFF; } .headerNav ul { margin: 0; padding: 0; list-style: none; display: -webkit-box; display: -ms-flexbox; display: flex; } .headerNav li { margin: 0; padding: 0; display: -webkit-inline-box; display: -ms-inline-flexbox; display: inline-flex; } .headerNav li + li { margin-left: 20px; } @media (max-width: 767px) { .headerNav ul { -webkit-box-orient: vertical; -webkit-box-direction: normal; -ms-flex-direction: column; flex-direction: column; } .headerNav li + li { margin-left: 0; margin-top: 8px; } } .mainContent { padding-top: 16px; padding-bottom: 16px; } python-oracledb-1.2.1/samples/tutorial/resources/favicon.ico000066400000000000000000000163561434177474600243030ustar00rootroot00000000000000006 h(0` :KK[4F=NbpN^@Q|IYivCT5G6Gq~lyUd>O8J9Jtu~gtP_YgIZBRJZRRRR<LRRRR( @_m:KK[4FIY5G6Gq~Ud>OO_9JYgIZBRxr\j6H?PHXmzB:: B::,B:: B::,B:: B::,B:: B::,B:: B::,B::*AB::,B:::::::: B::=::/1G:::>B:: &::#::::)B:: F::<::0 :.@B:: ;::0D:: E:: B:: !::::$::(B:: ?#::'C::4::8B:::::63::- ::4B:::::::5::9%2::"B::::7H::+#::( er:K6H}cqBS9Jy5G6Gro|iv:Lx4FWf&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& &&&&&$&&&&&& &&&&&$&&&&&&  &&&$&&&&&& &&&&&&  &!"&&&&& '  &&&&  %&&&& ( #&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&python-oracledb-1.2.1/samples/tutorial/resources/logo.png000066400000000000000000000454301434177474600236230ustar00rootroot00000000000000PNG  IHDR! pHYs."."ݒ IDATxuǹ|S T + X J+T!+Y%; ?/螕g _xwy!E6Kiߙ љ 2 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM ׮Цz)#:E){~MJf6_lw>Pv@ No|ńqWlATE60BjlLpj `e2' T:sy?[%;ѮS*~*?f:)+.r|1\ @&`l^Reꧥ_rAnOr]@,ծ>FQ@-7k6_ 7 rϩm3uV.{r%`TT"Vw Oq[@t{Q ފTCq[{o&``V/3 vCF!=8?i@ݞY' X@X9SV]zLyY2' G±DLhWͱi+&Z^'`{7eR2u@ XNf+]WCȹʏh!m_9$p7 ؉>ߊ^6M/B/,_^R|7.|l]8;:YBl `/J"S~/Ŧ 6&'! 6a+^y>ų_5w獣͖# ,` .\ݑez}* R7^e1y?'`[Ӯ9^Y~C} SP:_\S룣@, T)՗whWQʷ&tRz/P+ x2q&P]ٛek,+J$yR:\pK *?ӱ}*4\iG1u5jERl6_FCG4UN`~TC%lC7LW|qsۏo~T" P|qCeOXew@jA-rAj֮8?G:O3wHm6_U7뮇I"Dd *h*0U-+)1@TT H6=lծnf&}h,;+'O!`r |$D&`*dN!,`~ߨ@պ|MtBxC݅wRZ 0yMO&De ؇v,}jҮEx Xt{b]5muSW9K)ۆxoXS:4 kno,?6{` , 7yY.\*/M, Bc#\˱*0%\wK0fI̧@Hާ,! * *0ջM)}L)拫O (wS@TT`B)8O OQ XP^XMe:m@TT`tMeuAK9Us6dJ TJdVn<-Kx,Zyj~( XD%`AoXfPby`VXD%`Aɬ-i+j , )Lg[]).7u6E}ֱl8I_>>Q͝`7'9d <π #.X0?:Nt߼nr)ON]]+x< ػ|q6/ޥ-5= ]3 O#`at\wdnO_RJ#]~g⽛8 X@Hyy2Oe+ov Q]VK`, <{RJ9c]]L[јsCuOu>9 /06&-8iWM7m߅7N&/2_{NMY XG[gg6c?`,`21레Y?9<Ч"Z8hLR̰UǬΆug9X *%_^樘βo@}.) +C^j40i_X@r:p^~Fvl#'`rﬣ@X}:ʲ@ re:`o%Ax"h}Q,9샖Mr, XP,9]5"jxXu[__pw:,K .U|Gxa;?QФ0qdU.,J؃jAȞZH]H5XC"fa·SUW ·3͛G-tU}dʡX#`bx]neW&H,cb`M:*}*{T X(Z+/I<-a|.@% X|Q=bzk:a2EL[W*ǩ~i_6{P (^\ߵL[ǒń T*&ү|,v&H_J.YL5|Ǟ\_W޷`K,¹oA,u|2@ס '&x=&~- X2 65f%0BU[37L6}7 h% 6B}qO` Yq/~?:FD&`@h X&`@h X&`@h X&`@h X&`@h X&`@h X&`@h X&`@h X&`@h X&`@h X&`@h X&`@h X&`@h X&`@h X&`@h X&`@h X&`:?vx`UuJh6_x.Fj)Dx>Oc ؕ)6]5թCw]]5'8|^9pX% <jWQJL1CUsPvռO)W}yRjW;gtKऔX>u?yv՜}@`EH?|KjW~7<[jf Gs6_iwL`QtXv?nW .\K, )J`lrAK#YBu^zqbi!Q奂,! * *0:{d]xJ.O 2A ,DVS:Esn^MJ L0`T։,^ZjpT@TT`Me拓8O kWQVSZG * *PIu{ee!V,uYË8O hUsPDQI9,RLfZNy_J X0`Zָn1{!ܵѽ/PUCryG4K`{nu>seG\pF?:Xt߼[Kq9Kw= IB'/y?[g׳t׷:d X@(b6_MX\W7mu]ל9x> )//즲Le\9mu_ /.3}6eN, "]5-Y`GOMYX( v13 ]/&>v"f--3g `2Y2>hrV@<0Y2^;ջFT>RCjY#$`U*ߧEвfDX00X{fڃuwҺk&w?NIy@q9XG,BM 4 ,BM 4 ,BM 4 ,Bm~)Ng ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM O׮ұC3/?LsRz˱,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BM 4 ,BMvБ),`W~nW͑ c X.1,`ޤD,CvXP${GXJ)]5]rB6}{~7 nהqfʼnvy{ϟavՌuJIb^|kXBDjWͱ3P7 Y<ޅv9k16{9v՜u MeLny,+U)v\潘0~0UoRJOj0Y0P.f3,fۮ9K Mg2w2)?RjSJg)lqCX]кAL- ag Z;$`<]Ab6_\9!`l0h{h] i,1C{s-SZ'`觴nˠ%j'`ϫ"j}!j|Oe]Jq5/Μ78ZEÖu~9l]v1[7&1M~ u[]̺)&`ԣz[~֍D$`V'n]+Y슀}qGJ\[\VXK,`W.$_,CvۗY9G"ҟsȺu&XGj6_\uRҵ3@IB7l8 ?93$ (//|SYBl8H)b!@=,`tfI^bΝEw1kYe2 `b,`,1 `,`rɬ>f5w)bVJ4?RjRJ㍓ ,*b6_|GRCYNZW:OȺ鬔R~Zez8쇀l8K)Z -hM:׎% XOT,9]5e!X[7?͏/VIxA)$j<cDbM% QO0?Lk{j}s{:,&-`,W);k3 `"6[yj [X{lĭT,A=q& 5 ;~_1տJ" rW\$W7+"W4ɕb*W*-&S]P 0;)&z)VI605/uoG ,BM 4 ,BM 4 ,BM 4 ,BM 4 N(t0Cz5/<' r..f͞ËjWaJpOa_<]SJorLU3y~{U>fݽާ~sGJÞK;M)k 8+;=F 6Fl֍E\9K,D`3S#o##@RϜכdBUx.402;A)ymJ)l֫6f:v;xw<]t6ARʯ}Y;KGu2-޶髃O+!`+kW.Y߅ÀwxB-_ǐ:vD~?7fz_tVf50}u4;9 }Y[> X$a^{ɮ;V髏ni ` X{9fĪ;IŘzȖW w! )5bLahn[6zlBXL˱vb]JoQ{mKz)@`R~Gv}-iyݢ[ozy-?LDj<}ui +Sbe֢[Cן>2x^5c [髏n9;5]SuTˢ[R~nx Fqcjd~ ! z0R)zsfzVJykT?XLˑ.J)o^a [3}XLXÄ)WokLt./ٵ~T?x":_wQ4S)[=eB{&{'5t= ZX:'U ~ 1O_zsǹt- >dRʇ6S@wa4z܏nn5hԘ]<zNs1V<;hz|g-#goʘkj1j`1[+Zi{),W|Yx#~DT75W XbZF1>5^输)W e~a}E[ haÈtX}oբ[oxQVz k/8,<ɇXlCtOX*ȕm]:}uZ=.%`0I٬Wۘ:;Zx)VW?(M`[N]هcºQL_05[薭VvF-Q;,=jM[ئwۆCLam֫G^SlL6uXSe\?bsh[ئ ;i9u :}r04E|zex\fB`ZL`B?uN_[tGI ihk;jSX-eXN/q^ BIEȟww}TOafF XpGuywYyzt7O3G0CO)uAu' xz~m m77 Xlqۂ hVwn=`,>\ݺE,*7Ma54}6X<#W1z,Աۦvg`W4%`n?Tԅ'ki~|SoL/К{~;!k^Mg仜d! ~:宆y&3nyZJ9zJ_r8k |X7OX\?Ng]:"|Xt9L%Oaxww`,j3 -v.]&~ ~:,n Nĺ/Z[u8⇁_g~'`9m^ե/`ov? x*W.Þ%YuVw@q?O6Um[駂fyOv< 7f@dNCVJ Q xj旦}4~Z/Iz*mӺ.Znc}Wpr#_oH$`7꿁XWqy<˿EZ%E_믍|n SW2z=yZNk] !^[ \ֻ UPwOm֫<b^Qđ8WZϯ aZD~ `@2 n-n [aS=͢[$P=L J=RxYN4N՝"^*y& X;%=15;_[ \=;9H4ֱX`_ R po/O6az/Wa亊^&`>' !`YtzuX!_q0؛mCZe05^ O꞊] X kz sOj\\_a `[Qc^m֫z\Fppnɮgyl;FefzV',mTBq0w-_RZJyWJ5 dYo"n h;բ[-e1X ny·P, Q=^xMe `ѮMe]YG&Ne]Y!Yt#/K&m-O0_0b1C;&NfipjgVw`z,`j?)w)LGɢ[~*ԏYԿ y,`Y;hAx 0//}z8Y@# q/c쀀5 Z`\=,Y)lC h=1prqmJkإp; tG&Nh`puRkxw@G_[  Xᗦ-`oXtS*ٯhYL03u֟l֫azXl tXtˋR S[Z?,I$V X|`zFuVUSC`[_亊[W\EE`gNozNr-v],|a'׍ccG`,fap|#0=kO0 IDATL @4 h, XD&`갔% @2 `DnEph, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 ho^yh삀\9B@4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M o!b @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M @4 h, XD&`M W)[ )NHIENDB`python-oracledb-1.2.1/samples/tutorial/resources/python-oracledb-arch.svg000066400000000000000000014700311434177474600267030ustar00rootroot00000000000000 Python process Users Oracle Database python-oracledb Python module Oracle Client libraries (optional) python-oracledb-1.2.1/samples/tutorial/resources/python_nopool.png000066400000000000000000007342301434177474600255750ustar00rootroot00000000000000PNG  IHDR ]1*sRGB pHYs. . tIME  ^2! IDATxy}UД$)hB^itXh˒HNnػhlgcE̬cwfa;7Ga4uD NѸHRx^QzY*Η^:^7p"w0y p h8C/Kp,Kp,Kpos$`ĕ/+ځ:e,!,!,!,!ZͰ+)@jղQLMZ|z2RM^4MOO.Ko^Ʋ,6|Ybzjjzz~x/,\s䈆 iYMqS„ec)!)IJl -˕'y30(aYvs(ؾò,M+-K˲Y*,1+`oϞLT, FCBaM˒-ջizLapLRzV@#[ʅ%ĒPا*6@PD D5{n>&jep}ǑgE^eesSS"{XMW؋,KDSSo0z*Rѵ ͘%?Z$SSX S̲\Y9{_K_ecaAjj 2:e%޽p3%p}{vߗ7ò[gP& GYk±R0==UA',sFP^ȄSSwwߠaYi(/K(!h1S~ey-bR`YVv)s(tMŊEC-XEqȲ,?|LonE$V 55}ߪA0CJ̎33<Ќ||?X=·yw)_i65-͇yesdٗyH2V)ة)&rѲPػ)grEߛڞ+ ,FbԼJP C oRr,b.}'Xpβ̥'P>o*v-,VebV[WX堀(sXp˲N-R;,0+E\,rrWtrv 5S`YZSeYv( "݇>}3FN~rì m/\o{=(+S%Zx|SCJb.J=șb%`U=UJpŲǃG/}N,eYWhyůaq=ʔ_WBol,K %1 rbcc9Ѳs 1,MWwgyY*c3,b۶{b tGdS=K8c(.y~wnܶMm5fDQbᜥv?ӅIVxN+y,v%*8څ E;F33?j^K.\W\OQP@ǟe5~F^D`[=AAY2L$R{F^Tk~8vQ9aș%MLleeƐh)7py:_W۶n #a(>"Y;[6ݶrmE^ay_WzKT PT(4Il|\x{AE;r"ک)rEC^/ g5!ӉZ6reٳg y`>~#ӗ.y%J yP0饗)"D9cY_+N?)"@!3 l (}}{>Q'ѭp-[6{R5e`xEsP}׬i !.-DCeYz>ZOt07?77?kݥRLP~`))p+̌wr2ka2vĂ9\c{k˾2)XZb>+]6Qk2W؁Gu)`τ-M5o 0xP^륗_wտ;` ME* WjLQ0Ȋv7J1.=PG,\Y1A]tiƍBPw¤J}ˉ 8ų]|fF{Y*}ݥDس;9o'YܜT7_IJ؏1hTy"K.mٲ9VX}RhdX g *!b8bi,.n cx~|םw\Kvkx~ǹP-=KE_#p8׈\r_=5qLXn"?w!'O~.yP?#L= OF&N2ZUprTth'{*o\3:p9&\2%@I )QW:9L0zsU`(:p'K'w.yMF2 d̕`|F esstŜu8GIm P.Ԧ$x?!q1hHZ}?tm.=O7U%Lp8xx(U/õT^R,bM Bf{Sv"Rhn`!jrA, *܏8>wQ$1zB\&8]&Z a3>7z1W%Ǐ 6#ג 4%gtGs.‚,C}2Ӯ1V=dvQhe8D[+{5( Ri.i+(ru[ IpLꈼR ZI]Rot1}xZgօߧ'VEȤMgyeae9Ou;ЏW~2}Y\-d+ɿ_՝åph!Y NZ8o=]ɊeeU%G]؁*9Mz˗;RD.K/RJ&,*0 Ss"{ɜ:I@ɎG)PGd)ES?Ql~놈ժJTU& \Km6 dXpggoKh2őu }hnrY},ݥ%{^v\);֑ŹZQ;MyZϦ&БKpۺx-DK[]k.SWhia`8'v^?|.D\\3D]ctulY2k近֪5U³vbKq|rY!uX8UZwHgsYI`&e$(Rߖ XΞݥ#ҒKBoA5XzkͿ*C4@yϺ`ЦA821A, n i8Z V@Sd;5Xnmi n ņ9s X̛˲,mv}=c1ӕC%' e7WGKY1Rye¾fkZku47#;KąJOef[OmU3]W_e"+Wӽk 5KZcpDu6 Sm"Sp~c>?Н+emݥ˫-%ױF;Ë?o9>{lvun5[ncH=1|pd{fy]E/3PJ,jE6`\3[q655,.Lr5s]K`JNDexY&q ˙2yȕR̉,u/&=&v.-}#rZ:J(7kݮef\}1Ƽr>m8FuĆBW\)wOz ;kmM.K_A&&dwže"F],uil%7v5[ԁx)JGKl˝d֭Nj/N.tY&}_x59!H鋥v}WZ`S_-$_ܱ.)ԋф$]ʯE'a>4Gbº#hq`YV; ᨛ#ӕו}ͯ'+rj"|YATYE1 =1X$hKޕB˰st9+KαPȌ@u|Cn],}=ER9P#?98Fe%6(=F9 K]g9GCK5n=MLfn~>9]ZLڷe6ZwE .""wUn'\9Əm`sYOhn~%(Α&I21gg4s6mNPN=1Vę fP|^U.(Y%M~`JlnncGOl䚆`Lt'a7EaZ0{]U&o*B/i,'߲"9'7HѦp%F۞MTҟ:wM.4uz΅ܧ0՝AA_Aa]Z EFFR|X* 9OIǣkK/N>B(*XO PZ$1 (6.BjJk%rք*YDU:OgZ JQ %o5 DІ)_Hc%#R^ szY& e']"6~gXfՙ$g\&`v6U CŖLH!Tű$ɖ֤?p=4%غaUh6;+M },1KE:E:z$o2 ۗZ9Cs@k?HPbK0hk $ٲڑbuU齽APLrlL2ѦpË ֒y+A7onMiД0QMGK;JUS-؁TbyOI#_+'92NP_ ^ Ge;Usszx>ZNKKQx,TNjW 0x:+L™}Ge6(nf4QfNjX],Eo|}U2NKhW>٫\~2";r-Joi N0)r,Ɔ'Et-Kܾaڊ&lK[K͘?S$l(Uoj,Aط$pb"J7H5[y» =/> Z,K |%=|"2{Ӯ(]xp=Pb%^OgtNxBkrr\b٢޶níj" ^Wu-)wݗk-"';St-=>(}yeOלX̹~Nkp61f5{RΑ[%'lqW>с (RĄ 'WK)!EF&$Y>0DmVcRnK~aJ/<'JD_څe})8sSu^tVI -q-C$}D+sEҭ%`]r,r=/{Ea@X[3`YV5Cϳe6rj40Ϧ.oCLpEΞh<<5P-#s-- N] Gk79zY`YVZ ԆX t=@.<Ͼ%<܉DOq3=16&@%]7?6njwR51U%c1]2mo׎Rq0eY#J:DW,. >br48zxѨbiLGĚ' OXiY6|۽}׬epM NS,'^Mfʲt?y*j%m2hN45!9$"3rqS˧.,\X:ٱcZwO>3VU`8+5J}|1ׄfp?`cƆ%zctjǢvx&XU\![7Ao!-:o?H%Bfx8̎B[QݤGUkn :nQ|V_ֵDed_fjv˲!!Ld94OW9rvͩlPh'p-g޸?eaPٞZew8F'QkepDaM؁C XIVVV(2Lǧ6~= 'zεDǏ?6h\ BҲ k6aRfJeV{Y̰r|U^ɩǡj4hrS}AWWzzA ^Г?YЎZ`Bo͈rG(¬MY/Ť$$BVY2"E04E^|Tb)+;}, B74\&oA!KH.$K'Dz)NzYo?>3ED+D=>s'h#AcJ vRÛ#31?JI΅#p\%h̎i &=+Yptoi0H^S7o_٬]Z*PkI-3^Vr-Ͽ Ra6PhКնR.35Uh(_>ma h0U" s+ziaɑ(8+jn7W"k̡*QkXfzٵ±˻]Z \? \j\iΊq krE5()i ݥ1(11:Qj=6c+fWZb.\cbiȆH4̅6M(ڑw]ݥHd0%V[z1Oez/]:@ 9!"a(V\a8==Wݓٔ*<Ë (g*'tV#y{X1v;a&LZ9е_E~ Sk^q 77@/KQ4 Ơ՘Ëbkqe,y\U$5vfuc4g-xD*sТFfm~-*ަޣ6sb`yXpZ q+75vgVG/Fg8nRb6(LQeD 'cyZT6_l*F^xtJYصD,#AyuEr6wZ Gp$VVbO}`r sss;<0XEcգo󖖺KR>47?S/@כ.Dz`VNcBHD9sjH5G_Ghh1DgSY@?~>S   $Tr /0vkiS+BKSheib8SG':}pz&:uhCv+ ]w2l9Bg2HEL3"=%YIL5u}r_6![4=[ыjTN-.h] J> ~CZ.E#r)+`_Lƺd^%D.aԵ#3Q\X۲3ok?3(K}#JJtj14˯L29&ZSX*KO3R,>sss#-+u!7C BӹS˧ u.{ B;|?jXs'޶u 2&[$sI/uHac8| |)1[lL+oCp35ow­ 6ycEakZpđ+q6w99.7pL\;߼>?ڧ@UU45-գ9L=3xZiڧP -[D眙IgZ#ғtYy5|*l}o[jA-|27"ZΩH

U-GOIX>l޶vܜel`v-rek #F \1<]μWRw}Y&uڲԄQ;(_*{NݧҞ( rˈx=zbC'1q=Z|?^;Nĺxy˝N'DYnn]뤿{t&ҲyK˞.~RL ^z=eB<4纱4;vB[oc\3ڣO".)Fs>9!9E'>?iG_d '32I,DE/\ cNv5 k~󇜞"@s,0&!زyEs ox,={3|{q `Չ9+d9@5p|mބv\aHba6t\Y9KL0d ۰arä:;P?#BʀԁҭڂMPYPMNaP% WѧFqB;tMV?fhi6-],C0;*`<0\`ñ,-CF=@! 1 's:ZsԊM;g(LaGd sn&Rz \Kp &Q MO97oH7X<+sLOO&hǎ{iYm߾ݰ=AF2ܼi#U+Ze0 K;7AA`}~My_o~μ P!-Z>;xh2cNk Ҏf+>b.&+'7l )=C,?I̹y U<q-`4crT̵}>{\W zoLMm7#s"M٭feJ kC+n$ ri6WA W׾qy8 anٹ/?~?NJyNںuׯ;NӉ': iyWS8 =|_^MzczԿٳ_d9<v!>xʲ#WbuץuMI]VCyYN.[GVeYî}特*%zԦM_R YdeH5Y]KpX)&eWrP5p-At_uql]5;X]=_Ұ{キk7 7SBmb[o)13SCl+f٬йֱWS۷vyo=|487c-o[~M &f4JZd"xw,-usRcN{&7lھ-5J!+K[1*kb$-K!O:zʫB: |Th!ׯò4Se>!2gffv;q䤮jŲ%ǩ_{1@Io'O>f2 e\.z7߲}0 ` m3!9-$ of`x?7ZFػ*'9i*1/10|Pr &?]o=FЀmfvHU=s|UePNi+W!rU ˬB4#N2m|s^j)H) .tr]ra[,K}깿[CbiYs~'iYjtIr%&tDNәd/=E\rnE[_){G֯NB::]NZNYZ˲eoŋEtff{UWVV ޷y믟Ҥu[AQQX(OWVVV2+7lxM%Br,UY7[.\<DCMew-,\Ǟw-QwN6B.IO,y]Kv`YfPƲB|7ڻ!cB~6͛ 2WZB} )(,B+MҬX:.^TwM[Y^v`YfPҲz*1@j4T۵qTr8, ̲4VҲ+/G /\cOł 3en! "͂g9st)թHUd;$'JgYfp6Vfu, (TN;CD7wS-&jIJȅ { ƚ 7Eeoyy*(L}[:Rv? 3^Nnq2'|K}%KܰanqC7~է6hǘŋ~Ky;ޠ m[?O{χgNi&l/۶m?{(6lx7܁v8OLnp̎b\.Vz^ʫ/ 7`l޴i_>yv`xbuI+] ~?xWm@}ԳRD&}Wn`xt<:Yl4E?げV zjj%AP,EsԲHɺo`xXN8SȉP|Ow:O5沴 ަT֑2C&v˗eq]x_y뭷yT͛$#RKre)BuӧdMgѤw1X$Sk(3jYZ1aS*3MHbĨ^z͗_y7UyӦ 6aTgYg O| fH,I)ul^hB`0t͗^yET5y'wEJ)QI'=GќnY˺Խ~+`YZkw.TlgBWz V&7lܲyE1lݲe'\|G?[o(,6NNNJp]nĦzWc>ڰe&ᑳ!&F>x]^'B)+@Ѧwq]wқoF]l^;:9>x]BȻD`Ll x6Z`Y*}JSljDwy;rywBzbn6@$K&'7\arFպz;}ܴ2Zx^e/wLd-/p +JuiӦ_Xqz^wzrh2iF4nne~@5 a2[!6iGY:KY dzx+CyYrHՏKqw}zgY"'xV B2 5RWIJeBklޑ*-KPLтPt+7M8G&a]ߴq㦍ZN] !\,UqdKd sKNRah ڈeFeL5nY]ew,T7v"g\C/~Hˁ+l,lDmJ0A1ax65u);rG)~)6?r?PjQ߷՜]+LFl̰K']ФR=j-NvyT&c`88%8%8DC,SGA&1^/ V5 "II/흎T'lWz9Ȅ5!jĄ&H)̤MtɤVD UVh:'ә$n'o?ߨN?OXg@J(z*m=: BHLˣZ_GI&$!9B ud3!U} M7a@OW.x^J&Dî05쒕B2pCt) r4OHNA IDATB: 1Z"_r$c%pYe)zYC`YC`YCXLdgB%TWyd4Hz~6ÙuO*+*/'R*W Z04[D=D"&KՑ׏WdNzޱuD05):ɔ.e++,na21_T8= ]i*:3V sYӺh"=Eu3jQ_ sYdMIӦ󥼖r\M:0 Y.9~wA/,lNM.KA/3첑.tjt-*V%8%8%8Dӹ,=A.!/gEѤd~8V TiTER@Fl$wRD)?/˄?C!2GhiĠ&%Awվ'.s%,t~z::&7sYaK;O,FRMJO#^B. E;i.mB-ߗSxz^f"0%aqQ,@+T7ZR+vO"3Rͮ( j.C]]S؅qP.]2Odِ& S2K0%8%8%8D,ejJb&('O67Պ(6tO@5&n]R(y9YX[j"3øĽ˩$7`䅣yB-f.p[r |5VZh.˼He$+w.- }iTwmi4W14UtM*qIE-d8CLܼ10\*LRfipF4%%Q֔wrpM_Jf՚Q@t`䵣"B`4t͝-%8%8%8fFriմSGQ3IkR(e6o{}EdY;(m.tY-%8%8%8~ p4wԮ ΧcNP+$ @;.Fگ5zYC`YC`YC`YCxMB̙i$38,V#]#b"pw]&u)zY ~% 7kx@ l F(^ܚA/KprYw:NFJdRhzYC~/˼.>ܯ7Z h4%8,[$Yt4~1E˚P"Te A/K}tw3&8| zYC AM9+W`T!l4lR0u/)e PW) 1 @ݱ&rL 55,סyױQupp,KpNL)} ̅2# uz'e\+AMu}W_Pj7:eKqls_*X@Kb .mҲh\v$b۷XXE* !< Xᩧ7ߪI1 \/z-z E#̹ϟPnCA`5wqn7) hO-yD(=+S{Y>SgΜ9CiT-k?P^f,9Fy`XG`ܹsΟXv)jh7޸}vac 7T6e%I]:n`Yqm[^-<,////Q4>ɛooLxӉ25{4U5ݿfVom[mۦVYLcY*Y5ƾ]ƲbI,x3g80P?yJbQ+Ѹ/ٔ:RuSO=MM7t7}~|0Ʋ,ϝ;힧O%G>a,K"X@Lb,O<I*~c׌K,K(OQ_W-Xy`TzV{%U(|Q'%T%lcW^ȑ"h 333ѽ;wey ˞Y2=O*Nr@Y'u5y /v(R=lr=l۶5o1_o}ٲl.L}FE:n`Y1I>ryy'^f$8@ G~?e 6eeyܹH[ #νsDXP7X@Lò\^^#On|mVZrU|Ea?#yUf|9nZۿ}M;%:ťn^[ޱ yox/Px?[l{n݊e ue 4vm-?:WF6C}+++ Rde~̅o\,f_tN,K-˿ tܳg={(euuuusy>y7,,`Lۻ=+צ#FJY++ RdxA=>[;֭\}?es}٢MA o|3ױC?JnTϿwwSjE57,`L'ʑ=Xo5 y\~ev_)zx'߸w~p;vX4l eׯܳg7}*+;P?}͛7}cXPX0^K맿h=HLWVr1*~*j׌K6oٵIJy`l`uuޯĬ@2.#ƴkcY?a=6;C6ճl4sJ2;FůYE_o\ͯE0~eU+O@>~eʬ_)eۗӳ7o5Xϟ?vu/.u3y䑇+KE+czz嗧̫g?{).,K'|*ӯ?2~eg__WƯw[}~'ӵ˿k\KuyajW#<Fi|4կ.v{C}++ (Rd5~ee~eėS_=y+\SG a5l pu܋K. ƻ_2o$YG^z׻z?LԒ?gv𱥎,`L[ZJ['2Y ++SMJU4{߿A{Q76H"~~eM4p?]YX01d\3],++ ~eʬW=lAAXNOOS>9r\OT%/wnmw_YUWfGA"+WF,-A/bTxNįc?%\0-05'ЉϜyCGWW9Y޼ W^H>55 y$׭[ Mz5r5Zoʹst0*p Øܝ驩4_ޕ+Wz2 "URkuccTԟ,T__d+?Wf2#`N〡%^($~83z`YVCq;ǯ*+ JUnvݼTn{w e CXk+U+Uf}Y,W !g1̽v _9wǐ?@.e c8 dUhRWW9Y,WyG?mvWa?[r[)WR:6].K=)MHVwS$[Q_:_]]߯${RٛH0!B/KSpCq;ǯ*+ JUN' qD _{v領*,3~%~eL_W@B4qw*w_YUWfGA"+[WUH@Fq&gR__d+?Wf2^tzk){^H欔Y=/UVk`gSYw|2qe 4<º Sބ`Y6++ (Rd5~(ȵCe#pYKK]W䲴a\A0e~ojk w\~ev_)U~%!12C P1X4sJ2; ¯YE__ 0 @00Ff]UasJ2; ¯YE_9R~e40<'{Aг_̰,fCn! .Lϐ ð2 +L]zWX@ y\~e_)rWfX\pZS\xdJc0gcs;00ؐw_YUWfGA"+Wh,KC9~e%W_WƯl_I , $ꅁ@}zU_i4W~Pʵa`0 Bilc~#,. Fղ (z[rw0`YT++ (Rd5~xֈ NaBƵ_IVprP#.*r a`8@ؐw_YUWfGA"+0 JN``Yj(y\~ev_)`:$? (*++ Rd5~%~%(6tFwSi8P0`Y7m2;~W*VO@>~eʬWWa ~%%ZΖ~%~eL_JW~*˵|Azr^K9t_mj6}pAUuy <7;H:ywө$nJZ0 lZMQ`ݚ++ Rd5~HD0 .\5;ܝWVr!~*jJJQlWrwc005;ܝ_?{2_3$:vb/߻|%_'h}:]oс/%~^yuTT6S,b%%f^/}D4LD|z%͘_\=z|/וIKWoVy2P@LPOY/haLNN~ۛv|SbN%ՔjurUuAXp) rD%Pg8$Wq<::tG[W:k+mgD2\>qN+徒zӪu8r_I$>ʵq"k#"^Euf/Xf^wJ /5J?Y~U}}OO^0xe TG&ܺ\o7~\t _),?eS(ND|QM*~7cq/%)_Yg_ @"5O&+o͍ e TGtG6.w+2_Y _i~VxZ\6ʦWYs49}&24τ庮~:[/||ūzjj~=NO ^0zZEWzesrT_ÜbJqOrmz6EӫlޤձCY'^J2@4J7į?_zqwu`lLw^3m3@F69552<ܛscLQM*ηEdIaL/cRv/Ԕ۴1s~ѫSSaCOOO_yqtp)VGtlWWrx4ʩZ'WS7L>nˤ++z+XE ܂IF~dxxd8|-OO{髮n[; @Yqv(P_yLҾ[ +CkJc"+'VSK+ɹ֕%|e,y){ǯ 5Ґ6 HXJqiٷm۶fRJk@Rc 3ͦ1^PbaA "GƎ{#׻._`_b8|eb|cm'Vۖ.hr:?4_ _ @lOz뗏>d%. E54~USSyjoULMOO==sٞ,yFEQJ:RgnBƞZW$ XqI$"__9i+}O8|e|ej[dm_\;1r++Az n%V[}{sW3{s]7ox usMै$(K ._E+8z{uL'|eb}%y7ߓ_ _i|l[dm+ŕ _ _ Z/R B>r3leJsW_yE:|u GJ+{JF<Ѕ_8_iԊm%wNW14`_bfNw{wP4M7ׇ?M6v[vؿ 00믃47˽afv.$nL@@YTB E+'|ew]:_ @Un{۝(''/˜&|e(_V 4J#QK[oGW[jsJd.Z9r 7`p HH}%MLL-NwzWP+YeMm`ՀR$}1Z?ٚ3ƈ{/"Jaf\6{=̳nU7;;w[lNJUUQEUE{ղ2蒁F,!" +ǧ<mWQ,[q[0Rh _ _ }ɥoq=rW,cǏA84&&Xv;^{f™4QVYnj&_ _ t+ ܺ!t_Rqh1RmZ!p?Qqݾ˞~ٙj|Ua hH0勣Hc3ges^{_ @M,މo'gJ4Y ɭBOo_a`R9Η2 |%ukx}[K{uWQo9bc[]- [%yeG*ثP+" + WPۆG+˲eM]|7\:_ ׂT74A}{+-\.uj9???7?_ߐ:4h ^ *(Ht+q$jie*|QWxoW*vG~ĩN~϶V𼃉cVJa1[,KUUE{K%ᆁ-=Js-}5X9+A3Q,H#J"]e6MԸ rD& n772 v_>6>>AD>wfxG = mf6ݲe[ƛ:.T(Kãi0_I'&ݖ+щ z+qN/_B4X>qc?AMK²`V(D(7l ʐҘزy{j}X).WKa-5֟yg2T (K9x9SS;*/'00P~Wn%uK4;)jhM +3K1B4_ %fc}ۃ J԰|h_)˽9~%xJK)o4;}txW5>82ܱw>eiZJFr]4`VwsaJP@3wD0wX-{PAm|%esW_ @%-=_{=Rλ|:RB+ɷܵy mZ` jVTB5pⱜEtqZǂJZ^~N=X*8\5w~9Yӌׯ7X2i7o>7Hk׮fOe QyД{=0ҭWk @MOT_99؟M4tT_YY~PQ8ua1v+]) hKy럚޷K/{Tڴ^+)ڥ((Ksw֯7f[t ,y# %FRIVz9R4B}%T=|iuz?R&W-J>wZKFE ]/.܆RW̔-[6oesgOfhF2sn6T gEMB(KI'4D +eO#_ @-+݆q}h-hBkH:|aRҺp>c Rg WUޝxv*MB(KI$Mn}H K԰|E̙k| IDAT%DmJͧѰ3\"|;DwyV'0u_I!.(K /\Z0M`ܥ9(_iMk5=юѰ^x8^ WUEUݺXb[oK#r̡n5S1Jo_s8r$+.U@xPD W:2_t+Г-ژD+叔W{:_>m|%,LW~N+[ޚ6w<W]u-a`Y Z)K]'};_Ub-_x?ߺ6CQTU-]V\KY+t;W_o\1c$)ki_]k?pP[TfA{J԰|,|%HRlvhh/?w>{ {!-uT_J+]]4 Fu;"/>?_ma,6ϷJ6k'5ӹ^ӅH\;5@N~hJ(-//;vg}M7]s5'N/^p> &"JW9;~uoo.J[K~o}gTs_\+7o>ףuݰv6.RJPO,$׶7@KۑWKD_xǩSb_>x|K_ZZZB=7+) +ֶ~u=Ԕte-|eUWّXI~HRZJ+-r?]ࠡY΂J+N\:\Zv_۟Ř%%#"ɲhh\:K*/]i򪪛1 uM5,t6uEDQ[m S?1:)麮/ ;‰ӄŗWVMkvΝO?LC?m[REQT4'.U[[[y%Xsn\n)Ciqw$yPK0@x7ݻnڽkd8?ñwTf+t 4M[ZZ|/gY/~{ 94 ++ٹ˝B/ r-S/_ @u,2E(Μ9U+o0RvZc.e q ^MwM7Ǒ/xU /曯)ᅻ~7tYҧ88o;pɓ'ř#h}{'O:nݺ?nG?*F7|?Co;gs]vooow[|=OO/FGGϜ9󶷽n۳gzZ@oP_ɉBƯ,ڥ~wn,]"E^os#dtM1ktR`SEUUpU](n֡ziq(jHtIJ lBAE%Sǖu!ru nW( /h0::z9C%]/oVg$6q >:,}I_ _ @e %!+qM6_wu7|mSO=eJ"z{_.>}okᆪG.//?Gt饗-n{O>i+r}z뭛7owd7t]<<<|g4mjjG?~%:tŋ.~g#6~^MܙJ4Y7s ?786v,{!x_]u@^3wݙ&R_6={_?1;pwp ܸ w'?9>>'Oqwq8Lʭ/|aeE>vW^ ae-{GZjddd͚579 r|_Wrn 5knyPЬ?sY]"h@ce 1=x##ûw(-+J2?яďn'?9ӳ{ w^*&i? |;{`?WrmmWXwM\ۓb-%F4+9y/@[@IJxgٽ{W4W:|ew_z饿p t&yoW_}uǎbW^yettx-tvvOPxv&۾}Ww݌Gԧ7։G?*7_Bs^?99裏~_=pY?;O{e˖}k]w/~V"$'ZvgAKGgr.<ۂ뺠]ٜH zf6ϖZv-'Q4NK+Yx!oP3UbV2,@JIJt 0ڜtWF ~lQ5475Musaa!rUÇ<,1ʪB5맾PX]YY1nQUUUJo8UxerT {К@Yh޽kdxؘC~%i8|[w:l.8|eeeffȑ#?Ͼ9sVUo],gqH}N,p?Uׯկ~e}l~~; y$ٿz{)_6nxr-wq/CP6uۓ:wy'^y啖.'x|eyO?8Gs9%}Zm9#(K@tw t7϶TΓ,$3WSWP^ t|+%EܸqW_/}ID~v۶mkvƍw97o~;YɟR>֟?~ߝ7?رy{(yTZdQ}%YY\\ďUg&T|sJڵK_czg9yĉ>qϞ= 1GԦDŏ{F:y5wtt5ᇽu?.--U+]#Mh-#D}%s_Y^(K/pmB'W|esO9'?+*nyfǜLMl X_|ѣX%JnjJ-?~bӦJ~|e@_1,۴cB%9]7<8<<<<}mW%T|J8n0kuu?C?k̏'Oܺu[i:y<_`qq1+igl)_­]".$maFl ^ Pٻܾڵu]7 W)e J3|eU8sTFp1W7ޘ{G(c/ߊZ8|e)A,綄f K :,..NLL M@Z@J suKnK@dtfy+䢑 %$ugdxxddZ+[1 ?<77̜x1P#Z7eio~]vFom*AWqZttt]n,5D7LKi\M )׈gW򑕕G|#~849|%JQGGCF'IJG%x(Dn)'qt %uݻ4IJu}م@dYK?9|uY7n/?} f'~_))ARnn1  0W |%a %d9NA'h|eWWڻWo1sxxX}{tÇcܷL&\'>9J@RkjenRj)\Fd9|8*Iʊ~?д@Hd֝y<*+KGG-=Ē>OwyUDc}(644]J`%(1&t˚JkJ,{$hTЏ uR\|e- '"H6ݷo /0d2s=n?j⠅4j%X}ef> WvwwϗС$c +Д ԝe Ep|+K":۷ow$Zr6ͤ++pgyyyzz:⌱K[f +JJccbPgu'DI/,FH;v<]]]W _,//ONME^< +(vz{4PI n+Kb;D_ڛ+cppn^xa֭+g.,]Ʋo%+ϸ|L',ԓe Epb}e_vbrFb׺оjOz)TtW.)zȒ\lFͷW4ME&Wξ=L.CWr~1HDXe'M 2f(Ku#aYwBX 'y|e&v[han Z4r-)n+Ȥwj+]V_ ;KKK+_94tgX8"~esoӸ0(Ku14]R3_ @MiJΗT!8ǘ`b8FpXCsTssUUu21c͌1/>+sn.erj i^M_EQ_6TUMSbUjJ:JU(Z)샪Ð))չ.3kRUUUתyv1eκxnN4v1Z\\^Vq}C=uxyxJ6;rWQ_' o'ӬDjGr|%y*˙*޿+Mw_9͈(f-`_%:ЀYwZ+?"|%hv}%y M|%hrqw{勪1JT~<]J;e 4`֝FJ-;STJ!=~ s- 1*̊_[?Q)%qƌ3[|?iψaFbD2'ȀmQmq-SjTUE)S˲[-p98cYgGQĚ=9RZDs,cX'T*hBXLܼ>1W#(K1EQrR =ɘPfffW#RTJrUL6nD4?V8YlPjLeiE_ID2ei-[j`ڟ2_T&ʷc8gŤ9)HNĊjsb̖aFWJkfq^6~ _M_i*HEUU+KkZ];%e)JMI s\re)hs[*R,,,=z4c|*fRH 3p;;:(V`_,:GMWxe gU4Z֝$<W3sT)pND}`w}pe¾^1'OmGÇ#/nJ')l#bOO[J=J5رc %4#P1*sCD%hYwZWMYJ-.R _*klkIiw+Le:w}5}mTKKJT+侒JI6X֌hIJlj_IC,{s9{7Č;))wJd@]jGCeiu_iߗv+?7ݴ6B\RfSk=J[w}+-AfJn_)Iu]-m3+>+ Ksйy(.dսTt IDAT 7htQx}|;^|b)XLx$e5hT,.>`u=ͮ Y *^e/[Uy汱1sqJ;t;FȌ_]mWd>hB@P#*|e|>_r+?7KϷJOL:m{Ð<{?TY~,\u\ڲL|icӮOn7\Ɲ<2U^?*\3goqUa)vfG*!py92F*ɮ?> +~e( 9#絹cNXCѹU&v}|Ii;qt8|^Y;P;pL1 );T约R8n I췌NFiǂJztWX.+Sl 9bYR嫂̹Ʋ ^^âvhՐ*ڞ;vꫡ;pb,KAYz*%Wm[?nea/پ}\ufK^9~7}$|%bNFpJ4b8hYwW<EG!%jR@%t+)ӗU&5AZa^ǮpPw++6R }5===55exEů4xe 9ZWÅˋ0%/_We 4Z֝GDԕIg2."-ίW=Wz2T{nI6t٪Brc.Ƚ`21JD^#ƚ\GWϻg:6rZ38bT+T<<{@2Ne w}`Js}JCiP++Dž<*ʆN5NYU} KX\x|sq])h>s@(1. Ľ/i6OW,KEq_eivya\%Θp[iė ̫@UUXmbY:[Ex1\cz5.J'ϱZr+yN8WlA0ifPm> /]2?P;EZRԔeiB"ܕ#K{*K댸+,X&]{Wf2mE:PM(JʌeI-e8-^\]v#<K133333~}?#ƹ}e^/Ne!-}ޯC,]ɚ/h3@rKCe ڻ k*_ID]s^6ZWJr}x:+oWQ_OU7{+гpئIO71^|WVeIXelٲcѧ==95U$ǎx%z{mg^g)?kA.#HPKeiE_i/\_; S=W&"?ЕI++,%ZW[YjJm׬rnn#K8rٮ.SPZbRI1N%SD\ϠMF;'ЖZ;ND|e}SrhJlDy=jJ|:ݽiӦJ,^;w^Qk]~Z0~e0_M_ 'PBse݁tnZ|taWW|ɴ |%WΤּѹEVR=|eAWWP.KeAm_ bD5To֭oMMMyv}e:3BSjԔ럅NS'F%DYw+ʗOO./.WZN]ȶ_I󕋬m&FԚІ[A3F]!|%ռseA-ɰO==U!ޚK/gW<6[bY&ʺ_鵡s6{3_ ޕvc _RU|e}ڵvcSVŒfJƌ)L$9ȷ . .6ֵQU\"àALŧ)!8MDfK"jST[iEe eE$t,K&TJQUbpR:cY ?@ NZ,UUM BAӤ[,4+/!1yGGj\4ju]缿+.hL?۷m߶jn_i$>uc4@e f(|x&xt+V,hj;Sٝ,)Nhgx[ݕW+I+Oi*eb%rF"NLYq~pߖ9?xأGWw)\n֭>]MMMo߶7gǎ?vx:o}%vkm۶xO|e(YIuj@4Eʈ}=LlpK);f?hZȕ+cHWc}qܘo_OG_Sq-:ҸWƜbB,bOcACҭZOL2ߵ2m 7b cF>`(1b,,.L+ⴹf(LƘ-8` ,¶W2nE^)5vb(p Xq-SRicRsnTSefei.:/'N.۹sff WC:[sl.aeegggooo|e7&T(KDYw;d+1[/ 2+Wf:S}َLgJ !m&LV2JT[ߞdٷ]r/_)r[Pʹ1+M:;;Ϫ˿.W&@<4Q|Fq2W,_z+3lgS/X__D/H4"|% D]n^Áu4cL)m~lMLzu"ںe mݺ%ɾrnnnnn;zg+Z^-Ĭ4VQ[;5f"A1~% cWX\(K,Yw;d!6ڕhqa=!WF޺ʾl5p^*W+]6_ @6۳3{s}>412Ƿl,kO~.I-VFH{g r޴xș^۾+W&@ 4K|ewe]gwR~q)z+CZu}Lg*V]V^s_Y=(Gu!~%?I.iKg囧`6ʏ1k#Ƃ-[ޚ2>̚*j:o\wc.mm,-u FUaڊ*v jUgZ0u祗>_ɩ9H7mFkyf@YȺ! Qzhbr&f_|t_#|eT_Le)㿶25Brfi #n TƍKOBya2= {nxrj[2"ir.x@e ,=sڶIqEj vnT9N!$R)kb(siV1fl;cǎKjFM _ @@Y̺_Y V1{&/c0+CWF Rmv画ȣJyyyMvP(\YN+q6})_͘dn5{(K;{KtG,++8p2у UPQ,+Ɋ].FP3^5fi3feFw~e :ɺcJ"=aHcU\+C2W+#VWs"A8< q,k(bVD+̺.ĵ$.ԬUU6Oϟ8q-tuuL:m&Iv-d's{1-~jwi~1/}r3VnJPG,DJYwbn߶-Z_r=!w 2 _+#U,Jp.!feNV CX׳nmA"7]I7CPoeI[d sK>UU5Xn^ctN9PP;_,}o1v 2_"ޘD9_>ªoy qy%ňl%5*8W-gp![ _q+ƈNk5kJNp 6 Ъׯhjp@YB Dwc*C!5GԕWWWJ|e,is(52cfe;.)`/#xSPN;\TzNK#*cY ri ܜ}+T< )qͣL ʒlDe)}ldɂpL׭-%8_(Kj֝3PWYyW1p7|eꂯ:q`8ZP &ɹmڵbO/c7i1<&:ֲI$ (K!^7FGFhd"stto1#+e++$_{>_ Z(KAj۪FK6++_ _$s阣=\h),n Lziq]HD.N}D8|i]2p#pb%0*ŲcV q-۬zW ի("f P/+A2A,KAY֝7=̳*??WZ|e wyWF.JJPW02ZV8U_ Zd X֤S _j=!w 2 _++A pkIdV$mk_3ڌk)&R&/BN=᭱QJ-'/ɺS> om2z+C|e2buWW:!yo kW6U OZ_+ꡟ>2i8|eZ|e wyWF.Jp&Ĵ,F<H2&]j^j0看cDX>:5Hc_)kIX22˂ 9m4-e e{]೟U_j=!w 2 _++AiTˣʇ _ (K>,ΔJr]5P끯 c.oʈ_ _ ꈑ?Z3F42Vg*|9'+:&%/uᑑa% a𕾮$ԎW+^$𕁮@JD'5_ bYp% Yw_ M$(}eJN-|3|%|%|o+ Zi GιR|GYa$kIDbMZUlt36s+,&n΋y4fT\?AU@1&TXbJq*|žf q`+%+AcQ\{֝JMwﺱj_j=!w 2 _++$NnK\0 Yw|x޽p*}72z+C|e2buWWê@ |e_ @Nݳx,0Cw +ZJgJJJ{3^e붻EדVQ0b$hH,H{֝{+wRAct +ZJgJJJs8h9 7#Q p>0ups1n#GeiVb90' .,8{w)@Dt `|eW1p7|eꂯ)OιlvO)ng}spy#ֺzՄrQro/be%'uϺNw"9 IDATo)++}WWW$R?Vh *4:Pl==|ݻw&qVJN-|3|%|%|oWf6+/ɻ9|er7_ j %ͺS+[WJ;_3_grDOnU\Y ˘Xig}ݿVܣ#3 aevK/(e9`Q߬;>´i"tt+m=yt_=>fFXf||ۮtWW&]>zss _[η:܉w-%gBSZ[b}cg;_Y6O v3]W lx/_zHdEW:e Yw|xٻJ8|8ē6˜JW|emz؉'>64+W+|'A+g}'NۋqsZomWr,Wn6@,;k.!|Sˉ'maW|ez}~g3/ kpscu zP# 2M|%;Pu{]?~NoƔ_ _;1/ vM l ++I`7ƪIJ,թoҩ14+WJ:GxW6k|I֎W&NwC|8|oJ|~إj_'|%hb,թo֝Fz%үS{wҘ++yc';5oz+WY4O56 _ @E@Y@KS߬;ޯ 'pJN푣'TXǗ\qw9zpN4qrj)Jv\s* 4t9e.Gùω["/SZV)]2K/|a?_Қ ;j}[)U'[MpGa7>rs=q8sa' j5pnM8sNOsTU؎ ~LD^|k<'>GJJ/q^6(LV-Ŏ'O?65O|c.]4=P1~%9s+'|_=^vD+/9熜$u,1=2sƎ蜿׌2#l-,SF\/nSwI0vzc\+uEsrli.:m )׉%*)շPEH\$Y%ZO/]TܚY9LZ_ fZʚcxnMrn̫Zkֱpvbsk76w<H_3Ra41|);0֥YwhV9O.!|S{w8:v"1 g߰i򈾲#F.JD+4nWJzۮ#CJasʦ6ʵyoƼ/_s໮D_ @'$|%H PТ7w͑n=j٩ +7nyEgmuKD+𕜸(+lJЈsNie]٣)ޘO>s=pK{ |eP[ >,Ec֝{Ot9z_=Wn4pUo4@++Jވ_٣\A+%k _)𕠵~r9ݥ d-+Cm2:А ^ِ/@kQǬ; Jcv7Yn{Lԋ=\++o7uӧĊWl8}Z _ _ ZT&VV;b&|%hY0Zzey;#*-^y}eW] _IEqq'WW/lѣ'NW|%h^0~ِ%u̺JG73bW9zWt]}1|e}e J*}Vlhh45a6&+Ae -D<^W«Wѷtg=_L_$JcNkyߏG++A _YlMWZzeygJ+~W]FBL4漿m-N.<̛>-|%|%h6k_'1|%*7NwC^NA!+a DDD!AYљ̸0ϼg^3:LD_Ku6a@dS ,1QqBYoUunV~RnթSUVs e -A wt4z/߰m;m x+:,+W _;y,Z>i4ŀͦZW ?juB8; k,%U֝ի _;>@o/߰wMH3{+A|e |eJ㵓N 䌐sxCW.#h Wk=qPQQ @Y@S;ۥ,Ui/ȾrsBك82k!+W/i%ݶm_Je+A*-:41* TZei!-+har+W]_u+h /?uGۃNsvn%j+OkC_7/v*׾o6Il9j8ڷ|eC_ zY@S;C 5ΐυf|ec8I+͙z_hYiJuyK@-S#|e + T(Khfjug͚C³ڙY7.3{]pW6WFߵCsv9%+]3PN|eC_ %435ɺI3ڙY7 b _X\n€OoJӒSAwJ:TZuZiTcT|%>e MK{yCCuLWRh˾=3{g۳f|U`.no?ly{vgɍ_|;D_YA5FEWe MKM^}gDam#!X|yĽ @7Z4_OlfnӮK+>)Fq5MJ)AF*Q 3kh J_: [(JP+00d I EOWRT˾=$y̴$2fnhfW%N!ֽ!* (KhBju'_gvCk7I! OTNb_ID۶\tTw(^j|%A7򶑣If _%V|%-PЄ$w/ C>E&)$V)W$?|;C(罶Wђ܉yȱ|ey7uZ)u^Q %45ɺ,C>E&)$V)WS\4;`nӮK7SJEk k ƃI')$xTRI eϥTՅR+v(R@PƼ"~p _p,Ys_ fYwnlBg3Bb}eH ZJb"G=diR_Y-_YNs_ (Kh*ju'|Bg3yBbJWnoZJ"^ZMC)JJPw<|em6j:9o%45ɺf!Y{$*iE]J,KÐa/lHg^!|ei$%hc3ŵ 9pqW6PWje MBMh4ѯI UxrRom d̞}%H+o)ֆS_Y__YW P$T?w/piI Uxr_mhVSx  n%h]_YW@wlơ=ksBb ǃ?]Ԭ I^#}ePKo+n]ʨX),6$QR;2Wdi!+~CB4t]r&M9sn+VdU7?kF-7҈'\bů4?ZKβYXռQ#gd 21#Y į &"C_0-Ь{wnbYz+N,ˤ1.=5\+klǍX+g >ohWMWăaa:uСCÛ6m뮻.`` -s/W6u eƯj@UlZ=WfV m*g݉A̚=F&)$VIW{nSrgm?xŮ\@^GʘoWna_xJJ"jbS|(we #- %40Ϻ>}Pq3k% xDdѿ2lg[rN*;q؇&?zï}k{'~. -H^+2G 5&Z=r٧i_ͿbJ).92i=rY4]i3p[[2=ׄPRG=8}`xɜrr  Wҥ4m„ &L8wp \r(K-+C,uèo.豣G~9SG_/Tq:{h[$R/O'h,Qr֝𠙔M֭[oÏ|r֙gҾB jw~sdƍh@|ey3c0/$|%h4,!~֝Ylx[޷T^/_Y 4 _Y +WTp/۞={ oΝ}y```ttˍ̘1w!z{v5ys9o>ÏԩS>#qtttƍ\pAi 6l}%}1cF%oc{9{ٳg3<Gַ{Q=zt˖-[l{/vm!/}ill̞?򕯬YJJ7h_e-CW q"=,B  $pϫRe˖-W_}SZɏ+CHNP'B1HcP O@ wYlvYwnѣ3ud RX|X徲5p]]] eCSLY^ ׿wi_(&UVᅼkҍ~_Vp _ @@Y@Q;ᛛ74z^yOcQ;l_ιꪫ'^{ǭ[꫞l٢&ەje3WYg8p/ѡ~zWOs=ܻkΝccc###sϻ.u}{_#3~o>|_'Ys鞴+9@~6e5rf?\YP?0˧gRHxTbWU\L? 𕠁FYw֬Y>$)u-[+}sgqHGt Kl` єiOW*3_9E^:J"-L&J@Y@#Qͬ;#3>oh(r~cpuܹs-[VɟR>ۧ~#G>/M~϶9rt%㎞u-ܢ~ܿkh_ÆKo_r@?0Ǐ?~'NZN;uS(VmP0T9NMGyC rNZWZ[NW+/xf~M7wܹi&Ue3GզDtqڵSԄ |K0aݗ˗{xWWתUٵC}0?>Cy,@^xgPyx gϷ{M'|m~IC+˜m9yHm3Q|xɇZnL*w%=ǛBP^U\SWҚ!ɹuF Ǐ; k?n]9e\GH=W2ЋvlWԔ;vرcg\####Dif"[dQzU%R @= f֝!2|e_Yz_y[nG7hkk$&3fؾ}7w#ha'"WrA_ _ @ 7*W49;֯gyttttt6//^dE  !PPT3Κ5kÇG[9Y) }l_h2_ fϞlٲ{[nQs_}ӦMs9|ꩧBO?~?>oeN y+mz+VI ~W67{_nڴo…}}8]yk^EMqq(I<~|$tTTㅥ_~׾v)%O:e2YbNnE-Oxˀ& 'eݢ, /+_?mJ_222Ï]Q(E S{!xb_ Yw·5oh(ݤCCC 6-OKJRV+%KAxGdGٟѣGͯN?*h|>?66fяnݺ՞sr-8,}dwYhDdҐ}jl^)ez̉pj~]>2 Z&zکy~ߍuϘ=Q1;':鎎{Zu;nQ U ΘkpK/'*SDJהXak,SyFS\=6ZIx`|u>+J+4]~ӻv~{{{,o eҾmӏ _ Yw"`BXFU';>+$zUNTNJ{1L:u%oGJL4I~ɓ/ '?ι-Zt=ڵ{]x}ݧ.]]]8:FJUd&+Y QUHdľ;Z6vz~鳸`2( #,~Z֝p7JiwLckN~3Y|eT|%2 W/\gAGGǂ 1}җpBu+rmz뭿կ~~8Nk / T羄t~zoaq_w3x |%hz00jf wvO̧?|eپOa\}SL9t:shh .YƍK)ĉk矏'>o|8)@FJb*h:/W7`"O3Vixxx Db7vӧtBUMu;9%؏*FbOwP퓉q=O>cZfzjYwvHxx Pgя4XrWRZđZJUB[o5|Snܸ_<9眐=?;rԧ`͚5<ȦMo~юYf-^CЇ>|>Sڋi.6_A2ak_nyI~|pϵ0V'Mj4e~]t(BFe%}gȺ~d?w9aGb{+7埅.A (K;u''c hw|&wzl⌮@;KE]4wȵ&M|%{nn!z.(_9"edf 4X_TD|Y^dl(_n}fW Cәѷ7:%|%h),Z֝R~CCgY> 6=<?r_pN<}N4"WW&[JPۛi}e5~9lv+gg9a}5G=tOܰ=?=˻ghY tM\]#s iת)H(K/ug͚f0E7^QplyCC))DʉOJ5+gϚyM׻c9rƽ'w/W˺g]ʳ5:jYwǞ.3>qRԕ+W\UGz+ykU_/|e8묳i;뮻nkkkCX7M^_)<1% ( N u, FKi e"!|2kTFOsP_ٵ4Mu% W=ty麮5;G= tR=!ɶqsz3OSpʐcYt4 t`pV qwT. ![8\!XA*)yxx}tf~@;OՖ-Θ1/۹+*zjYwpHx)«:i׽r WY~aTm["|K+JZBؑHXvOVa]sr|Aec!H3SdYi<yz1xF#}脟Y{7m_՝iO?)u:jJeZ,բ4!\/N*. (KJ)eNQ.!XwrP*J(KTut)KrAbh.]]8Ct.WWgk^{siYϖ$&f2`2WP(eX/eɯYcw~4.lbVpox>&+fNÔ}ݗٲS;k֬ i=Oq5Kz`kT_9{78JWoQ3bRڬEǯW&D3jo&}T%g3ݦ [IRJJf`;bHuώ a B3'!-C 8-{ ],T'NtGƞKTo`e_nW"%_ Wr;\RtF+ z׿ooP1|[nY0O7_+}vFoyvs4㎮"0~,4PjLuw74qp߷Dl =<:@jv*s.yՅmbԽV$1\.ߖuݎG9֦fjoo;:ieZ ShRX;!?gj2b0R\.p5gQWGGh7x ?7g%"iHjwՕrI٨<\>ODsiD#b7|e$ ttj>}zroO3e :Yw"EVy5|e'j*CTă.ɛ#ΠU5Okq۔IVN5IWFM+2ΛoYF!^t|e4gyIWXu !J@Y@ͨN֝T]>F>5sك=A }%P;Mm V%o1fW+?׭D3V ZZE/9.B#7 ꇯ fma-S( NJWJp5kd @@Y@ͨN֝՗"~,>\ a{+)W.[4U-5HW'"W(;~%|%hB<+7stax{XٳfWfw5,m}& ʟgǕm'RZ\H*K4 %ԆdY,+<WY~y^ҿS/ _I/uv5!9r>q5A,0=!#&&.X=- sXmj,6g]kuXñ,3MӅBbY&~4_*%(=q{AK3f&cݰ S'7>#w}G5ky;RbY2kf^Z!H';x[cYhÆ!PVŭ\t+bI3}1|WrHyy73O}%T*d݉Ye?hYx`kr_ig/z _$nܾŒXwW _d9 R8:FS9w|+jZ~wb%3+t)BJ?_I̋BNʁS}XM*e U Yw7Qe?)[WeC𞣇Wd^+׆u+4;u+L̩ Ð;˖.vJ|(ky:>:G^}g𕱟W~!_?mJl\r$?1xH8ѭ++C(_ ZZYK's1si}Lb_,~BP%ccx/xF2HjspajbY@B֝QTݔܑ4 O{fJRoTM_}{ޑWIM+vFrNN,\!2' 2 _E싽TKW7,9 }u0m-;QBSSNj4߉_fOs,K4h9'CAӲhfNSlQ_;3o>;~Pbf]̭KLl5eYcBHXJAd 14? J%΀V #NUy%bPPP%u~GY?-Oě̾|tywA5udWygL_^^X˨+%A攋Ӥn?k~qXYVWweXO_ݯl{U;$nf%뉽D4闧{`g Ν~ΙR3zS_- dŪ(Wj k1+=ImoGPs:WkOsY1#ʛrX=.*4%B%9j8(?9'W_hdV4K[1 ⪂k.'ThǼ6 Kgԗ-;S(+O+my.̴8m>*KєJzc~%'RTR^^FIUa]SUsf>eokLh_B#-eLDa7paU&;wv_@`f*vJEEH.ھ\1DY[s̩y ``8TIeu'|Hx5`7EpJe`率yx=uo֛|eU}e :k o-+#ZD6$:];PDE 3Ν}֬Y2K ! IDAT8|_W2|Wq6^\P!;SI_قdf u+Y5g}h[W:*2`sJiTm-! '+9SZ`t>_Wb6b];Z)8*6hܣ*`poQkQOY଎db,Ļ'"N%iְTNeiXISaH#ט6խPK)͏gvmw}wgꡇZreo_/+ahf뼘!(%3tfc/T!a0F$Y [}BUE贐 s|I]RH}#Jd>j>JJ0x Wʀ;ni"$}Y200d2tv5PA$U9i_,KǙ tKї2?Κ5o?+]@o_~t]H"2<e!gTvW(L.0̛]Rϑ_v W:2DYW-Yg݉USM+o \zWoo=_Y#W6D2` ##V 8/{vGAUgppo{lt/gKvJw8KIJ_^_Y?f%.Yg /;2&UQWf+D4y'_+NW神8 |%hJ\ZiYKeCmG$%"W*tJ _Y+C[omT F1~ҙ]0\JJvOpȐ_߫)O+\q׽On^^=pt߉޳uNo_j_Y7r$ԹqZ|%'0z%duyeg?ECNBĈY3W,hxi:W>)!iCN euΒ9`Q0=K鴏qPp0xG?>fnVk)sq"Rn޼Y=j&(‹ .^8"!% r(KKJf^$mZ>W(Kheκz*1E8|eD1}'x~gϼWWFdeWW;_m묦tfWLҊ'q0q-YS%7Hg.§9j" M)d kdSs9+fUeR1jdLHR]yʵĕW^[EhdrB7PZӒlf)΢tE6yj@)Ke@_ ,54NdnʳWW v[K}׉9:uV|2c:+K}%G\f_+I'1~>Y|}:&\(`_+}nl|!! } CIRaU[:_(;&|1:5kP ㏿=1c"[J|ӆrshv1>^3W54(H!lE JWj*+kv%lDȬNHmJJ_Y[_i[˿['cW ?ۥ_OͷOVL&ѩS Aw&hlR#+3 bA} 1 EzU2SgTWL ev`"c't`;yHj}{/Vk{-?eLpp(jV^%;C^?=.s-#}D3N%*6RVYp%ܼLgC++: ^l&gY|ywwwUl]Q9H+ݫW&{L홶6%PP&Yg YHJ)1}=wD"ƗO/kyuO# _ j2@dug;BI_W3z+5sTpDUg&e ޺2|Jbn:W٫Մ8DQ:Xd?'\JJɎO.KσKqIJ*)էb zY@jdu'pJZ0={V/0N2Cx!pFaP+/]#IJivyҊ*P3~ ^o7/nP[, 52ͺSCVm7WF8gg`[wK)ujl}岅S.Y4SbYA;@V`ݺu^zc7{aLnVdg߲ cWo|%h|, 2ͺ>$:[$Ex _w9=sfuѺ^yv+YRvCYKN--[JYvEs#uQ¹}ef%ϴFKJnǮ\E;(KHLD箰f+cχYqZ~om߳mxϳ_ͩl#2M _ _ @KnW+Yil Ri7Jjq_)–$%h, ˺-C+")U%OʲWd"3=g{2%/++-FD,wWWJ<؄ :6Ar.&O>ujtҐRskIJ4uXJ)Q,E1L10(.[WrTß?JP?@Y@du'\f̣ wT?uY+#W.cK"^~DdL{gwɘ=kƶά+j2}E ]tKwWW!,>}zU,XYtHतߡJW@e ]֝Ymlw`93_tn8j:/wCL ELb䷼waNH異8W cIϗ_weW%,6mZ9Ac@T|%KbюXIXr]Lq+Ae ]֝Λ}F b(rWWW&+A3sҥSLqߗ1+wHSD)fPՎ$\ R8ROw]KJRҙv׉\QzZ!WP#c*(K(L Ǜ!|Lzu,{EJs_ _ _we֕D?#t,^d)N1Kf6 CJ)=٨UU% bQG*x4]*3rqv>^K,|˺z d=$R'z\\WWW]M =ꊅ N2ŖneY#M{߲$&,$k<8Km\ xSW}efVw(K(|H8|e<_I1GT9 |%|esWkÐ(qBʈW9r4 SM\d>%w߾}9瞛ف]Wz_JujF{Z4Eܫ[Trϟ^q3Jf&r߁#c R$;$+5kֆJʾg_Yʄlj<3ohDFx1+W JJJ+~}%^SB1 mCӎsJIs񣦞3%MN$m9=TUzoc)LY*S 5DdE_>A]>pL’Λ#Kã`R3"kŠ_)'$I&2wH5-+@P %$&;=")PpHxdʸg_"|﹀2+y`~'?ٳgO%̟?=0*im~eS^<>'ɸ-([W&}"|%P ϴc;xs K$*2ɞ3d3Gr|xpCo}_JeVqGP|eKk,w.Q֝NenjsD"d^K{ {!qbDWWRTWWW&X⠪+90p.GDD~o_IXI ʊR}h~bRJк%";# gu+h޼y9 U<`0++}|%|%|ߕ _xWjtH;͎'He[[s,0Qohn2I遫s"5bl0 2s18 [NV- Sr (K#;W@F0r;eٻ=LY K$*2ɞWzbsEyW,['\=+ۏ%RWb_Yji=]!7e𕠆``8E֝Z yT|egJʠ+w${^9虢xi\.ӧ@܃>GsB<%9OLb dm %aC]Ikg+AmȺiN>^v 2R3WF_ _ _{WWPҞa-SHp1dWC^JV<rҤI 2RbROZʚ }-x@݀OYw˜74T^ KYFϬrD6j{T|eT󂯄,s+++3D+5ƥ(5+׾WeWTP0uq+ ;rG{{‡܍7ܹ3NU_3fV{.޲NT2Ve)%YA9FDaҘ0a_{5"Bϔ M~gBBo՜e}ª\3%+bQ0 5{xq[Wn8* W:PCeYYw"Sߔ3MW~3fe_ _{.+++LJB)]_?00Y_nʘ[,jKY\+j~pVjJa],Rʾjqz|H=N0e7\Θr޼,^.()q]2ɞ+{R?C{zzljq$__閕+ݾs#KfqQ"\P'@Y, eG̲WBf-g}WWc>|eU_<|%|%|e+0 _u1]҇> k+"$H4 ȉZ:,=++y@) WhBstOhajXRwtR!HSsP\$,EYwV3¾e #2N^ I|弌WF_ _ _{WWSꄕH%3k4kk$V IDATZqZu]o\.GDRJ )df3%3www۷au_?{D~Dj,{i=5EO юIAN™/~%|%H(Kpz֝𰘔7l|e_U|%|%|_ _설Qi)e )7N~=΅(||_c6 Y0:nlkyUW+ Ұ !4Qys;4̓RJ)e(K99r:X0 f f!N%$+iqIGxpJ6Ydݩ&;ߪݞWW&.XW |2I<}_nPBk pA\=o_~@Υ#|dbΐ.=ɮ1J%8u'|HxFo;Rf3f֭k<W4&|%|eꂯuW._d+AQXkƜ( qpBաGiesݲe;xqxSa.JS{Ԣ7O 9)==P !4MӴbh38ǻ* N +,HYw"ߔ@#iq|%|e:JJoeגJ\3eO4OM¹\e; )mޑXؔ9g`9%KŸB˘̟?Ut 3Ok bVWZaщ],{kxodEYe Eqn֝<|efr庳(o_ _M*(2++};uWDP,ˠ9XG\3H%cY(K4UYǿ}pī,-fրdi!Ru7/ \o\˟<=ee |%(K Nx<ŵcW^rW+%'Og9D++]x=.Ti_w]vηO=ugw[N ݿ5Re g݉혺:I٤&dkV( 54tVrw++@Je+9|%3W $Bҵ ([0*mѱGNj#)%rPI!"MhjD2H_5eJOXM׭.4 w_f4/Z {, ;2P "<>WW&.XW |2IkI,C\!:z`gO)RhZu]0.A- a@CNi(\ ݶQrr0k}d Sӡ g޽p~?Μ6Й*J aIDg17#i-W% PV';2.l +#>WVWn޻m>sٟ hس`ȷ;;fN-[x22ɞWAĵL&3RXs_L7 _'G*umOYk,L#")|BB9@sux^M G7\3; +#_Y%I=w/𠙔$Jm{ 6oFk0W6r%)z9liDlWWJ< 6W\䩐~ם1eVgW:|ĉΝ}.+ACe hiͺS!ፕ"A|e8o1+MM--f _Yv޶ -ڴek a* DDzzz{zzCϝ<01ܡ ==Bv^3x앬LpuR֊H4!qWe h]ͺ9@1WVe2I!_-V}̅o_}]c\koB _ _ iAq&Tv`7l~`_|%|%}a+b#jfK!/OCD;O?>q+G;7O NDsϘkwMUGp 3ů,eYihKJP;,HYw­\ +k_1|gKWK} l^_i}\6sق2xaJҍX2FeYQ\HhBcziD뎲4+.ɟDnc/;jnd}#gvLsBBə@V Ca 1NL!u{(K@ˑn֝ի _ ].[)mCZI%W6]kFm'a`2ݿw􀧐%6ęδg+rλۥxu zٵKr l"":9i"y|[[ۙsgI~p\lӻY}LOɞ.-vwkY{S۰.9: ;eGϿ|tS|p|%|edFO?~%a<8hx,-GYw'PQR׬\nJXBJ.\Â2d-Iy1,*>KAt9_2Ko̵E4_x,Ee Du؞.~%Rb'jp>1wXL,;}0SU҃]2u]NdRe9w^~|)I"zW SXBכ*y]7[bJ!wb\r> Ⱦ(&.-(o2Q{}m-b偯W-I]QOO%,۲}YCƋO&WNС-18n)Zy̅g20^wݺu{썹Gh+G:G;7OMZs9mM׊4]ӕAߺ{2)!2cuRt OW Ff$GGWle_7gw,qiJWJ;+J?_i |%ݙgvh3)ƃdm5vWWʲ} 4]OYND}{_yW^u@^%zmfj3vuuQU+iGMN S[Q|%:"Ŭ;1p"M+=2r[d\ofϢ ~o+W|e}zzNΟ,6/8l{;t++ 3KZrPJeQm^AJeuCٴΖKkZR9^EփtRUc$D1 yս9U3: 3 1LzLĠbDaƄDk4>s<7y܋`-.D4j0,8[GuW:vwu睉TWW:u9,erVX)4ƀ)\IZZ SO;u̙7nqAИ [v9Ξ4eʔyAniH#EȒD"B̺l\|HNJWZ%۱طUrFCr+ӇOdgQtoO3,ܙW"$#P-+G@9sW~ꝷyxd6kj[YY0j@"%B$Tbf[,VVy?+^` WWdx%W''J-+mz╖$R3l <̙΁KmfUUWibYHPYw\!,OnxeDR1"^f}5J ^%^)2@ ^w^= øKzKNWZNGn<)}RǷ$ƹ+2j~|Us@6fNͬY{K,:u͸RT.ө,KZJISerYń ryoKP&ee%IdI_\B@r=I!KT 1ػB)xx%䕗]I ++7WFWL'_Q,xޱVtJg^IpdzS$M8rITw3 bY2I\RuGg5TQ.<a3gΜ9yᕳgWO:uԩLR+.$^IʒYHWYwn!<ttZBc+M.XxY O'^5^f E2:RݽWPDOjYerJ}GEhq%lU`ɨ-DQCi73QQ Ȓǚ\ mY54D5D`¸r%ZPk!4X+͜9sڴiaܖe^9eʔSXȱB_IYH"WYw֬Y+ ,6瘎Bu?ת+xwye埜SWM2E,KQZ+?$w~+m '^I"73f̘1{wwUfmL2u)SL:I B$V֝;CjHJWU+9-(Jua᧏^lCJR[OEx%ciq<QT7sPFeIdjYrMMI'͘r{{䮝ִ!meev1 (,=2RMʲ\-!/'NWVT[.K$"IJBf6RIʶYHbVXYw!>i`~n䕗u6=S +7%^QhJ͝WFW +O]B@-gL$^i8)|1n^bLq*8.%,MdͲ&: Lz#L;?>qcۿooXl٩ ^I9Jq1mh օ$i_q']$-f,edYR[2Q6ԢdvYNe2fv9KJdI;wc\Kk = v mXu1-;=F"!KT +뎸WFp34{=H$Sf+z<)`݃[^={̈f G<ݱ\T̆WZbK]oBb g Xʝ&ceȧsx?ؕ֩=wk.P8*Ӥ!KT +Κ5k:?&? ^SW ^WRD"9C^9Pߗ;P-mj_ tak[^F3"!$&o'KoNG2&Q.0WH$Vk9xaȟʶXOx%n~}k,<<ۡD9s6Y9~aMWHd Ȓ@+IDV$V֝G9Ex<v+hi@ʷW*4%|5 >V1goox2+I%CZCf|DhB=&ŲQ&iuD׫+e>~,K%yi؝1LE @;:c Zj^M$YHRXYw.pJǺJǁ\!JHXZ[SW]SWW |Ο3x%"~bY}I= !^iH2P"\ٽp| M>d)θeEQ4bSxd`z3Կ uՊR|YJ}%˲\N$~-"Ju"IW\MxzinN1)T$HEP8tGxrxJnVrw},<A/o m?+~ml[#Qy%{^[1Z7tIOӋ;٤C1cR'.;Zc3lqU[!bĨ ?ꅆ(;ͼįI!bYHQ(YwCa#PdSh+!ebioM]uMm$reJY <1D^ishJH5D3c9;ܖYYH"QXYwƏmr*t^yYG{F(B8x+Z^ >ziWD9e>|m|շ7 gGx%ώ!l0Ā! 0ƌEc5r㳩.|E=YÍޢ"!24?_q^$勗eIbYJPy/M.JRq%D*u&'^Ň\|1pM1ͯ]Vͩ+i K"#_d_uZ啨}Gv6d U/߀&N@R[NL0Z.Y$޾\5ZjC2jcKKǸDMQcTM8! sw[H@ȒD"Bɺ#.dBl Q|ȑju+z^nV̼RZs[[Q!g]q9̷>yzt(ZcϛS<)J@RH7ٝpA*v6ƥ,|w}^X/|d,~ hIŲ$HŠP ')‰Wz?L G>7,hJl+a4rm\aC-^I"E7yC2:7%D$T%D*xug͚ :k,@+`v9 IDATmU^I?ے-}s(6WH BXi.$1LXdLw?z+˖*Bi:w Ӆc;>p$<c77˒L *1IV,YǬsJCyU|b$EM,I$Ra+;9p ڄ08}x[Iͯm5uUy=J)ŽW+!p(_" d!~%j+k3G`SHRE1ůLw~&dx9W_Q>Ƨ%$@^=%ǯ4Rc$¦T"dI" [8?()|.y W|ٵUWҸD3#t8Kkdٷ7P-vbM>~%Z7^x%j*݊WCTP>L*iŲL&(ᘢHɤ1̪Z ΅ٜDHVl61R(f<\b'YHV(Ywг-sc)Pye<&^eV:wk*$Rλ"^)~ҍ6^, ]VJ)z}@ebA3/Q7)B$|۶mկ-Z6G??gW+,8 *?8Avz/\ W"جGqia) ** Krn}2ieU"Ү|dY =X+'܉%D*<Ϻ#.k*6bnxe)j!bJmW//Zh˖-g[neǎoo+]t>Wײr'ҼDCNUx%Z,I$R)x֝G-Ec}TEWib?s╛_y~tK2v駟؎2.xW[\KA+Hk-C7c2XZM 2"PIj'nw w{xGhl!w]:C(~%DȒD"g+R<'jոmX> fiW;9 ,Ѯ;vlhh{:m3ϼ \p$onp╘i,)UW>ʇ&j T,@SePܛ>F4N%-Ǵ cC{$I<%IyHP}y^b/g@̍x;g袥`t k L2^@إP+1 ,mn5JRa%D*$Ϻ%Fxn4}BdzUJ뫟A0c*hKW`(,!$I&L0aI'ti]~w z!B$?]4>{_o?8[fgzc;ݒoz2,GvTb\ |6JR%D*$̺L<]i.B畎̮nZb1`tJzO>֭ԓ6mG>򑫮jժUrJd8Ju w2WD"k-3BЖ|Xj|IEPp迵l^~n^v[IͷT"dI" F8R<.RRZS2,A#x[nuQJR?0x[UUO~'T?ٳg֭ .,a˖-K.d֬YAt믿G߿7n6l`}(__;v~z9s|^z(7oj+<>sOgggKK \J畆h))2q?S/'gAk:G`ęډVx%p%QHP;b M-6ބ)KE`_'*5QoQJt,6@UUj*ܲk6`.W?|rױc:::򕯘xICCC^{^rʯΟ?ef:|pښ?/u+U]}ǃKt-6UO>*6):IȈi|lLS0֌7xۡw)"dI""Ywm?k֬u+s'")jW'^yeGsooo7aZ)W^}|׆۶m>Z@.b]]~n0ae&L5@Kn%K֘{0^iߕ).z$\2I2[^ ,%Dfqt[h j^ް8xe</}FWl}kׯ$}|pDOu\᪚Lk&OL&Ƿzx 7}Mq]]i 'yxCD/]Q&G92 W+Mb~ $x%D,I$Rt$# ĵ`ݫx˓TG EQ:&0AxeI i%I0aBeeY;Kn~'|q7pmv9瘜}˄@e&?q>yL߾jR^^N>~F+唑x%G92+1.Q?3L&2>0W2bCj tp U$HU;bCHT1")KWԙy%+wPpĿjժ[]޸q#GN<ÇoڴI&;_Yx*hRyydfR#T^>eYI,ݔ$K*,k=X LDݔ9q^=6Dp7]Q╤9H*H֝5k֊ G Wb?}B^#W;vcMW5\ 6 ԕUYfh?D00Tg9HzP&\0\1MMT"dI" Yw7cdLKB+9 >+ν'E+J$ bF>%bŊ3fzr l /$ʒD"EKA8n#`Շxenɸ-/hD0q#.4+ӧ\ז?eTYY||-[Uگ|+w ,x衇366wއ~~oSNOJDݚWQ,[Gr'th MvW))xeևeym:$RNDV$)Zuٝ\NbO|ܥ\:QWJdri08ՋW +[$kZj^+ookv…a[͛x m?qž/~ _dݑW:nJ.+t^$SK2.0rMGDʕYH)H1[l|wmUI(dS಄x%ڷAp^IC]VX1mڴÇd1yKnݚ~ǎ!mO?ʕ+_z%7p 隆*J*N;Fd_x%٬tYPҷ>"A2$\I$RT$뎣Kw^Qpy״uuy8Y╆J k„ W\qiؒnO>9s}fΜ /[n֬Y??$d7=v7G'^Y<7e'4'"IL&EC^vʐ!N 2ɾ2_$,I$RT;#ۑ/ ^.m W4䮾u!ri08WUV=G5\#e[n]nݣ> UUUr cQcYmv7vvvnذa۶m}}}ǎ4iR}}YgO}STyy9]ʰgAy=Ft%+1f硼>}QAEIKjR5))~C4! Bl;y*x%uIM@" YwĦ>bM:2PAݫyȕBp1 vT +ƼY:-KO|MMM{UVV~_׿Ϩ/˳nHs#K 䕈"B;^f*FT(x[+I$"dI""!Yw֬Y+.٫-dS7 ˸Rpr$laQ#v PXw}7ĒT D#F4]+=/#WJ1%^j#^I"K,I$R;6Bv ,)\ԝ;%j_'45kݳgoUeeUW]EMDr+휪+-"D>Dj$m1d"HLN=(6. R μr=Zhmk0cv[1r*IQc(e5HC}NAy|3ƔtA SW *h1}/P+#^I"Q,I$R;#g8&r3+d\c}(y^I,35w\niӦQRW MA9):r?JzmӤ‘ @RPAtlҴlˆihI1Eq"KIRA2iQt| hnYeY',r}M6cz_f@XEA7`چW=uM%h9ʰx%hDȒD"YwlmLcp+a\7H[쮶CD';++sV8tWɖ [z w*8&ج; 2TDrx:P&J-~}$%Dʧ|gq-i)Pn"Wh0Ǭ%+MWR,K?~ww)S)HS)Ob=;0x%DTlfeLӑ}e R%Dʧeq%n4>.U2ƕX_Z-g4Pug9rD3gqgV^]QQAC2^YƕPju'5D[-4%uABJMoS?b[^y,╤zWFCo:,HK IDATW[,I$R/Κ5k{yMP+ڸJ\<9J+iDK"mgJjlJ0teSN"0ʙ#i5 PAtqsCAf; 䌮D]͎z( ,e1eee,k(劊 >wĉIen=_ I˒;D›'I}ee^ semMZe _#^;D_$)u1PTƕP-/dyoߑՕx%iI$A}%Pd}󃻱5{D3ش,$/]zdG֒G8y%z?qM2#| o‚n╤R!KS˺W=G s^%T q%}ec݌mg^ x%1KP+TL۵ pЕ?e#$cM~[>nMaEWJD,I$RN/xX̽#9|hxe WB7M4͡}8-7╀(0\tKJ H$Ri║UjDD%DàrG_GH//)]B%`\iI J*~^}k+p{fh+Uv;5T!j%RQ"2@t\Ke|JZʲi_LQnąh&vҢ@)ɲܩeů4Ų0aů8i_u,K!%c>`Yj*ޞ 2L*RV&{-\{1Ƙ|lO2:"^;D~RHGGH+ss.-HʌYȬ8y%|íJI$3C^m,~;z ;kD6 ѱ@.ۼұeHp|Ny\%}gv(~rJ@$[$H9;bFfgiSpOQ13RX7nz!ˊUm+I/3x%" L,{ pƖWKܴWJQ.EJw]Px,;g̺[`>LbU$YH\_֝5k֊uOr"%)bIWBJunFLmsxCޟV5x%ԑ/+f4 84p3~+I1G <s%TcWO޳_CWIWJpBz>ʏ7^I#]+ʼnxeN@=J*| "h1+SIœW3@G gr!&™.\a\4%KX2˲eY/OZɲu,KI U9e)_kX'jK$`L?criWF,sqR+I%+B$) Yw#ykM7r"+KոȬ$x|y/nawo=s-WRwJ@>i`sO b_icSiݫX'%7:qysiMP^pF ?+taYG8JR)%DʺY98d[,撎9:kM7*^YƕnFf%+!eh?vx q_wdb*lFtoDzWJ-*^)ȍc:SZ(c>co-JgT3 K2[va>3G$I M5DʪBXZK:EZRL>Exx<_{^yYG]wQlXfrRؼRժZ/l?K^IV$ώCSL*ꥊxdOD %4] TSI SdeI"`% Gw'Ȳ$c.%#^SPUƕ~F`+nM_o"^I"e]/:ՎWZrg$ R0aLV `4y0#!WI-gTHF̝uኡf`uPQĘ$$lJNJl^_)ɲ-G9%Q3q?V.ʨ4A=R"dI"(1| lGp!,Fn`_I~BE;+r^jf4?7eDb\Z0JD Mh˾ -@<31t}i\cLb]8 pX|$EC,I$Ru=%x<99n#+ɸ2Txg!{M/g ++4Y8?!^ NU!tDx%ͽI9OW.sֿh-S1(u|`C9˚555wB@d╤Ȉ%AIfVe#&bҏ1u6 EWqeXiJUϞ:z;/n ^ Q=7,*1L+5goТ=ڤ弫iG^{$]L>JA2lR sHiX$ncY+!q̼B(zrA hDя~jjfϙm!^IYHEXք!c< R\R?=Tͱ WqeX)J@l zT@-lݶxeTy%:RWFWWzW24CDW2}{)~7)3ܤbXϘA4ģbzzb,dnTind)IoPfrLj2]qҴ{`v#K&ޑq^?2剿V7-/X𑚚(K$B$䨮noYw:Bs.kÅ%&#J>xocnzqNG|DuSgN"^!pDp.$^Y +yo'iv}t6OpNۮH]/dL |J>$"l2tp̮={Iy$}2voqW'B$)dD̬;x…y[pc^B4d\pVҼRx[祁g̞?]S7t t ^ +C4'yB%j_)[nٲ/ ٍq%W c}a=:if6AiY1+1s0c}ѪXp GC%ݍ!i!|e7ۉi c@Ѿz+y۷g\]]1zvQ&^I*N$HaJ²}E[`b&dR($P$6㕪8j);0=mӪڦxexqOxe>y`קVW W2f bScflJw2dƅh&9kΎBwTNc3t~'cL=D ._2_*~%g۾}!M2+)n7 pe``G{gfWW_rɲ,lC╤%D %bdi̺㘥'3X⼒+C=،KWJSC΃}UmӪb'/t '^7^9T>ykX Y} :Ȯ'1J.Aqѕ) 'b£p*:IY꿊}G+MXk}|Wgqig`G1&Qmc +MIW-B$)4yfZ#v!όz.GW+ǖB+/C\gN|҄3&9T=*m,WfEiNN z-4""u6|1+4[C{zکU2KGaƔȘ$0`Jy%Q`V퐊G,I$RH,!κjuΖq0_ʰl9W5ڷ;N-7DWGwz豿lw 3[+'T1iF9Jti %*)J+MgAAQJ|d`gB>Ql0O:_N>k;vS?A`K!{¥27!|;!K$rwrtj;^W}G{{!뎘ow&>x<zpxx%WfcdNz{Dh3qO-U9 =3ZN*+U=4>zX`2J^ɜ%J?Ԃ^mL*|_}^Tq:l^ a"t7>TDCKc͙Ձr rS nUPbG2rtMS,K7_ƴBU, xœѾD6i-Ȥ02?-^yecݜ9(J@uiGp'sϟzکgyF䦺+I,I$RH$bXޫ!v o7f݉'bѾB|R9n_x%W1LO?}~MN~,sgfȡCIHLaAfBӀߐi7֋MX_1W <-3 ۀ+vF-/x W ŗl,\1 .rL=D+l\@|n2!6aMOKI~8Gi4*lyT0_Ҧv 6MZVqq6ҶoDsxR  q)3%#~e_#^j,^x#+MUNZ޲\pAj!1-$Ie)ds5x,M(Sa1k󖣾7$ R*"fŊоby+H$d}Efd-+5^Iƕ9DqX3w~77CcFw{? Y3a2?29E i2XlR jٳ&=qxM6iG'╤%9us1x)KN}oMb{̬&uvu'l s+ Ed\JR]X7muݙm޺;kxe+\M7^T<4.^iѾ ĽW{d_I* iY {E_#w;rv_شqyl-%IW2Jvc8H~ZV?fdpoR╤l% %=D'{G _>!4\by,֪}\; qmoDS˻=$Qd\,W<.#^i(vL[wœ$^JWaCXر|WfW̛aF*8dDd`?W.^p)XR-n[ ~ wƍ{.Hq݃G`+]"o\EbC6T;>+i=JRGn۷䣎^ݲqW^PWR$OzCZZ\;wy-r%^IY.vQLl7^ dZGؼ]ƱXk[,6+u;no,wwBd\zxG^-5Mkfſ'@+WZtsGx%F&~h?z{w FSp] eucY%I#& cGltډ |88^(x|̙UUUٞ5kƳ{ RIʪYHYWns۷}ڪ]»c8X+ɸ2׳ e~y%kOo݃Gv]ڣT sNz?W7@6 $^& W.:ߜY>w+ WC|zQ*~ZfVdUJR3s*'/rJ,\̐_+I!K)[RScwEN M߷3gw vr=D'|{ub !,pY+2 ^D'Sb7\IN$cy^n6h}xQ :eAA*\oYݍ+FW,hMJU*ljj h2`)}@_Sy% ٧7&h͛7seYqUAM%IL5g~3W#ojɧYHa*7q^C}S}u :w$X,I$TLꁖ,,@A`^aW~^lPekkK,j "+ɸWggʮn肶X-atdx%J!x+]7/JRd^?ܹsè[Q,W_Y#TÉWy%ΩM>;zw%}tu;r/B$o|"`mӓ3L K$Œu.eΝ~9]ɋPQ۷^|&պ{+ZO֋d\xoyW{$^I2ߘx3d^yBe6b&ߎXC W_}Hވ,%I=6I&V$Sԩ/żkjTT^["GWUBMVtD;whyY3%,:sΛ;yys͓xiWb>߿ {{(te-_q,/Ny%WکƉDbtȚ8J+C8JWnެJɤcW+U?W^yݕy*lD%mB;" 2$^X[sи5ihl>|$"CJɤ,I(KT8-IJRa%wS65ϝ֮mh }}﮺tАW uiNCɀ+y^nʱ`?1o<+I&BN 26<MM*4#+*'?^$g.Yv(,ͤgO<)no,$JS:xggWvHIWX~is+W=JWn^%3oq#FkǬb1á;J6sɚyo/ۗ~7g/l\Q.Iz\h_"zphhpp1Ց1Egɤ$B@Th+jzIQ!KRA*r.]L{╊"-"Jsa _rK~_ڵ_4w^S8%n{ŤI aRd\V3kg,K+Wp tݼ+IQUѮwԒ׷ٛX~|A@/r(,yqR畈xVȶ F>r[ R)%;p.ĵaǼuE +ZxWs]Ѽ Dz?J\n~|sw޽iwk,>oll1ary66"f^lmm֛j+ɸ2wv/[`56$^IxmWoLuWiJF/(fe鄼DͿv?>|rnD@)~'} mFWx23E"2sfLgpQBER,|yvYҼR欲m֩> D?7!>A%,%f{_2$M"ӳ'P)S[w ƿx宝vؽ{;|k^iy-˒N $$=FAs6^pq.~T|7S!uFWq?eV1yÃR! aW$^^Uvu"ILJ`Q$1ۆ 5(IᛔMi| GSHGh&I%#繑i݄| L&WɤV> ]]K/Z*I,C9cqEATIoljg0i02iReaڃ˒x%( _ONr0x|v<3K$B$D'H _73qvVyƍM RyW"yŗ|g|ŶFx%WS:bupBMv_u?zW$^Ixe{ؽ] ˠ r r=gQSSF4?٩nvU?4vT@aEAH%˴ʽCtNJ'kK-(DzTHKhGɲ3Leg"pREȒ %xGY=P S.TVر<#ܵwiCq2;rw_ߋ/,\yOرc`[n}5"+ɸҫ.WC:3+Wz`a+Wqdr tm Z?_ٳg; yPX_5~%K9W*DDHgGD-+JRu$WfJ1ÇgUȯW]sj܌(~%DȒ-%D'IdS.[v) CbIC;wٻkW];T$-3'+WLΝ4wi3lnM_q_72u9@od\ł;.۫*ҥ>_u?z̄x%JXJ9 @+W|W\j6CTƯDW8Ri|f畈s`u>z(,]RoB╤B!ȿςun"K(J{w8}/[v)\bJ=Sd0y /<_dJQ@J×h@*)5/l$ ǻpDd\ISЮ4555E+H, ^iܜxc$^ڈWFWjʘ>mZj2%:~% Lq(7X2=%*ՔFGaeL2 / Za,w("T^z)89>^]]-.%KfURcY* JX84n쪑auQ&I>DRIR V)R-p,SVk`4Db}J!KRHM!ɾ[ZZZ[[ZZ[3;pyΝ;w /<) S+z0_'Bի6ٹk 79JH$z9n^Iƕ*,ʌ̎v Y&z$ZjW$^Ixe)JXmlƔ6`Nc̈ZfM}CCC`ln}9bYM`ӦM 6mZdI]]eCiR@)HeLK\ѠeHDȒSÔ-_GڏJ= ;v**pfv Y䕏<^Νȯٹk/~+q_-n*WymY3ɸҥibviNk+DoY$^+W:6_mf+<72+޴iҋj郃^) @RabP.t,KeUbgͣ8{_ $6 Hh"%) -[xK8v\r|{sq,DZk<[vE6 ."EnQ`Nʉt?zz4^3U 'qUi Gu3Č1YVb1F,`tt?eex蚲ʌ DDG>k(o;T/>ct[hy2`/ç+Wn|'> q|%WJ%u  ;^GF]m۾oZWz}|%|%+W:>dexOJөA ԔxqKOMp^|c\d3IJW;O ߴV5ișs>U==gIW Uʜ7<{CK~Ŵ{}pҒэC|(t+++˕v׊˜nbU q/>~έw656i-eMJ3ΓJ&*(<<\"WXJ;|2}%'7v@)eYxZDb|O tbpeNOksvw;ھqᇏ%|%|%|%|%|eJ!K\bPzF(.kˬ/dSO>umuH)KnteTI5yU{/,U(!WT&眥;$Z+3KWe H$ĸ&+ה488jlƁ#G?gI΂r+jY~qO;::V42_eb"wnZ t o*۵OlwܵNYуJ?WWWqaDZZKYs=u֖mC$QU( 4'Q1+ 8U?ߪiU1q0ߧ#c¸8f1P@YV4c)MH$ՔiMi/=Jn|fd0C)%5eҜ]g[K!g |Ϝt{ퟞvX{c9J" W2+n0Sܴm^Gvuu:utbr Gq`+++ݸ0JBʯw ݙ^r'ܺukSW_پCP5jjjRf99Aq5{yr"rYu̱z^|%%I9{VCpbm+VX?ߜ9{?9G>-e/o&'V+-OKA[)ˁ}n [ 7t]]]NpJJJJʲD7zZP!0,-_cJWcY`<7oْL&f\ұ,L&5ѦhMzQu1>k*SZXf>ZqO<_W(KP(d}}}}}}}}9 WNLONNh[W3[WQkk JO}5e=J-];gfg8dٳ~9}ewww>W=@P`pe6yN#/.ѦNя|ݧ +/_t)\illܼeURmq"93g3 IDATΈ9lqUt) $+!'ɤ]xTU%jTH4=_ (r#?5xփ4n}c3kI?S+򕺠.h|M/s ]]9}cKol >!E22&t?3v>>uk22+Qg'Xqƍ.H5GUf_dd mɄ0,+3*W)0 PUJD"?RF;_i;WNLOLoMjw[['^WWWWWW=ʬl7ee(fAˈh퍟yӍ7._L&Os9PWg?AA>2&&&enE;_)1Y"LSX'O9MؕNœP_ _ _…WWW:$b ʘo9&y5LISEUUUkSSUFjJkN\qό\Y9RN6+% &! =7}J_9>1>7O39񶶶b*OyR\8[Yцx HD󏌱wS+;>M)ɩ}9I_oJι|( OESN E4z;w(=߷zt++ +CXuwvF#RJxFQuU*lqq-?K$~;,`EM7ݤ*'&DTjV,KOUjZ>rdfn8OOgF)_\ }%'b+eF/uXT4e"p5&(q'S *+<@Dow]}z va)7ZD2 |g?sDy4J]V<ӽ;|ie| }e+SArM7566|N_K3;$eK9鱚z5N@>9zeY$|hʑa/@_9>>>11>>w(DԱCF&+bɅ(^}e(m^w^w>$}]tWij²wNLN9sֶfÇ>]鸖.\|%|eʵ^P߰:d,SiPFJ^YRsj[2Aʊ&y1PE673oSN)Nqg4SU+OD3.]J$vDdR|Ů^b>ʐڒ9/_gJ&o}}}DeİJg>;y/r 2S _n+\JηCDu\Yaeܯv|V2L<2=+]|%(N,dʝ|)uR ?~it|t JJ!,Œ__,^r.UӇq=IU%rGM}O<<|S%!ZII\Y!un.~DQ?{wq>4c 2S _n+\Juu_'TTvcY_(ιcY`T>pN& Yj򸖉fj}L'gW28?4)O_2H'RI::-U`Rf]*{_moy+WޠvWejPꔱ0s0|dpW^wt ƍh/^{-Fэ7`Xe&xk,1f{u{DpniU5I+~e5nCXMg}W+ )UQ+yiuEW lJASLevW߯NiҥK-[te˖e=^Hʕ+"Қy|ffWw=E++yp%;y_|w,*_)3R2pv{BP|MDf^Cxjc-(L^ǎ+]R/M_1|e**gWꂒ2+W~yP0Ң>q+xbgߴ$7㻻_teKWJt +u3(B{zXJ+G>QrbW":}?zќK[^o$_ _ _ _ _Y_yxcTuumGرcsssW_nA˟39c-~$b$;_i}eQ?įe9022LD{F-:YL$웫=DcѿOKxESCtl\*.X߫ϿtDBh2H<#?}s Vԟ|BQC;O%/ViyfWFQ˧1U={~sCSUˍ2S _n+\󕇎2#CmN>QW&/uqPZ\ ݃c+o&oUu׬YZ_ kg<3K WwsRU(V[tWꂯ2}e$`>|%& (<4CbR͍7:ʉFGG}ƆƆF5]g%KŋaJYJnξyK W(?;_弛˗ēj@ɤJDgϞI]} },!S7Hb2kmqE4å=:>daOUU!c1}KHLQWaB*\xMII1p%Iԙ~ɿ~emүk7TUQR!izw5ӊb,Uñ,6q 2n5JU( QԀ,Mrʱy?Mjwo_/YB!*PCX.^8VUỤd42WO /]wXu]wHWnzm[}~-|٢ YSz*%{Jv[R諨ʣkQWLO_@%DʎΕ 4DM6h, t"kWe/<ēZlYڈaFK LJrRڎ|;ǯ̬Ƿ_YS_+A@YI97JZxJ.C~wcScSce%9Z0l[ɓ\d6[n8 t}W‹/}W~>gă_##{FK:Wq ukimȷ\tiWw75KNEJ8J}Ԥixjuc6,_Q+{_ eiM ݷ/3HzhdABX= K,T[q+bzě6ݺZ޵}͹Z"#>}%5GjbmŊl@qR_W}Yݔ/}b-[~oq +2G|%e|%e&>i)WrYFQEJ^i eAmQ?ՌNߴ&Q<;(T˪%Khv8 _.x,Ɲ+ؗN'_UUX1̳]$-j_^WW\Ve_җo\9a:z,hDqޱ]/~qqxXtiWzW1 &O_WUe&Gh&e.]\˲mm~w1nlcYrïR &( zѨkU#Lq{@UW#G<nѲ9giZoQ"DĘ<>q"obbBhz,To"RγcY22j;Utz+ *2|f "ʲoDr#)+eX9Uc!,%Kv+v2W:U*pjse;=n-q{Ks! d~対 J;bήs,z*QUK InoazzW_EEaFY ?Mu>_~2ĵ4ߩT|444td3$sfTƘ=ň1))3KVRS2)EJIyKN\e3!N%dJUTJǻsO9Wbʲ̑5)7śn.Y;Si a)a+FWE[︽Ay_m\*aVYXbmD7qqfIWʆt v<؆o% b!',4Յ_uif 9WmTMIe cMS#~A8'Wp,7\kkq%*|UUU A[&pRA:l~X]8ʎ~C_Ĥ}#å1uʏ׭%W/\ӹ1չ20l3?sJObًMk_ɹ0ՔGLC7RT _ٯ=',&ɮsE|%9g|777777Ǜ>H:. a'woxx!,sL7JGTII+k= ~D/^|7e|^qLd{>>>52<4R+Z8ĵԿ8HV25L\Xm[m "J&%'8v/adX<=ʹμ\Ҧ!>'G&^ 3+Mse&WB 09v+&OUT U$~'3N eIܾz *nϝ;$755mڴ‚5X 9W618KM '"FkY#ƲԿo1)ƈ(˜X@ gZJyL1Ư?%_VSSS_ (CMd8Mȵ]KiYiY+*euuuMMMaQg_)#.N1vTΕ܊lk[p‘ǟy4_)ON LwC/#ULSatɘx&2L*Dkd2zuA$-45$܂LVYfLŬ}W*//RTw]?O孥+4/kjjtYMtKwzJTt _"G/i%>({K3[r |Aހ,C[׮]IV9!,Mh#++o!,}<\S\b}KC9}!2y{|rl&o/?448<4Xl_PЎsg'?1Jo}GFfk/e۱l 3j;n25U<3O<2#U$Ώ7ڝ|;6q- ?szQ!|%( PZdRSf;XABXW y /$+wr+o^3K!ň޲[S6lM_Jt,K-x Wj+RNl0d2)<gT Ҫ"v'甾9pQUUT9OL&j2VQRQjDQHD!;0f]o<:>_o'R-|ej?LwOO(j½t'P%I *BXb a),ɗ4$Uo+/\8x|["\aݵ\q-㘾bAS> 7F2tpr23qOPQ eiz-N@1j ܡ湹Kv O@#~+_U )' {%HdZj$&sPMN%⣑]wCy~{ݳ RWZ=V$bDpSGv8qEQ !/,-e6%²)ՎNHх5Z6LH!Jg-|RvЂ.0;}GpXohzRWVWW777~|@Pod*)ŝ aWtK!Le&0v[H_

9%)(IXRD!,-Z $eRDiu+Cn`.}T|!,Λ+M y}vdUOԉ9Whm V<|䔟BFF1; ZpZD Sdvr*H$r5kfϽ077[wBKKK<g6I"ESQ"(aUiTUcLa8< eiuK_l0|tq|%w6ij_yb뮽a9|F1; Wr<׈C-x*7K&x&WvLbp%"85͵1"Hz7_x_5??_AMMMOO*.3jJrGa"SJk.NOLq2v9u cJPl@YyY !,,Ѫ5SդeUvs!}Kɽ*.}%y<}zk ,ԩ;v?  8ȂNaXI ؅19#F 1=~evƈ(*85'N}r鎻liiϘF.AY*XQV1Lr20}립r"W"ʲ[PCX:Ǻ:RrKݓ|&<7_IDMMn%\Ǟ=z{wYwPVp.A+~%Qyzzz^z)r555555iMꄽJW(ˢl9:%vb11pABXs RD}e m~ Ǫ̙/ٳ]QQc]JuVY,(ΚYL'MBI̜^渊Ub;9LMNDjvzK&}ή.Lo&դ)gUUYxm + e鶔~[X,D|v a{hU+yϜ=GVݽnqry `M$qzK5yݢA ,,LHnyāYbrJ>W1YOW2C0ʈ?3/E7:7NwE?WsWq+A,͖RV܊9BX|K+9+ Ĝ;U?ܹ}P^Xv9.hK*r'KY'f^!U1g:FSOgܠ,ŃN2UY(-gzN0XvWrNDѨAYZ_WZ;8#bLDVGĦxʲ[SS W2$_FK\"eg_iZ1L_<~O!;w{S @AzX迈qݻ7qV;`ʿ[x<J'3nv4ԷԴb~^Sª;waX%(́ 8ණ3k`ɤzs1;x%Ft!u7_ eVҵLBXRr*7%IKlI}*+3/~˙Y?ܵ}]۷1h %' Y ϕ0auxY PJW#|%(,K9evʝC,]^L/ 7 ogffoCjP]I-͐ !53F"@J(4ߊ’ɨRePBFJ$~G5tӄ1ZeƘ!c~V,DXBJEQ"J*~"!XL E)}EQD@X WʲT:a,o!, !,mjn+CssssSoY_kPݸ,̜Y_TPh2$_T0|K-o_a͍OL]䧐-o۲es4AjP=8w̢@YC匩Y}}-5q}+mrW4ya)_$9!,3BʒR؟= I?%ttq]]"l@]co~VՓܴ''n]lpwM`>8(i,j]0RHW{g#PR|N_SȦ[߱iӭDxb|@>a-+D05|\D"`BI, LXLUzMS,KXTnN~lϏ?-_GQ!0VTQ2/E c*STU4b O&^B+.Pm _ ʲ$YݔhK*2gS _H,KϾmE{[ۖ-FJu;nЯ8|%(N,ݻ[G4!n#WQRT (yd*jjUU9W-O>W: Sr:cI1~eDxω3Ƙ˒eZ2ᵾ֓SC(Lq7M41;ȁ,&MlKCXq IDAT +)˒zWۥ _{ݵ^FY+n_6 :^W}J_&] | _ eyz.2_42LsJ$[@~vג o[JZjk(VV Wz[fs߆Ƙd7˛ YΉ1SJ2]_.'(RmmY#98%N9|2NWP,󕦈Ƹ SXEa'2t#&z7Q=HSwR5Р]HG2THˣ],EѸįʲ[GMQK g0RRW:kM*u>x͋xs|pPQ(8:|N P>CXaXW  |eی_<$|%40ʲrJ,?Xjf0eX)wdܹvr+ӟ8Ќb9 Kϛfc"Ĭ vq12ՠO&;`zyap8cn2xy^I,խby a *-OR76/ohj<[l_zU[@vېqGx41py:PR9WmYSpy;I141ESJ>:ǬO"~%(C,Km kgKWmmCXUn}Z 0] _qP' bY3"1濊9%<;dbr'_`\K^J2o%pn$|e:JPN`ey6br+cp=DKa)<>1f` ojvT} IOËTec +AeYM[_9ײP!,C[[_fe[cc4F񦦖ׯW3Jm~"0BIQ;ig19w钇Ξ=GGD[6߆:i~ c^E0(a:yTU52(9s< ٥qNRWʲtB-]ba $eྒK1V=ۏx<:;777w魱1o<{g=w[VƗz'oe;2/C?t,QjYw,WsK: !,]4n|ÁC1ʐ"4ꭱٙY<̳O?lwwWOw]۷{ ʶ{7a+J#|exgI,˹ANDuuu%UUEp?uX:"83_қʲzJ,3U|!kLt²Pgp*S'NS-yɧ[[;;޹cbr#SJH$ƿhdd.@A o'+XK,K5';t’&6)wjK_-.~D/qCy+d߾}FGF橣绫@o3|B,ٖ/ׄl_ nNcQ45J]<9Zj1ᡐ}F c%正緫@o3|rPp ^~qUaMMMq #NP!,KڶE+5nx;6,6)v^o%$}Fػo  t'_/>QJ"UuPr_ *,j>U!-RwCXٽ@}5+9֤456˯H}mm+ٳ'C[!l={Pb$`2y1C]DW?_rĉsJ_ ,&Pfjkk aF/Q]Jǽ 2Xq[6cO_ cC~߁Dݻ zK(R_*,fPbzpS a 6evW5p}מxS?ݻ~ }||!υ7˱1T0 uF+O1\^'?vKy־sWl|ܱ瞗Ys~K}ϻWi ݟH$q7JHK,g࢏@r0vB ϱSa99b,QGU>a aRʝ" alUW׾4}O?slfvVfGCj  %FG˱Db,ػot0%YQPP^ a1mzy8m _ +01tܑ,eXJ60 a+9}s9AʰmwnzgϞ_/N:wd`?׾Ј0[8vܯ_ Lb>xhPdb1S]m1!,]aY+yWXoKs}{#. kgVRkz|t>PJC*2oDa)&5%WG-o<;t2 >}ȇ?zժ<@K%z+dQm@j!P9҃r\GL.s#'5꓎Yia,Weu>Ү@Kâҵpұ+oyla(uZ6OSɂ:Lnk!5BXft<{BSE!Wz;/0ݻvJv'Nh>fk.1[2' T8c >YNO+|%:P%pTb*S+ aRʝBX\ֹ+E +_|^xqvny?nk[!Y}oK400}_ꞑaυ E@%vǼ+C;Lʀ|%(,Ktjjja"sK-I44c;Wr_~e~2{֒ٳ]SN?95u"_={Ft=s |ex _yEbYn#T5$ISːRTB0|% ɻ4 }Ʊ_30nk'xٳdo|sUO'>>^dCs> (eb\\V#ApOB*˯asTnsrxwb~UM[[1*9{WuEOTqse>fey6W\0;e(Rb|W7LKI.41Jo3{{Μ9Hf[S'N??]:_={FݧePRTt,ӔN"Pxv:|6n䷈BQaIDP+AeX&IUM^xQJ#?&﹎Wr=e|ԉپRcfvOre]r=տ}~>= &Dc_>WeYʍ%ݶ-+W .e)w aRʝC,=Y#Q N%9_A|'D> ONN}dkH2Ä & |+Ark2\I6)HKW]CXzرpBX8Q-mF侣Y6,[vЬ;)>rz諽ӟ*7nϞ={F6[\KCD{F1[;_9;~7Z+b՘_fgX/zCcCcc#+BvlO20c (RKbwiBhL e+jK}ƘF3gR8me75l8vs5ffg}bpݒ~>p>='&&>?3>.詌W#-}s}O5&rrp$@b(")+Z^rk[V%Q}ݽk޻2Hde9޵mI1H$K008zШnt79GBoN|/\CCcC@E|L[=HtqCCß>= +#mmō Q?,((Fe O!KM a3`ݒՅZҪ}_7N-{3g~_#63+g#imme'A^2;eO?%؅..,MʼnqaI ƑJLc]TkE''?AuO?аa#I&2kqعsWNy=ZmT$sp>ӧDII $䧒"q~*(BޤH`[ǹd,CW@^*2U#PcGar3_dbZMs*%wΞ|Wd>Ȇp4&ϋ!zfC˛"-_ɹzkdzyǝ J -,߁lԉ8lp\y=q*qM ukk{5 J,2Kwޱw]]g" *&tabi F^ Jguk֌|~?eZF?ԎƖ7 ,b IDAT$k!zPok񴶵akD'og+#uuu8( DJW YiSSaಮΒǛ'u2GI=O?U9nit8U, aTT,Y"D+hxgNٳg%[o3|[nrBAϦ,%y%_)1_axx>^n/7ޤaȶ~2h UFjޟɉ'*Uh{M 2]- )s@!X ( ,SzjLЅ3х.*}ty͛.,#T@WlJ&ew#9~l?xֶHf?hiyp$ !zRhwag(6, ʛo o… -\x7Hm6AR!k i+LʻŔ$r/5DsgEatOVATM%\P8f5s`ٲ-Iyl.[BXS .K—ƍGFG8jJJY(uk]A%X>*ـ~%{F^Ad2|>efeeeeeO@FFfFF$ ˈ3-gx%R| oӻ{ ۶/j O%% 3d߾V$=yl;ozl8I24OĻU\|ng:aW`.[URU翬\X7&|+QRUYm .\pXIGZZzz:(g H C鑩2ҙ-b)oO>5Gg3[nly<ټ[!zfݻU$λ?|[%۱FU_y8˗/_^QaPuKf^/))Yf \.cOplax*AP BnNN]sttbb{Ŋ:E4짒;︽udd;|^v $eVii) þ-/.С."Lݞ?JS>8sܼ9BWN{Nvt4J,YW55\+Qn{z+yר~n_n͊sss!y%_ZII###Fڡ ##ST4W($V|Yd%4q^^y)ռyyyyy/LHC .-BzB܏D" ko#Lgm08{IRӼ2M"=yҒyaphҹ[`AaaA,}h|Fٵ{(--Mt[tx5|Y/ J:Lkf~*SOݻ%dllS0 L(RJiPABBKCX$I3EQJտS պKȜHXQX4GL<+}d1^UnuŊe V,Re^~^ff&OPJE*-@*)ͱEj ;W"DJ0<s ! ~(Sr<ޡ֭- C__/W=… Cccر n[=p86ojI!z-v{vЙNJ)=7&y%Ĉ,GGN|x2Ғn>Yaj:dO~PL*=L0DOԃ,|jcs%# +W-ʈ A^i8J>dz1u,t;eLzW7,uT,7B4YoN5?>;=\%ޑ";Zly~~~p<'f6Eʯ?؏^"Ke슣C P+ԊC aS>2u:b@r _`'1"ﶄWRd+r/#ڹ+m~;߮]Ev{|j-SkڹsE2-5-Z_!O?cW&*y%J3E^i!L^`^`7~"6R_hKAd|Qkmq?dCV+u&v/(jD%I^GƑ~848t{7;2Z/Y$|%p.J@HCuH cpx10#DJY&tcȒAݧGI+ӰJMFj=O>w.UT$s|uw jW^`_ 'Gt)QK5B-on!jd$ uTZ}WxVcSBy%J3E^i!LA^)} ~#k_jfˏ 4 3T׸Dd9SL"KQݲ[ᤒ.QYd™ ^GԿ;FF#:-&QKȒRA2'di !DJY&te2,Q`WjȨx%Pz4DEqG$N&m'URY'g#5,83.GۢG`Z.]x% REwbAdz}S[xԵ%D ۷h}'w3ј+W-JStye*J)n>#\,g?SȒW营_TF,DHλC!wW,'(=z>\xqA~Qd)imk KJZ"#`Ph-z*Adi,E?#m8^)Y'ăW*h+ӕ3%U;IY}]ُr+%`&岯Jc+bELnUU8{cAlٶIۨؼyc6tWdiL4 J+W"Կ@z{[2jew%Wr12J6X+7h1r)wf,u|_---%//WWTrs&ꉉIF;Vp~GnizGv֭MtJ~^7vvzs!~#+nii)J+W"LZ^ %y ;y_"+G `҂@b٤TDEd"/w(VFaUzlE]\\I# NfBa;Ju};vΟ?+o6Zz{5]t)???33Spʭ SdcK"xNF=u#՟8&˅8Pqc\94ծ3␛"`Z^Iy6f12&Wr䕁N[+ҥ.))nlSZ?TկTW/㥗_›6lly8}\6o޸ySሮ۽sNUW67 +W"D^d⨯lyNG]~?(L1Ldr oa% |r@^F:faB)J|Q,D< ռRF'-:X+CBT[+!` 3X!I3v^I++Ɔ" {~ewtomkNww[n߲e[Ns捯bs ar˖mq͊ٷ/M-3y%J3E^g<+RlD-=  Ux%ӧju [XQaG^F~vHs599911y%4 OYʹa89s yeX|k̼j J-J*VJUrRa+)óz9VxpEUW:9>>R?~\ݺNF^F׻snӑp_Y;7:kq{a%sv[Ko?!ϧ+W-ʈ<3Udq?m!>99ӳ+48wͧw0++ Wؓz聆z/jUjd?evکL\!s*DO~;ztM‘W"D^yeJ)N|םwԤspΞ%CCe \Ni+ Pp~ر4</%#W;:Rի*NTbLu'&FP9 )uO|[7D^L۵kccc^^^@R")" A> el/UaZ>ykY7:?'? |? jhy%yVJŴ'^vJ]|kYjVLJ3ef˗׵,e:ƴ,k榺%`(z<{뙄;G^y%TԵǞ֫WC= _p(AO+(p; {,j#iW@___aaavTddJ,zc 14:ԡ?:]O`W&^)mTO%RY+J)=na8[ 8~UUZ&Ti6[WwO;n; 6p^\.mۍܹ+Fkqܼ>' ]vՍ/"D^y%+Y /fZax7MqZnpuhCȫV(,*JR)?4άؖ^"sD˗/[|Y8Ju^J;qؘn(ϔ'= MjB`bU4 GJҰ Ptσ㪔*+- g?^7͚+rBj-wZGjvq:-\Ƽ[nٺ}ydrv2۽evhnZ?v&-4a畛7$3\8JKyed̠C䕖JpWu ]GҝwS0~YF3F9O-l?G"-ėF/Zє&!~Rڔ~%/mK,5j% řS'әƥf*%dWa{ԛy%J+**REAYƼ0(A0ݍtʉwx{1QVVUYOe!w•nw>a?-[n'HUzYn"d2p+ASk[:N"z83@A"Mz QY"r䨷w;:ho=>m':NPѾޱ>Yy0C2ܱ24*WRJϏ{T: d:>8 4 SˮKRTTXXW[ɓ}cdd\{U'(%kMsٻ$s*D,wxF'gUkW"D^ye"x%8q͘yrVo &dI"?덚ZNhP˨Gl [n&-+C^WZ=)^ђ5333OF$&%+dU5,#W"DJY& UcCCcCCC}}}}b+g|oAgX߅ Ѿ~+XyD<v֨,Nsʞ JS}(AR9$D)+TU2">%5w1Fti{"ynkhm 4~x KU noͯPD^n- ]{Toz~ՂjB;Ogo딖\uiiOCogv_V:N<^~=bB! mx4'*-aƢH'Z4DNNj:==]]qa"iPP|,--qhh7oû~#K_z%"p4rs\.'cPfk0f:D$q\.g/B|ǒ"D^i+M=+#Oy#2PŦ)N5 YITTȒ_ӯFwQƑ5dBQ}($ʈ:::v:rnpF%nY*ǙH D( F Cv͵?LOOOauো.D3 ͦ].l=&+)--W9e9eЌ D$E^mCCTVV|;ͅ _ᥗ=ZZ%N6pPhJvڽsN#jOYɦ t8WZ׬Ϗyy;]rd988vQL QzU'N|]SSSmmm!jI+kM ȉny%Uuy9y%Fei$@A D(2rŋ+ &R::6666 |C|C] J4+&c=^_ uKwפVԏZ͈CfZ[\Pt:,D3ǎNrݰ1AGJZeY7s: p+- [sJNN΂5,x"RJyk bG)DrvO,]dŬ(";-='$3hw+|cJ!/+-LWǥ>tr1 Wa8UZ$F]ɣqqds<5BQv)",Z@Q_.rÊQ~zV (;O׬^}LcCCv{ҝ{ӡo f=ѓOhy8š0kq-D7prҝvovPJuT,jk)Ƶp䕖dy%A+gWzNi.6fP esI ޯ&Y&r_r}>=käEE__t'5:sLuu5,mU#( ",ʐ`m)ZRWJF+pLێڗKPhBb{'Re^CCuzٔ)ZiZQ*<4̂'? ^^zAM (xG}.ksm wpٝm ^m6W]*. Lgee Ew䇯}>HظWp!,]XY=^Rb * 10x%tllL Y+?HiVr<7# >5;Q2j^GꮁQ}ΫgpiqU e2&Y, J~~YTV̴̬̬ʊںڪ }de^xtjj?H?vڅEE'kրȒɺukN*)Seg;wv:'_I$JKJwC}ZTGS":(WV w=p+- 䕑1+#W&GNh-Gd9#;X%9%,Y %)1Y2o^MRo^C^~ ,H!ԔRA $|wʢH,Ձ[a* W"DJY҄mYHN e P)"g/I?xp(J(ML2TR5>n<---+(_yEeEUV^jUWbիV@eeESYYY >o"˂A&&0HKqy󲻺{t =ȲxOQ˓>]TTX?6[IIj0jd nieDvݬ^ҥ*uMrEK?kZ3Ώ y%J+W&#,)Ǫ - fÛN§J&fX! xaJudI 8+paǵc(4K2Wʕ+\YHEJ]ec޵))~RJE:NKhYe Ġ(~'WÕdG'(RQoB]WҜ-,,誕+?5YR@oo___B5>͐TBHYi5׾wVY|x*H- !N ~ӟߩ] *C޼yu=v/lj@ ruAhЗW\pPI3s7(鸠JU^Ҕˈ%J,S`72kUBC|2p"n0/ooGc *¬)VVU0VUYYUUYUYV}^}RUU 6 7\7oް׫cZͤ@~~>TctlU+cZ,QRRp:FFFFF"{#.{5Rib[juel.Dj+=}ŶAuR+W4Stl&RrrrF dqzy w9ו Urp yy_%գf,ßآ7:,QԲDBSLG[ȧ0Ѐ&;hCY lrMbo\oYU@ ĀMniW9SW^`jymx\|lfmٺpl޼qξNsfr:!teZ\+v8A[yAeX‘WZr+#oWF!Lj^eZ,@жIq?`xt%d̸pʷp%EEEE&%(Ӳ euL,##@.!EQɣ0JI:"s;&A y9x{u_j42m|d"wLW$^Y& 7+n//+j={?p߽z7mW&lx㭷o-S [=icKiq:,DΝ\.wtJ:s=M9W*\+xܑb\ G^i] /WF!LIJTӢ9FGGG***`E;PZ2ֱ,ܙ%`q^iB+Qf(R<Be_U~LA Ҫ*K XjU+WV>U hoL%V1_6'7|x5ZZs46F̶uv6aƁז۷lنokss7ojڽ'~@hgAf+AkoeŐW+$1XѱK.ATPU8L,Ɵƞy%,D(TZHA ˑ`~ c*q=wBj%u~?Q55_ž+K-7Wtn޼_Ա6+n;9-y%w2yJ((3/c.tDlF`D^inmC^̂evLY2$j3jb4ryy>|55=%+kX"fKDuX镗_DfZ"D^i+3)Wk QPE.\hf4+! R^~ϜMEҐa/>N(+b{'S!51BE0"q>8[9. aN AR>mM`Bf"RZm6!DJ diePZ=ϰwD hH) :B-oQiiңW=VM`JǖfNOOO:FzzpzABHen:6K,&I5))-g;uutt&TyASJKKJJ#GաÇou:Czwt:JK1)@ii)kҒXcIIIccc~ 'N"+W-ʨQJּ_!POTsQJ$|!~f [Ki H ---|%H+IG'````W5I(Νc;MJt0E QbQRfc +QHF:+N6x%={f[ ip`qqt'% c !1ʏ>H׼<)0"BzF:/34[<9JcJașeT-CX0I5\Tw=gO֠wv)==.\sb[[۳?~TƖ"f۵k 9 [ހL鲹i}tJrgi 4#.WF"+ul'"T}8g~-T0Qnܭ6YJ)_MWV__ e P1yṋ S4 SJsgydnw=΁Cґ> ym:'vtuD orjt8PK۝0j %DLPK+y~#Js폼Rp2d||x8 XF}^ye+Qf D"'TT \ U S՘.gh+- 䕑w#+#W"DAIvzG.\(G FUR(2Q> ז }">gK>_4ap݄C(J\ehlr&4'M>kI.}3.YGזjz*_cQTEWz$6q@-/|'aG\nV|$Ǩ|M'mmmV"o%-j112L,Due][l38h@^y%J䕩+uTPp/,?4 1bH\$\xŋR}OO_t˗/OOOOO_<==|y~@,1/S݁tʤ};W DAF|Q_ ![h*]Z{EWT+tC :WKwdRSWW{_gjcƖҕpjQΝ;w"Io G^i] A^ yeJJ{-¡hQDwPujE^ixA`puZ`8fQYNsa)*5L)?a/Kty: +ʫV_&jjgRK0H-̈rmٲ(N_{HZ,gp䕖ȻW"DAI3G@LDݱyelCӼ2R(I',QRU|SSC::%֨aJoyyV/'%֭N箹z~UN{폢F%lٺ=L-# Sܲu{j,|/<״Ty%J3E^H!\#Ը#(sZeg䕊x;yLr.Jk7+y[clfp݄c&J,_LUD #0ĦEPIӬhŎL;?0q'uj<0L;HqQÌr!L'N'/D%p^^f//תa__?;νtvv8jw-7imQ4঍-d0PR~](jjZK/llyf]řY]8JKyeʈ{;䕳WJM IDATK:J>k, Š Lf \|Yu: ~iz?:~_+v)R̟+{@:D^y "rL/@bCȢDI j|RI |xO}1084 MZB-w0AԝZqTJKydq\ @}8-}8YlN޳[S]w_N=sV>x}})j*D7%/DuK-in^o<`i}by%J+DAM23#&SXpշJ%h:SWm9Ic=r@Aa$tuJ_Oo_,ETD`s!$0sB%h*fV _Y2G\HdRR˖~'Z?T,ϟ?G+O~|ڵٳ7 ׄ7\۶g۲uM-N݈C͛7\-[-gwԴp8#JfjR?ByG^qG+W.4ߍy8E)bC"~1SWB2*!m99T%JBe||b|bB庺{,D~~>GP@Ҁx0MçUBt ! (-(,JYfAM |o=#W{՟nQ={566Pӱicjܴ)-lfu㖭۝gܴ^+ÔM֩Lr yeE^i&+\F^bZ-/JG1r0{DYI>%)oܹsϟO%MNN-%$@y%=Jݖ TmI=-V+Q+,SJ,M ?[ |tHi6Ʌ!(%~&8p;|IJ(C Olf!J2UR5!d˧#vokoUǎ/\w?V3gv9S[[kC}z9]jԎ{QGc]].@T䥹)Y67w:Uׯ +ye( y%Jk/S2G~e<2 5ɤ~KGDѯ@Ϟ(6MY8F04884k"Ř;*b&~?SSS}AHE*JJ-B_)\Q RJ tTY)aM\ BY ZX!$|2'e2rHIx̤ MM^/an$i˖ P C!80 `T﷗/OPd }<]*\7kǔ6[ddi+++*魩 j,j_  :mSO?؆+0 RK۽e˶n h9#گ _!+MdŐW޳WXY+4ZVx0E/0U@qiZÝ'ׄ5 d)E|(2ٟj42@BOt Ԩ̀qxi+Q ,QT䓿~A" {+V,+,( 9ࣦGK& )A J*=VX7~կC-_}wO wqdQ KHj n=,ܼ>in y%J3E^5B^Қ+%n4jbb"v^K=U9K: Uƚ!'J|\޾>iRK ),,(*,\hap%@:%AI$p #C Sb,+ܵ{OtV:y!j9-a6Å#.WFf#C^9y%a0Jh`A#SJj={TJ,䕩!%΂2Cg56ֳA(bd֒7J8(SRT FjC U1Й󫪪*{{Tb {jooz~ _󎧟yV2wq8GtbZgV{-xg/M}qlvk^&k8ӥkaA~aa5wGc)dƛFȑ/+B݋"Ԧ+Qp3>>2U3+15TX␉Ad,Q28cEviP43f5] #d<]}o~wwZTmllذ;ѩOmly-rgM)`eܛ}nBjԯo`L:ҹ9yYyYyZy%J^%Ds 222rX*f!SYc2T[Z!am2׿8d$DY_2xx˛TFj$ 8쌂%=} (~D)rbRL)(D3)FWjM]]Iu/K\_HRnFyc@iH%HPR '/gf0/DP&gJx-_YIN' !P>d-^Z+,3226nZduu-[Z}߿?_xYy8t瞽tJص{b&DulfY+'&|7fG^sWF몵o`\bU 9ʤ(h؈JE<Ôa-5bQj8y6nVeh4ʄ)W/zr ~lBBZ#D o$.e^ jȒeʲj͋%%!Yl)L.VFo4XA2N6q .\gF sjjJXBf%W~?h,{w_}Z[[< ZKn ųuM[ӹySjܴif-ۚSWNLz{8o䕉+I^ɧss22qWjJ]XD`.\r?4dK_0Q y%D( GXLTNN!G &BGdM%mĠ&!I(@0VÔBO0 :5L dFI oАr$#==:OMMuMuuWz3gϞ9s)Z@]m7*g{PSO= ;wv3Vl)+`% Hdᕒ.rs2yYZ'"Ը#(I+bBjJkI:;jj%-Xx+'cZ D2%lggirv\bժR2lT.ӑdL9BYҐr =xO6QO @I3)zjCqN &'}S>.!!׮] O-7t̙pZZ۾)}jkQߑAjrlٖ -ShbE敧]maquWyY^H5+7u>Xi/uy%J+HN?!*^קsa/}RE^,NLLhu!{ w_WBtJ ň (癝Lj n[<+$/TS4 qdJQ`dH=fQi-//ֈ 4և굕|PNϵe_,+)*+-TpvQ L".Uy F=TQ#S*q{sHnqFEf'#L^2'r%"<:[TUWư@|ccUgEMqaxY+TFW2Ad9;%======FpK&7.U MH$ڞj S 4n TsL7~h[o_w]+O\.qr3bRQKTLmYWNL\ ,+/b !M>%e&;8P OqT` "QlSB+>OPiةH#(.A 72l ,,/+)hp@rZ WT)]@8J>ASpv{ 3+R*2+lqRUPs+xxDAJY9aist23p0pIf`Ӳ%fOOϱ?g;b&']n^Q+͚~GW%y:=={Fhvrc;JF---+!''{rgr y<,?jA J,mt@#7u1ky%J181H-xnSu3ZQF-yQktukjeff7̊ݳ#0Y(샼%}Y&0gQr[{02;;/Oӥ[V$üaJ!&oG2oAהaNtus5'*! Ta%\ٌ7Mez/y2%MxZq w_T}}]mmmGGꝶKGcÝwܾokZg>өGt:6n||۶'uW\IӹySKDc-[77onnJ["Lɝ7+gWR4ʤx j/ lob_x=y%Y c`kSeɻVJ|Y D{HGeddhE.Yk,|8oȥ/"defj]^^oL )lf۪t7ZL9s.8 G|YjJ W{09Ws}W-[#lx`.I 8yIHBtqrxOx8y0l 1"ȶdi+i"iou虞Ɪ\ߧ꣪ZQ)M :W"DUpe} N^h晙i}щcI[[Ë,/ IDAT0haÜxDn ea\% $s?{^Lp\H1/ lJ]|EͿ.PBn}7m2oT2s({SKF+p5D^*lnnD^Yi=++hvcÊ;ur~~a y%JTͨWWKx?9=3sjy%LLb4'y% %* ݵjNOg'۲2La27W^~yOOLL͛ ǎ=sǎ˵-`+wT.dBjY::5JRt&S-CS-ʹ)^y䕕+s0y%TԷ7tcJ_ރN%J7#DU67[ ׭[[2^ n.c( XWR<IJ켒"DUYVI: ч^V% !0L4 s~nng~W^~y~~L8cvb"+;6\|Vbњ䕺-,H-e3 &$.@Xpeq^μ!G&x*x sk'F!jW2C,I>"1´4uri N}O ^b)Tkkȑ\s…G'{uuunn<| FF#etnu~6! @޵8%`]r#Gy`fIlmdnnk? bJ4uE"|>c()9HUUȊ`]UU5eO.^' D#LRlNTEQTx;!dM?s~s xG( B|ӼCyfୖ^M7nM3333tfffM7N3f {xxH\\~ʼqUi5M3ֿ?JRIa^ ]^^A^YstuzxC<O4%WV4d4B =]^Ο[hB^|H$b8,캪 ,Z9Rvux5S_4-k4h%p cW,3|ha=xVJ$~'NڢkzŌI31=~ȟBQ`"wsEы.ڠiqP&)&YE59E>LQ#j$1.?ƔI$”U4bO;f Q &(̙3]]M%d[Iʣ^بw }7EQ5*bF_ߔXoix\(LgzhRDk"at})z|E Ŧ׾ʳg|l TM4%Z|,ۿ#˃tF(  # s@%pĭϭٹ=( &6_:W6ɨ(]ˠtoRol-ZTK}Щ-WE^WlGԗRIopRf fgg.EO޿[o_)ӿRW^;A?b]7))V@^5 ? ap93s:9xjfƕǥXy&TB8Ç>tG?SO>o>;n'W-1' yțW/oh-M}=u+`ӥ{#O|ϲ%|뭷ٳtRw964X`T.I׼;(T U 5]ԙCv; d7#D1Eu#a:::xa1X%#AfӦMxskpȏM&7ZxV{h"wwwɧw~g,3C<s sbxJ)}R_6H#P(Z9~+saĕ@KUDyp[lR&hEMyF t՚jNO?~~buoՏV"Llm6+@]& @}(F PfX4*9Kvּ666-/-LfY&: K?W^R%}75tXO0wcUՠ#O}Wp"j ڗԌƭ7=:! pyy#8nsi#VTFIM;ve 9s쫯ꫯ=aeKKBVvض{L*lVy-[,e$,!K{JM.}q7nR8]]]Gu$SK';Ϝ r Y>(}6^>W"Dh+żbDT@-W qxWz╈,+YZ,GebQ#Kֿ2ƴlKAt(SǏ?neyu[jXie)M`Ŕl;Ŋ䙥/ L*-2#jĂ,E;rRԸ%ǎMvuv6556ھkyQ{)P^!ᙼ,+YN M?d@C:Xմ1[ı'W:O Ɛ̶ _!sx<c@n޴iӦM_<lL&7oiAcdРxv:ٵkO+9gCRfq9ce.x1p>͍-Mx;(gcW]n"Ya -I=T(P<4:N暫mg{z;Xy5xp>8Ȳfao?}Fg]9:qJn~hk~n^f3gùICCo_8 [Ϙyg,8dJ+QU[TVfPK^IW$uy% k{Ʈ[vځ99`'@SS  p…j,c_C>|ʶIJhMvw6cjD4d]) (ϛ0X[ ls^>A^H22ƭ[ I37ٙZ|mcywww̭Ng2YAO ^yl9>/YE2/ U!Zz 7ucP<=.d[cX M&AMN 75łHTY>κ[㍉ǧ/}um0n· )s-@hsdg['3uS)݌275\_rUU"H45̠ (BH4R8FTvV{D)...Z@cǎuuv DҮ7^ٷfMccźr)yI}Jqk"W$^HpN5y&DQ +qb8*(EHJ֯[uG'&nb$tR7]]U]gΜ9{1 c*sp5іh5-vaZXW%lU:[ݫjt}- p-Mj/_E'ڷﻩ *ھ{-L&NglSGhxxp2H-B~ȎJ~SFFvik1{ٹ=¬Y'R_҈+Q{^h4x/¹Dsy%%X|yR.n!h^gWeͽ;_RPMc ئ8{~E;K3壣L}]Z$16Y3iP-׾?OfYsNy:;;b!=rvnc!p8U~zu>;;k\NgtK}ڸ1szԗS33R-erСz ^׿o@ְ2kkMlX7֚gLF%u|̀J3= սe}K}zty%6aQ(W/Y{IN#+C$@ΕѮy5{>};}tWkz{:ۉU‰Uݚ2LK[[W\~;6JIgG_S<>sjƈ|!/esJ"cb3g?Y\\xCCKKbM@CCx57/dn'EuӧOsq7nݚdŲ +d6J4Nj]X?p>P41R&A^[vKKm''&l~ݖ-~:#'lR&un;ʒb5(Pvvot*4|jhhhmk;ydŦ Y93s ++d3rW_U644Jޖ[D$ %{]>yf6ԯg M ?B:;KgDgN4ZVeȲx%gX,8% Yxvr]:Wz,Y,xKY Idic.,Uή@U*UaM8UUD&664SK[d3z\(|'&&x/g4$FFT}a}-uS/WeDJUe,U5V@>y%"KTB/˺v:ؿ_ >Sө%@/4,#ta7|;ƥ{ʆxcCl,\*Mӿp}W\/KZl\/Kff+LI4ᰗR2ﻼߧSK.,544866=jlWBx8&W.5b}TUU#j@)?_?ɚL;R_T!5=kz{+ԉ+Z^5"̟gt16ئ񯌚 Kw丌=:xhq*|-w8t ݺw m,ޞJ%ɤ`z{y![JFvnSː*PS-W v"DJhdżR_3 ʒ&7qXp }z|6RРx0J@ZWiddu{  h R}zt"DJZHJz 疑WJ$uc^ih`` L%Z/mmØ_#5(*p!DM^b,7[ꢋ 6_ZL656nXehJ%!@=O% I-@H:+9w܍+ @]#+{ˎ1A(߯k x% 4WYjnq>Ϝ(e^(zL/leڭo͚(RXS)YTf1Iߟr-;JQ(G%k=D5ZrRj_iZK)-I֋NȚ(Sv3;-OY=*i񓪪:rK9D1Qrɡ!c%%L%L'Ngl  Ȉ D"KR3 R[C:d>?>$pJW@Ss#JǚMUH$ؘH$֬YsWvm[YYo|Ss-iq1=F]Wy:W55-V:oe{ IDAT~*dc)4BF, Q'hkPVig#CҢf9'E-Т.jElFJ)!`ׅikokjj\XX^h)kwB}769qL}7E!>@ j >P(BdYgzM>$@lM$Zl ę-S6)B캦Q+v/JC7I[~Gp8/)iBf|I0$lܳA^޳6  ?2mҩ8v5RPәt&3V4wl|vAG W@SsCq+ݚfٳg=_馛k׆qſZM1rjza E^p|Q}Sqi)rDD"Hϟ={m A)2ۇUE‰(JCRIѱqNӔ.ǙU7C\WHLZ敶MX䕒OzM7?~<ǯ}s+kOW\ G^iiRBDbMzƍm Rǔ}}}}}}e3Wj],Q(T%6IRb'9%M\uIR*W/-yq2r#:k#4JlZon5o[:qTNwFSRia;Hkkkkk+Rɓ')TVkdjJsZl쁢r2K!ǮNߍ(QŮh䕨 "&k,UY~QuRLgቼ-a4I$ˇ1.N Y^s(\`[O]H^ 2/K^ t,)J%IL&dxJLәL:vżRpƱ>T2XwKc+G ^YmXEQx<ow]333="KΞ_no"d6Wb/%4.K)vاk(:P{eLSnP~WLɽ#EflΙQP4/^uQW,'ːY%`jN>}=i))3CBr (co"畔KH_~UBu!DUYPJQ*Z~r|c%2PzeأWĆ!ϣG+'OF=6Q:00`Fhx.P?뮋/a͚5~/8^׿+lmmD"D+뮻e{7o{[[[$~{|ENpp+)fy%{Y. pEFgcz8\{G :"IհTEpnW"V%ZiwK+)6Q e0u‘b)9V<gHbOblMWB} jH,ٵ~N7`@Kqnqt&# ~d]A=Ijj6BᖡDA^2[…\L=)OMMofOO,vm7|c=o.--=c|6]XX8pc=vwlܸk_L64M?d2g?\6}c^{khhuJf{J l4^4svAǞMaaRK)ڧPIy>s2 cRX%( +"gTmzCXLiC-Icc_蝨H {E\Am#6q y% %*t(9hw[IeSg^7`Q%ivѳz揫_>algW Ú~;Djtt\@Oo$sSѱҍ@ nd{"KuRio۶m w},7aK/t-044ɓ'''qw~7zR??m9#_Nyw^<J;_ԝyeQc,<ƹ"-R(XAjg`L BO{sU9Jt&B!DPT*+=WGJǙ=5[Z=Q=lyn4,G;>y%6Xo]XOwc[f{s/r0l6|)m۶MiWq"L+_J??퍑 x%ocxctK+QuWM0N$i/qYx%Rgy%* !DP'qxWZ7Բdr;rv 1"/[nXojj›N꫖8*~{CCQə7o7q3g?Ccc#뷾-{~n+կ;vlyyyrrꪫ__c]r%7gggݫ(8'u=^){/)JPAyS^5++6JT(EB*Mz1ѱahXSJ\f)5<48:&9::>2 XR#;.: Ȏt:-= OQ>3g>|{ݻϝ;gwm6vH㷾|3fw]wdooO~cG?ѹᥗ^b?{__[G}4y睷~wɟ]wO?fرc{JJIJYA:Jοpd쉼O0WOIƺ(f8?B^r'S vK/Kȫ01I#S m`@wlBfB%1J(C02"jFRl!'BWHnV"rP&40I9Bru7Q1DIW yj Nhd&'Df'HԽ P:G" N6sD%K2]L bSN?ܜe]4}.277'322bJ]?gff)q+!x^)>t 192, T)Z 49@VHG%oycLiuAX("K UσR:1FdJRKKd|ܸ[+<+M3T%x%6^M>g٨walrrرc?mHZΟ?966yq۔8;Zx AN~~ͲeÆ 쟋̴7E4d^)A^\Urnh÷Ȭ͗, ^hWjM,Q(TJ)lt_ٻ#>rOEAoqx X9z10ݵ{nxyvI;96&3aKn)sA‚ ڿxe]a}6ٷm?яfggΞ=SOx SQGUoE!NTOxUA H)AlTmW:oМ^*~=ٶ(e jm: l*ao);4_jʀ&y%{otfx;D ^)%\VO7~beT^^8JTGYPְ8<4"$nZ)Fᇵ1>yLye;ʾ}`߾}+++X,~\ad6511iSGF@C4(rqwEJJZpŸyeT^mRTpBdB*]~8Eϓ';-(Sص{o L6X:y%2`]tEl(p=J8+[o,W |Ik﯏n+^)C8/;J(i=J@^~+Q.B*]zt`=`ֳpص{#?!c)"ǹ㳙'@U\eo`UJ.J۶m?_w!f7uD7|3| %[\$y)J۾Tz P*^əl.{ 빝(aNjI&r8|{F=Zƹ5 $\,严Y)Ň@ Z0 H=~I>QA e Rmn/c0Nb\jb(٬fvao q0ߪwyYWJZ;OMEZLl@Z,#I^-mS.~>28G82{,:IV,Y~|jttLaJcn}!"^ic2(~'`aanށhtyy… C<`G}QG>vzWWׇ?a*y%Ͽ2am(>4:Ҷq6ܤ5p΀yFY ,fcVff5U?)³%*X+vgdݽ\zrq&UMxҸєRu% *Dn(8cWq>8.Q(Tqgi}9F顔df4\^Y6utt\ ~8LD"裏... *ok׮?n׿~u=cSSS+++Ǐ믿_:ߴQT u|%r;D%(E*ʰz<- bxeOJT% ãO8>Q7'SKnSKGʧT2<NW" L۶mcoڴDcc[/}K-[-/]w500_|cG?Q*7RY啶;#DY_PP+o'G3'*4!DPU#?qx@b8gW9sqRxj)0'T2yp[0,a_t뭷=nApgxol ~_2*^6Wr*^(Xc-HQEQZUUEUl!ȔC̙5e:Q b䕨zYV,FlQG²j9-PT(1Lp݂gflҠf+!xY2jT׉&/K̺ſv+˅'OL}x8 oVvٹ?ɦ~{_ɲ:$ + ^IRxoٍ!O~?pe{|/~W5==mÆ wHU+ֿ uenxQMb[jOv;SM+8 ǬfWlȮ[|Í&‹_^,k-}Fk-T, Cm9/#SQJ6NQbۏ!u.SK;)14xWplbP @Q+;lRF5BjC,+]&jn c(<ǰ3 B dhM~GQ}c{4M.~M_ MATT ~(LiE̝DxS|=1q]FS78PK(-Huw 3L&SI}O(]+CѶmXdI;Ňttt<{7@ooW\!㽽'FGG9rdaaq_hoey_x%xWR1vW++Qrjd>  X 4^ݡ_BI6AP(Tə9: J3*:&^N-w?=.[\u+MyeHO%\׍7޸qFǣg?~~r+EXnB*7 7oKJ dg"DWS,P-;xJT ,Q(TgC&2Ki")k %ڔW*L%CC<#?42cxxr@jWJhV<q%.$+_J T> UB%JqE]S-"/"r,jr6`,\Dq24|,E^[(eQXV$LR*.c?i)BƝ^䍒;DY39&&csSͳ9HUp21< I2] .+Į2$)bw4[~cHJkJJ6>J|;_|^xD"}<ɿm@*=H"ߖr2*WVr96l=-K mTX`pF+NUՈ)lgl"|sS3Ī(/KViҙӧݞKO|F<"`ᑷ{aΓ?u?QQS7J24پ~R7ź7Ʊc#zI]]t6DէYVx&F A<crbq4smcfG(e~S+NCY&\HyHXhd=|diǗ4[38<@S,&~V< IDAT3Vbс,0Ɗ&_2W" P_|1??( ZGWRf$/f`E~+v_6%:_ގ(A L1)+Q(‰(*5<<(Ց́p3}[Gv.L`tlkRM‘Wbk֏6ol$ךGWJXI:rLtrDJZ@^X LJHy% ,Q(TJ#att11GkaRmVy%f}.{'(q+Oy% U䕶fV;2nEΘ!Y߅gtOʢe.~>䕨:"ڗڗk}uvQxpᬫ j^;n,2MCl.X=s*TH>Tl=}"E`tt2HJks֙WbYx;h<_v-#򗿼KpPAi Q|oQ!MtLPvl:ޤq)&)p%̝Ǯ3q l,ѐWjFeYeQx^`Lk8`]6~GT4yV2(;<JxVh3T9^ J;Mؠ?P:Z$'}:RP:,HS m]L+jX(WWRe)! xmûwg qWm/,)!M1(#eBXzޯ3Ŷzto?!$cHc*rXI:Ja *rHLŷcPW%Y!\ %/D(?\i9bPK`4\JiY=ڟkr~W(Ti"p^^hv(TYZ +,䕨r % nGlL5d9*)IN{+QU!DwHl&[0TIl14-fx›Yi}#E qzY2?]#ٲb,|J%NRv|*927x*L(GFvs}|;c9s#;;f5Xصkײ\y s _I .9di"D¬^y%P83o=_I (|QG y0Yxk{ekmgfY2lat.RpB^(K U r-[3@2Ѻ2Z v4^IfEdxt${y{l^pdB#^ !i!<ʠ/y%jBՂSA8ZzlUD9*]%+dd)Q9 DJ/쏼2 < 2"K U#$=<Qdj9:6<R~3=&"eJ ߹ ,`Jy% *H@K9V-] _^T%|e)X QoE!u6~ ضCh,pKLf|e/y%,Q(THL%Cr; "pN %Zk(h{Rw JƁ+Qpk!"cQJjWˋBye0#DUYPR qxnb7)QBjDS/_XY: B^I)6aQ(5Jm+QG^e"~+Q*D(HzUlq#oaZW|pp:7s^+ Bqx%P i{BJ_24<;OBL(IeYIJ&kɊ5$y?Y,mkYʯ^+˒w e Rvx-䅸}J+Ab3]>C2]uJdU;r#;<ǼĞP>w@\ERagrjV. JK{)?;mr AY-4ޖvjBX.g5he5M֩f4ʴ MꮮsD|>hlh4667'Fcz4Du A@y-85}s=\g>,UU cU5TՈF튪ؖQv K*`|'VX+>W"D(eBjMfr gn!y L{+hВ;W{;3@+[PT??!KM4\ 3O9N%Nĭ++Q(_頋Ws/y%BՠN iю֙-C-|s߹ysϳۑWPAW?@0%bY^I̓CW: }u?`΁#DUYPT qxӑ7t8Dq߹zy%Zp҅eYJB~r:z^ߖh)K  DUb䕁e?=(Y֑$[H8R?L;_>2劷( !"Y2{ᣮ+8<t2RrJ{?jYR@y@^BW Jܮ@֗@}W]D(H4b,h մK4eɚv2'_T]9CVϥeu5Ȕ 4 ,Xtm]9WԼApVTtFۜre@^RU.Fl|H*v@8LkRap3= p q5B-W4ߛhi?sly% J-T+HG>X⃻Dx:RWC< ,OWP>BլN01zq>293Y j啺qx/>6_+BRx囱Xr^) JA^ye@2rB^=!DP8? /fAYݼf@K+ gY^MW*+,, orv`Exç^brdqas~$4v&BJSkg>ȕШy52R+5]5%M5Z4@^( :1qg[-mMf6#<0IE:@jՔk:\ڴjM)Psphy\bת,jt jgʛ.PWT%@(tBG&B ) 00@yIR0'UxVH!3@)@mb`J2;0t,; Q,]*O:;+^Ue֙튢[U<5%[ SZ*j~~mx„ye`/JT GYPWPqxi̦| rI/FZ:űu+ hJm(ge1{v8 E^Y}2?%JW܁JtYMt~2D^T9!DնYPWqxJ6=eyܤR)LBQzE6b%J4^i`< ^yNh\'+Qm:%u겺;Dy%2Z+LRӢiԵ4ϸ7[/UTpBd X +KbXrN%Ň*G$C!?~$I-goEAYRWjʼn&+Ӈ/"^?-vR"^9i=q|x%C 4oRsqTQ(& -on_0yŔ=X&{ fѴMߨ2 k*[F*I[D92c=eT9yiI&ʰxey`+Q%zY(tUd]JVUtxAgLEW&^II&5<<(HJ(ܵ{vQ]7+'+_5ڼ1}yγGԶ&䕕+ͨA^Y?i\i+y㕤I(_n_߮d޳@sfrNh!7e! {Q=QW!⹥ᵅt@ eu11?U&mUTSͧ}#TSۑ .yeP@^‘WjB8BՋs]P:4nLx]VX ɪއDyekiM|e1{6ye u+aJ78h]wm{Wb{ ŀ2s ~+Q"D(`!1*ێ4yVRKQKn.E^ 敼X< ,Hu2o9x%uHd#䕨J1n*:̓D~bՊ&NSsy%Q(TIgc\jBȎ{sfddG 9J3γFFviG[U3đWڟ( _]O,@C[-+W"dd:8i{{J^5XhӼ3Qb&Rд%B&+Tј1TBc39WJ R4É>a71[d(FL]3bd@N )&bLVT՘nΛ .pR |n8ʠP!>E^7!qY\#g;@Pg)$4<<$BcTR&A9G 2k@?KC^9jyN2ٕH\4 mQE=JK|!,~*?$+J5>8)MBtHsLJ("}q-ՔB4J4+W4h@]%TqeiaƟh$eYLeף:Yl-k gK-<$?S*,d+",~;>A ZI"}+Q(‰($t5Z .';i%, cFWA+O|qjŮ_]gW9?biivuuQ+K<]+y%J*9-+x44MC44(a/rZML-k4=IWzڇ8M0gxX^$ʀ΁#Dբp% ;霑7\Hpv8X9(^!ˌM0J$S.<~FHvI.be"DeR1agLӜJR#RhrB}߹:i6} k,>}"lwX\@4Bbv ¢[cKX˜3X\$/tlp7M @v)1)5a+7c޼Wz#DUW\&/5*KiPE'{/C4σt.])䕨"K U]>Crz j+Tb~2 J7#^iDS94,+QU)@Q)Hp4#%BђͯQtYILk,`ݲş䕨:"K U=(]@]tLꢖkފ+=_r"є|opRfܯ%73㕚SJ_ U5B^)L+c3`^|+!尢`qZ*3R&k{ '$ DU g#_;zVzVqxı=$訑vQ)skR%qJ_]D9ǥSTqđW+}]5J_EM4G{8/c(Ed뾖2 P(PQTƞ&$e/ݯDaYF@.֌bXxY/ G"̍1jRkVO<A]ftKy@}d`U7Q(h v6c ='C;Z"q66]tuu{Ny%WYxeO۶\ٓ^b^%ɿհ4B +L_Hٮ^D^'!DP`Er6zddc˞g>.a@_@_^~6m fD^)?W xeKS[mN4LWZh,gy%ꅼ2 WR;2 'P(.>::>2br駇Rp PI%3yxhڑWd-M%C-epA&PaUL^>i |;'hdҩL2JWm]U ^),tu[n8 PӨC5n8OJe byL'wG AA^v!DP(ȨtMw8%  ea O3n*d2`hV T䕮NW#z+}JOJI|G׀kI) 󾖄2^xY|-)Ռi[8&Q+TO2)jPUU#aL'MѨasFz,ʳ2{D~"._v1`%l+=A^|pf[*j89B:5 Qq* rS%=:6{n_DF^ȹ\f yyׇrW=R{D] WP ,kW:LG^[!DP(8'BT65L-n_DF^i F^B^3!z+Q(/_^Io4jH. % k*xJT= 'P(TNCգcTtF IDAT:#Lm9DSqA!O-M﯉1qW t1o4B^B9~_4K?\03P֌2?e xlI &b]flnEQl`0Wi9(,Pt"DUYP(TA{א0x&RHJLZ_SytWJw7J="@( a!l_jؙ„0h`MC㜈%w8 X[ X+*D1<1-0s aR$sSArmjjjjǏx/z^ꫯ&鳤8WjM,Q%j^R*N[WQ]s4>T/{9G^阸tWJw7J3@X~T4`;c(%)O.O!mqW͞oP#5K:i$y%>Ӈ>($G*(C8,ݶ^,+翘 Sک|ŗ2b 䕨zYP(I1hiʜ#rigycA^2c+=+eSEsPU5to4fq> Ɓ?ϪR~PxTW˶Y>S~_+-zŗ~O8ᥤ3WQ,Q(ʤ\0b8^WZW6D^阸tWJw7J-yv䕨U)W+裬K3 h?e%eW5mUӞ_#훚*ݽ(-S{ŗѣK uYY]YY]]Ͳ!v=VӲo3,Ϥ>+Q)D( e84H10.̈dJ1J..J䕁J cPJ¬u>6 _S"9Vj{{95_kEܫ.u 2_yUVj6-l0;V2felB0s!)eB*y%"@P([S#FGĮi.cT2J~-{Gvn%y]`f++B^>x% Awv*A(2$c t+%RI/>7Ƽy۷ }ji*X`RȁYi@5˖1!Zah*-| 虅`n/mS'oz݊R(M!QO(^`cbZTJTMGYP(WaO97e8bLayX]`f+++WWJJy6/Mn#(qigqfޔh+S,؁r,/RM/K~IV|%WWirrܰn]7\G?zz[+WomܺsOonX۶.wu?ӯnJF DUYP(H0x8! 4?M9ٹ]&x]`f+++WVJu8 a>) X|9rx ^awm}] [Rв$䕐OR .am.@ L$ugSʒYe-m 0[  $C`։ V,$;`4g7'L2gߙyg&0 '8H"$@ `ɶMk}ow~}>yBT"YBqx(6P!3~#9$Ik^! mWWW~" GN/Ba΋=ގPҢi\vyhshc,ž-QrQi_igHH;'GXy +W? vYu@%-n}٩ɩ)/iõ_K_V.=i+%AP T.xyR0OgHn 2C ^IAU ^ ^YY2bPթ+cuD[tZˌ={FE ;t7ݰM+s-$% ἖KZnT.ib4IE~+r%APoaeb5 WW+++W]F^(}]>gbl'rO#<։ bv9ŨԞ*X^GFFx"ⲕosa;q^}RQ/ii¹s:tWz1s;ڃHJ A^:)un5(y*.|`S$WB_JʴWF+XF` Yu^haW.h3_ܩ♻ߝ|l:t~ҕ;џWB*̲  B +(ySs޾~y`C}@V`ʸ xe J8x;B /kJy:4W˔saJE_W^qXSRFu]տ~ƞyhweikoK}:BIgw!(,!B@qx.w9SˮtLLk`^J" 2P u+++!(vgEES2Mܷ}ˁ+.[+ BQCo:ѲRZ- KWb:,!ªqxgނ Id4I0 d% ^WWBP:ᕅ?1::f-7.ZQ2K:0o}"O}Сѽ{O닗&%(܌cp)w;E٣[+)* $}FG(j݈+F=32Iaݽ.go_W BQ |/g,=$xC@V`ʸ x%x%'=䕺tjy.BcFFGGI^+ٲjdܳ_}ի>xU{DT@AO6޻;flu_}>9sw@ܟZ^o?Z/FW2WlWWWJlJԕߖb"cl%x˳ne62YΒ%K ԥ#p*\`8߭SK4TX@?b d% ^WWo|lQ`~ߎj]NtM!wr=ilRt)E%xί;/:v\~J-'xԷ_<<|;;ÿ_ӃWBfYBQwZIlp>%@DӅg 4HX9;GGc|N0}zcTDyl},2L^ UrSXu~~n""Mn991ƌ1LFN(KED/еTo(qM"}hϊK'ގtjIEBW^ykoEmRWzJ<+NjW@A2 X XhbZvYCQn}ꤲzU0^c[_ fbWۋ+r2Irb㾙۝CZs!\sΝp׮ݏ>蔡ɩIsMiSKWKӯ%\r W^|M_ヂsi$yh0$_Y|^ ( K*\"򏄓dgHY"P<]ꜟʯ7ͷ:9>/&&&&'&EShJ\lL, KD򏄓<=qFJVVδr,xe\c-C}e_}{۷+V+ +xe+h7h[j;ʈRњ FǎKA@AIU8<d] VҥC|CWxUu,˫skg^ܝW*ʂJ~-C9e+1QX+mҸ;wvvƮ瑑^{夡ɉ eKMiTW^~'^yS\s_i ^i$DmݵkWZaTJ4ʠ {{7}ߌN {<`o_WҮB^ځAzA=29^shg_J+kioh fʳd#3`%wu]&rV"[?71օTB&٧RޥH^g9lNnH)Dt5f8uu !_4NJ" {%5@ږ©.iRÿƃ[~^Y`^8T<^S>,-X$$=Jd A ?s"qPqʔxe[G?_<%^Yl^)axeQyUo$.|aqJwpx^ ^YfL0;vn.)RľXshSSEj399q=i"̙\n][[[|jwO?y}SҪĸ ] ڢMMi皾 BhBhsq#2G[ WEq_iQ͹EEHp93|~>+! K<ĄT1SdkZW+oih!WZbJs%_t*baZZWWBe_HEHH-8ԓOJh^>R"X- Sq̆nyFGG잻J#,{z,7/56,!*Rz =yx5Rb+x*<cq,)t@% xeix^2B>-9ǿWG>: /dڒW WZ_2'iyҶO@k YB 'gcwP%;~ݩ8,]Zyo ^ YBU ԇϦr%; ^Y2hl^YƼROnĖu>rWWW|Tu;xeJm<^YR?|E+9JWLqMJCcnݺj*۬fTcfFUY+"P.q5M#!Hȑ\a&Lt#qNB3B#1lkWS_J\YA?On<)\,L:ݶ+Wn-l2 hWTOpDKJ<m659955599M喉ɉI%5MӴIM4qM\8+D8#\8xi ˒ssV5o|*.]AAi  n t%!OMB-kW˶V%GAn{>nkix%T:tЋ/Dr!qɳ+܄wB盵+K+xd`(+ KbqxgbjYۼ2heTh (Yd󪹢n+15 x%Š2sJ :Wr92+S^`<x%,!Bᡠѐ U"Nׯy%MlX5ZJaii'}n>WWHZ܅j*W:tnJEӴ)M3m&Ɯ q=U6YӸ&.k9KeM˙iZ.F4hM^YPYBI'{" zx`4m8xʝC#>'pCƈӈ!"{e9WW9QjL5{SO;M4iSn^,sqjJ&&kE\pMǜكs\)J.(/DY9cQM8Jƅ2 *,!B p"*&%PԲy% {"֎֎=YJchJ,MhIy%+5{|Q(Ja28)Jtx쿒n.D.2OMʔ~*'*[YBUCdNe%ixˋWU+7"׋4Q xe)6E> ^ V@AEUZ9OJiziIA#޲Vm-1x%APJ$^wC7Sy'<=)3͚5-9YnsI)\.s3 d䦝8w@@\ 䟅^ \@AV,fj ^oJ pyx%:BcMz[ LWb],R3g<4ick2G'uO7.D:+aNj^J!5s̳-$lXl.!Wxexo+Y%%"r38*YB@EH ljs?oLBe)*ZWBP7^I$Vyu>f 2ҕBP+W-Av^)o|>.E7畢fye9)؃'O*,!J"I)WzY1,=|z-y%m %m{\x66nh ^%%_I&_y= xxU^+NIi2f>h"q)!(2H{8<]]] ӳӟ'ƍl(Ulظ>&ZH:!F{;RWk<+yTڟ<^)hc#"zȉEm/cN50s-׊^PFaY3͉13#FD6{)^{OɹO!i;#qʧudں[LsŊ5^vmҢP+D;n1+# BW>7FxJV!Ds<ڈA^ D@ATPE Yik}~%]p9IxeM4m||ȑ#vھ}<}ٳ@gX$=xA jf#i'O12&IKdTK(f&: cL!HaBQ &#!WȏRQB!jW57B3A79iJC@AwI١Gi5ȋW  WFJ#0^DQC8.̙3-[fklůAVrx?_u\gM gb6nv&ye9P-  \T8<dND7.3$ᕵ۵js^xattyΛ7/I?~7~Cڼyz;s^r[nټy~Ǐkvĉ7xc7|%K|0_]t׾^zرcϧ> .୷w=+sLJ%,!غukR͜9s9$ xx%AW+)+_>enU7A@,!HʼnCeiZ靟;?{}N_'{c+O}$ᕵLdHD7ovy׏c㎄Eꪫ~zׯ]'NXv>d>22rwq6o/}K^)x믿~||[+EԦ *[[Q2 x`.N.˗e^I8Mbk%APhqxOT)JE x"t&ᕵ}G>SN9,qs}Kڱc$5MG?}|o$裏g288};+0JBS./Ce%)r{hh=Kyʨ'+)9uTr1JQyRTER*Dr֬s=QMLZ2K))þ| YN*-sk1,;t+ +K`N@ R(eYB{S˴g[*tBR<_(f8)RJ9M<^Y/ouY{Goc>}q?я:t~ -[8oڴ?eٲe<={&&&FFF6o|} SO=;~ѣG7mڤ]嗤?0q+.$.9$YApɨ5ftFFc=ɘRFY ~Ֆ،*|'cWٳ5yT\SŪ,l3iM?.݅y%#RQV\y%(/=7Ar`.\`8NJ *;'١=Nq!R7&W ^ 1>>w^x_%\rQsvaO[֯_goᆦٳg׿.'xGy7/_-Ͷvm;v馛d_>S{nݺur̙3뮻KNo 5 RWWB!Lo_O1{ . IƵKN=;;cګ< {m3\P???"@do^ D@Ae tY8eq*x*VWO?]}o.oYdի//eDσ?zcǎ?<uuul3+%2^WNkj'L>1m D9݇ $ KrTtϕb P[_wDIW$Nh͚5_WUU͟/ȈsϞ=۷o7~Wl}vl){Ͼ>۟iӦ%+[:;;'Oēc{yS4JXSUն603g΅]dQF1*a"~~s63ʘj"K#"!bB9  a5^IBxIuE6=\tvd8x%T~5 lU8SzĞ#GDOLNNyF|޽1 I @q4Iy>-\& # EOeb!!.NEle(1I Z/3/#Xr]cYΙKYӑete)n{f\O pkQ3d:::-ě6_z٥ź8tX 墥K{ 9Vrx'x jW2 ^ YBARA'+ +)^-Z$ף˱¯z/g-/'& k 9GnRg.$yˍYi0۵miW s}SZgI_1x2OdvPvi.;::z 7oիUo҆C3ʨT-z)-sͳL{y*ySeILYjWx<ґ^c!Km&TLYB'\:Ќ@j饥KHn`^_$ɶ?O=x1Q\??o7!oYf nnjm++WrEQy%~)#>0>ڮͻꪫ;l/d/  w`D7:~eH^xslmeXLf$#qݣ_OXJ *w3Ov4-:#kWxeA}̌'~ĉ>qu(hOlrt7'>111H611O|BnoVܴ48';B>Mxe~ۈ F+u|Wr޶% +x+_,%Uxͽ+  T8'oyG.͛7NNNݻ袋y9??r))2w^ӵd2p ].+}vs IDAT ,X`R2Y9Be3$w^hjWOlhhH` ^ U2g`pu+sOV^F$`~[lɿO?1?Dtw\2}k_{g^~es믿_O}S'q[Sd$E㕒H׆Bx7S+'#"GQ}擑)0d³~$Mχ2qaæb~4_V(6F3G}]?iv=^2/У0ݚLn\eT1򴼿 o')䚦<4Ms?;BDkIϏ( e묉u:3l6I~E珍19#G:f7.h L~&b9n\[[`m M?WB*̲  3O>ypcU+dy%&Y_?{lƥKGPovekllo9F>~[=MyJ;q+$>|y%ځt. G\{@:ӯҮCBmί$a'؃iFMeohlh7)w^`"%MG(T0YBUÉ}i.} ^ikڴig2:+{ȒΝOnڴi޼y>:;;ۿ9 J)*ܹsh0?vw_$J# +hG>>0\MBUhҁkW゘ 7Dijjկ~/smlllll\xu]/u]w~G?Os93gTegy裏[J9&vLWb ҁjV΀#G{?70y/\%qz [sWB%|YBU=9bR3q㽛zN-vF՚5'n ^i F"2K,YxԌ3/}K_JRWTWWw7x7JWJUG}.оm-@3JR%ӄnнdX͚\VJ\ȇڜ 2Ovi~EX_:`ŒۊI5cyfy벖F%ߗqdQlΏa)Uu(Yu1fxd S4Qe'-՛(/Kݗe6[gͨ&jkm;̿O?yϗw FMft $Q->4!; _iB?e8pr ^ U0 e2kZ@*:Jtjɓw'o b Ւy^"j&=LJy%~YyGϲ_˖-OepK|paLjI^}*PRQrm(Sa ;KF0>QA%APqxO708rA@^icJk5444>>~رm۶]s5kƌr ^P^LHJ+y92*Ux^8饗^lω.s%_z6E/KiJPR&Uۋ+iօK,3ٌy߲yUU5K"t+xǏ\w'N+|fϞ]u'6zO&qJ H3lgIq}==: @ "v*WK+с3< zJcU`ϯ^ A K7NFvLtk߮՗~v>'c8rÒY_Win#"3T*fLIQU%SM0D}+  "?QÓn,oHZ3.3/F(3ҷ?tiBjWJ+у%\?ѣ̝;Ͽ֯__WWʁVw^)h[fMxJ4<@wxH'Y͛{!G.јvX;妖WދvV;C ^ ,!*Uk|blյTzn)UxeNc{$ƺ^@%@nZ}͚2.B RWvp95o޼9sI罩-CuިkW>~ކSE݅er"d_D-%APJԲϜzz6lxL<!>=W™% ytJV%זN1ҀYqsڄ w`*b K&A/KJS5nkvH6C݊e)_օ5t12.fȖvcrٍ#Ie aJQ =WGzWYSc6_fJX*fy@7"Zj Ͽ066_[O76沫^9:mOO󜴶6H.,CHyJ|d AT*r 908:'- ^).xz۫IJ*]+w44'ol5E4vq.o\44'$ye¯E܊qkFd(.չr }F׋vyU׺˜˜ TE/./(3Kn?2"L?(^Df{T|^wdn 3d$bL^YW^m۶1{us.nV76妖0+Ѻ;f'"UUU (W3:gl/EQT @%APeqxv DU כILY+B/'j4z1ǎ "l6kn6mDT3 d@D)Ȇ-ڵ)Y<-6wv;j?Jr !.{nB\׋&^<4qqSٙ/ny%dյ7D f }Jx J l?OwځA9iy7W.\@|*#&DKJ9#F5~&:ԱcǏ; R= vEK饗>Ç9S/xz,:xiJ*P|QA5>{}`Ɩʁ@yQ+wzkñy81RtcpK3n{.m3u";un/$-La(˜b椪֒JCԌ*K>Q̲3ݽ'3kXuQ_'^<+w̜D˒,85<+ü&vp+fYBU 1c0Bo{pAD.ڵ5C{n ^ Ac9;z%_}wT3D/Gu˽8I!f^ / KQn4OZ]Բyϕ^Eqx%qk1}WֆW֫ A *G%yT3a'4wr T ޾Oݽgv'W3A\^ID̏?vxѨ(RhBJcJ 7!() qHtP-Z}r0OiƦ9s_,0Ls]] j ZGzA߇ƒWB%APU?2xo_!<,*-/Ah^ID;,Z; ^ A%hxD>rM>R` Aq2>{v Ο9kf{b%֜3.X1#ej f ^ P@A&p ҂J畺2JSPT$RaLxd'~0ׅ {UY?,\k0HQPTU nXd1MNL3rѣG+͛بW g Mr)3Ąf31i4IsF@Ζ"~m`ӫʹmGNuA{k+546lXf *f6mtHV*q"Ϡϥ.aY9%ι2K/氣x+W?4<؁w;g]؛W!=[, r\cF[b9C, ˤN(g+(u~aq e'![9f͗;0lQSnf.է| pllHsiV-j\=3wbֱrU!n՟0-slK+#\s6~xulOtR ™w*ufK+;[翼BDu!"86AMVy%zP B8Y,>~O٥d8*OYBUזdcOφONԲx.p":~7~+JEWg/6g++W޶- +a\)׉HqYƘœ%'Hpkbи0-W|m@{fFin Sι5"Ҧ4ኦiS{1@i6]UlϲdxՌϸW'[ȕ *jTՌjX*j8Z%y Wi?䄋?qQnǥ;&K}秹^YR{+! K T8<-3Mty%-\P˃+:++Wʷxe`]7aҸoLi'-DK+lVS42Fvt8gt( 9\2hP.}Xm zre<;S<@x%YYQxeиlxHvZ>^Tt^I'3N7W֎cd,RlREi=`Sߐ*+->)?q!Ӥ=JV3U+)T_B&P_AR ZM=]6N-+2֫x>u8[i9i$A)RJyfRʑ{JZMT^IyKKb?QeJ } sI;t.fv&\q¤X:>UJxtiefDZsX2>X {NOi )9?3f9R !Wpfk5M ͨ1SSS4e !Ʉ IDATP1qvpX*ۗ%c|,/K!-c՛!`rII+JfYBՐ'<kZ44eDξ%~1+ˁW>iIH^yzp25WWB)⠟(h-B ^{pd ATC*U r908X8T9qՋY7ȻOWڲ- $ƹfGWWdf/svLr1H L*MfEbŠAW-R !jKk|lwwB瞻}N]8z㖫w{ YQiӚ ʢJ=RHWWdF3A,d/>z%kHE!"ui`CjNDL83A]=(R\mfvأ$qli>V3M>b. g{nZd °eo̡LhZ0\6wԛvx.wK*WFi`R'+WG\ALP`s&1 AL ̤W(#Ţ~,WH& f +Mr T\5YQx%c2od;12Ф)RUuTɚyS\22w ,UU5=eB"K7_O!DLW: _)eiNzen\K,ºamVu4Vm3j&#%a,%Zю)m3&=U/KvX/p2~ds'{Tm)׭y#r<3c +hM2Ry Zb_EH^iJ1e0Ff."Re'R($aw`i[QHqBdzЫ71qA.J~™Q~1G2fmOrp,^ ,!jT%CAt`pgO։ 8pqg7p ^%Ҟrx%HEp9'=>F#Q3B76)v,l^k#h nr>9w-p6"E/?{!K R7 ØyEr1?WBU UATg}:LKuʰ.l~+.=Z jJo>sjyoʀB0x%TtWbHR|*WBPHYBմzgoooNݽkR=V 2΁W^v{nZ vYA#xeu\QiOT^¦[״͟vLK+)"BWͲye2A<JRpV QOφONpj ^z ŝwοsvڕ[5h+*䕫/CD_PzRCĥX:Š{Ì bF<3N " CĹBYKNdƽy FcE&⛲oD)fDTWWgn6m^WWg׳l6:\C3莴\$RgMh(Һ,#խRQ<?KTץLT|wkiPS:ԁ&QzIYo0Vy_ հ,!j]%CD=Բ4q2@g]۟~:'+W[ڰҋ.oddz: jUʀWW)w^`pryZoDcFAܖN2˄. %W!םKFIg:,z0cr^z# %T{ u64O*<3M0 <0W^L>KώQ"2С.;D^gbB6O"6b,AO]JFD.JU+-HRE_IIE؃'7 Se A2~ox_H+z"^Wz\x%x׽+E"^*SqKU|^82^ ]e Aht3} x,rOg #AoJv(RU9Yv8M}\Fy5:WBQUS6Q4veuˇ^iY,ͤ w@iQ<QU|YԞ%ZWz1l.-~j+z *3g\ْؑ.CP,K 0ګCÂ{TD陁W^z;++6Rx;U]ǧ(4Jǔ3d%os.cGiSb-dd"0DP@b)ǢK5䕏!EUX"(']0f+BĤQRm3$}h+%ASww4,0S"/ xe5s0WBWͳ1][s'~DͽaU,!:t`pA%a!ҀWF++SnWBU'ʸyW=E+*s%Ad{_)Rct=vWW ݫ !N 2f3΄.TTYu+ȁynWq<>HUl6KDu(euuuI%_vVkhn—,pTOOKKQo]RzzKZOP,! zgo]jRC+B .JO&X i+ s%(8woq[\K3(R[v*Sw{fڛ$KeKZYI6IxSǩ`KK.XvI޼@U8PJ*6<@B )0@x( OB^Kz_Į%jk;ӿ>}N_{vnOm==Ow>ss~ByaR bI a./NS2e%?uWF8g _YQ|%|eT1+A혴'a)^_g}৾PTOpmyOuWF8g _u++/Y8C>1s_YN{𕠉@,K~yx‚Z;R ?19K#g?Yi/}M:+ck\՗L+èΰ xIL']eLI) ئFYPKþ3GS(޷1B;<ѷ|4 0ˆiLt7-waug/|e\e,+c&|%h.,(<'¬ß@3dm:+#I\՗L,I92Yޡ(Om~oI+A-(,cJ+^JԿs0l4hlJɫ&M(3J,+#/Tm.cJO!E)f$ןЙgyx݅څi\7 >t{s|%t& PɠTJYWFn_ ٿ9.>;fЭ jrHÝړWRoBF耙 _9BA+Ae @K;MDd 2ڀT^&Pr,KA$9 aѴ$B0Dy~+ٛ} i==weBK( aH'}!6"9ۢYWtf> Jм %-u?};99#aO_ _ _Y$Fn5KP2Uyî iYoGbPF^f̱ǎH'+}입?~,Aq C/RLc'̥-.՛h}Nt,˘ǯy T5#A<+SD3< UJT&DLw+AS^7Ozx8EE՜ ~w+++\.&_WSXu%Qd4w>+e;[e4;P"<g)ںrCMSL9W8*Fq|eQctupwGyyd2N_ ,K&п2$W6,d?s*S3K.̕25lWV2ɫWf+xe?8yj++-eBW? &b^2^# lֵYYLl|^>kzd5<uuPw41YeMWW zYȆ!S4{G"٬k[_nl4_+Y0'ӹ+yY++AW/qBCZ[%fbeSlT*YV_uxf^9%ʟTC BuJ@YȆd!Yמrjf'g8df3|: |e5t -WW&+3RClBMCSW@f vzaum+frgS3KQi_ _!m _,+$J  e~$W|e%*%3|esg$YIޕ.{ԥk9_$pz\FԻ_Qb\J ~@<GQD$ѝM+A+e cL#g?د 1CT|bJ_ٙ*|X[]_[-!mGe69L`-`VyE`=/c2N1сr eB.DD~hNٽG88 SwN`v tggeRa9z(VJQU~zh6JX@4YYӧ<83XmfM,H 9&w˗4L(Kc3~\;||xx˺Pf]sgn+!BgWṌ^_iw +]+gdּ+|E(}e0DA+SJ\ IDATR_o"g[zg\r'"tɾzN { {KBꪞ@|oV̞4$5W`C1/ureaR+%Z)HnyV }#JJl̲6nnr"_ (K36v$LY?1:ᇇWF섙U)Wvv:; $im9:s%|%ΑdGQo}eBsʲtMeuRra]e剨8nXb2'":ոa_ _ >,Kmc+Je[_ Z(K52<||ıc,+#vWvv: C$  2W_mi(o)7 3st[++A_8o[OVi ʳ+IT5 YoE4 `iɆg <~\-ڻ$(t!Pyʈ$򕝝ޮɯ'Wj%WWJŒr\ifØ-0kUaDmWZ6WBۧ,~X _jf*eY ! X;7ġ~Dl"k/Y %ZќWF$ܹsN\l}+7W:\ihfy]v_KJSJf miJ|"pW !+/ZLlnH9ݳΤД`S 7OLLL4[JŐm:R&])`eM, JJPB>cfت,j,n,fž+OSq}w?ZWʛۿ{޷o_011y5_N2pe"|%hp,l6>oO4;H;J"X, 2rZSFdV|Á3S^۷G6tݿwi߾ql-3_ (K=[{yTl _ WFhXa |%|%ȸ6+įܾ}ѣؾ}Jh9_m޽w~P䕷%|%P|2E澃$7bBggIk2D}}4uI$v,Ol)+-vRke*Y?td,_iwwCwqPdy鉙 篸Qt666^JVU|m-oKyCfUV֫ (Ku'$z\^Yvp7L*AD&I)ʋZRJ֗_)(糧SdJ&>sldJOJj_pF3g.^[LTiUп4 PC#ξW6tvv =|%Jn@Y9OJI28i'hUd?_pVrС'NhWCWj줢^^U5_)n_ @<_crk׮4ǨVm۶o.?\h9uu* %i__95eT[zx__z3':я~tuuE{[$.ڈQ }<8|%h'5+mXҿfݹ7|gsU6niz.////-k,|%h,4 1W*30#RK/;;ȑ#ᯎ=,W;5X_+R!я~h瓓/OR]_i9LHWvpWz6g#DYѕ+WhE;[a.h)oxd1@,K ı?d c6-%|%)|e3,kcccqqҥK_W?Ou7 o jV-)|e'W >+PL_)sT陨)t{9CRH;OկqOW˒5? rͤYe2I],%*9*(x gΞ} 0 gىkID\a-a4=3Sqv䆯[o=dY]DdrE!|f&W~Oyg][rʂUĵd!]!&oф8@辵41~H̾2b+=-'>}{ |%hO,44EW&ڎ=r•+Wsΐ;wtRW=Ov }߾o۷ og>yFkkk_Cr_җC f綾^o}˖-\ndd䮻>z052rHֿR*+ x<}=02{ҥ byۭ|%"ZVɓ(V+AkX#O?~btte}eM\l_/\YYmv]w}_?^|ɓwygpO=˗뿾u4Yv~_v\rʕ+w?O=䓻wVnhY֧?|#W\/..>}ӏ?Ν;̆eY>vrzz[ַGyd|||޽ f+JȾ4otv8bz/i^郑, 9زXijPgq'eɜ"n_1+KU+mroo~b3_}fWf{VDm6gѣW_W{Rf ox+e}ٻᄏX,Z\\.KWrx-TvJ2|%h2)bYt_m߶ҳyU┡ |f4+c$@R<s)t>NLL'?Ӳ{Q~ި$T~%o߾/333%O?75?K.=z?OE^n+_ʵk}Qy2' d~=lXeQ5#g`-Jf+CRB_Y } t02_ ZIJРTNsēD49Bݷo߾}{oڳ'HKJif+[!볳o|GYZZr5M=zTO|]l۶??!z׻|A'>+>&yc ;{җ$Ї>tk?%ڹsm߾^xӨ뒽&TMJ_(fRfvm`k)tbxsuwH7)k鄶,v#Z̖noJyYq-TԺSKBRKAuILʻ,z,ӳϜ=[)gWFFF <FaEOA2Df@cŖAXydd)\m}% lЖ^R()(Iɔ+K7h, AB,6%|%=e q !{~/p_~*+;lO_̂mFgg;^}}%}'7qǎpy͞={~9}nODIDozӛSSS\XXp>8p_P({8kce;JwJwP+9SH'--sU{Ӕ'8bvyT,飽lPZ,Ձ`K0sh9&d/?++V`_9wז f y_4JX*bIP *YVI{%n9xa2oh0J+JѿQ{x~pJe 'Ww >|oio{ۜ/]|xO?|/}"|-D0gXzYj§ʟ+b"ؾ2 fgH*l*t**Yr2YZʄq,ݩueJAza۹S`<\(lݺ5™ri0RqE!I0*50,fro isT~2$&7T+A#dR4k__m+s}9acc'$'|r2|P(oWn*ڻ… G:>Vja2_9b:dhv7DPKPg7髙oQJt5Qat(S7xc-~R"YJ9@Yh9~Do8D[x+Y瞡zeo!&9xJJlUL*8󏬲Ws|no+&J_ 6 LL'ۙ|abb2$e|%Wѣa/_7|2<,2o|O~|\YY6ooz!(O~;c"Qp 4LLֿz%2~ekk˫y;4SiS|em*J @Yh8&&&Ə?~Ч/ rO5,W4F+{ܿw՛ޤ./by7ypw3=Tj@˲|uEbT2$b˷&9xӣ[e1@,K Ąg'CD V_ {g?YyexK"z/}Ӿ*K"wO}꯮^K{Gy{ T"|%Q*@48)wRGGYǝ3`E"2 4_p,767,[<[̾)H/[iv41U&} _ %ç,elqSkLѣGee)7/ٳDm۶S}0{?O?sn׼5o~7yzʇ,qJ<kkk&MDJ̈́L3-3g9<{8|%/PZ2W6SF=ΜYm~qϞ=[}?&ϨP(-oy[R|I!S蜼a>YД'̤xpbN#HC8K@c111ގDz̈́F2"^Ck"X=L+Z.˹}-,Y2 }?/&N\ij#F OJp@Yh,BFWW⥗^֞y??|<_}%GI>n$;(nlX9!ۥRRJJ2MQ,7sfNXd$6*^SR(L i>%M_ (K-Y |7?x=WVSZ0LԺ0qcE% jt{@JИ@Yh,Ǝ?N'ɶ&De߾}A{L+ q-h#La3px(BG+E,|eŸ#ve ;j}Y} 6̊8q$TX+p_)||(J[ !?B)@",4:$N23㎳g122s?s[u < iݤ+S:3]l=6EJSoE7K%'%tRbU,#p9@tWJ+ziֶ,v.Xyw %fB֗_?ND5@Z[һ _z -4}ٿT%-OX̙$Io4;H?V|eԣ!HM%@2v_Of)W+і E+r^_)odjW"[GURW/N8N_ %VȑaGRMގ2dDjo WPgbt okWrw\@s+)<%|eth W&@NV|e0k,|%Zz.GJ_~e"*:|]/kM@-CY&{^4nŲBP+6qpjx^(t"BU9 u,gL>GAP_J,9ayl!zY6 0ܞ"q;h~/Cˤ+![-p(XZYWJYZ]ϩwB3 !Y>  Mj_Ys_ Z(K \ZWډG;fW;\\X4f|L+]uyꞯBIPQ,\Oa΂Yl9*JAӈBBbT5(gNYAJRx_>ْ|gB, a-z&eMS)-{;^jay,.DOd;Z-b_[춯)ra8aXU6BVB%#K} 'TQ瑕 $gWf l3@p DZW=6\ÍjhATyf͞Zh/U>M: LO,K5S P>.>F~G$ 8gS*EիWJd%?;\tsr94saʡW00襗ۿ4$a CZɒ$\b`uTڿ(ϔ4?e R3k׷G;Cr8{u%d(KRWLĺ!o( ]KP~Q룫I]5EWF)Sվ(ʵ"g2B퉷(#T:+I+9d`xUVшէQ__5M&D42rkR"_1%zWݳiwLPKd+}cWVҨE62M9]&{{urivvKQs6!w/yWceP535F d}+J;{CO+,+Fa.קxo-A-(ᐉ}W)M8J. w`'Wz:]Ea nz/Jr3dDAv %Pu㫕}%uw;s˫E]ؾmXx(\B<ܷaj9~ bv9" ƯdY/p^-ˍ)/Wżc_鄛doIZoq*#e2e3ГrJ9aƯCзlyؾ{tL[yf`bKLFʮBJ+YSőƬ}%$k(p۶mrZN6W,@D gYQ}% m9&lTxL?WHj/_i32yFŲZ*YQ`N+7W'|Ǔ|e#!+YXl'! iq]/W`J2K \P)8&̐z9إ04n(}qa,+_+:IY !ueH l#Q\N˲3 gJ,˲,dp/^\ǵq$Ŏ9TrT0T1UX;& o80 &R\3aF]tN_ ̷#1??,UӅ{;'AB&0yIUW@W;Jb~Ŋ:|eCJ_وr(sG$um8uu{85II9?xU2A9N}@5cٵkW&Bt ޱc3333z0Dұ~~pYd .讼v35',o!m+ww奠XR%[ +wJwGTEw}.~eu?;JМ-j ƵjĠWgΜJJ+}s\XXggJv@YqBWF;;sW΋3"|%50_4|L7W^. 5U76l晍'Wt({f433333nGK/=+(%F0MbY em+{j1t?X,^,|Sj+5DWzT_"/w >Rg.vKyBvGOGTNLT.!=BmCGA+WΝ4vt),׏A'IQLt>fYrޜ,D,Z]71Mn=fck)T{;v/qX|Ss!ir!)%U!CL={vxd؞>Ui }s|exKEukN;r!!"#.2eUM4@Yqem+m$k_LFg _Yq% +ѿ27W\w(>兮BioTE7A.ɲED2,"*OLBpl!iA |Ҷ |.22|߰;EYڰ*_3\ ðOSzeE 9ә$gzfٔOMBYf,%g)fNE7g3QB[SNQ޹sgXcl{ҫSh%ND\ΩID9KA~ץ0Hٹg_ݿ90 fіފUm+)eOOOjV$M (tؽē~mњycu"JJzWz/g IDATra(axeȊ湋3|[3##,'M*$iTе6ƃ T/,v Is3Ӂm^pBH_j |%h*,F|/KhͿ\\[|T6W2x瀯TiM}(\㺹\oԳ&~e-|edԦxyZ!|];CRpJ3W.=chwNV+AMp Q_L{GJKX""3OfAzDWWn,_" igxaPvɐVCxن'y(ת 1H59!ܹ8!mDz-;M4*! Ѕ44c10^7X7N0B"퓵bڔ/S`L6ʥ]IOqa )rJC0{X;W8Bί WkWjΈ+s//^R~ڠ҆DDdK#OfAzH+ 6Y#%ʪ|%WW7NucWar=S%xeu,/")yBЕ/,I Uᆒuk(LF2$% ŸGÍ$WfF {߽W$c6|i]ҲRQCF} Q`n?VQ}әĴT5e$$$HțUdxIe VE~UgpJcs2@+ڰ[:mO>u94trh9z0 !}ȃvu 9" z;_nFϨj~!ޠ'Ґ\[’׳#_΅gI,{G4 Es`惇ny!J멧NqrrKo !"(r]W2d%ir1+9y֖WK#6 jK&|ero}o]sT#.=Q;,/v:)]9~|eB͘@y0PFY9!=@e)0 C8Ȑ2ev?,V]|C,=:%`C,GPW& |eľro}t$+ >R9k |em<$NpraaSOE|>+6V)UMJP00lW*_c|dcdcyZ[ g0roP(ERq JPϦAdV2FDx7CdÊOd233'O{^{..\xB ) NK{47vx+ n3/^@Y5Jww庻rewZg遯 gQo_9ܷAD#D+Y TC= ߁?R,K!eRGY>ŷMNw"4c^ɀP&, oJoI-HShWY\+Dx?Q*PL1IJ%Y39p Ðܱ͒Av4P!on9s0%KȞ}Je˖Qإ=5{o޻w/\XXpBdJ'r1K+7x^fcϴ>N|%#P@[TWNxuvMN?5_sY+J+);KӧRs1QF $%g @  Q?W Feh}3g|{ 8+ml_ڂDd _ %e_ :!:XHtb͕d9t(\NUrr6u^L}_\.N+Ae n?WWWV{ J}bʸž}%h@꾍}8!BSj)S݋+Sɏ2 {x#ӗv4P@9|%|%|e'[+A b+|krt9l\-М8X,۴oM'?95 0,rvk%cK-[v\wҵkצbB/Ĝ5 ,̜霓!Ų4 C^Iyى_I,7 F 4P@hS- _ _ _$ |>1|eb_ `%yԄ g*}[4x8}kr֗cqd$Ȅ@;~ Igzzz::;F,---/ĕ^͚P(::We]i5 Rqlb7eZs'J.{:;~ΙYY /x],d Pu,J*P9wS?"QM.\@J2I*%XH$^I'>ݧm{f";==9[l3YfN6Z#˞k@?NOԲ쵚,ֲbZ ɮZ~U[hn{F𼈓a^;r+' ?E^I^YWr+ yjhmt+gƴZ/i,r&JogkT6~,A=B _ ]yFCd Er-JJWg& =w+v{S$ u%ӭvoqrt[= jה tG:e|y:dQEڪȲ0+edd/%3x,E_*hښ_"Ҙ[rkX7y%,ѣξuT"$$+vcןߖL^XwY[uV#|wKKsw<:Y#09%y%y%yey+3WU+ wVb3I^ tt |AKg/\|g˄Wiy%ye{",ʰO2FjӅG;+gSyófZǵ,ҊJkEoӗ\ԯEXvgj1IV]S[9=;kcܲ";WtѰGd Q+G뷔zҝ2dW\Bvy%yenYw&| A'MfI+[~g4vT<ݷy(mF^[`"m}&Ȅ55IsD:t˗ImlOEf؎peSz}_g3tJ  ʄ}/]~LA^wy%y%ygD^Y{9΅,SS̯4ItСsW$,XèlqJٚb10r}ҫ̷0?'SѐeVԲW_o?$$ PTQn+=yeW+n?b93=Y{?EOXK`z)]43m[oׯʵ9+afZk#kmۅ䚵I9KvT~?x2y֯TC"jƍc++/| q2 M $l`P@^Y>TJ|1Kܻwt'̛[:|ʳ}w3śʜ6!(0Br`k_{1glu' P2/[++;yeWW~ܱzz _'T2j5ZMֳg SVܳg^rT.l*=fEgIH^Y%yrpD1"KR_y7~xHJJE^I^YW3JRjyG,>2yeB 敶kіP}2g읉ƄVIK!S5L,PJ‡޼yoo=y璑pwWWƾ%,RG8ΜʥK?|'O啩THt#UZi罌S1ٓUX㼵 NKOН%lov7ީwiԇM[(exx ֯W?hж,`rTJymy%y%ye"$,ڍ+KXNNN.99q8p7+K̡:}8+hWDQhm^9\ZV]Kʀ4AWoyeoJc3WmLj1 jACRŃWCu+zWJ*>NnֵvkYz"KPxĔY^o};y.ĔN?󆿃WK"$,ڍ+L<;#$x‚M>C Z~js|2ye W[_;2W [c_q0o\̀Y*M %䱅Wa*<772hW\Bvy%yenx;+RSSK:xEKkkݻ777}%V/`S-K6{}+w\?笍RIDATWS2~V4][hW,Ho ϾӢC h?󆿃WK"$,ڍ}+wXa^9:-ȩhyYVLÓWz3vdGM0`WG,/Ӥ=DQI1K/\ɹ䕞yen}^yvO3L6駞>'x6o_Zevc rmmmehv-W񡪟WG,R'h%`8*U2jݧ.//?N{㍻ٷϽ#$T"ʰ#2wmD;VX%+k088eD_}6gw}tD^Y|e|6J^;oq.Kqg#lKKk!s{߸pv%ϼ`A2++vcTJzg=gyUv',.rFfEc(^/6K3B1S@|弿+Pw:|ڟ7~ͷce՗O?ylTByn3oUGyWWWfޑWR^y,O?u>_3?}}߫W\RQcW}&;Vn]c巵H6k58鼙JJg|!w>*\A~9$:+UQYtBw^B]bQE}oٌx>?x?+>oo,#6&'&ٳgkzllllkXRjl{ֻxnxA'Ȟ^(676GbÇ?9RvQ.4$WڢP-:ewoei+Fh-]+s?*!حh$[3'mmlknlKiH%`ITdE$Vd>>Vme<pH^I^9yeH^6yۍkƯnMMM>sm|[2 1Ba%ddYZ*wi@)O?z(~P)ܹ{^x {^Z"o "vJr"K2-֥BVdYr}}ҥ9K]^h4ddjc2iWIdXܸeMDƓtYF垤Ȳugnno 7O)\؂ʑH^6yK[.\oA^Zx}3 ; 9Z;rTVdܱ yU٤+`*[<[VqOlvR)+1b8%:xO?9k弲l88Lye=kHmΧ,(66 mck랎Z됶t%8x?|WPYS>ySq^ gaa/}=Գx6aƞm,1Q, N>}Sw~'#OMNNMMC7@t7?qaj޻=srnoC.mR*,Ze%ʋ/>x^⢬tZa#OmT-xZz(Znllll϶!7W~?ݻw.gf$׌ 9"e^VY䒨R)\^^'- }+K*eJE^^ ^`ydTcfƆ\E򇟋iQٌ;.Ȓ;AAўo>?֙-O:5Κi1hӲB_FEDKl2@L7Կ_/_)UO ,I 2Ǚ^zȀRc||aa>+e-!gD=CUAS/awD111>AY;iM4CߏE):^DtăPDdolZ(*-j51tE"r:*9<dPCV;WڝM,).,s1d[*l Vԯ#0KZ1FJufRƺl#+~$hW. FHhWek޸ki6V(d_gM5״#,T% !P!ԲTY)NK9,eEz.o=?N-KQpKr&QWP~_GӞsVVQ֯4qͿֹ[]QXU\RkhjR&W`P-Kz |}_XK``````Pz|ߧq*y@> B UR4P HBh Ґ<| F'p! 4 d51`/8`/8`/8`/5[ɧIY 鄕 ,K4 | 7H4Yzic0^p0^p0^F&(󥪩@$)'v+  A2ka & & &؋y0MJUM ,-`NTE WҼ+ y H*$ }-S}DySߥ 2[|:\ 4D*pNB,tFʍYyŻZȇ,Z/PO"p0ˑb.h(c{̴Roe&M5kb`M^.`_|̂  z,wfnp0~ cO*֭[~_*tPdZp)'6:REcQ(zx`|`f}c> 3X`J)cCjri<[lrлt1@Q-o}ӟ-`J)lp4M`Z( '/m[fV9ȐQ 9܆ls?W-ؗ1dĵTli:ff=h} ͐ǾϷniA2 eȠ *iPNP&[nM8$ %{ B!מ}@9x`v>/]Z*;LOo?C,4E`-;}),#o}[H>zǐR+j9u˖o=̴3, S!,Q_}U,w}Wϥݻv3)2T!B.޽T1,--҃#`OO^*ؗ`}Gb^ɋ/QQ\5U 93[K!;jԅ$5u:n =֟Bͭm߿Ǟ ^sȽ Vw^>ɒ!T#I>"|d!dX̀ *98 I:Y#&d@hb!€H̰g웟<,u}=OWW/SQWj ,Е^~Z"_;OT ; ) }ּ{! 1pY `ꗟ )5gD+@/}) Qh=%sX\5~iik~i_|n? !e%'sq9KwDfr.^tpY}Xyswj2&&|B L~+|8a)s͖QV΄MLL,̗[q|!?C Lܐ<#PvLOk_|ҥ0eȩtnNe#(W_+4>W/w$2a"H`WZ#iA5A,sZg`FJ0$W_ D#Lz׮\zU ~s ѐs0a DW^r*@^*}kF '1cM09R' N:zwuƍBq5eqǗIi]M<,dtcA2ȥU,Z#I=:UC%};w=x d߾?֕+WWW/OL !uSVʇ$ YNdq)9eD2|'?LzT)2N$ۮxᩊ ^D%ȕa`5l 21_ka꫐lGyOgIeQt]WjF(c)YZZn.//ShbW@{ݞ1?i]{.[Qo/ox}GXH,O#L V>N?Cb. ʫ#H2帒H9zI\9ƑGoH ʖB.  d0&Īؐv}Tg8H7g%&}H=( 0WGSM0zTpH9̆D%!?=Ӫgv C̥ @,\FD7.ha^v/q05j09FY! 3E=:7n&] my*]j2wwdVN<ؕW7MLPK͎rÖc# $?% e9]͈R I/.<9@0$0(kNG8N8O5tL I4W, i`W\x]rYL,r|uZm #CߗSMͣb# 04 IA ֚O# N8~~YP1!qVV ]\q6Y `[5!ny Bzc0RyjOWy-Ц3͊ M-a&0Qݪy3m<4ao_~b\@,U☈ܷsױFC|]6>N4jsy333<$# fxF;XمGb5pL'Ȍf"ߦ~$5Ğ}$?/Tŗ9zgijˡó̉)̻kxax`gf4GzOAU)^Vw3Cѡ]<[뜫s\{SCzf- jO=B]c'g?@h2M٨)AFig-:<k0'Zd"yt(J9N%dõ(y fOdPEkcQV3 oTN foS 7oܼyO')Rj)cb Ѭ V/ S:=QLz :F>nwьP;y៧D]: I$홙c'O;q-h@ehRyٮxIuƍPnD5'&Y4}|P(0ت| R3fK63Ӟ s*1XTNs۽z&oF|tndrS|@x2ԅ6K5%@cr;TB$b &p9xcy240A-y0>6[*LyL;fS}Pl$f94;[l8u72 ޴EՆzsrzjN&HR=vel<Čy@~1C0葹e%n5;iIJV[6st?K[3 .z<31 ,ߩ A诲Rj?440AÈ=ׯ!f|dt 0N/Fئ k=ss RLfײ"FVYk;c/Oj-<ǖz/GxEOIoiֽWvjhVfF8}a0 !MX`<3*zG%6#AVeorVn7cT+ҩ2jL+hή5<*\Jb-&lmҽW[-%r _eߥ~x1= -O_}7EH<fiiRaj`ؾ۾HԀ'&Y4g0?]&Hu oztnrPލ0?#(AOX$g++ؼLjkM^&gy̠q$,BGoR9j [{] 3T\c' $+FtĔaIq+Gݿy0aP\ݪ {_e3BU]W7k葹 f2ЌzZLUvD7; }uC\EI`ʥxbI0{㱴uicY6⨦i 6Ա?x+F AY5"恙`H|Xz6[}YAc2s ybnΆ\U4XJzY~uw2#JW fT㍊i٨ty'}MK-iU~c}'vmUsc{N=2wdс4,vb; _,,*68v?I7Ό9Wj~D"h*;m,`&4M̲8Mޫx)b׬H7=jT\cP=L#%~} YqWw)^q 1^T`.k@VUSoēJO;]Y!fcmG Z9J4DUf5z۠*E:Ȃڛo.DD;˼Q/(Lzn}H%x1Qk ?$Ԫl0?o h|7nCg 3-fT4R϶gڱ&^AjLH\47HQR"V p;-J__7b 9诼Rj?|1xMư/kGf.C&֍쉅&m;q\n2(*RvMMM=>2I߻* U\ k씎5>l +PXΜvT1laCqFḎ뺩TjߒvOy^8Nwq >]>Dʖצ3v[f.la/mYH1iYoKRꤹ Y~2?ϫvDvh{k2rẲȧu: 8n㈔R{ (CJRj' }#zY.J.חk,q+U捛A.+vֶ[-(X7$ɐ! ޒc y0#'1-6hs彂&X>vx@IA!== Rj?| 1xpp5vhvD=úP8eF/&I0S0˚ՉӉbĮܽ M&!g(}Y{_whgn7 vjXdRq̜1Q[4=֦+hR C"%A\6۬VEߥ~@bZ3kہwIKX p0Kl+⁏* fßj~QcIsz`rDyoظ F扯<?Xlhb R|2X~"'Ne<_!X]Y?HË%շA2"8|otl'IDk>S6fQlsY l/Tp&,497]а*%a$׽mߗsƒԾtzMó p&4IhGt ۶wZBapzZ:[կl{|eZRR?˃y MdI"Q! ڎaK8% L?l{.=RgXKG[s|U:E˶6ݢUaP/Z]k۹}~GQl9ˊM S z%/!<1z`zSRȒ¿KIZ B3,CgcdUIzt!~-jCFf (fEiJn𳂡[F#m׃6 H `V3LyZP|Q,5+黔E > 4a\FL(Ybq¡ógR;q\ۅBOTgFgL8Ϊp'IVijI6zAlD8$%,rq׶#]UM(EhNCRRB]臃mtg˂Gf,rI|Uҭe)QgPQA(4/:= o-U*“D oupc'7c'Onsi}穷#utṇ:*I'Ǎɱkʓ^fjfhML" aE/vNmw|M)ug5-]Oߓ'dw IDAT Ax!jQHK p2Jʃ F|x*|#BW.`aFDzygiiQ?hpw$P]ܳz'S-ͻN8xCfg5 gA8Lyc/--'P%D'bcEIds~04 L5—H:b$"'+r7E^l{;u[%ޕ1t{]- Z2$p f\b* % ϭJl߾}HXa91Mdߓx5 ؉1211{ _A}}'nk`h;q&"ehB!;qTXzpq/z{m7HJ#PpF;ukۍڞ9vD   6)ݰa"R M%4L0ԦE ܨ1kI5\UkQż^`p5|]BtF͓`&{`?T9]o{5/ڱF@^E5gaIZK5%ZIAF2M@&5Sc] µ5́_ȈY]q[ֲeNWcW(q Y{cq9H{ӉJ|Q$Ɗi8!1_!ZVA0#MC^3m$iLRaM oz6Y8! _dydި֪pʳf%\=$AFSF_'wԂ_B޷sW=ScsC{M302*1TX.g6 9诌R^?vky6,ʃ0 1O0ö>MK-^?zdqs7zWч)R5G|rגFZsY<j20$ S͍tSMX)ck.,,ܷsiW0 pڻGC"j{#sܕVj};0MaCTEمgHsJQLlfv% Mn诲RR?pBlۺP̩!ꌖ##NHh D;@ZGDzaڻ4ʔl$d4ȰwutnN&Ʈu֚'?U9a3|P57jPKJ0ώ7@V>\p{d# m|f&$-_0m oO\7Rc[3c.Ea#/n'vA_}g{.AhVV *Kc3<592RDŽݧjzG=<=4vádM;q׎ D?:7wtn.8m{&r۟<;%&fbk{l rD촃oss覻n9WH[ިAbTBձ3&jF;N(d)n˓dIێ8nk|˻ȯ'y7q*Lӧ&/\XItz߷s)IӂKll8mJj,1ZNKtwrpێJI *Ɉڶ* Y|+WgyGU~:lrgL옷 YAWw)^q ބpԘ1Bi n;/)%H O8Nxâ2RGOulyi4bQNiL[Z̄%灉?v&YCef=6{y\UrqLf).--`7imR DeZ!?v#sC2+ӯ-7Q?I1ԅ !skN2\yx 12*Rt{BcbpڛM$ Um~1Ӧq0.Cg#PqG^?<7i͂uΐդ}˩Ci:16SҒtc f"ۇ)2ԅ޻ $8JBբT{8zFrڻG`>74-)~=4:uzL)$PְwσjU_",K/($4OMMf@yi4xu[KKoyIb.+з3?s05!`a챱ѱѵo~!wĎxAYA Pxob|\U $CSڽ{$8r&M"DY1w۶9ۧ0'׭ ibBT-?#_ {e5 )0Lb. #_zM~=y  W݈@]CmDA; l @L9559~$̼A'ן|Elq8 $(`AN4x85#DZlۜҎxUP0}װ+63\{@3FėD;}CO"M AY}cb^;1M-t0JB?`lttS y.946=LLfڡy J4si}[ߪELMNC}ݥ}@gf?*A&ێ#YvmuC\ԭj%nB=8\J%.oP9$NM^_Ju;3;jB;QIqVɇdȇGrIFZ90xm[_,1@ؗN!eQ8K>D>m\DN}&PI ;6C,X/a!䶭vۚ\`f YM;6l<#BʫBLhǎn Pb~++@P*}˱ѩm[o8g)#^^^~zǝw0T!ׯJ#####v BGOQU׍v:wlغygj]ZZ&dclttx%j/zɐH C/T&joaNxJ5>\k_p0GhD%iLu'8)1tI2 `` AGG?66J]Umkx5 >`v޽c;uU@ 1@)db_=ٰe bmDl~奄=: !¹BY0Z8Syއ?5SI jGԾtLfZg;HF#EA0SaÆm\~ۧ֯SI,rix f_LL6l`bb2 ڗ3lgW!I UܩS}C1umEo㌃zֆ l|ω N=wOoD8z42Q/= ׅ7o 6VV._]oÏ{_ xkiԩ}}ώFI:^n]v"PO!e Z~=v-6ᆋEsLCCʐW!ğ/.]Z5< ̶N,&Gqd&I$Cږ?d*Ɉ vTKy'vƍ/.1@NE%@O&du[R`2We c>9\ 5.&Xh$jj庡llذaM?g  j`  F_Fd] }3,,Pcl]`ڹK{W^y2"~S7!F0[ܠd dҰaÆm۶IW4UPD2)qb`[O/wgxLOo߷aFTsN:upp+I2c0 `޸qC_#1QX(03w{ߢGi* 1HL ` fX` !RqGE4%fA9"*vWHsN/--ՎGL`-)XZZS'(C75h>;혎ŋvu5]5=}zz@ (B4 AE$ʯ˒jFZ9*cTZ5=,̴*X^6d W!+hxki)z* \Ku6o{'~*QnWu &e )7.^x @ eS J(Yeʒ|ÖZ*c`B3@e7S]LTA~iM̘+H\ːۧz`FyKV3+,Bl^biN~3>u ִ,ˎ#Pp0SݧҥlCEbFG?1L^;FDŽjfT`JCJS|<wMo68!.w֭[[l``K%ؗP8 H\v@t.^/_|`AXjjyܥj䣈? *k)I9czO$^zʝ U21>ib|Ln{$#lU}b%Q Wf\M:$2 ̼y<tAֶWWW_|˗EmX#$X]rӊJF85M;Fc0[IS%1YGMG9svĖ-l"Q=Node'>O|'/ӟBm@^׻b-_ċ/"(F]VHMO|>K?rʕWa|]c{|cN68yOl]q]#őe%TuEqJ؎ LDzM7mz1yC Mߥ`2[\$8mY]*Z@.#e9WQ.5 -; )哖sKx=uo-ul[>{B7@~BPm̓8}dSڎ.p[R rز |TTGJv t#ȫڵvӹ~obz~ll1d$O3\Q-62.QJT!$CzO|Q M!DKV^WQ.ˁ"on*9t:^믽N??mMeb|\166~l,Ur*5+DґUqVQKEJ")$mqP3W1/7޾~wB (zLC chi(ugs Ihk۞'-[$+QER#o}w|;kO:9ym׷7n ϭ'B12$s]wmXL!udTXgS&2@{nɑ׻-cfaDEP[`:h"i7`w?׽p#l$Mzxad ?B'!`8Mp-%sqSCi9b\'!7lp]t]fpS~潉]~uē+Y2"&WRWp[;_*9 EbMHQy2ZF h4a6vx>?V Urc$<ʜTI-9\rs'UёJA Z⫒d;*}()؜p3|EXKn_aa2 lrrU0v0s_ƒ)R׍Un e\ƨIU;yu]wM-ۑ)+Q4ڑ'UmTKvueB:븲9>ÅrF[9\}ZuҐ 6\U~hhʃE PxO,"/ExJT*vcU XRrG`XOAƏfcq\mq aK׉"e,<\^8U=A5 p`C^iFGER7;9m_ǍS4zµ#o3kv5sȫ3Tړ':,ryiٞ% RK )%,X|ETh*1)˃٠O,As# KhyE'3B!48Y䪰(&RJ)9K8Ը.yd.IOڕ2?ǼBl,zک"ITp.ц h@$5K}`+}𮛩.&'fDf_jUivZ^#qc+QjRW<SJzwT4ΚZv>"if IDATJ2"R.m[W9XeaDD#iHvU#M"/eȃVK^<"钑 (<rD&1aj :$5KܜA*8f`` `%eN>:sT)#9T\ҫtNy/]Qq[tV_HXU) sx)(A6?Ae3E`+Xn^u]nCZ-7̃溮+DU#Px~yuo-iEUv&_NfE3V'_Qٕ ya;NTCS76Ebhjh_\Um a&KFzSގDB6Z왌- y3W5rՒq'|xuZ'ZDE^!4&qPV);#;4[4jR EiOr,rLK`F?(ސs_`*vEҬts ;7q߉nins:R\8GNt E܎%vEf3Ǜr˫pnnhו<<>C4ӾYb&Z*J[C E|P"Y&"y0R' )rZEбU91U٫iR#d#ѹ@&YRX|#D^P@~E.erP\u嶬q< ,2KzRr.Y]ǍHQܣjSz wS^a`[xME4DCGJpzUΉ)ic9̺z'OTi̗"o#S,r(f;S'Z>Lfˍ*syU<<̶,rTy18 *e&IL¥|)Pj#^maLYh6qpאWWDW'!" BXQ.qqOr0J[- ꞜEbT#BjHIr3yu"IOuh\4Te('xlEҊ7e@A&Na*\ENLv0t <ߏ< O%.4 9Ϩ*s+z,733&X&>2Sqr,rLL*Vɐ$Ō%6?vpSMN^)//K9F3O/yz` &9aY4P*u,"ZK/Fy:GLfiڊk 9NL#+eTJ3m$`l- jЬ``r*|agm!,1r丮r죊"EN-a#~eSC!Ɋc2hUfUhM, n!O$k{{q|&zK$_/9m%R9KKS\XRYnORT&7QR)6̃6ESs*euRA"{UL(ӅuDo#lضkrIM2L$$(OsǤAn;rkr_qF"e" Ӷ,qsr΃ L%"_ :k` & & &KYy0eIIR&W"G>=m/] fмe$YS s_*>:CT~y'Jt XYRNmR2bHy ߢb(O"Ed?}eYyJ L,N=5yMDܗEk{{oS-ܖpbYCXY`^'=穀Ą28΃)oW1W_ל7I jRjx@G%7VD&D_yub a2īiyeɃi 4rـNIc8;gni>Yf;"&0m^1p0٢@i; y/vtJWCM̝4YXA}4FWP򑡵̳[ 9H:4 ҸiE`*t-2tŊTš^p0^F5by0E:hr.Hix0 ` sLfg(Ts5|@]Z'LM鋎d.n4rqȗprP5kL{H]}LVI+=Qd[QHNmM5fNt`4Ƥ6̶<@|Xyo!A"^*HG+ӉvFVr-Uf5[`<Ֆ, +++/]'?-[lݲ4yjS( es0/? #.,g~fT[paŋ T,=lٲy떭'o5=Mg`d:-~gn `!̣gW`6Ke:+++g̯b>O~Sp02Bŧ~fq^>xݟsO fT[F`^pF|ꓟAٷq)8OSnw:ygZӿȾ;#)on誶-y{d$mL`Sc\Kyę3g*_6o,b5}:5hcf&i,\\\'@x}>Zv WT&@}ٴiy ` r0|)/wڵ{U^>O^B(eVo_te/K&Kyr_ !.\pae%9>>GO c , ii @KK`/cz㠪c_R0KT'ľTxUQSSY?E`/È*pb\pG/11=11o4!p0&f`/+.%e//n_޼^S8)zNa'O?3?y/%e///bϧ=w&/˝+/yi7n<ZR`q3hV  "?3?ohv#eI&*KVeLL̥O`pQqV,fO?Bb_VEؗؗؗkŝܤ/&Zʊ#e)=K. 655w:s\ })xǪ ؗ 2 /K/51\,})ػw/TpK651oבv&,._\<9-KTHҪ?{vﮨ慅ݲEױBHۮ-k3 %:4_rÉ,Sj d)fu7Ξݹƶk apyXUoѶ1}zK|}dNs,eAľ,///_H/hn蕋owCLyjdA3x_n{`_ڃ.٠?1997vnܼQcPhW &@ ~Yͻw¾,ؗU%%ek??y%BΜ׼;55Nj1pl*7Kxp L! &@r)$3}>~2ؗ 2 /a_ۿ{aVWצwn޸L(Orz7c/997 :^&*Kfaقwy9PK4v,A^qK4;c_b_b_T啕7n(`Y#.JKOMM d֖mσj]<V6LmNLv `?Xa_b_:!K!cf.+ {IY`Kfw٦&5&߿D0bqQ+ؗؗivƾľľ-|*}aW^E+-a~9by IDATH/du(۞=}arcL#4 `b_b_SU{6ixaZŋ1 ,ǀ}%O40Êa*zo5AľľL3%%ekg_^I|K}VK^OݒBnpqPsb}r r`ݛ.eӮ{V*A[ HXVMz8a &@40wUMnǾ,`ؗؗN}i})xsUL$)Bz ؗ\M(sL?ra_b_:!ep/ML&.z){ ؗ\zmJ`>8}L!dľľL3%%ek˾B&N0'=K.Ym˦Yzu wx@;ؗؗivƾľľ-ڗB]L~];ktu{ɵH۪tS<+L!\mrrree%׮yn,Iͮ%4L^L{ `apSȱ//}}}Z`_/h!++J)z ؗ\M3w垱L>́|~B }}ؗ5/QN$H"=K.Y#3 #e&ľľL3%%ekQ})Zw*!cKֈ?wk|`>T_a_b_:!eKt{=!o~n-~ƍގ'L8NY!o41yRL!u\-m Żƍ-)a]:BxDj'7Nm. c02{2Xew|=O}}fþ,޾H@2>/d(ѓT`?8ٽ{w]pE|>er'ǾľLwa_h_~$6` K.Y#Ɲ$p0l ؗ 2 /k?e!þľLuBڗaL+Gk['`[s +.D.Yʦ)uL_9t-hYa Y ؗ 2 /m_ԵG%kDٸ & {w>?e!þľLuBfؗ>SY=L.Y#Ɲ`ED ¾ľL3%%ekQ}wyd?e!þľLuBڗz`pQ6!YڞP]"K4;c_b_b_6ڗYr5T5y0MR5Dկ^X%Wيhߏ5Ա[ԯ-_.րϏ}YH//Sr(G_ Q5b{ ]\F؏8s+fT/ )%eb_2y ~ eN6p0b ؗ 2 //,`pQ6!.րϏ}YH//S eNNȃ e">Qɱ//lؗ5/}/ȃ/H+t:^^E(h#.e++x~R{'T4P(5c_R0KT'ľ"`#[5l &@Y]a_b_:!%%=L.Y#Ɲ<`/ )%eb_b_X#5l@LSX ؗ 2 //#}Os_91#1=_ێ]#%ԣrɚm_{FXʐ/|_8Ռ; 1|~B }}ؗ?{o%GqFfU/Ջ^V0t dI4 H:0FH oy#x<m|ec*M !XllRjYҗ&F^[WmEKKt;TQY$҆ X`0gX>E})*䄆t*۩WE_^T*J//+-O ~ccik!/mBFNkHFhB_L_YMzue_ J+ɖZS\51ZH }PdH秦DTy10#㏖O&}YqKg\F[_[U(L:K/)A"6d &P/Ǔ#&ĥ}YqKg,FN'[We5/M֪ x#HY$҆ T b$Qϳ}DeO:rjlXfJfE)%%vF" bP0!__Zw҅򂾴W})!>Z^(YR:/`RRiy5Biu`@"@e e~aΙǓ'GFnH } }銾HNʡ/ (@/q+ EЗ(H 5 q-op'oܡ///K+/%ۚkC_H-1@"6d` &cۺH_'///K+/ b*4ZK,iCMf5z,s[wK;*"'* eXr.sg !-Q[J2K)OCd v엨Љ4HS3<\S(tW_+> E} B &cd7sOvVm("Ij2,=@vû/kf74@_B_1A_2yLIsY4]<bAE"m60oC?7nxwemֆD}IZ@_/sDžƛ95|"DPA!` | G~rd0斆ں21K 2DR|f9AN&'@_B_ }PdHj2(L!\d7?Џ!QQ}4}i/IPDZ JLKG%nYA_:u-f ; &m9{/ǓЗ$(mK1#15ȩ$%%(A&"6d0C?u읾, }DN}Iu+edZEb2KNLĄ1@"6d0wC?u쑾$ЗLҗ2Ros\.14ȩdrb!}rBPdЗ ` ݲw28%}8e~B_F_kOs%MꖯZqШЗoUQ`="1QdHj2pL!\ۺ[N_BЉn6%Tf?%[bKP ҞeI3*[@_VDM >+s[w˞˓ã:q$eu//IwUnH// ! .)ǾЗZЗ,iCMn ;+s[w˞KB[//T*}K>KB)$ \ZJ5%%q"DPKđ{C?u쵾7nxg%!dd2jml[-a$'ՎC_B_O+~QdHj2LQwPї}Y(KTK<|,ɡ//'(H 5x &;(s[wžKB" %GRkԙɉK㫀7V"Y҆ pfWԗ&*ʠ/FD !5ܔKNN| >;"DP`;ۺ+Y_B^S;ˌİ{L[!SZ | )}> j>(H 5xOY8' |nNٗڋ`&e%ƾ#er$qcy} } k>apW 5`N;ۺzTs%#%!4AZ.; @_',iCM~ 8" |nN8k_'/FKBH91+Ո Lj , iCM> ( d7R{LH*З%/q,IY$҆ `n G <LK|}~+ш &Q[–&n&keB_bs0W#M_BĜHWz-}I:i8&Q[PdHj20@v/a }0.tARC_֗QsAE"m )rd]]b=u#}9It,/8% PPAhP ,==Lj{KaB_j~l/1:Vw6g}k_{gL#h ,[F`@u:,̣ 60>f~0KDvU9RЗC_Doog??F»m7z-8%%pg-š5J ɠ#=#=UdP# +T-==|K}}YH%NONNwkjj`/+XnҠA_ZҗP&Lǻ5D} .{キ i&4^`C_]%pAP&Fa,LVXTT~_o=rAd -K}i)[@59HO2YB8Tr'[k!-EK P*`Q@( zfK)/X_|qɒ.KSS֝I YRR2<w\:}饗/~}wQZ8R3HJ?L n%ڶm{:_t=ǎutXKKJ|$̛7Oznݺ^z}Kǰ}vwY`^{Mzo|7qxrlllտ#;_|{D'5k~iO=zM7eee^ %5]Pā<8884յ+/onn*} `.x_<Ȇ Xr/ !< }rï/1m6msso*LĎ%sQve+/ }:64o}=r7J+**nmOO;Cѣ===UV3%AՊxq.h֭N:}c=H$O~^x=dɒ_׃CCC6m?زe~3, mvg><매9T̢*wyj3l -vm~47%LfRIISi!-QJ?6:ɛ]}/A2zzzvzV'nNÄ盂(!D(%tjjĉꪳgJb; n:WSY~}ቜ7o޽{fΜ9_~z-3gG֬Y믯_;xV^{O>}r-UUUfͺ뮻.6ѣG=fApj IDATȄ000W/[5W)<=x%)0BΝzr=wkw4LKձ1:<ϗ766.[gs=^x!{oll?HYh5\SxjqB>OoGFFطoΝ;']x~RE?ŋ#ΝgÆ o};::DPa``p6ի:;BZ׮M!1_?:A_ Loѡi0]YMK1Y|߯8֬Y#=|Pnj#G֭8|Pa-*!$Low풭p]wiSVVDnU?qő6$jݸB_B_ m Te[[+{$_b?~cm@gŞ?., }:6Oii鷾.U>HN)eٱc{[R."-R`ߦi'D7 Ң\l֬Y R; %Тнk>j})PO\駷>lM֖on[/E֮eOloo$ô WT*ؕ33IMO?u?\LA+Kҏ0M;KB)0D"1o޼%K,_? >vKoم/˖-S+W^{i|`}0}w !N|[oڵfkkiϙ3gv-wd|p%ҏ91MG^b8c#B`1n[[BbB\З.^ AQ nþv)LN$`2d/1ڴw!{s̈KKK|{ܹsٷJj7[F= +- ЗЗ)nRKi3S, /?`}BMvL!>#-i%R_*K 9-`vqqqvn֯]r%ۗ_~Y'+¾G?uїF8B7GKBẖfvLO %Rt*۷ukA7999%MOMϰLOK1\_Z\ S{kbB_( GR޻wˢ}?{{>i 8믿}djjJ5O~QNt9ЗK5.- R$$:tkNHkk })/1Gv59FP\`:7aa$_b<%DE_biUVI;?ؘobʉ ӶvZvʣG~sV>#Qu;Зy%0ߊ EB¶a֗5)y'QvhE &@ci+G\| wA_З{ڢՔmnݺ]/Q헿/˄;sҥNKGz-}_buP_Vϒ+-P<JuP|'U%槞fzB<&Ma9N!-q5'- ]&دd)qЗ `&buwq#PtMsQlooU{ݐ>_җ~L]1@_hS0[0 Kon[,{^}Tj&%mғJS ==Tگ'fϞ@p* K/ݦl*n !_W/LBH}}+ϝ;Wm˖-o>x㪦/'neL},my/%۽QkȃE>\ L¢/&g/ݑqwܡw}^ziEEEEE… ov>\ L/+*ɤNӓ3J{ԗE2uX\ꫯ^hᷪ|;)$毨[o[]7x<=־,4c]T m۶o֮]sdddm\Y[[(Ly!4sZ']-qzi8c,_6P`& b{U&}YY0KLNN~fNt}uNKtch.(D]c H/e˼ԗ"_ʗ BiA(*lY"e@ >𸹁 }z{{Ν;O~7ސ>^v-K:ɕXRgZsA! }feGUb&e=tfj '%[ j[K<ꄘ1'`B_B_֗"SGE} 9|#Z$VV$**ʓIՓ&NKcDC_?JedG[OưT8Tͷ1T=Zș 3Z' %мb],RGm*%B|dLKY OiW-;T| *e3r)/@UU䱼Ԭ,zbDe 4L禠//)U_WMe:Xld7b``p[]n"O$KLKѢg. `0xM602ƅU_,lk LP$ԗT]%[Y_Ng:~N7vKK% ,ۓM###ܽgff&K! P ?k[-x)h}I[_}~/+1/u>KJ4Ϳa $ρ{ } adoЗMDCbv'J?-(cY,s 9}%xUЗ`0xJT602ED_,lk^Ԕ}R5՗Z}V<^B_U$Fء/,nc``_r۠/Yn_:oS<B_ ;0.RR @ZS{)(1/컾T+7QЗԗЗK<d``P%!dZoOFЗ |`%!"1F{)T:ЗK)1j/Wugq3*;zڗjN>#Tl &/( |/3,lkԐS™3ЗЗJ&=K_zDi }WЗ8~o }ٹ *]ٹ=2::w>'GЗ đܦ6_~dr0ԔPVq7}Z3*28/09vLJU`WŅ/ |u鼺[#;vZlimm ISkOvM&v3t6:B0>З,lk9=M sgn` z`&) |tqЗVOJ !Ο[7v$ЗTI/ !Fwnro;rKKk Zȋ%!} 8@7zRKW}i)rKK<"EلD7ݽ>QpRps>Zw*)( |G"M }iЗvRЗЗ=%(.L #vCw哛t^-B:U kx!G#8 IDAT- 삧U_T`dkNvh}\:`(79F|Zc~/8=ȡ//U6K"חnD *) h%'mmm}`25)xA2@)7wo7Zݩ萣 &Bq{WQ_?$Fn$zCaKbЗЗЗE} @޷{})?' З`0{l _CaێZe2(IO=6sUOwww)!pӱB_^>NKqoX9%} } }e(I G }cbܼů miiN g*̟5  &y=I /QVC_B_B_}Q>DL<[hį$yeE 4_Ofd5;l6. &oaێZeԭL75$&=CoOpG'fX/3`\ܩz 9%1v# C_x@_B_B_%]W^.rf__6>ЗD¯U]dz(hC_ m}IHЗ(/// eٕibطGlmm>fOKkK]]-{DfjQy|!L} &˸DGG+\=8}X9%} } }iL-?ƾ}rطGlm ** <!!uuu-yoO__"۵!gedmdmG78;E![XТ#+Kb$F7$FY } } }iXЗ?!xrkns)/ !mɩщ 6٭-];VxxLr_?޾a7pS`w }ILH KB_ZlЗЗЗЗ(盚oo_|e`1Z[;; ]B*++](\YYYy%߫"ݽ'٢\ϏKiXbOK / ЗЗЗE} jS*n]?TUW555UVVFe5ohǓcccV8jV,V/ܽ la$;]V$zCaK17%%a@_ڔ%BO?c1j]fj:n N<~!ĉ:I>P-EK9¹K=@؁`ܹKqawxp} }i)BKK…4,W%AnVUW_D9v/ P{lٳgWϪ%44BҒ]jlOijj* Z g:` |g=þr ϟ"ޝҰ/PmJ,5\+ǮHb"quIIbɒKRT:h'R)TAr=C:5Wj"^qsk>dcg5!-x<&M)%#X-55kSNj OC_=xd|GhG{gw5/mEwg}i8憾4,KT(pkqK @7ٸ!M,)­0/mE74sC_B_B_%M Kqe0}؉瘘H7GЗ"C_/// զġ8B~K+0}p{~hC(KywoH:{k^" ՙ/pߦyŶ Ho60=ӗ7,v_i982مckHL%ǥ9Y4UTxiK^x%:lD&Z.)C ϼVd;(ͅ/`P |6n|H?ǃpa2=GW}U <+sdͳ́Csk)'yDRҔdG`~=8#9q|nmWǥg*O`rj/5Z"m#cX,*)^F8K`0>0r=݅!ʝCz >r}_olK{7 P"-yЗ@`0dwa7BqaqLYtr#v$?rX7TC#GUc"4?UT^F7,cBIҫrIW:L"U_k0Ydw $7uzf"=w^$ʮfme\cU^e_gX~M1_abGq*UV*J`)(q[Ğrj{TO%nKNhGggOsIM881nqbUJ)rʟ(g=ENʞ"<16%ZЗp`0>Ota}rgov [o4W7@3J7;KH;͊ѓG_C4_'Y*J==9JJRZA ň=>Ib}/a0;ue,"s0S1b02O)6|4/d,LIf (!WPYSd檆(!TokUYƠeN*a6%]|f Ggu7_y%%(֗*+$r#cC:ihZrM~xN ~%͚Ma"''&[}RZ51,9`7Pbb\\|!TM1ځZ } !~oǡ/uCN UonhlKϼd\!!76@_ })eFM?TїeͳH[f%PRkI%`mnkh>Q} } %Sv:A_>ϏwqKGecKeKyd ԗѣB_jˌ/*K/e)@_EzUmUJJ<66Ƌo}C(KPؼljilG[2ҟٗ8Keb\ӗR Wᯚ8#:kPsMۏyFev|U'o$Ox3ӈb1!iBn/:kvia|,eILc8hIITzkr>bKNiR68 ڗh5ew[RDr[@_P!ʼ\y'WIK%S|T})%5w}C7mǴ#ЗЗ |eS-&`5MҝB_ |6n|`/im}BSKuם- U_R|:}髾ws- 8+1/֗`?ЗrM|Yq0`L(^|G9Ʈ\|Ҡ˕~rOJ.R:I_RJjTӠ//ŏZ-L= fK08EYЗG `L(^|Gn~8͙0Ħ\|R%!Ĥljm |wЗAїb<-%l%z $|,:Ƴî)X-K&&'׻X]睜} m(N|gƇqK;c/ZC_JKC5}$}I2[j%7;zz[a/5^x>EXay<\} `@1>z~!b=Gzumjref<}i@_N_ooM5һR3 `]2Eu.[/ &#~mc躛s?=|wh:UfЗI%5 H9ohY@_B_MVy6w} B  |iG{{s?} Z}Iח˗~@_jUV>B_N_o=udpFsBXЗЗ  51 !5*)G8«/q\:BntZ}Lgm%G8.'51DƘxNcq6bڗ t;kjB_:PDƍذy r}I1ЗїJ})eE%!9|dV19|+e }B6Os(r'qu0&k1*j]'_=R%|fyJpe?RD~E+d!;R|  ł_蟗iN }q>ħjcsĂ2I//@V*.$%y )j%X{E \˞BP]! xJ3+..2o=£׆ A"_kӎv&~mv!C2%!O`Z} }WL옪Vm}9:=RzD >Y[S+N6`&Ys`YH?}\q\]^qh&~d6{]xVSq|gƇ 򗶆Rҗ8MQ43Z yϑ=g/x/e}ojr`TKBі;%:0e|G'qZ#,} pPgs`ZIҗnb З} B_8q*'ؗ!ˮ(ou+̭,҉C_b4}`0 O[-[BdKǮ=Goe]jh{|`MA_ _NK-d,Wj9ˏ_a>R*y&3H95F}  &D_6ٸ!x%!Aon'/9!6q3o)ںvb|ї O̾ [8t.u}d| x+^Ȑg}I;uƖĊmrKBI3?3.Q k/oPkچg_u nL»/J=ȩPݙ{9K/BdwЗЗE`@4~f0& /u>L,K =p.Q/ MܴKӗjЗ=VdwЗ莋 >wf& B_sD.Ȣn}I)m"O_lЗЗvq}ifDٵ/!g}rL IDAT!ͧ{ՔR75As0 jΝwcg0& zyV%faNoӗT_2X"1H=LE} `@~CgុqKBihkU_ z<}IZeh,w\ڗf"5ԗf:gj` _]V2-ˤJ)@ٯȾά}WlRڑ6:sDRJ ;²>&eĮyF̾D-N0"//I) ,![C_#`KVP%Դ]qgwA{+V0Za.:2qҡ/0/QNwa8 &D 7ٸ!6|(d;dC{_OE}Yct:=55u̙#G׿^r^;88'''%K<3#'KjԠ/-KDmHPR}>S>;SXC<$ KBG\BKQy c~g,YrONN"/K=&zWڗЗ iC;7܄D[`LlNjU_%Ɔ0"ͣgzrDYM_^A!オ־dgZX3g&U [PbU_ʺ4( twEB:-Bm*e?cWRI9ŲsR:ZH<~f$ З+%s0 *xE8zeq Jit:=99922o~߭a :7`sʛ}ibխ{LtY׆aB/0ٔPdc.ЗĹn>`@~;w??ƚA0ĊdB@_|YYY]]ݥ^zvwwձ6mڄ ؽ3U՗ء/57)!m!Ȗ _ %Y |Xwa X bEM_f|E}_o=rAd /Kj/"180>KddK/@_ |Xhowc@kCKїSJXsk>/N<,-~888㙡^<>444w\i/={N81{K.dڵKJJhjj{d"hmm˖/_~-j}7?y> /3>>>gΜ^vut)V/L%!֯ D_҉°15]԰ڈ5jK\R&f:ƥiVz3eHxLK)5wBt[nxuqm۶|+턐.|rɒ%xϝ;wdd_~z-)-[:;;կ .| 7cs=c]]]0U՗JcO^{/UEf^˓!{h\ncwO!C !dv1;Y8b1u<:ݶbWתESC@JZ82,51=nmA }꾾Vt&,79c0M`L+oǭqkc!KmsbE_bD)>}?8+)6u$IַbߚY_ҐyݻWz _¹s.)+" !gΜYf?.xwZ;|ꩧ`{kjjRԴo߾]w588CY9z(~DM_Rjk@&(h0!.\e0 ZS&&?'}Lꦹ㜏D7Ya |zzz {SW ʳE/ }YlcL. 卍˖-Y}I{[\[Œ#-k O׿uv9NBȧ?i7͹s礷/կ~Qxii?!=wgٰa/Env(~wyo'P, ҵSSE8?y5=Tڵx3D L%ovwG۠ONN0UU_Rs:-\ ڗwgf%Yo겶H@o1F6P6) .M|IzҖ$9Mz?rN{ҴMmH CCl$_Ml 5r,~n;ٝyfv33;Q33\ zgqnK=},ۍK5U*++/=s,2 ,ͩ?f2vEo~mtsfΜV̞*m 2)#cS&t@o0˨U 4VExG |l?rJ_ĵssS3f444,X`7nlo7t뮻N `\R7`thTΧSG'\ ߊ' /MoFwWPH455%q0M*0m|LI 3*&e%Ok˧VЗ`@8-QHV/};}fG~__s͚5ٳ۷oW$O7WTTLMMV%,UH\d%MЗx/}+rꎊ kf*=XV@1vfaЗ`@8c}zGN֬ӗ^՗ht&''_|M6⋓ׯ/544 (j5)uzܽ|d淛} ,͛U/(WjSKD |m4ӗ,o}i_D㓌,w}w}}}v? kŋzi/%UЗ b }  !>!?ԃU}I їhy_ܹs/2"$I>1q%ǿs֮EZ06 ͦ䘕LNC#Lb1) 399yK\&?]9O&2&I,GXx}KA_FZ}З`0 2zvgG-tz+}]DŋE9s.uo\mܸXyСO&&&>O+s̙i&\CFY?Y[% >==۬h{kK]}iz+Tq/uG]])(ׯx0c 㱱޽۫}kmm?ٺukWWדO>yرɑ͛7/[l֭bOH_2g%?17f%)$OR4we|u6crTB&3#^b,!wOAym V.{ &D otαQre`5=Gd/-~ 9_Yȯ%Ko|cҥw}jiiL&<;/gqZ=}&F9b}} |o&J"@ |msۿΩ/F\K%#fWLNN^|+ٿ+W)cߤTIҝ_l %)N |l-aa;},ZDC~L˿{k0 !s}RO?//F%C}ɬiGU[A_@@ ,*%^wpLjZ=|W }8eK.<{,K'=/n K$=#$>|2u~F(eaJiqA((O`dp wʤ Pe)jpbdZ-N\N&L5A&VvtRM̮֗yN_}{ԧT| ,]kƌ_WΏ|7̘Ջ,P >weoP]C_d$9%?P_juk+-&;}8066/c长 }i q;o[Ѩ< &&D!d*#e2\HvY&'NIe>A)sS<.tep^=B9ӪZeBJN$|J(eyB`P1?ru1NMM)"DAEpI_cDIEBi&IRY6 &Зa;?}MiTxB@x ,OhǏ՗jmQ_f}[} 鐣G?8|5cƌ7wa6XLʱ}DN_x/A7ЗlPrr c]C_oAL)%Xuv -vGM2חGwj #ULK4L1|/"?591F/NU]sL1x՗.b.taЗ{2x%4jZyLOeb2f[ )+PVHГ;Imwi!#WPF` E5<ծNRX@OK90RKcӳñ.S֯NuЗkV-{ S#g/;%ڦE}H7} 7ɡ/AtoK_+yJ\E:_!-MD a$>==۬E9~˞m/ܳc{Kӗhõ^ /a0֗Ґk%eY<jzeVJ2ƌ%35jFR/9M׼\iCnY}={_b!nm?}iM>x%%~ӗc3&wxͣ~g/A]Fb_q0v~'O,_ֵt>|$2QLf2RiW3-=$ͽO.x+###󋗒ŋ555f\JM|L5 &^Q~1J,Jf]U B/`@,mL:ԗ!K}IȚUKh җhN[r)N &/< }@_P%З$Y١"7w/^hɒE{LR7LIʄr4pȑ?ջo^IHAe>Ըp&͆~SJΔRd zXЗo`0 \v==:v}Y;wҚzaʥDմr\)fLQ4TF)TɈK@IvCwoFu;Cjg4ӌOF)#,}TIC,(ӂ ɴNk>X+ȨY1@QJ%~&ޥ9h&a!З^{||We8###/J&={ѣΝ-ޖJkUK]+at~…YdwXb_B_0"K}BKWDE뢫/ټ7VJWdl4檮zYd퓝rObVe%RN23`` aG&1Boa4їvjݛ2-i%%uiRjJ~f:)ܴ`D`×|^Z2&1e%QoŠK/ˋb볘 $З=744?_toXWoK/x"Cٳg< 743%W5N o~wyG8/0K 0Kc H.y|%^=uuO}Sj{WnVB$]ה$BH{.\}aLoN~}Z>wC?stL}C_0 IcӻLHCf~%!d~ɬ3NkP_J i1٣KȰyL5 |&%LrIF)|"G\XIuv<\h~K`p; &#J^ؗ`>\yJK,"#DQTńHpq|~9UsS*Y]f(_fEbh:͢2"Y *RՑۼ)^b8q⩧֏Ν;7OYFe{{ j+oNo4_-I{ԧfnHBA0 |yvvtx5P!GZ!8-})O^f\1U IDATa!gFnT_ /ת9Vij0Wu7eWؗLBI)U\ YEkHy"+yk?nTcj!m6s[}?bpQ}0MA45ԚJiZL$ TNb9M:LA'/I+h^E3Мe;<O}Y<=m6M y[Gy׬Y07d\8_q R>Ǜ]%U%'>P_4{OїDHn3GIC_P'0 }B IKfCX,̌3#u A&wU8y湡aeIej߸Sm۷o߾}*G`Jxbܰ~%_-_)OGC$I?IM24sCrC_+0PbIc;X;З&ͬKB|~䝴c}&)ִNƵ4[@x @_fJM$'x㍒޾tmm$ё#G+>m$kmheF/,ʄ 34sCrC_10PbIc b,dDoVX3'7O-MolG*KvӃCCCC:bԩӧNY__WӹKu'uoC޻k]H=x/Aa`@) OƏC_4"/ڛG ;gK?e{H19vĸ n`n6y)eΟh?s{l7l]el\3 ky*r_”t̨Xaʾc4V9Z;u!s}W7,<>~wSĝaȵ\mqWZUDwhaG}jVWRRʡq_1=yzMLJ#1(+tdov)]HΏNOPKY 1 Il!O y{!+vgf0C/A`9f*M֗$6\/}IYjE|"e)%Qu}}9\QkÇq0K1ЗڳnZȅ LS>a$+cccĄ ojjr*Q2Gi]L+'~/c &`Xd+I_N2?<ɬj;2 q 7KB«/_[`q١/= "Y t ;`*)!mҗ(^ЗH0JC |zzYwxdChfYjOҬ~{lV Зaח0=ЗQЗYgDƾTT6.&Ac11rL.rb.(ib\Ve2X`tnY[;}Im4~eZOg6mwb\L9qa  ne,ӗ] ަ{v*7A4$jO wis?e@L($.4jf$O5璒k =񚺁$՗.ެO"3h(36U7Ofփtx6]^2o[ͯsX/K 0PHc ;d 'WU_ܲjueЗQЗ% ֗nq/ w'/AdCUrX̒ۆP_NMMMOO?OLLLNNN)<>j$$IK2`@-|/e;,K{ːKO/#/_M%kyܜdJ/I%`{T %S/?GdǾsq03i1ϓ&\yL~X.& PQ$IILL%?lڸ~{I`JhH/B؂>($ q[J @sA3lÛ"/9!‰K2Y]З$rN *`0&?a#+$Mfųizs[^z+x &G |Ǐarj9hf yv$՗ e27K~xqlGK}i {4gqi$&)20) |BL&339995LeyHȸT&é;OO6>rC}=n_%!l~額[ׅ=/#/Z*e2d}ӔQHbL`B_7ڪvNqJ#{E4/@ `@@>¦9}I/j{Ꮎ+wkC_J_2k1}&}9\Q:8L_C !q7Q8X1&Pף&Ku-Ne6;;:߄Cl33FfVњJ?}A͏%TW-=exxX۹)T !dK 0>) ЗV>v>2O^yw{/IcЗ՗f/p/7ݝ4yM䍗9x/`|szR*+/˗w)2};mm1$Ib?bpLK w۔߁%̉t̊q(1 !yh˒K+Yei._[`2ry-e|tҒTfo4NYIɇ"mkk E:iO:`B_(rǺlۊT`Wp4/etރ;qR?1se0/ח5!/ A/]K`j- eO+qIMY#өn-FCSJå-UCkZeEb>\R.EB(ʆz8 .4^ֵgE-Omڸ,Ht !mmTC+;m`qz{TeMyMOl}aM8|s5U5sv̄ԝ?ELePrXfmQOe.%?ӆjɅ0$-,o(wp ԗJƵ2]R)DNL|" B. PT0[dڹ4$ , fy֗d2L6;6"L.1#/u]_zO!̕ }  |B2~ܡ,=,K}q~ \bB.|zWW̩VWY=H}yЗRr}i/I0"H-J1r8Qs0UKVwܼ{_- |:Bjg@r5ԳbvĔח2w)#-1/7oyӃw7nT*ՎSrӧ3&>4ӄihEUݨ/0~'֣Evt0q) }ip|x}00_;`c'tZPi{G/]~uy|{| 6O˪l˽G?fRcjScQXhMWRӭj1nV`mA?𜋉v_%:N&wuK-[ČHRB%hXЗ"5OO6'rtO>}H3y|"K GT /SinW_2F Z/O1)2uyUgm*pӹV$>LF5B.ц0;1yiQwaGM3[>=0)9~5WC_:RA kچtpԬƏ=6K } C_jik[& i"Yo9n`J2R'Aj`ӜRN&cɡ(3IZЗ";u`Fm&"AAI͛-g~郁}DŽ\1GT /W-[U+2@})O/N'Awq/jtTsn5f(BIPJ@ EeB_'`6da˸QЗeytZp LK_Rj%8%%ڜc*QQח ,(ʄ5mC}Y{lu7oo\}wW٩,#*|їRyi%H_huP2LƘ܇c䦩\8ɭbQx|un"*PW՜->bc6f4ii'dZ~CR<̼Q}S_s l2'l ʡ/KeB_`' ǡ/le157S%zer/J!/e*YjN%kο_oeabt]9ȟ9:we /c ! k֬%& MKTj@a)+>ǃIc;=/_@dÄ2M,A_|CEKWӫd e%ޙ^*MK$0AljjG׼vؒKHyɁ/Sv1QL+Ah +s}'`03|Mӳzx;ㅪKR7s~q~Bȇo'Ʈp'n|trHˋmejԝ\7sq8S ګΗ+o}Ɋ.@_1w[nllԧ_=u2s֧J3*og_.~NMMMCcCmm'U떆A_ U^_{7Z\N'@_L,V_ZwB_B_/if2G;0u&r%@\F aToF?pui(fݲu,lśWΙ3#)s-OySK[8GЗ>nޝ _`r/// . }i@/\<|sut+;:;9KK]ޞԼT$Z8Gx"o/mGաq~q08p U} }YplK/mϵe?}I LSB:]QQA755WXHK s! X)hWVVVT{;5D.Df"!*Ӣ(ȝA~8D:<>Ve@_.Ṁ42/A1}ݕнf-[H)sqޞy)Y_} >0З6}ЗāS:K˂ gC_-}i{/K}m7eӽ};9P_R555q/A`9 |{ =~vPQ|1yvq9ЗЗЗφ4[\Kn5_qK3~Μ~̾]ioGB_LbrG_i`0(KSH>^t8p U} }YplK/m5e~cR(q_ pE>TLg2K /rޓ'O)_ɹ}՗5WNaKQTYm˪*5&feeR |LQUDQc_M9yյ } 0ħ>'踠/mA_Nq="KK72Vq<VVܴkח|}9wE_AKv`0pKc=2ؚv$`⋏eˁ,@~6З2NЗ~r5W/q?',2O$喭Oe*>w]<З `9 |zzY Ăv:<.'{|G쫺]}I8۪,@~6ЗZrQ88З߸ll}zOR:e oe)L(؊Q_s;ԗd1 !أV=ЗЗЗU42/A}iQ_%PnȘ;>& EBHCCÍ+gΜQܲo)sHoξ]ꆆE_j})REzj\rFMBK._З@ & |߹ã/ ! YS,% } $J(ﲔII Uc1A͟3gKLW ï/ӎK Z?: ̩FcIꄕ3ӔRjU8P>%yZ/kIۭOcF`r/// . }iӯ8eX|Y]]?GSf/CxTЗ?\S9FDqHЗЗʌd_nK?/_v}+sdf!75!x/AALp |J8~ҳ6%42/AЗ&PBs|*Y̑#M%Ą(546,]ߺEhӹ*ʟXU $>^KIs \@_B_B_]З _K^[{,EĤT/Y&A`)˚1nܹs-{b!חy{!jkkͳiRc]FH$ؗA֙ PÓNK~mC_K\SmNIz6%42/A}cua1l#f IDAT}Z zۼ婼5_]mmmۮa/ .#Bk:t:yH~a^rz+]k\@_B_B_]֗xS Z|={%_ȨKkutt4t\5A/m͹ؗQ__h"̐H[k\_2³P }jN@@_B_B_-}i{/A X>^ T 4~LKE ˜21RLF+V}sorQҎS.$T#PKBREQLq0bbB**++MTrgr{B|L(ؗЗQ}0 ?ʸӚ]9){4<ЗЗЗFW&%}!o\YbyrK7/%О^ЗP`0 ?XQXZs["/{eIw҉ (/=מK 6Вv^uՂڰr`bin=^ݘ1L#OO6Eմ6m'iǡ/]} }izr/mO%}b M,f/.En޾:}YSS퟾ 2K{З8L,aޞyҟgN=%=~$ ~C}=Nuػ+:&(A_B_9wЗЗ $rDD4-e*p7zk"QPjS2I2o"d2ЙS)ymh}bܸST{al/,X0}z */E%x |UU2]QƾVUԩ(`\11bM,QMKϧ&'/Ad@Ly>8bފ!4KwC_B_@_B_VK}xޗ~7ni('-Oemm 7|Xїa{B_0 e *_=З6J%%Зe%ز޾S[[`x^З ` | ,(5K>g@|Y Зf @_ڞkK?ehZZWMP޷k󖧜>0ח З%/A> *'.֔t2潳S^FW>f/]m OMTf^,_.]r Zu. dTsU*Yj,KK;%%԰BQTu q ڧ()+sYetzp6m\o/Zu&%RA0}2|[FEQPb_ |HM~iM\Kn1q } |xV&znm3bpv/d+:&#}c!BȎEqU}sY 7INkJ,dg%{+jf-^!DD u+3wTkYב9rYb0暫Ϙ=v>5*zQ/"##7jL%V-awB_: KP]ޗ檫ۿ-eYӧ677e/  `0XD8ԗQЗc/w= }Y־I ҝܱPnxЗy-7} Ts%ӵˡ/T%(.tf9-ʺ˚ȒqLL8Tpn[Μ9|em6ח3gPGa̰  q d)jzʳD&EF |MK7ؗ́?~VzȽ՗ܳ6Ҭ}B_y0}S3Pe/F&6trd%GAe!}hwic\IO$~>wJ|Q7޸73ַm믿n̙15SRTeBw)Te(R`2BMc_jj /A ;&]tk"|'2T\/K$З5X^Ra`dbKS>^oyB_B_BRn|C֗![ З r`⎷ |svvtnҗC /O!/noKoFW-]>ޥЗ>Ro7޸|ttg͚6cƌXVzwmC_`oln}5Չ7 UKҗy]/ o/Z~}b>u)u]r7͹`t2L>2$S[Z܆31MθҞ7[L;Sp3g&uV[[Kefzw]E[;F+~EnygI3܆eyww k5G_/M ]@=pq`d</,i:|E]GX)iCe&,I0wٲeS^x܈tIf[3AJxK'1K2ִ즅ͭЗ}} V_*{bbsy޾msVXK+T_."J@}Yd*FSJ5+[[ET~ E1H2L&#qkSd2L&]|QfA٢e!Tp4YK}32 & vxǺG>2oC_毳~R#.yKeΊYdL 24nxJs&/Ѡ*;}zؗ~]`xf7:Acd l\`#YC =@'AIe@zn#Ef2rZ] С/AԀ'^% `%FB6NZ^їG',ڮKW|8< ZajXJ75@_B_([nf)k(UU#% %e=IR% p|8\ѷߘi,B.R2ޑc__鋢jE-  & *qOC}YȒЗΚn՗f5tҤ7<Ɩ'aQF~l^V:9L*>G_蕾ă 5~J|gQ_cĸpKLs{JJ> tY.Щi TmSuV? \LΨ*fծW5З ` XpXM>nc2Зv-2חrq/ Ru}xeTB}'V-p5Y} FDŲ e}MdV*P_i>t |^B)3.Yy=MMGy}w(r@YI[WX,OSܓuK/lnmPJ%Rb-t%% |>_ycJwA_>r“>օ/҂nx!I ң],%6m4>%ҫ܃-px yU* ӗE <Θ/]KXo_F*\K{t|VѼ(B/`OXR|`JKP{З޴/-ZKV@\KKI7Z!^}遾&u(r-TiKzSOK?0&Ia>|ju_`%$?⤲47[OTR[2ͧMugrՙ!i}rY`Oئ)kC}"t/;;:g-e.Fr۬.%d=r6Vb:|`$CVЗP)uod?ekR TIc IDATӃ=o-zo?+ds2$&ɖd)LJ5&'TNOJR$ǻ &$OC/{z^p/K2=8inmp/Qr ˀ QƇTEBwb xݻ   qȱc#PB~CrBkiяooS/ )} t`ʁt؋Qw>]%!e[}ɠї}lG^ǻ՗%XQ3uOc3>:'bn(-@Y-DjW,&I_E;t盍iu,Dܾ:}KCoB~SF_2BЗ T`"' |hgG^08mvmr֗F_} ?B}iޗ9|8 !җ߷?\g] za@Q]]K!'27T\P)QT )P }~4RN3b/?e & g^tt/eL]Y %P P B;46ЗXܻhKr Nno{KK{6Դhd2EfcR*. |+B*14Mi^ ]T/ !_OK|{DYރU@_B_߸e=x<ܵkErFGG_~W-ZxGM!/0IkWdlJ4'$)ͣeV3חl9ʴD_Ҕ.naI̞rL$e&LgLF~dW(fLV2 yI:l%o[Fd,pT.u]ڵ{׮ݞQD9x =G//L@)>mvb:!:ԗAB%٫xyE_2RC& w b"1cFSS_}]R9r_ml"4 <IRF+0@p"%}988s}˘u=R>rN-%L@T)>m{wY~Q},2@7-#ݲbiM!FdXʂ%cmvuje&V'ߕ>=xqWꫯ566vۭa~ hA_DX"؄-xtevaȧЗ!N=gg/-vݺu89ڦc_JٓϞuym-\jM挡/mlZfv0>xa?~W_}<%(0Q|mFIN3/;;:gmy&ve [Y$irrŋ?Ͽ9sFY_~nV@Al%)pzrK!L<]g*q[{jJ ՗$ڔ1njQJ_,( I*J/o*JU7BfASQ͟q&g3M[Vx圲A_oէNRx'a0AQ(G$fah:ܲpj} "c_U~KW/W|-+oդ2g->Lz䭒mȏVN_vVTX{m+㯼[nY@AQ4:[tUW1ľ1Oc-@;;: ^VQ;CB/]kfGGGgg=zЗ B_u]/Z}Pٳ FԆOOb_B_'bK}:ח[\ISKJQ$dvُʓ@)T_;sScm[mKCl+dǎ7neuӞ}YX)xGL@(2uC}HЗ^qÇb0ݼH_p/%9ǺuxNǏNMe_Аa,3XkkcDzDbxxpaH_}GoȬY7~3>{_|뭷˗ۻ֮]{VVV{G/_tiK,ٸq>hQN|N_2ŏS裏xJ-if~qǔ",3yf:x&0">&}NREQs}DQTR(&Ԭ QTG|Ac).AtNҟ< 'סb& (g[\fus}ІRm-AK%Bo?*c5C˕'~k})?Zk?cGƚN0x@_L@(2b:HB_q ^+}#N_ݘxyo/~!s2ݴdEʩ=vz!尘FRKf)L\K6%/ !>ܰ~X?r뭼yw.g(ŋ׭[^/u ??nlذ+_ /y:twʻ9}>Lw/lY3;=Ib?Id保;wٖІb/liĉ~cccccW._"]T&'2RIƝ6~lƣ=З& 2V2l“|ܡ 7htvv)?lT$rח6/ g5XZ[[??>}ŋah?Ͽo+ӳg^ؔVO<|d>O7OoݺgOj]Ç?s" їͣ$%nY !í/)?{vppz67-R.O۪/O=74fQQN[ۏ|]З1}0Ѡ>|{sϺG?p7 З6 ЗhYfoOOK{'{ァ+СC|ڴiܹaF'1ЇyӧO9s??o{9?N_~O~2444111<_~=??9sbOZZZvyN>}̙<# yfpj((V{޴>KE_+MmɃ׷&Gxx{Q ce & [[h=6!P?җ=]눨:|NO}ɬ q/cEhAML&W\}ҥKbKM},L*Cas,XjժpG]?>kDᕕ?񏯽Zex أlڴx)hK! %d݋~]r15fsnKwRn6|G ^f=m3s/ & ƪ>oZUHJ_l{;}gK&/e]k_n(6lP>ݻwxxX844o>>6֕!wqnoQ !/_?nۦgGyʰ䪪*]'J%ٺ!k֬ѥAÕyx/1wo7xPgɮ]ƏC_KyJro9y6$x6Q"_З ?$𱵟{Ύ2Зt A{ T_j _җ{9C$gy{y,2 ,ͩ?f2vEo~mtsfΜ+}T K0Rm{,ЗJjs=ER]З[jLcmW7-.y%@qJO϶bei|[ї}f UUU3fhhhX`ի7nnҥK:%s?{fGUkHws$}޻*$CH:QeΨ#yyy9y(88#$!(Ό{w@.!IwQUk_z;L˪UVGȑrPBP?7SN=_oViVN2`h_|(qÜqx=Sb16)kJ$m̒k HgǫF a3u@3眙G0ƀ1gz^xklo#|/5Z <[2f3q?/I#"$UH@UB0>P2$-? oU e͍5 4oh<̡C׮3Rj_Fzv̀Wb 3ު5|iV&؅Q)'YW^=rUO|ꩧ|v~?O=T wG2Ps?U%4_RXaƩ={z'h_ѶeH| zuI^l߾5%x"/rT*$x<2xQۤFeڵ~jǗ50c{{СC7o6a!k@Bt˕{o׬DG{5tslisy/o39H%Їo1b_R)3~YS8wXBE}>&!DT]~GGҗSb(0bSs8gZ!{K6OP P(ŗYD4ǷgE$|IV &DDgÆ3S!w2wޮ5pv)%B4|I3>ϘL_sV:Б?Ln\sncǜI ;v7k,X}nn_w7wD;(rԖ|KK/e? ( _KDXKR«aGFHnʋ?/I"I"*QFόW(>{Bb@/Fu2,—_[͓1}sꫯxcQ>rk/&UK~_??LNNNMM۷g?g\&/%@IƘ$R@RSTU/?ѧe?3_"@|x~y:=7ZK4e)UO+"$IV{ޟư i/f s3_bzxk1kq| _wj<]wusQ,}}}7|i= >k?]K,[x^;|_@5I—7DV)h Pr]'?Ԃ&/1 _KXR(W8p0 y4Dq0I$Re>/{uٓW=2KƧx1$OW;wL6=s|_z%l D͞={˖-W\q?fn{|L57\zv-3cǬUUUX0j-OM .>FMm~UPte~RϤFp˹z׶?/N9o ͒t*6wؙt9bT8j:aq9Cy-K h};咢>:J}UC󟿡KZMyinLR!1Ƙv$W8g\Ĺp%wBh]X$Kw|?@qY۷oN칭I|IC-$HƳb& L#ˢx— _#y<͚53c&|k_;'K0`…O?-Z[GG/~'"ˉA݁Rx"&AѱAW%X|cǎ]kLzqEK47c H@4D6$TH01(2 0&tʔƇewe)uu=C柌kyKׯGOIJvm7| ^~]v|{{gO~#)> l֗lܓԳJT| 3_RU/YKM?Ύpv>{Q7rK,2_@_jg},[۳+~);SIu{$ClREQ { _J,"$;O`ʨ1d" aLL;f D?Od}q$4o;h4</"]\'>`o~7 m+ 0 IDATꪫ"E!,|i!/Ɨc˨4•FǗZJ/;!񥦇 7\bFމIΙ;u)2 |_%|`:tbrBvED"Ub'I<$,cÌŗɚ^^BF࿲/yDƗDȑ#75Z,&ԧ^N%J+_of G .YrBsA0`@VQ^v^ČGKO4~$cn췿@B@i]Y~E=^v0Ț밹3s3[!/)Bcb1J@—| KgF !/IeLT'.wbM/;9OQ( &FǗ4 ;w=z_^{x5Pl//gv )|ہ, T 3SksZ'9Q]XZ*ցeq˖cKM?Ϯ6O\"-9D[|WP 2}I+ 2Ȍ|(Iҁ/Hyy8zࡦMe9$ELTl6j$@!5CeM/=<<<<+ $|k;2v, [ΝKMDtkz,K PU/.[1:6Ʌ\/~񋫯l[[Wa,ڹyg&;3@/CKg SǦ5ǔc沒j.*ȿI~P{(%9 yJf]*XD"_lذߦ'tʘ|ZeY]L j_.?';^/)Od2rɁ/}9_\| /B KRMUŗnM= /~{D991R\KaKb:_D<N_l0I$R/OyT_Ǘ^/JbhA|Y_j]G×4,LG)Hٛ/_($|ɤ̙vm욊"2`ZsUt([$ٯWj3AbyRj1Ƙq<̬3SpM̴rtCaE'< 0ٺNxx=fxŻT1 D3krR(H#&l}_wcǎx/+\t20Q4$/rι:sbb/nʀG+7bhO;.@/[ *%$H\>[}X#ށ zB $g,jˊEՋ/;gKdg=66v!… O9O_}]]5}g;'$rHD!gIq,34X̙Y ̠'qnmAf5Ze$"9s.{\ ,4~%3kb\[HA 6*GdźWݞǁ -F4 @)(%aEAkGYv1B\Xʌ3^9q(#dyZUr@!8BSj-fE/pީ={QKR&D*7犕'0Ze$2x1Z*e^BmZ_v~e1/R#"OanvζؗAa(A]Y} 2Azږʋal^ߤU ܰa 1B 9 A3 @Ǘ&3"9%KO|xet]NV)kY/Lz.H(;͗7%8$'^2m /ت_%ekj_"`G|yC%$GE^{i|Qe^ə/I5& /~|R{o;QV2aY_Pw)>/+'^r'UD"M$?N2)դ%,Noywyм9!%G"F$84h/0x2/umuh[q4X`dF\y( 2.@e'xsnk1Bܸ"# y@M#Q\#QƪwH8B]t}2 m"a~k ZZXfζLpo?(vr 6 Oy1+^*W8c朙,?fi (@pQ/i)AXl^+]— LT6K?5N7zTr1z?-|i9Ͻ8ہ7ޝ_ҀD*/1dgsO#"c ƥ0z3$ 7CR14x,/s$k1/ԕ4tpzUg҉p8 (&RE A0 pմEB#Ǿb=^FOg^|4p5=vUžSi5cCV^ALDwDƭx *2DkB 綻ǻBUURjГ@% !N,|i;)O-˲?/I%"$pK"zJ jEx _$fcK3_jZvNWv7$$R;Y{D%y5t؇oټerr2+͈RHȽ%9fR)—`HR+^@/dįj%^j_rP>_ub—4$>ėLhiEtDDn)nQ׀f.R f<1ߴH'Ź녽9Du)pw:U'1mf2/$]NRZ?NEz9a  E%|]9K|vs= ˪:m3zS0s0εRU-bVUUѣG@fTE7yBjen6ˈk6l09Dnڴ颋.jnnUBϨ( WST1~lꘪB0-Rǎ@&3Ӌ# p1O + ׳<4 |+ |P)AuQC"LTjHh28ėj&|IPU/hU<{!%@I| f`/ !#uMN|3F8 E"R5E{:Xv%cSOcnZGf:l&D@oYyB_e$oyKgB"$MD0I$RI/?.bwp|9<<|]Oڳvw^I{j_r# ėH D* /!*U :%y ŗ+9=@/1_b>tƗ#^pRlD%=*N#|Y"/rbV_s |bÃ"Аg c86M\p<V%vhk׫>;V|CFR4 S:1(tHnjL_^yVIRSȸ-T eFlnMt\A1Xٚb׽1q+6glbs0$HHLgׁU~YmX#c^ծRᖕ٥[u.^[Ytt! KDH\{w( }IDXÏkNSOlŢEqm*]fy_^i (k_@%j_`H$ENx铏W/L㗟_Nk_Ss}^RFx/ˈ/1Ĵ?~IrK0' = 2vzƘQ5ܺ|pcY#>y5B_10Bo|;gYǹGt: uuu+/ϚUg>uuu;K"O#X!csΘ~]Q#T W+eb= Ǘ-[h"0`j{*bRU1豣&R4:a )* }3d`} Zx×H%)"'H%a\>^^/E75| m󽓒//g8{rVm4R{?Oˀe (<ɧ'IL _B`R=[o`HiQ{3!8[ԯ6%QWaSh&—`H{"'fC²@CN^#L/ _Ǘ<[g.—3_bMD]?"$"K /K/Iꅘ˖-kmmM _I\X/?nc~5/C3^npm#$HqFKޛ&D"+ ) +t%qa_VxHzI2*tUgw |9Sfh) _ʔAD /hii =r&G{雘`R.rfCҿI<$$HW!+ 65kt ZHŐ2)n1qE/cK{J/y8—/1WŗEH|wXff *PQ+ƙ8Z XzKf $maR?\@re&;^* fGJ ӓڳNS#07g1i>Ӥhzg= hO•)UU9gk =c ube#2m3k_z$|I&D"M,]c%Ɨp|I/ \Wv??[w~dyJƗ = v;n1d| !%Hүp0֗4&U~}O,\0ar?swBHH nf$|IDD"[ |l։';#'U/e"R+|9=?y',տeK0/+5+xe %TwĹhmm(P5[V|aABa6ҧ>`×X P?esb;7"}+>Bx$*EN">k?G g:/ė6+?{w:Lmda.%Fŗ!/—$  I]_" R+`Q<=|gvL$% $*O, _`ϐV/WL/ _&/?:-;7{Z?2#_VDGeEˉW3G×H=v܁q.<1@aE4OYiIc,cJ:] qs颙31ιp49,zJƘwI*e |@UUBUʵ 2sCCu$B0l_ nJDD z2Z|5$|I&D"%Orq< \e6E,֬M|szv9kABn|}eW^j o}t(31n7hK1D4 6!-sSgqW g^Q+-#CsF+shr!1L 9Q2Zռy>lUU*:= Ӫj2;RLȀ c_28pgVg"la5(%$/%&D"yi`>6|ڵ Ő~ X|I//5]GFh?e~—Ɨ{5˺05ѝ/˂/˸{jC fJGt_p=z;"È=|#A39&̐>90=%s5@ߤx3B*TFyL婪 B( QrGžD`i2ƌ!c>nj_&|IDD"G/ v`}zM}]2 PK2$|Y|sz:m![ܻoޝ=nY /K/f/K/'ҳ'/%%/R~+^ 38hV P gCif;nI17ѭs9+R1`1O:Șk0H[o,j~c*5/b'4vP/F;A@#% DkZ #tmK!_$$Hp.7O0 |-bf T*0^,/\:O_Ys;w ꎛWwۏi;w _KK|9&,\G㇅ˉlxe]ҹ'Nc_BzerOLq\QǏTSO<D-#2InF/u*JѩsKDD"Uns7ϏY.|&J^|ogۼo|}5)Ay(}α9fy YvZGyWaY9LOPi'J*Wnf- ,r 6%Vm< :RJ>Tjc{˻zCf_pyς-u/ns]^R84 ͧOt;\l,lxEjG $/(!) _HRts2RĈ_Jρ/_&E^,h`\BET\$XRqx R.7'f%x rT1|d* _ei7_sc{ŘD,Ehb7V~+q*8TtU sh1aWǜWҭ#5>,6m=CiE?WB5UZVUm(0Yz;ň"p 'TCBm嵼ǮUSSG?zڒ1fsݥAV qEsfPwD"~HN6@P-`r@6(L_""C|YV"GT"I"c%.46\|XmCl@ۂ ]{xi3_ek—i|;3L͠_ q> IDATVj;W~&~3ތ$:Z-5hD&}\bIAHcb2`\Zh:ϸ#%sY\i+;O \7E*ve+dRysSSSS/@eKh] eV*?>xכqiU^xXڬlCmڸJMMMK@T"ھ}6RJB "=:yQK>nJ1$T;D($|IL$frDիXY|8[wXė/<—u];x*]*P&-34^[֗"&b^ſ<O*"VTS ؇755я5 nK@ Ѻ {.A9&"6x*KRŊ&4#Q\n$7baW\.78g5vLܙf E2_dzY) ˨|l{}۩;,2$|YVy_{wk+—Wʍ/=,|)Yκ8#EH$U/}jFM:^g`hPN0>Cy^1-ɴclp_vۭ_Ś?Ձ`?- ,&"2,wD224:6]Sv9sDĪC $|YAMZ|ތ͐meeԑfRJ/À=d K0ѡEpe`aG}Z×uKi'|I'"$R6Ц╩M1Aee2l&.֚?]w9x`~sM/no^QRTִg_ڹsL2 _Uޤe瞄ﲜR9i4J!HϐhD&dyRDfV* ]y#CX59*u8LDi8NJ[ˊb.S)kO*t9Z!aľ&0e qER#7,6 UwR|h "2([1׿%UEyP8oKRl$*QefL__&ӧH}|:{lexeeR^Nmٗv@V/Z*ofhfh 巾UBF;t9(yf '%U~&˭[><l;C.?1a/|iK8nd`B 1hnPHftЬb|ׇ}8].Q $""$Rhpp@-]52::2:-kMWf3{u~LJs ZW;L/+av B —.Z~V.?v=s!mawˁJv"—5/5^鼓s>—.WLjXQ򣧜bzL5rÿ́/dԬ-hƻ`m&ng]rۜ(=R, mɪ90lLT$U ) X022ˍ ZzI]dlddT3Ul}c_}Ͻs/3—@ҦΥMћ `wTRV1T#6# Q'k"|X&%|Y94O)ė>#!g,q×rf )hE o}6>8_L\F|ddddr.\d _E{?ݳ>_{+ KF2#e||%(h\4iK<smF!d;w!,UgKD'呁lb`9uJR8rK39RQMUUZM(8Mp!2RpcToy,- ! o/Ν/9f+1Asłwe#" 0Hc"d֫青Gs57o{-ms[`}CCr^)—DD*r##H.7222RM522:::5חXߺ>Eeː&G,PdzY"|Ie{Ge\K2IEՑe|+OVY `5%tA15b{Gw{G7|avo߽{;| f_ye+TUА_h@r%/3Ս/R_͗l%Y—/ _ ׄ/ _Vʱ/=FȜ1SS)mXru9&ѓbDZjeiEqqݖK)7D!r޼yqtwum㮙o(/Js΅PNؽkYxONk[ŷT4p3 ~1)dƵ3N|)-T/U?y'lH.fy1/ _L)CEN#c'-KsA[^/t"{q!-|\[= c./۫_饩 6jp.a`e])K—/ _k—/I+7l}8 G"?}ʒy(L ǵLu rrqa|`hC]uTyNMsu&AC`⩳G9/zX$4:66667ק-˯5Mùėwޱ\dzik {Sl8a`Qf 3K—/ _k—/cgeF^s|y9g6FfD`lfOv|/92&k& M^p%p69r=o}hťtwu-BAXwlߒ—4 *_eW L2c _K—A܍e&%|N^A0eEQwFz\!}`8\sN)\ۼ7 vjX3 k\/SW|7&,XpqH 1skJQ2nZq0sD%0,X;hwq_wsvFlLSF(_V93D V;8Uo̖֖N%&Tk첨'K .ͳcKIL/ǟS>;Xqi-^{5LY_Wl$~_ C="/—/hMJWyK۲^1fiBx@U$)D>` ^vJqŃ`K2 =Lq,!PhooW\jy᥀p+m?+_Fƾi~2>M~>l6/e%K2ùhK~m%—/ _$|Iҗ' B|Y5*FѺ񶶶knذ!!-:8Ї-*E ϟoa:ھw;^p03hF68KKPݩ{C&k767s?(ZL)~ >F _—&588Y\,^x)\x i _nZc}||^[{{zz{{-z/|ob˻eY ^\50< D6ldY3PY"| QKB*$|y _ŗ4m&%o|K. 15|d3/mgf.@!Xd-PFb_""ԖixYc=/I"YVbKR&XQ|)P6B1w=zTR[x?/pzj S㗭X*H?!2D`ldd40/}G܆aw^L/ȀP~ 5kc$|zD_/ _q7—њeAbR KqwxKQιimruC;, @BЍLRP<\ߓ1!5+xQOw .@[VX3up~gTR 8"OR WEhQ…<(edG_wFm=p%cbA2rkP[Ӳ9$|8j%$*Gݽ]ݽݽݽ&>/11|i!IG3[}^rE==0>?P_7}t֭zjg}8Xb|IQڸqpx8w_$|I%K—^ҶOlMELyYzt^(&t̵A)^!\&qW>#e^r%03[_rfݝT*eLtJ87%ROGN^6ZR_C` L4i":>L%& 5QˡMŦ=}rp?|i74Kgޞe }4{{{;[ѲT,$Bv횵k0Ɍ%K—/ _,̄9cqRTQ@lmm]yJ_Nx>F#ttj>fyѢosj6g~[c׃ZjІ8En×=fu_]熴h͹WRRL9>n9@QEљOJIЙ roGN+kUwe͊&%,>3A ×\/ y_gr/~񯴅!|02[L/˨bxF'}KFD/! "| AH"q—/C4/D/ؤ_gӜ9٭ ,]T lQ{7kkX`J B/IRM~s֖bӽ_K?^ID4U,`rùH`u%\ʒ];wܾsxG{] իHgG篿nΝ?ןjk.`ǀz{,m_U9cTH|yJmߤOzDD%K—/k_NI <èe&n^6Pҋrk#}FFEIyLrxLs`Bș|KVpB=S.B3TJ1JLU5`3EQZ߿{K8<^ !&0Ĕv|yV˅ ή] XADTҬ,5PK/]gh?6H_>ouv2chN_&gcuuu~?&}H|nw߳> ~TJ/f5A s6啁g$|I%!s6i[9nee1h=`QOmkk;rHQ*GkQ1GK{-[sx&>zt3V2lGk,e oni望P^^ˌsNXbO;KR$UJ!~饺e,llB!b_;Tp){D*T?n>([wBz%"YђFL}N. __Gرc|q]Fn@|)th8J6(6rsg>S_ե_Qg/vm= /%x72 twiނxO'eG!&/ _BRH%K2ؗcUENdXyE[~zſtǟN?n j×Cm}f76fX%"I*O[֨ЦNJz^Z Ksg@g}2fex(|߅ӶߤDx@#lSlWWE^`F8~*_L/|CU$؊'y /X:76pzhF@%!q_ Ѽ/ǗyΎ!rHw+ϘTic`rHUU8Wcf>)|2$9Bq8\H(Z"D4En `]b:i9GǾlTa U/9NMimy%Y}?}dm5| (k4Ղ/'k65 oTb'9Fؗ$"Zfe9Td[KZj!&/#`;`' (/'?ٮ]}v_Y|f׮($JF0׮)d>I—/ _$|YS|]0̕1t椙BjF_&fg+GU~He˗=sP佳ם5/ 6q)V64ygWW}}3|gQ3cb:# qMUǒ%!")r#+Ve];Fw߾cl$/&i|l_s.|W7ސ>_l___x|9i!Swfwޱ0^BYzW!n9|CO(- \x%K—/hMZR| c<]fyy- bV&Q\[iA F\L+ŞjFinnY~󍉩{X=k^j,Vk+K IDATr⸆MKNmm{7#zqS^f3(͛8[X%rDjC!ZrЙZ$`Ŋ.҂@履~yBjιb >}O_"r*lG[~S`gGGG{Oqt(=|S!DFF. $HJ$KfT?<49F!_b;R|W+cI˭,$ˢ6W6v/?M_rES^[sپ%/ _/ _q7—њ9;zfT?SILA!9c+S"e^v{SJF˃_.R K,FM)jTU3ZI̻c%Jis(ι"SQ5?EQM*mT2&8---\<{LYsY|FYo(:%&ePˑ"Z\SKGVDi_>+Z=%Bx\|Uٳw_9ǚ;vKu߷w+$"U*6@{ sۺx%K—/hMZ|c mm>rl;ι6uxyxu\53eLOxTc&5?s|޲swwK`i8@4K/ȣ>;,_v^Wgf?՛eo+ޓIEjaFϣC/7"0Z%\C?t,&scYb e$Ce%|YD| | 0g3H9{aᢅKѯw?zRm^?眳tH _˼Lpdt _L/4QYzq/ x /DwE%`xD2y __Noy3ݜ9s`Zl[3+q'BA5s"aZrrwxh20K,sfLB>#~E35T:RPK( g ]@+&(fLXgf…|\n䝷ߙA3";;caPFW:Gr_Լ4v9{6x0bL~64t!2$jbb/g?sU{{g?ys}h%^ng,EY矿I|>]m %$|$|y __m~9:4ZԈsn81X4>dU$NxKY ,/R> 뛛&)!=KRDd)r#ZZh2L&ktW5tŗhEuErlltllTs/‘5j,WEH|'a| |]wbSė}=%K/KOyG /ߺY߶U$—/#q—/C4/˖}MshYcqdVKs9ڗV; YȪ񥯛/1ƁRNEQL{LylX]Z5UUfRK,Y5.vJ{[Z[-Km[P˔-+fJRi ^u4uMTV.KK؋/I."aZr"SUaժyI_|iG_>c[ pxzMWƗQ 0m_b / b6pg, L/=;]:g|ǎ;wtݚ۷MnZE%K—܍e&-6Vt(,RdN@kMf2Hnwޮ<Ec t:"Yaea|X:[UAcV]фǗccҜc+{xKk 477ԛ2ktz=)=irr+>i%k.| __azqc_"}l630:ځL/T,=vM6#Cؾ͗}LI%h%˲KΦ>BӜ9sh"Sb1";hX !5ǎُu˶ &r{JOyM)EAnYa8W8c°,9rc V3L[oo?x`,>{e$rzj[4y#5<7CQMJ3ƙN0X{`W k;n̛7vuzz׮{]- e"9f`VCE=K&fv|t/7oy |s hb5hmf\x'%`gG}Կ?}WW>c}{auZ NGIj5k׮+:s9! o>fb$|$|y __N(B2oš{/uOyjKT``m8,L6dlk[ |l&ؼ9CK8gΜ9ɠF~92e|iE4jJy &&&}oook:ח%^MQ,=3E˫_C{-I%h%JǗ'xG W$哉胝`mcl11Xn;07y-\17c{&LaV5->/3,sw?/A/I\..~Bl?EÎ]~|9222::222HKۖҥK@ AbK'8GV{{jBB[vK&I"K**KO'Ne99sI7\?Y՗2(8%Dh—`b/—!X5'Λ7W()38]*Jf5_tZevVR(ZhEQxtSD˷*UUeSY931Z*em9 UՌ稚11b+[TjZ?҃ϪdQgm9 KLnɱ/(J}iCTX!7<<\j9k7?A|!ea -̙3i ]d2LGW KII| `|s%/7lX0^s?z?scVm 1&#| Ze0eFzm-[^m ۖ.3H%M/-y2%}OT.,W7*ƍvxN-@|i<ئBj=|Μ9MMMs455Ʉч*FkɆ F+PTU9xln&t}olڼſg4+qaR d n4C smz9T:;;@̑7ޖ՗21K/ľ_/=eZ=`Ncc{RYL×֖^zeEdS6eنƮ!J/{R ΛM2fK—."(ZY3`##MFr@vG}eqBU&/CtQ=L. k͈x͏?_?{U{{K ?[n)K-qK2L\W^q#>ay3 |Ietm^—e46ʼnLM/dznQg 9?%9: MF@L]`0D—M(%ifH jSKx||9446m*ÛΝZZg7@ K̐}{|/|cyQ@|yte˾de^RGL&N[Qzzg~<~LOJiʁer_$|id5oy%gi)'8t:]gŻ5kyx]]kt:(#q*bv3x3*r&;4(cv䚗`5:0O,q0Yn uV ɪ͡MbNJe}؃W*\`1bGh%c'8$%+"լl6ߟp+_"@.ˍ$[;;;inܹs!QF2_څ#~(sVē;,]mU|ۻr劤e͚^fҫ=$c>i 8er_$| 0:Y?:Y]@ҥݝ#$/ŜMf3jNHȌsӄc`-2`z%˜!Ls7XQ#L99dZ"[?0ƹL'=rnzΝ!4%D$SKW:"r###ڿT/{xWWtvuC|Y/EN[p_Z'!|/r&B}ۧyͷ| \d>V(a׮] 7<{ؗ @$Va! TH-K2ܲ-Ϝ>}9vkϱ-}ZE U i4j@ U@- iQwȈx"2rVӁ̈/˶ , `%%g榦?9>[CWo}OG}ƍS֯|#bM%U!O^ES?hlllF|I%ɗ)5ɗ(_%p&x!Ȏjb9N|K1w̖ c -][b*6$_/ X+*2 6H,5H,Tt@r/ mWz`h6P2hDK=MCL D2ܾ=-_GwJ]M}}xgz!]=%HO9wz0?5M'9 Sa5:Fz=su}O!N)-'%-E Z쑣)Oy%yK/~`*q^Ӵ1_M;};.ghvo3s @Lxf[or{ƴ cRD$.8?Pׅ0<(P"w?NycL/`5qH9LLAwfr C#"폒DR= R0 {0սNhMkWƵKpR-=`/QV-1}Sʗh?ne(ܾ=y uz}eB/JO7FsQUHrKsӟ~I]ӤA8L5_d8`eHĪfZ JCj1lD.s2w$1f`&2)d5J9)v0զW %BC^LS* &ɗDA f֞tA,eo_I頞^^^^QQQ^Q.'rwf"_ZׅKGV:n<3Wv2)ɗ?%D jtNMϼryG~OS_{>t}}%zIUzw}F_!:AAsScsS#"4$[b-@#a[ȍ/zل|~>b{7A32.JoooGtrbݥyܹs{|7ކ |齇BȄfjru5'ѱ>>֖ |`C/)aP_/|Ջ^xADޢ^/CAlY@9٧A<71f(wr1&}IHғ/_|BR0խ0>ГP-U+****+C4T ;er޼yrza)ͷÕ/R3:~>?E}vŊ\}}-zztUMMJ r#i ֭xK%+.Yܴx\NL"y_er2,ri]9䄙dŅvԛ/_ IDAT*WѕN=dҬ%0ƙ!nD(ޗu{7;K%A fsOOO_᝝թ892M˄͛WV˹Xb`Dğ ٲ?qꕄkl?>>?#_ ,;[wo[oqM: AY|ҥKС 2l5BdB!ywDO;,D@ f><17h&zzxeUUVVz(?$fRLwD\ _ZL|i4(흇|3٘RWYYmS 1w>C4+p!P?8>:3dKsb~F'2c׏˕9|IdR0_w\]T 5Z/ϟo]Tò,Rꙧ{Nիjj9>?|o>}zٱ`P\0mܚd'#Ӂ(,ֲg+7YA˖-[Vz;uڣ-D ·j )zeiM"#ư_Lcsiᚦ lfLd H̀,VC]KF;dUfxH2Ɛs03"qmFD1>kR?;9h3eӘ<560fUa1c6iq[/B"|VIe Wzt|pO[:?kl֭rۦÇGStIA[ w}+Kp/0[?K 3>nwUUve_͟?_R\̸cbi _XS:/Xc1sǸ@q֭#/Mylw?!{ >t \/JAAD2jN#_9T?(Py_|[H,'uWZVLȗcijɂ޽4Ru*12ù/2@##/yI={_O|*=AAy6_1/n @L9W%b87e4YɗDA fѲzUeJ pP@eee ,|ɗYL)ɗh?je&[n{D˗/x(ݽ3C/)a<J&a ؔi1-r^쬾<;;3@Ӭ'oZkew"=Lz#yY/u9i@e_c샩E">s7w9ΉF*`A-J}ӽW4ս@D?ѷk% 4xys񉉉TF`͚rMmMmmmdqά]f1 X|ɀq<`ca)n34K~˕=1I$ R0 !GP;rkF,X]zDDemO:$_b0oON?q*j|t[[ƇGF^xUgKCA'/s7 ^EW_}5/7޸,#5k5ap;ɗDB f!`W2eee .4`ٱU*/_z^1%_sl爈OJy`yzC}픁m1#/y箣{tK  |e|=w@Kt۷ouuk׮M:nɗD@ f?m=+\Kc"%$Qn~TKfȗƗ/OLȗG^K8rGG{}yuoRBNJ4hq}-3+#X,Y֙ek5u=sY)IʸEPLouAi35]=15M3ˑ9F(->HDa:ә:#LH !:Yo61&Ex/eiNNޞoZ:0Mxc"na93M0q3&8<θt,8)`IDVˤɸq̭]8{brG|09s.16RV&" Ƅ gybaGd\5 ] Yخj8O%cLc"n |5Riԝ1>:ƽ/])xI$R0kO 2J&Dъ PȾ/KT(˄n&"_TV"o8qԍ7jϓOF3U! (Tֻzw6C>>sٳaYK2ܾ!DE϶B @adMSOce+2ƛ Bl|D|RI!Nߘ>}7~ѣǪvz:4xH "^u:$%V*C{|{ڔAiK/}KhTHNexp/Q)bBʗ.-T?k'Y lkk}r+W70NAQӭ`u/X#I㩛inoi߾a@B@^ɗVa)}wٛɩcGܵ+hR'kW ɗD p7dF͉vaJR/8"_Jgx.Sɗ)?Kq6._*;ϜUٽ'w?AAKՔ8ɈtWsJ+N9sN]lnlhi߱e#!P|)mذhNeΌ7ߎʩGw1s] W`/Yȏ`rrr2p$9}ˌʗmd:[6m?B;ݱNgN9smmw?֖. u7x={5,kutŔ į@d3 I>P9.>|c֐ΘKf/Q@"B;d%!S;&_6԰cƖ@,\6/[`_{vWcS9/;Gjj5iQKDpPbI_]<11q@%`*`۷?yIP  Y43eՈd@mPF79'{m[Rn86vŋꡗ?k'\+dC_ǡcVZWc5!UuMj֦`^U' # |ÓdK*dW(y9")xΥe`jJMj& p0{BXK?}Ä 0::T ~q!,|ij\ml-ݳ?P1O?}v+#h֩]@ӬHu5[+P˄qH2{T0C/}G*+Or> *^E+V<خ_JAA |o?yNw|dXQʗ戍# s<&'OܱcGuuuvO<ɗD@ f?8Պ|)gC˦H|XIt4 4C1mHU˱7_pbv>^Y䲳m"gIYs9m 0 m6ї^""0`R!Y P1qKq>GywtI&ܳ*z |׳+uyz<7LHp įLyfd)IP04˜^7M/Vvې0G^?Mwlݼcf| +U46UDRdH$B̢~?` KTQ!}Ȯɼ/}^677|֬u@o U!  |H$M24U#gΜM)_~? ˗FY ޙyDZ9}m2tʂ^)$_9l='S5z!cX¯/R1Wm(QĔK۷/_~߾m"Y)8*=AAo$#Co-|B~B suy\4d[8hcrY`9{{I4bL]Gljjjjj*e/R~Ã-rDCh4ZUU&M%ƃ,SK49f<o^~-ŕoҜ{e``opp(F(aL-fh0NlUX"'F`ׯ䕜jHoEQ /X5}DA3gH!_4eԖ=q;uG?Q)ȥ̲q|IdR09ZhRociOP/M\̤y+ꡗ[6oڹ,&}}@Uz[Y=m.}*9MAdLNgsW%b6a eydcMб$z+'ܿFFl"1Ɏybscc|,_9&"N8qFM"RU36c9n 9-dR1G=,|Ie )E-KST TUA_f[X/W (ذe榦ƌ^}}tIAA5,L#bb;w ;n&Q0}lJ.@W`~KB R0)Q)M0}6}KL_rgb$˗Ҥ}ξ|995u[6oڲesFotKCAA@&K/uPX:7eɗ:w} ˗/Ŝ`6R@i1Dܹd kijͺHL/ua!;+axx8A$H,UH,kN/I̲!5V1 M uP mX>GU!  |̲Ԧilη푫WWws+KS>!3&7L ]wc FGnz} KIv$Ғ/m\cwK]+Wed8/<B~x_ї‹2|inh?K+= ggf(a֯Ѝ044t CC0N_KbnNFy<$`9iy1 wngWXBQ?, ;U31YD Y{a 000|rHIÈqh&L3\1M .$_Y"_B/U0ջ9/K4p  (\WWiƆL\:xp:-tvv^.  9SXx˗:C1s lijhnlɗ% 6x:0<< vJO%H̃d;*fvDz/*~m-K@֧,{:c23ͅWgϞs*ӿ1+2IK]vt82ɖ{ 񟨲Gzm@>$_kGD}Dܜu%\v-Ksff ͚B&S<^NAH$tH,GА?rq KY /aɗ)~I79wۚիrdd_ &ӽfAADJF1y ?EP.\tvۖ q@'RɗI~u<^@$ R0! 睙?/3$_"q,_''* IDATJ jeeeEEEc.UY*_}Gi. ()$RY-#;*;LaӛPmޔqo Di~{B79ye"0pBRj{۶i!2}J>G/4Qn4dzy˗R8Q Y/(&\h8iٱTLs`iMUV/Ю-6zArUjo^>~4ݽ{AA;GN*NBg}elk=@+ZwG?>566s:o 1DY6C 0KvKDK ᆱxSI$ R0 ]".M9sRok MDcMAYu2*_ȹ+[hiy׮_OGGD^Z뙬3 ,T.K+b>DL%'sxEp% I{_|)_9.uM$b'HZA #Җ1a2Ƹ| eE{n4/dH,ǭ-sKST TX/}Џ|*pWo K_*~ƍ7Nz%FAAA9U%j[[?ܚikB47.Ѱ4/eL6'2"ٛYy?eRB rٺɔ~1CH$!$ɹF_*/Sʗݱe4-C~Snx'}j޽OiF;AAQU%Ir+VȮiM\LҥKnm%_"2S4dTIXcn f.0[o9ɗDtrСO=Jd2fBT TLsm4}aN>stZhoo۷ $ 䱗qPϒH\/B??]E(٥-@hāeL)пb1۳XSsg?~\wOOCCl Ѩ9׸ws{F ]NJ<Ztg r:9F]D@\x{|\T݄KR0)DI{Ɏ^|oDzlAF/Sw&L2#͛^8::N#{<Ԟ'LAA%9/ivi}={ q Y K5-* T|, /I2K^XT#'cY>H2ɗD@ f!?zQe_e_ȗ?2y{1|i| Yhx?͛7onmާ'>(" h$ ZY|'? |ɀyiY%d|))̒/>|56h6mf4cȸ,wslWB+L/L@ fQ?sdxb#_|K߹sqѪG1z/ĭKN#w?Fw4AA(yVok4"b.[VJϛk0D ˗=h} gH$_,z&)0|Y$ɳ!iop2e2C{2C<h e6K>1q+FlOP8AADʗ[d+K[橴M9fk2$xJJ\alHvNw9=Ɔ%WXJ{[Vw~w)}'ޛ5:EDyv69㜛˳ݻw1q43YKCFY˗l\kpŗi]/ʍ  2#RgypaGSWtϕ0m=e~{v/KJض_1]Lݛ<1ޭ ܷÇ?#MMƕ 8HPi{b1!Vvνw@3ҨL9"9!2l&`ʨEe&'Nv#_bH̝tٲyΝ]LAA+Cx77gi_>OԪFb&A$_JV7KI4V+qxC ¹I$) zϢE/pKL=[NC e?xt6ollزysSS#݆AA- ȑ#qT0ifW$n)PRum%qh) ƏY,_"> z-L>M~̈5g8kbg\pC0!1Y>$_ل̂G$̱XL/bfE/^ %c>(/P2**ʃE_nXΛK,AAA'Y]]s;& 9X$$B˗3 E_|YYT0!qTUUED/KHG4/C`656nXV  9sΚxGD,& )/@z 3Kb(8LBwQ(ғ/#X!Qar*/S`_|ˤ@>mC#*k[`]]sJAA±cy:ytШ`fgPĘ) MKCW` @賔^1#SFVe@U1ɗD A%(շ+֬K  (&=cǎ` 4[0;MNp ŧ1gJkjdUї!ⱙPj[K@ӞK"DHq(pB/`z._BCq%bv=w꾕]t#AADQrĉ;ń@DD\2DE<;  3g( eAaf Rfzɗ'L&F}IR0 |–/+++!UrKR/̰c0++*٭i$  䉓'T"SDI?E5A\.y\BSjbV6KV6Mbxc|IM*_m43gN/3$_f|rRʗn߂T   ۷oW9ʗ4-AcQ ]@E_y)ʭ鋪l_KL;tze} ._7   JM6-]4 C!82LbcU 9'R1d H0g{eZ|'(}I`/LNG#=ɉρ1ihdXWT  ^Syo7WctȈ|bӦMXq.@zq0"9"r&0ngF]r4>PKTD%9H,`FFS0˔jCSFRfH%NAA6mU$,4p ɋе1$Oc3YIʗ"&_)Oђ{ڽ/y_9!:?zu8;ӗ| ![_/_L_IAAD a )|i)ʉ$_%_3|:7tؚ_=ON `I%iH,::ӹ3C\h_:nɗPDIit?AAC=T^^c˷QR?L/ɗ-{\)K"PyNw )\W0~"o/3R'TKq/Ls5E–/@9AAD2: ]vų>wBh8"(2֬B1w D#)PꞘd clǬi|)Rq"O!ī% Trcg;˄":[|IAQD>c?g3, !LB!QXSrn-3ƥ]\u2ӒkC9ĩ_:fgn}~055500NOt2N|!HїɎWR/mx笎'> $_@ fO1ٱTO_fJ,AA;u㔤655+š5kʗmf B/!`IB!|)'q!K" H,t-gRqsboTZAK`zf 2  9DQ1+s.oq+XN^E0# a*EWM9 #/!52hDX,&7{/.!qb0O)[\^bӧON?W^x&IG!1aD2ԺF)t) ]2Ʊx%0ns2VRuC/3"$_A fVd{$7 DPB_&/CKCCCCU+Wv-   |G(%K ( zԥ9ҵISc̒>qRnj'ٗ|,R|I )__bŊ`%"zȗ*ῶ _&֪ɗ!C#W~ Wݿf*+  Bq:f/x3d`*wquR >@)zœ;| %/!(cNq(_FѲ2ȰzoeCPFxY[?q۷'$  D LNNP13yêM/_/=|0ʗ 33^~oUWWn' ~]&|0ek6-67q36Id-$KθcS畈`Ql(&N:Nt^ƙEɞU09g<~A0q]0.OffCW4KVP@ ʗ ^pB$!|?p!dx;msA33 7&&n]zukܲy3ݪAAaM#,2]z40&2nz pEX$7[%\$_nYOwxRw̖%*(e‡v2 -܍K~mXnY  'pi(o| (_$_—O|M/=di4/-W_ dy*`j.q̓9gqL7ysjf~^Q' ̐/e鰴K0$_)*ߺGDLSޛ'_"dVVTTVTt (zb&sYM-۷o[  RM]+0`F"&3L!e:ݸ*rL?\ܥ9W0ѮJB%$He751/Ӈ׮]tzCDι9B*ήᑄKT_f 8w pf91}ØQGRKOɗUg@7,lB%9H,l)՝m43gq_;⸜#K?AADܸuѣGiA/dqq@CEY4jY*ї'˖ՙ T@%QYܻw7!ۗ/u{r2@ ׯصOK.ER+wWz8jqM3;ɗ YzQ`~ 䩍ĺrRD0KX 1E*7e8H_w/?/]˔az K ӓښ]5SSSӗ^{=X#G|ˏ?uŊvCAD"{_ڼ/|0ccs ۻ4DJ> in~HYgkɧcb c`&ׯBӑ*DB88&483?oMon M1cY)bV]d=%,_ΙvBc|I`>|YB`gX/C2UlQWWԸn탗^{}|bbbVF^Qhkkmkk{rtAAE3MSocݺu6ܽ[Qh`gB_4$YU)UaKhRQ|I3A?QC z2i|)n{uaǯ]Y-@%L_B/t RiG˗IB*`$L o߾wFO93::ᑿo=OvKAAD:):zbBBI<&0d;R}奄o SsV0*d)z"}ׄ(_ڷo_m۶ "a`Rm f$b̃5nU5/ǘиl.66J3.Q_ aѓǡK sO%E^A=p/3QGET2 2ʗSȇqw>O|c۷m /|vࡡ!  ߸']ٷo߲e)ܙt2Q%a}+=FJiv)+"*c)er|yUr&zȑݩ¦S^,?M-I$҄b0 I ȓ?Feee揫1d *V>]4DM3Kȝ;V!;:ڷov䩷oܸq]Zr+Wt}%Xᥖ̜XQI%a&/_r)ʲ"c?,]T/K I,)((A74dzl>eaPD KƜ~&ijm'?O-__Wh@AAT@'J]ٷo_mmm;wQrj_?%Ҫc%:˗%} sx`L!ǬX|Ig!C1)[Zʢh{>C{ O˗޻M.$24-Sr;ip68c/H:;i|  "ȱK o}nhgjnG (DzDf8n>^X-_͙]Λ?2i" 3\_BS6E}{}v]WWbQb"slqĄ9Qs}5xB3}0c1xCC}]]$K$W[_ܰB "p`\:?&Z2i)ȗ i5![(mE%`>~/4Z& sq\w'ґ/b҈=4<23sGdjz=#=ϷXo|~K_  dHF CW1=1u2c_o cw3'5Dk:'^bޗ܅RėX-GKf\.$_!B1U h4F!q_OLȗOoK~2b_=v]++mի׮]~_k{_gggWdAAi1(*===jeeeF˸rիz-M^nӒyB9M xڒԛo8 o@:;D@ԔBH$|A f=`…@Fm΄%*Sɗ)Fm7RO>xCC}ں}Ϟ'_xwvvtuvC#AA?e?o~Wz7ݾ=?vZYD ɗȬY3Aj,K`ۅK"tH,]C40Sޓ:Sy<-V ㎼p䥺ڽ{ ~rO 9ҰPo_OOn -Od}St%N^R~.0)}09y;wܐw?=(Z_eLdO{I[2{ jXl6fx_07(6VѲ^E`ƽ/LL`L|u3VG]`Ŀn\Hz%3v6O*-er|‰HD0X|IdSC|`?wɲbS˗n fyMWT+J<_Վ`- =}4AAK0'KKuuwI M[s2$9˗"_or555<3'-8/A fi=qdO˰%p_LxǏFڹ7nhoS\b77C;::?ڿާnҁ4JAA(VrF^I1rgvFfn}i5jCɗG*!ɗDAY{ŋFsp!y$_!IVQQ>4腮ή.CADp3 iy` >X/RDtdeet-2^fe{[ޓN:C/@a4?͙ Ρh%s_rB ]ȁ޾}]ww)KX7-C!bDIϻR7;g; LD#1Q+qp>{yף篽E ˘%*clZ37#ψE,Ikrd.# 5#ܭ H{b0c+ %3mk0"KKk|$ _/`TWUm߶V*?::e.S{wٟͯtv˿dAA|+)[vyeHKRQljj F&|Id,ǩ|)c`& 95reKLԘj/_~?sM'3zwtvv׻{ I!AAd43ݚ +s4}^Ƒ5ƩCEib ,j1PDc >֬DYDD,OL Ƙ5V ^ls#O?}MiZ(_e}q{|I#`6*wB/K6}˗HWIIkkjjwռqo'9P  B$Diҭ(hh/sw S0!Cbz)~Æy|cya+n}|"CҬ_4R->/=EZs&=?OOH"|0 0x%3j8e({AKL亵*;׮_-gwj_Q  fY9/!/#[9xoݹWť۶NQL Ry8QP f<\S|o`BN/}YٷTUC/* ɚO2^zu`̿?,_Az{O`oo@А̿oěL  rAH 귅{m&3m}J":dj8pֲ%L9 >VkDHx8"cXdf[ \UUuG^y*ܻޝ6.x/,byB%|I(@ fL#zy+_> >-S7_xU_a{[[Ε+>00gkA 2D">hP0~BT0SS_\̈&MIBJyץ'+<ެX\D1Y4̄J>ruum[\2==rn6 ƅ|yqI܅sZlX{H,`H, :ۇ??u[=|߽{W<F+2ymR%S^|ȗ~BϜ;?3sgfffʮ*57____…7UZۿ+gڿށC?oIy\+ ¯-9.{*E<Bk0ޗ!E9GDR34!V1?)-[Vw)_?x_6,@0?ˋz5 ,9F1znh-"]FD-A,⼬E"Z8M)ykk0 0.s =z|IdR0 g3ݮi/mȲ:gvC}}/\P Ƽz_|{G{5]y8p xj9U! (8r'_֏l}뭷z?KoŬ3h_kRI `<} +M#4Q.\CuuUuu5C}2wA׽yM37yp&UB"GaW^WКZ`~MM… ֑"3[&<, qTwTdYj}ߺmجbkI&@B$ws?w dd&̚ 1H[e[-[M:S˩M:?UN:u:-VT5@ԩ$& SS1zFUiiiiii9}t$2ybh(S׶% G3WK& D\3ɳ c86eK/56xyY6޸7 F\\Kxӂ - Ȅ &Y.G#`2k5̈5Te$V A@EFk})qGNpl|֖ɩlUb}hn7^j+r^'r\I9!ј%F0oX/m c`JF'3j>N"5|rHWRGIB'&rɔM[s:\Μ"?g`ר& [X58C!09EƎ<ۍxǣI991va#hF//K7ތr=YR\—.bMN6F _| Ki֖x`JO?P<,c &n_W_[nh&E|b$..)).)c PRAMԴ?f/50I0HdK+tb2҂gBS]*V~H$ﻻWZe%@9֖W^}W_Dz{~A#sJ퇉 ٻ5&LF|ZZZZ[n&lohf'&hұLǽV Xmݬl$+Gk17c)"wL4}e7%as!L838\8)M/ߓKLI'Lla1##/u-!&zͭ--Ee'th? {v^F`߾w &L%ZkK7mjjn`hu連Qt ]](#5&i:B*oO9m<%A82!.-q#Rw5×d.*,t@L zɭ-W^}mVu=P ϴEYk"C)̞{2Sq&L0hjjڴ&LyUʐ-jq\|0ed\76H\! JbKhS%Qiy :/}pCSG1\(C|g(RE099jB* `r|9jߗ<2!1L+/d0H$ǛJQqqqQ2Rxވ ^/s`HV/7h[/54*nu}}Vo)ckh?Yvx&Ld?~~1[ZY01xwLJ|yS'=N:N Bsj<%ׅJ3i<;5uvjJQeR-.KdgH|6Pߴ7:x)?~b1C}]] |fY&{e.0a$p榛6mlinf D>`f]Qq'Ģ :⣆k2XGƉjTWeZ{iU/$C\}&ϝ;w9w]dIݴL$rrrrrr w_;2s-7o?y4 b~߱opoxdkWpgK(ԳkL0a-28xW6miFLT{ 4޽݉<9oۚ1P%.d$a3'TLڑK.^t2y'G(sŗ?l:ǗJ+'񤪲6Zu߸ww}ラ{JPow֮33,,O5h]F{K\sEb7&_KƎ$W\8v\8NbΉT5Kͨ :fo_ߍnh1is9J:'GNs* # `\0@# I_RR/HdfWuu՝wܾw~N8 Xw/H1ڕʘDXlPOۻg 3|Itzu _*\Z{-JL3w?f7o7l 7 nF1)×-VDΉ]3Gi|/d>(kt==F$tĕz1fZf`f4~01tY`Fb`–t0M&O`ʗiIA&%^a:!Or||!^6m@=×*v f.|-]N .[F 6=k|{S3 Pj|+^{Mbj V ߺmmݻB61T+%*,OFE|K`ҕL _::/m08O 3M,}iG L)o7;i_er&i_2a:?[uk6^w&PZ\Ǘ2l/q |4—t K ۍ"N}$ѩ3c8vJ@szjlxR;RHmKaϹ&XPoXW[$ ηdiWvڽkn덜8פ{g0JD%3f6I#s0b< 3|Itzu _* |)gQ?2檕׮_zYBx i7kQ:J1LHf˰ϫ۳L\dwDuNɫ8Q0A\:n&_d&`yŤB(6x T)"B!D_2KS)Jޣ$:ԠACPD0fxS nK5s_j!crƚ_b E%5fEU}-Xk`xIQ,19y~jrp_&.%m=?Ouٽkp,=wZ[[wvӤ"iy0 BD%qL[bc/JZ8× KW]ŗo麿fOC̣kjlcZQE1e9Ii<`H1K&Y$>Z!Nc7${ uu͵uM:J7񥎝Rni_3K-0/ '[*(/VVx+ + +KʥʢҖ˪./rn/**,,.*x PUY<Q=wS+g>lmi)--Eg& -+/_v-1#H]e֯_'Lc!!@JF!9R{bQ8Nڎ)988)8DUUVESgbTISҜ|YHmb\'{j-SK`:`2I/m2F|dRA}ė]ė Mq%k.t汱B7`'ƁO$ q{"bU-q(cHT2e8cֈD"G%, 䤙VG%× _2|eKMFxdt"R%lIy:mN2}L &× f,,l 5w Cuv [$͜Ir|\,$X:xDHWGUL*qNN8KA'#X$taKF0dN9>~[G##Oxx8xم/|1w_j[M|Elk _bϝ F0V|'LUWWLmAD&yEeLK: CSi8 ٷ_2ߗ _2|%×َ/q!e-I-q Ɗ6<7(7ya3q譩0́øX4mNjGdw)BE Ӟ1jԨj3Pg3 X}_g3{a)SXN[E9JJ555M]s#|!)(MB ̓cYjR%6q 4ou_b͵'/NݎQW[X_O4ӃoNͥe3g>y-rty<8~|NOș`0ԽsWȲmu)7y3 eҲ_l?Ii 3|Itzu _*˟3|%× _2|%Ioq}W]s/Q99999⋓'C\-ނPib n|)+X*%qu0-BO*.JXcڧ?*9_SHc,HzF#`2-6*i`bV hIG*JRL$%cqj_a:`2Yӊ*+*ayEW_j]ŗX a.KmpweS|1eH0iibLO!+Νl4y9Ltw C@鰡Nܽg={JK8× _2|%×Ƥ[d3 fvKsUB^9420L}q~Q+.W^Tk_Z"&#ꛨ nPaj_a3{ÚIReĩaզtuպ-ǗI—:5J_R`lKD,(Its߼io})GFF=ze} |ٟ<.[C$ݻ,»߾ 1 } ֛B#PDI/Ȥ _ u3|Itzu _*6壣c55?cecBXǗɻ>8&W~G:QYYYUUIh훌v{wR/d0dNLU)---Ux!`hi[ Vt _%CR[BbHAcEN9u0*'O= <>29koįIy#*eQwtqPn7BUt~,F3tp/8)×NK[g$bbK^]]u-:F:;K<* O2t0@EI%] 4 6j7DYX LG?]LT+."83KꗈCrvD(}?@ l0cpꮆjL3{L4^K vKjKŗX} w&.[c<9[Ÿ{<'CL#CxZnL 1iID%SRtO?ۛ݀K g22|UxK=Fk_K*W^i͊&tr \TqN0ө}s k 3* @('pxddh\澤WTT\~eA*D*R 9D^`r─zJf-.x,`h_2fzF{YF`E)*.*I}]պ^%U!we^i7f$2y->_7@&X&+"$”15E~)LRtp/8)×NK[g$$۷_vW3eTjfǵ5!L_BK0ᑣG(]&֤ TTT\q啕GxuICI=)z7‹`f0.ԝCCrQwfźԯrboaeAQ$/%0b| N0$ IEPV TQ}>O$_0WlB0IQ}%CF/[&0eLh?2`ٻo}ptp/L IDAT8)×NK[g0]?6y_}-R@0Ӄ/ Y ]µ @j/̻!N0_z:t"sLH%7%{j`.| f #R@0/]H/&I+w)e<g]*kꥎ L)pb8=R:^_$D0'O8}n 7o({xSs944opy~p^$[OO'wȚ.J ض-`C2fg-dC!B!d Ӗi_@O[q: gҕ1|~~/_j7'{OndAWk{ѣG?H6Zv%]6&"+)S#K *BRZW}j"=>%?S&nܸ;k0t:PK&Y-`fXU1CniEtCq8F|A$fHʃ|>O 30(yǚLls͖ܼܥ>Mقaĥy܅IKJYOp^ɧZyջ \y͍MMMT]Tgq!Oؼ@}ٹ%[t=}(cț`w'~;ݽk6 fqR@0tp/8)×NK[g[3p)`mQ}c,B0XWM2YQr:wjgSR,qt0rGFFRc}MS]mc]uSm1h&`0!&4` 崨*1 (:#ʼl<<wغڴixC'1u\/IG}BvM(Fdq 1tÚ >(uOJJɧ^4-Y.^zD_>gϝ ;83<1"}&ye! e K9sybV\yqš^3IScciidի''O<~\.zKHE_,BH$ҽsW d=H$⠐`(t`0DpL^H$F'=ٳw~K>f҅L _::/mURӯn)"z+*-YdIŋYq  &477@?&^XXy0hrǟ|TVZJMk׮5p1]$D@ LD,:\$֯_gbc0$ssLgiאhh}> _P8× KW³_Oh4ZVZ*?GQF OT=hY%f1iqSƴVW^J\=5WLDl/ɹ _P8× KW³_}@?˖/%;:}iR1&E^#3uqU1?hyW>&o A,))8)j8<ݽ+/߸z,cD1hG/TPZ|I2yPR~zN7FeeerkcXFt1L!VF-HqF0d6)G*D ([0Ƙ}V9:$:\I4$.N#&Uikr .F5̕4v#1MPϞ$O}-z ނBo .Y8^oEyyqqQQQaQQe~9{e(/o &y^N{<tuUu$=gefz&xr䨃ee &u>vVqR!`b_6StF~]c:gL7dK/d2k%#_3ySZKA&!\jkѥRtWu]sE?sx̙+RT%*`G`,h=LU $K3ˬ{妆ʼn/E-/ϛCp8\^^^PHLJ[%{vdL&MThLΦ iHSK)ہ:#đۥzHF9Mi47\r_Et.6iSS#6*iNOOG333333/\( kInn+~ ^oyYYMu&-*,nmnSj8NaR Z!=4Qnǵ~jS93332:z!رֲֶ2qczyyŃ`0 1]2cK"#9VɌ[~L/dK/ _.ᢙב83YTGA}i$g_~ŕ 1@2DiYdX5]{=_rƗ2^hb?o`ʫ~I, f ǚɂQMft<<_t>iNLJ\jkɧ0 |&{GGdžÚ Fꐲʫ~a-$NtvnHWo5 co'mۺ푮-],6 Y-ƀTh0|B _&pR/^× _X\xB LҶHǼOnz\|Idhh/PPa}SwՒt>垯.vKȉX[g"ȡwS"$1'jtB[FwPմ"3I0dfc5ŏ!3L)LJRoPx^+&~_quMMf?/Wwu=dbZGZ 8$. } 3K1;lNAæp 3|Itzu _*|K}:)p$ĨKk^ D&'_>}ru 1Ϙ^~dG׊O7V _2|%× _f'4"w1"+rpgV&&{t~0##pr}hj2( ~lijjjвedgQ!\i>Jcs.,^_$ߗH ō/.|OG h[[+YB2t Mq—̊<{`fhQ )+܏p^N89FWLo[PMV*&sJ^o\q97 څ8C}1'P$y;o} [na!ok`$]]q=D*41)cԀD%q!&~* _2|%× _f+dITWG?b\b,.$rĉ#fssԇ@V(Ϝ&GA(pxDv+h%! _ _q潽uwCΘeڗl g ~ yu4;4*xIo}zꚫ29?~_'coFӽ^֮3 bR>˟jFZw::Re҅L _::/m᳣ReC.XrRs+M̌Wnc8991)4LJ۰nMSC_r'`KI _(7}}yC[}bJۈWL&֌yǟ|q\mmwashİkS[nyF^_[,94/](NӫcV _P-u Ƽ]3ߗ.ϩԾ*b)!96:{nuy,$Z'CA8fsj#JƗaP\d.v| Q~piL-` =@i;-'K&qL&Y/3l -tKIS3k*iaɲ@d{I;houwlr`OoBLSJƉtn.3]_ p/]× _gKhxmgznC1-Ֆ^00:2266fE#JOD _KkҨ}Tؤ6z%(lIxdbEd23;;vSFjkktAfvKIfYm|Yh؈iRЉLJtwY23 IDAT{[@uk&BL`ʘ{3ˀ߹esyg@`҅L _::/m%LƗD܇N_66(P$g\tިmuQN3q'K&Ú yBKQ?uQFL8v {^HPywD_GɴDM)Q*-m1>*кM'xdt ggf!)2 qe/R̩3KJTke@x%']YqqOByxy+Qc}xoll F9_?co֟9[gホb̳սsWT@m۶r۶wZtDs#]S؅!v<ܜI 3|Itzu _*T6KnnyZD427?"Ft6`0lv}]9j/?~笂cc/uzۭ+T`>,x*/j1b4rFE%Mi!?_ {G**+=I/XWWgRGmmm=jxP H?#7fʘw$J۶me%^v dVgc2%[/(I64dK_jцx||$ۺV:rqLi_bLTK%ٸjɜQNMM%oy`I&#L2BΞ>1,ޑc GY\\,9Ur+PLpmyҤ fv&{R:_{ ] S;v1*||{!&+q$o{[ziK]4U/](NӫcV _51JT%y0$ b*^gXx|̐`jb4—qC/]tŋ/]tҥKs%rh4GQ /IPß@w~OfMaIЉa+~9;=JJE@qq4%MiT; I4[c 1_́A]mm~y5vV &bSk]t'/K.9dڗL2pB|)lNS36~'F9׭VbB4 #A=->ċ_@oٳY]م/1Y#~X}II(&/@Kqlw,E&OON pӧϤV\{x|P"F0&$1Gd=Od'M⍮ǗT~0|_m^JiߗD=U<~5Wt:!zx{|{ZM2Ϙܲnh\q5tb _2|K/Ӷ S^)h|[,egmEF煨>xR_f7䈞溈\S̊tUU՝wݵ'OܷwMyL&<҂yS=K~EUFznnNTq"5F-  skK*ѹ%F"NdJa:Lg?'ٽeٳޗ_y}}—WGÈJj+r)c49L%M!&{7?+m&lrKpML`ʘ;lH׶m[L _zga/2|i1|%&fRSSs]w9>ԩSoC>S]"h-.(жhG`c\:{7% d?g'O:5E/))؄"i00`J1ӑ8)iZZqWWWUWUa~wׯk508800hD*~>{zzvڽeb#]`'gK&&0eL:llj-×OB  _mg/4֧a g@Lǚbʚ> Fd^*1F"`!*B:8(nbHō/ZА| PX2R4l(?`FVGAG#×LE'#cpX|;a\TIIIiiqiIIII 0"4@4X1FF@4@S3P"\?;;+SgNfc}Ꟍ~=`656666ؒ;gbR]vuhO,ͳ=߿mִ4ug@ݽ+=jFȎE _rzzfzfIJꪝF+.\pdRRRdŭ$N( ۬RnB-ЛW͋sv/_2a@. ļ[d6qYWSQ})@hd tB6f.^|I\8:Pq8^ _2I0YBaxGT<|=9{/jkmfZRR$ Q+V;uJނٙ=Auվ/i}ߗtɡ;KChߗFiWZyFWā7er?soHM;&۷?tәrH$XPǞHdʘٱHc _rdtċ_@ 3S ͓O4|Ȗ9o^#dT ,`#(I%`f _Y|e&'1->IRqŖ4!%J# -1Q|7޸|iƁF?JGG޻ﺳ(C('ZČvML`ʘ% _&Z陑q%NC3|ei×tzf"5UŵU%{×j@/0qE"wqS a_ sCxRGԾ/Ubŗo!}iT/&>Z!{e}Sgfr +|Ɗ _Eoe+W^|y`2@| ?x{D&J I쨤INSl?M1ۨHʝFSUK.!c$uܹs@l$oAƙ#␁(;xJsx'gUIyN'WJ<4iF&e~QӧOsutWHׯ陜4|LCuʇtϷ~`0D^"`0|t=\`bF"B;Ke<12zҥ9/3_^p0(|LpL0|j|??{$Nޔxݑ%yʍ[Dq+r`S]&B!{0"ϟLL47@^0D"X je*E> XQ!=&XL+@FrE/1@׽GEEEEL*xN8^ {@*(q⋳:|i[PP\\L'=i5A7y"-؄`_D1C'NW\q9]J_EES鄤EF X#*<*!wD-Hir;W4)3L4@II)Pb^aUH>5rRIz ։ޣc1-~u&~,ηWF]i,SC>sM̝;wm16mcO||{C0e̴ × W]&LK7f2mR)Iˌmf 9u[on:< BT0 $D%*'D9DH0 ԉ$) T2DZH%`\F!7#uuu!1d.r8.ed&8RUUYUUEo˱l<4ȯq2(RhdkR  &Jq`)@Raeh<d~&9^Ýff90.~npx֖恁AҞ}w_?*AaJk 7 L4KlIj23RX+F LIDIS5)E)6&!J@J"4UҜC#s9),P&RNYV-e7)ahH߳o*og?Fw='2iǟx]D2/#zA-a24%UPcV ] Cih]V̈yC5q'ZPRXo9zPVԡ*0+c} 6ivť9sѹ-inU+RRCqH7d #V]#oUyT .akk!uΛÑ1T|g9K+JuE| K "/2|_aĀs=1` @hhY;Z!Ea2i2Mj&39g⺴Af4AT&y,a80p,H#ITc `i  pr3|$cMP%''K#s^(;^!q8633#:D(@<77'{;ކ֛/Z[Mxm|a! Ņq%l{$>}=D&܅@ mNXFe`0FNj0/Nc[+;[Ǘ͌u _5wC{IZWAK>.౥gb2,YreYi)5w~ _Q \Q *:ri_J[ f.×LdBr, 4E(D!^dz<2٠d(S(Ĉ@SJ&RO3ME)-O5DTSJK58&N+L$fgf{B`0_=pÆ [nVzzzwk~ƭ]g B1s˶G~FC`'2]R~K+/_ᯨ,9×RL&К[ E8:~6wK/0q_Yu%%(SvI!E'KyjXT҄RH/0m9>4d1g28f,4SxM&giOI3 Bi@97;;>>z $7dMMCoMg(чvJ6mha^y5$$Bvʴ+G`~EP7:Ŷm[yW_ӑ b1&pX j/$ɹ//JJZcK Ѥ_2eK(5˗M| ue&2f%%|i.ezyq?"KLG=g1'&xŢh,[rE,VIi`Fسf&H4 >Cǎ)4Υ4< oe IDAT,Beݙ6Ƈ5|H'n!A̮m3T1_,,<*24R( m(ZٓC#Ӏ/_@.eMͫsDdėD3n×/u#e[~SO'mO>|h]x\FUs!BIoУcQ;ӿg!Lpj:;cLbv&d9rdzjjzz:T .杞9z fy 2(PʬY<ёHvƬ[B_/cj15D]~ݟ_ӟdw?H"і9AW__}]?wʲВ&m@;p0ܱkD"WMggG"xmm]~tIG|YY|N|I/C/֊w'5E3Oh+gP*S&}Kt1Lu>b2b`vʝ9)Ml3M%BOxttt|9#"ѸղjW䒾}i)o_Zb?Ǐܪ?oY2-t21^YOKZR+vapaa&)H0 TZd@d|l0 [z Q~iG eRZeZH3dHSib7F33^٧tˆL$Ѹh&|dTlm iږO}j3[G9/e+VTL4@ƨX#i*4gg9޽ot*%P[XAHAhfzoq JK/>d!6 v/`!&Ș/.%—N/Ê/֕/M b| |iŗZVLt;K#B/E"b-u2LBxB<əK0Z|  AD}i12U4Vn6,1fR933}{ګGa9$JG{q`z:.!/uEΨ/J|Iuu.?-7p bsNօ=}~!Ům]۷(cL~UneaKIKZ67Xb/k* &K V|*KPf}P/}Qɐy5jiiƘࠤSI0LjOst2L^FБÇ>|W9Nу)V|ѽ{>r( KljW.o[(C|PEU(ahD_,}(}pȇ:L^[o6{oe8۷Msm۷mRL$36` 2_(A/È/[RN_9O(=p+-,.Bfa6/-.鋦~%.5eď/Q%K343B4<|J _z$/9|  BOmin^#(@#cN9X,{SpZh"( F3~<đÇ>pz_Ӂ2Yp>gSZ^B]%|:!Ӗ w~w%'/ cLٔ TV_||ImPrBb_Ju[|RP`bb:1y&k3piR959uWcwMMM3 )q1:|={XvI]ʰڕV֔ΝfoB 6꼫n9*N',d3Pc#ֳZ_Y_";"|Ts 94:%(T(;gϞqe/Ƙ:#n/_×qJkH#(!)4a #gE--ͷl1blo;>}TfVZ.RR^~W^y&vx\]%|/Z7nu[ u;fcLn 4|YR e%Xq\MƗ%KKΑqO?M'W-\ Uؗ>߄/ BV /bN#d ޽8rmuuu'bx6s!-ӧ#33-ٳiKB=$^^QQQT+m3HP̺튢Ef몚)Ʈ캪GG3^#^U?KLUGu}_#Kz׻>oD/[w'1D/ _ 94:ذ w|}H#()fr)Żɤ%1[6Fg 3PTI_$ͳi}{*7#-0&$zD5}'_j^cL MyM 1e,[Ä7/!_t$`- #E`Y7oIlsL65<|/(LԃywߥRO#/UWiZe/KJ#s/Á/A9G}t< |yGbbfiߦ+dH4 eiY_"Bd %ȣܲO1S@7<}49 N8SK5+d ˪+UW?C޷}~9mwumvݵ]}NyOo_"іH$:߮m;vlkVxX1)аN/*jO'_5;Tq-yLcCe,`\?E/<|h EIGjK,/Q K\$ks0)K@`Y˷mR¬I&˞nIce037zw{R>QBv嚦U/Mڰa o7 8+G&Bɩd,18cLGfE/ A33xU /A P@C_iPyYKxzŗȕȄ/v) f0SR=֗=-vjd Tt/[f3fpǩw6vou%:>/D".6$ L$]]^IWjȬ@2VY| qYtlhl6H,Bc}IY PcCV8H0ف"2 LX<1C04z7qZ΋#t>`6})3֡~`*: h(FJ* 029N&]fG U KdGhKNW7'>?v٥ݷS7s='&9zD@!:a?8q:Yztq:/nYӴn0m mIk[Vpc]uW6Gvzwr3#N '9{OϼKtr62$&DKygc?Þk~޷bŊ,['{ \(T Qy$Mc䏀`֬Y}e|+yVCiS{R'әCJ67H8+jWkY|Y5Qnu 9;BLj,,#A@L@@xDt=/:|/A'e|YOO=K/-\"=H!1%pF#ǴKTj쫪vj/| K]\K.y߱cϿJ&&&&(LӉںTL!DS=nPwK|8/-6%BfYuEť!֥f{Bϰn‹C^ULSPKؗ9.&fmÆ^{vL }wӟut ĉ4$!2섶ogI| M^Kz#9K2Go0=H|=vnQɺcAܒ5i"( b+9ՒJʋ<zK}/M!5M#GFG=4K.IKbi L@HU ( !i97佪T(1~ L!DKu5"B&%IJ _ nUVH}/A~ &!VnZiqa}+Ui DF:gDdzN,ya["*%BhEͲ5xn܅']g̙TTEU3QDO7LCL&j|tcMcU8L>֭_/}}iiڽk/\;Ygg&%mp,2 1K_W% .A^L S?8;Ee-;5^7PK5s_E"LU}t:.TLLՇL>9>p.:3M#ΞC Uy! |)=| \q+VH&x"xxBݽ3"Q Q#IK0uNk{M | {]x{!4<rxTTVƐ,!$T_"B*"ћ9% 1Tl&=2Ǿ_7b"'bctw^L'„L*L{+{sMxcMSS:::w ɯi5uqd"W#V/rBBeFTw_:4zq*z6 #1#L+HeK;YR 6{?7Lnܨ!FFG!L)jWTTTc$/Xs܁xhȏi`HQEmsFLU `bR > ;;ž䮒tvvd P(u@DƔI^%ʘ$%(Hte̜d,|I 7cX늢0Ƙ7TIJ".FnH1yu4Mӈ=c<.̌ՙS2GTBb,3ihhO_hLAaPz2`Ј83l=j#kHbuYjJMTUUl 4| F tț7nL}vUW]w_aYtYrEN,VQrEbÙ+WP| ʙl]y'm۷m"@W׶D[A㡩vvuFUggG2祟]@ݛioσ^Ɛ%אGg}/K_CB =>xԟ_w:s6H&ix]N$4@0R/.De)8\OKP Mm_뮥Yg[K!lv]Ս-kjWE̗7 L$ڄ1UI27(JH$ٚ _N0QKv]_X 5dk}I\_JKxCRjӦM&idmmF|I)fK&&w;aVȰ`+W]Ҽ.kV7i˚M9hX<]m}}x<W*hֶ5g=Uݚuv:6*Jgߟ*|iIăT PN_C$%ˢ{H _R3!Ti}2i%2KDPYene `fH\^%Ɛ-ͫSUMc nE14ΌmqpGo)6{ZMcW|Ci_25um1l !v=HF@BlJ=)&ߧY7Eݝ!7Rc.܇wuͻzzr‘󕚜k[hj1)֏ّGd}4%%'Ag6"1ۡɅ2_%;bGyl+* ;XwA4:5(++3XEgSq}Flo@M)O2S6'Ս7 ^ZL#*,Z4&&-`P=ݴ,b0A(+ 'J* &ؗiÛ}?m%/!jVz>D|0~? 6"貪Y٥.]\mb3~@̄bg)stTZ8t IDATQ#5رOiW4>/]"HEEŊ+ZZZ.|39owttȴ._He/t<T|ˀNc/=Iu|¡chL/BRQ7ؗ|x<Ы%KPE<|2% ==A2I\fȘ/$/a\d29???99yȑ^xᗿeggW_}ر877os/\Hc/g}/]K/B{2_fZ JzAdƍoN!9Kjd8V0㭃͟0_r, (D9ů#_QQCLT|i_I_B>_ozꩧak;sA?.K"z_ŗRxJ_zKdyd%Dc@R3BIidr(%\.4X_ E$PM.`udZ?2a6lULKx$IaɲQbl66#! z[FcN.T8H&x8l"^LL1.ՈL}*Ύy{l#RI]Oo_"**bW׶;4bW׶ gA!8w]EA6v/`z֡C>|PK}x9}seˡrf]}7!KPa & 6>C)7'OGGJB'2f"+9|I_8DS?SO=U[kDw=@/)K0X>C×9IYH#TE|ɓc_ D%ȾgCUUU&%6xH앲HXI951G`<&}| B@0A P%);?075y$!fNc\%g)0t7~`<- c y|J _fy q@w2"e*_"`%(0dMjh$ؿ01*uyjZ2o[-&O1E?LD_"QhD_e2S~QΈW_̰ G n߷8-j;FySD"ᐗ)dB4#88M9K(S#GЕh4N!䚚htll Yb{<裷~u***VZu5'?Y\\t<_׷~瞻lٲH$R]]}9~?jhtСK/_DꮻzbƗ2Xe1ϒy;ia3K#>a! fkfx4F]"j|g ]Ѩ":0LF**L***bU̬lJRQ^QQQDԥ[Q{=:6= a: i*M|9T^Q7HL"A_`@ ?h ЕcZ4S81= cLGo|֬Y~<}4]ihhCCCSfffnk{9t?~w\tEiڏ~[n{y7fff-[Wiiwھ=drbb? /7|6,| zBӑsyQy{>{lib_r%Ⱦ"qɟl"LRyN%GrQ Ƙ&a Ua B m~ζOY6kokkv 7,--Y<::*|ppӟ?iGs-[|[Z8p`ӦMA`_ _"{x{r|\ྗѪURd#}4kx'j'Qb`T gwB@0A PhI>&Lj@LGvTy2kKc"×d% 9c?^Z_?^U #<^SScrK_|Q믿n5L&^կn6{W200H K<C}!Ɨ{s=WG| _UTes /}8P$-m_r )lflJ&`%-FNT.y*HRvCn7lRx[exV״.>&ޜe ".Maɭ1eS+_Gv 7ܠo1"ӟd6n把 :Yg ۰aoǏ8q??bo*ݽ{nv˹_رc syo~ PẐ9 Zn1 _||?3/첩)[UUoVɑ L[nаgϞO|+V>l^{899OO-[۷oojjFz/yf7'NZԴwojٲewywi/_R gH< 3N8녗q@ efa?>g,L#cF#&_P Sa2b7khhhhhm_|;nٲE; /o6_|6ma)*BԩS^wY^^n[syyɈiKKK qnnq2}#._/|O|9e.$e֝#'S/%e<!Y\Ee} ̖!$'E@0KNK1-$mrVS6ڿ<(c{CDb_ <Ǎu]zRssӊ)3xW69& B^Cmz\8 _1_f}P2_*++6o< BNjk$_f?ٟ溦o_yA5k&*˖- V:t _3̬bʳ#X-ϥ`m7;b=sL\!f*&agL(*TJ ĞʪU6n<[|=uۓR_b5 |_W ?8 ?W *S,|fvQj0XbܱCKر&v7c"Nt;oe6uL1U<ʬg~9TUͬ3WG.XW0aEcݏSź}wpAao_g(n[KꊞH$>}> \/c]bKCژvg*uvv$m==}򖕽}pO/(W_}- uEmܸQxK_b_~ݳLcɩMn۱1W-F{|jDC8c`M,XytjїKRs&hKQeZ2N0gw:]hECD' aL+ Fac !C s6O ةޝbML-M|kzqIw{36.@H/9KMGǖo{JcL\/SSSa$Ɣ#x]dԩS###Zoo%k?O<ݗ>Td`+hC5[iQ qyecTI .85\UWW'ة=XjrR_ | K@0A Pa(>Hy2[s[:B====_=}nvkK_̏nV-..ڵ !k׮E̔;g?::z(єǤ啼'_/K($|)20FBb>FׯC?r#J_Yk|\U]eIDˢѲ(K3䣏e" b #EA$í| R+KKx*yzRTpYZڇL}Ƶt4A?yaS8 g׌&=iB_;vvb|*(c/B.|_,ҙ5Rn8s.нO* 5D s C$}UyuԂ>d/Ç>Z?v:M,7*Ym41vuẕ %&(?6K h2yrYo1\s0@ł%˔j=o9.Yq̽3$5yA̞^2< %_2>C=o15Mm^{--9??w[ ,%[r,7ғ7zFt8N,G:Z#L#SN*ټPb,%SF_q2!܉3܉×C8kWlիWb3t"&%&s"h!{ b:6'hr0ė_2 |zٿۿkkkoގ Omr gL>ϲ/O}Spq<|Idv@Vb.^]~kke Lsڱ9Rr򆟵^qPJV^eˁ8(<  L٤AN 4_rGO!IDCyW] Kv /aVXb͛otUW"sۿ3LW+Lʶ߰bj$'nIӇ:yyU5{njIZct!Zv]>QODggݼo{z"H$]vOm]]vKP9%bSPD!r$&'֖H!_}Ͻ'Ɨ̎iӦM555ƶg}ֺmx#SK_Cn/s/%VT7';ǘ1E:0NÊf,_DSOXϝڕ^zoyq]:1BYl$R5X0o _z*@,S '_r$.ӳ*KLSc,wum|AKvGUyy7l(6D_ƍ%{^P7Ϳۿ) Wݿ%ݴt {l}Iroڷ[}eK/dt߆YX,h - N| R0"@,S '_rot:;bfVb:cbD23__#9Bc|뭷wYbų>oϏx|ݺu7p޼H$rw=z{g?{y-[LQʳ>n{|M+I-l%>/߁/K1p;yK!bl6)_:\1]L$>X"'}R5k֮[.ƞJUtNx I09 l i<2~\D :;-&հkD"m2=] {;JCܴJB S2*N|99>~[n^G|9*G(֗(`ˬk\5/=նɸ_b5--uu_r) }JD7Yp1M>B$eR,`Rt`RAeaG9_J<|x̓e=Xɤ\R~mۄ)/A,< " S1#]n%J&Ίľ4U$3.ـ.IcbNFu1dGfL"6awcL~J6`%R5F0'RfWXeJPW׶/|޷0:V"_2cW׶~qNHG _65V/ugUVVBWk_ K:A9IhӬLcnQ.% A!!c'.BR]vsTVU"ZZB333f+3Csʪ,g=Nxy#%(@S Yw$+6] {z\B\C;mm\P1Z_rw8eue[SSS]p'n2R{1J|T 8{Ir"-^,eEi>Ј'L{^nXKfF!\| A P1(>䎤ϱ~qMmV|PȾ|euU^3<366077w]v}|Yo{KQƞʨ%˒xd_SzE6ML Lo=&-QnfC9 i4+.'%Y3#_ʇK0&fK(?,K#x~ue͛%˒xd_hsKWLjLLEcLcEI7.22lD%6j~T ŗFA 2WoũNHL< _,Eae̘RrScbdw@ɓLN e]FEpcg9rJP1.h2q@L9Hh9jQ_kLj/O*e*| _ eyBgG~%=Kp]8z􁵄so8 _@0+tI GIN%=*?s-C<#y"6ßFQNǻ }s$=}YHvvv-59JCL1qXK ErfL1 A)r/K) )n&|i7lS%ƌCg@t/LZ0&žpfɲ䇍z SսgFy) | cI(9}R+^U| 0;{;L.xl[IPܥA^DPr@8wOO_Wsﮮm_o/yggG6ڶc]~%Ѷݵ}k.3ȗ<^]]93{Sյq}_ PΦږ9` | X#ah[QX>PcMM+IUR]2B Vj3닑L5Msc>d2i|֕wuӷASFwލ}U}!a,Z[fTʴ$c59ԍgSg,| ʙԕ}9Ks)1>c K]:+؋%BH2{_rsӯx%&GҡBR>! czK(o/L(Z2Y+X/݂rg9gB(UM-^X+ATY𒉍ێ"$zk*x~g6y|Dy쒟;  _RI(BH`)p& fN+/sf$K|A^D S J66|2TFKۑ9FXȉ#@b~/ | :=~2d @ P/"_"ƣ^ )/y(&KP0$I|-R@6dqw*p!)/إ &xg8_)}$ e;3- ,|K5pzi-?)5$I=w qKՊ)*4|M+- K-5< 3=i}ZDc'% ZEV]]۾;xuvv.%镬J;yx ѝBLE |(_໼}^<CoOҝLĮ,Nb,ElObry'omÆXm35{N$HHb=kdNcSNsevB]1])࠰ &*1N_vB\u{id[(U1SOM //YHǛV <9;>. _ `_%Of]֔thH)[KGLϴ;ijg-d/̸q&{kKkLLCCKPq &*!DZ* b=vgnENӄǗb:4T8wfan e񥛻e~塲z;q%H'eaFFϋp-zfT&!9JW_Zr,[ Al1eLmInϜM01゚adߣ@KP1 fQ)YMrRp9n}K^E!$fv>w;#>b_ҍvӾç璀//e嘺cz %e.e6> 1A.S.DKjϽ=%L.%I$%eZi$M9"Aˬ;C&*&!ߓ/A@0A cJ[:Bh4 <|iSb*_fFfFN/%-5;uL\K<˓JX@Ka=/KQ/=7.ؗj?=O<'M &TIT JN~ANȥQ b:&(/F [Ibú3%+_fGO>uzjqIG/G45|I///"ՇjՍGI>U(Ka=/KQ/=7Mo67ۏ_H1dR u{'|IbbBI{g}8<=Suhd%\J 'T JN>AN䮌:%1M:!f$(|PISc}b:WM-ΎO>5787Ɨy04i"hDtc4~uEP0$.SETTRުxmKA=/AD>4DfH^23OSU3PtnRUǟ'811.XIC)LKG1A[dXUՔ,T@~!%.yدEA[GסA( _mOo_gg)2 sv;yxZXM k-9}d2ćߑY}Ɨr_:̚Hv>G/_`N!Ǘ-[6ܼf }||O!^RD% Y@0A Pǔ>TGHG]e\?8; 1_r+LlXX6%K€/}(|Y^tN]s!`\|DcDvpb4^TU $#J>Ykoes-o^B{-+ؖLLL%$-* _2 O(c64&[>d^4&VGFeoɖxiiie斖f(>ӦUEQ`Bc]*a(OwiKKP &_PcJ$a/+!&,ʃ:M_P&"KJ-ecC%y| /y?t]$308_[GMjhnSU7,-?iس%FKB$ɥLj1ƴILjg΄4'[5| *E @d(G)û;pRϢRG ;;*tlb;oM$a[{zxҎwCA䠻oI$~Vq9XOVLryn6:o\Gtq aMy| /y?|D4-֢,oG/l6oɧ>w>~+B3/+V)v%]`%*ݢ0gb>]y`@ B~I${v; =:Z4 mrYT_Vtg/_:^/*G7s?~?>q|nWfu/AE# )}5Qwq:~PNؽr𨰺Ng "K `*k]:|)WVtg/_:^/*9Ǘ=L[ m |8cgQ->gE"%6ORDNKww`K2)}xz` B;$(r=}%s$^+q_:3K/ڀ IDAT#$`S|Y֗w?#nin^ӵ}RbNNNoMm=e0 &oJ ɾ2쮮mm,hObR9yb2n_B& d)}/*D)fPbTk%1.K~| |Y|Yrxx_޲esbjYsLNNǏwm,v鉬Y}'c. .Pp=O"pDǃgf|DF/Vtg/_:^/ ,Kž$ĺїcdžzh _n[lβe_:}܂2 3L-iXR,.5E]T @6=``AC b+^+q_:3K/|@@-A; i͟liiβ%nܵ}),+˯m8%(L @6=rIM^4#SG/_ڗ| ˀO^lJ;N__ڿe_}/A. d/S8R@ i6}嘠42b'E'Nx? /07ha!,..R.4dnԒIvI .| ʱ`@ WANbjr fRx;|π/_t^%̍M/T.oKNpS@0A H$S 0uTqC̼6%^+q_:3K/KOOb_5<<"_RB_>qDnnK=eL)>;vvt.bx{T͜sly/_:_ /__ _R&(i}/oOp{J$JP#GLM.cyT͜sly/_:_ /__z9e6%?Ɔj!l&}v%DKD?`0A! LnjrJĄ6.m=C_B_B_ڞ/K''R><ޥnyؗg%rdd駟m0o%D_S7|キ''IGE3<\tݕ } DJ"qL$%qA=o_\@_B_ї I2CR1 O?N_ӭe/eⰘ߹sk){J-Ʃ{4s4$*T8cyr"̚jYG=ȱ8koo_ww& GZ-H WtuA_B_B_B_VЗq8fy{(Rk7=64t@311{_~YaЗ q& >d׻;/(ƩcZ䵙 }I$ B_ؠ////+w23VZ}9/jDC_$ "ٍ%FAjrH̒_";C_B_B_ڟ KKƉt{_ؗGFGy{U_ʴX^|8rԹsSSSS9P&Q1ug!?\S3 } b &#>n IDbS<^@_IjЗ.6KKKK|ؗES}y{\awGM'11uJXo%Gvr%C_Ёv@zr %[ 5@_ڟ KKK` }!>ϱC/e `.K_`0 6%ֈvLzj^p@_IjЗ.6KKKK|4駟y79rї2 u$B_z[ &x!>1Ў*1[l-[ %96}b~K3~]K=TRXbʹ}ܞDKo@_&*TxsU fuwg&*%"҇Gs{lS{Yi/C> ͖/#}e·奝Зq/eG=xpb뫃>{?4)%%(ҟC}iiǩL[:zͺJx=iGu }  &D@)}V[6 N$fpie綡e/yq908WJ,eGաG⫃ };qYbC_sO=N_ӭu,ՖNw{Sݿu/.3`0$JCbн Sie綡ח[LVeW0ˈZV?yb%]/9I$&2%I9] ֗ ]oIjr{;_{mrcT.')1D+dWpFLb֧  & hA, /wYN"Q0˨zGx'O -^~҇>3gX}g떢2_9a b=(gX ^ \ڍJԗ&]%e|1ߴ| @_B__lKl3<{萦e[-Ҏ5]v31q|˖\JQTZO'Zʐ!֊ӗYoym/wԃvA_/r\З>/u3mmi$~[ }IЗK|cyrZݽw}[gggGّg-Pːfk%G~k_*iyB3e5x&Vh2候VQXԦs>Ƶ)Ƹb?5syپ+RnR9I)uJ;79vNWӧWsEf#c[sG3M-(Ǫ0|vƟfM|5SWry`zk=)ϔ~Gl}ko~4.eEbC_//1fG*~w~*nm_SB7=w/[t̙NMQwSE1JK#|cAծlNA_H|rJL&mc|ws_&{]OKЗm܇;0xC_H.zr$K&11u,ؖ^GaDGF9fbJy +?-|/UXנu$&)Jl巩L*ɔ2\Ix/lICKHOQT{`?A I۰iA-0~kD6#Sxs415[POƽW S^]3w`Dz43 ۔KEꎔˇ5):prr;epx{w| ·ľ,橧q/hkK}7>64{< ۺP} b &Fg*+Q2ʡ6-b ?z@Ή 7h_\6[+K_Nk.uq=AKkh=y#j/O bP_֙\Qɹ䪂Tk}vN|Ӓ_ẏ-խEЗЗCK=З XuNb?~я^^RB_)p|#>djzzsSb2YTq򑟹}too5/e.}i/Fڂ$}-*y֗MkTق/T_3;< V_2'CZ}+zlM׭nu }Ixl'LRuZpyzGv%K}W֗ qg:n\=P%2h}Y8VfvT_3Ktx KL#ftqD QЗTb%/%#<Ӻ!KO'(%f.ҒSrR&1eZʩ%;ɚs,iO טϵ}  >ciC%Ks}ْwinme/֗DЗKR'5į/L]=Sp@_탾 Zeƾ䥖_A_NJz)'1ILLbRN ?Ĝ_C__K^ݳ8}"5bee䪾7ЗQK1hRkf 75 ??xWo9t} } \Ou >/~YtuЗA?%{oW#}@@_`XNj,Yoh`#Rө"eKk_}gsk"/#՗JY(@_F/<)PЗ~K8ؗz~O'y{'%}K `0Mc;;t: 17/ :o"/)"}iC_*ODRu-q3k.Ċ'qdʰ㑑Q>uezPb"$ݟ|XL#)IA_`ŷ%6 n'3oV$-/-p͟jeЗҗBf: }IЗvZ:?Ku /`wkR`C_F++;vL)?s{g'A_.?Zi:ƸZq lnlK 2nRbVeH mn:;pOKN\: ]̮շQ2mL{rω{K/A)`@P҇ǒ}' 5Mܺ2ꀾ9S %듷aOKN\ƿЗ2N"uOz944ᒃ`@v,y !#$OM[^z掀T^ /keLwTہ/AH`@҇yKzm/t}J_6]2Kf􇾌Wyg,N֝Egl2͆&#B7=m24t`ӦǷm^7D7%L@SX0R}Bc+}ID0;o,eKV%[PXYӨdC#1T(Hj;V2KhH2/LB i@^ѡ/WyHIR.'(B}94t mMJrҹsgJLrG)IrR0ԏĬ/K I)}:!xhްRSWy3e3DKϫk&[RSS}K"Ma$WY^ޗ̧'?o6z_TQXBԗc:q};@_B@8tw]_1ws_ggG)ۗ%il~Ѓ!,1{oXg{d2e|%Yvlimjnm֗]>sbEvWL[[_&?IЗLŖlKj_H1 شV{. $ 4ٍ%e/D!F ibЗ02ޗG R,aЗЗ{@_PQ|H;"ԗ 6W=1 }  &G)}n,/`LRW$f;hnmjnmr2xcI337ߣddd|H?pܲ&,5HT]F2q)qLLw,Ct.yǡP7KARBs0ب| ܎D_n8<6ƍфyr'WuMMMA1u!15LI?Û &J)}l;?Ij$K̊חk/ +ZqE2ٷK_:x{0_2> lK/+͟ǚm۶/BC  |c_|ӧ{$LRv~ sZ_|x$R}بRǪO>O3-r}X%JjdR沏\Lo $Q!e<14tu 604E遾q:ZefuOϐua(<ٽ ҞF_e+x k/hAjEfѡ*r\.'YPmW%dSo"I' IDATƈ5oM;cqc:}Y[[;ںl6o,*۶.vQ3Q Qr5Hؗ^.dKdL40!N%`Y" \P_S/N[w2zIdzh>Ǿt҉yOqϖJx$Bj/.tgu_67ϯ f~:X$ 麂n۾#аu0ڀa tJrCT 5Z/_P_ @o3-Vr}<Kh޼yK^:qSPnphgC}F?I'1尘An۶Q[Зhv0 !!"랏ttm&7ij-/2_s Z/A$`@҇HÐf ^Se/ ѹd @ !díEw㌘4ZOɨ#rHeaI]̉IyZ4ݑdG&I9BJnyL>wSҤ$鶠1^.,=􈆾tFcc6=^zvuuu]t/EQWI[i9C _EttƔM˶ӧ @G.\,|'$@_ey QBJ1.* T45YK(!d/0ɸIb_2r8RWG]vR^t/ `Eҵ0}쀩OV4_ u!6%"=7z* =$Տ1 ӭi{+si"rfAAف@$3ƔcR.')B.'1Ir}0~?<^W)O} (u][onCCoAD+L?].MK4$c6S3WEr I)r\OAI9%r3cQ7 EFB>/^r1ímw9a^u*a `A|ӧO?uѣNJ= ;Q. LVvCr,Π A_x8 BHCDcm'ÀRbRW\ˁM e r񡡡;w>#]w3gK3}X 4[ip,(ãUt20<#~@_z;DcW_ /p)'d^㼥˖ˀlS__N.hh~ݺ3b/"]NЗ `0 Gn p<dFML{m>::.]|̙32X_4\o~jXɇhZ9s\xх.ZpѢWojj\t˖666f %K2Kd64̭khhhkk[rqma[=I X%"BJKND==tw <`3ЗfKtt?3nu[~ '%P+b_J81ud c_2. ˢÌT9})|s9Ӈ2J)*sP?7=5txHlM$I򟂐Irv4c ~j=Cm̘UH{EQPcG0ƦO?oz+QpAvKm?F[Db~z^'PcMh/ S[2&*㗩m;^IucR[bKP#7( j)1L/ZHSo-͂3"^SW? $%orb4ڑw *r h:E_DQ-LAPfiOl F@h9r zo9 zKu}v$&iӦ͝;K/ַm۶s5}Qp+L%sq6xЗ>$H$ AN%𹵌*XNJ:aHĬP}iQ*K߸⋿os^yT p\2֗n_𘛧ew3jG/ QS22>} |E8)}JʃC>vΡZu֜•D.ktq*v>\]]-hii1IXKKduuOEe]vEjkk>O?}Zhll?SO+^{m^~呑??7o^)E/2Ç~~i#5Q$VT2MK CNc#wDux^g]*Yb~'zo]_]"Zrl%/;WtO|ח<{ 'N׾ӗ:;Ӷ;gWW׷m'''+/ *rry52q^ =?Ar"k"1} H8)}lwd =|[VZo0/MٜKHLk~_.X@?y)_Yt=k.o|͟'\.~q?nǕzKf *[2ӑ~yǨH &ĔpRu#o;N\,`B_эDҔ7|97p2]__M7i6[o鶰o>[jkkTt1]tE/;666uuuO#<{>l2~{?m}C/LLLlذgXJB_2f7 $ܹlR 9-v/ 2Sf5K90SBKc;"Vfp$25ZKr/Kw&''GGG_~kPMR~;n yq_ҖƗ^z _ٳg͚/}9>>~nڵ---ͷ~]V^/o}kll̢$---۷o馛;?W/#Ƙ$s1f,۶,$&NI%sLiwvmd HE1R?URʧZKMiӦ+P1vVQi,͞BJBJSU)SUU3fᏵvڴV?ju8;I)QStAȧY( )hV1|@_b #K^F"%gKe^˗;9y$ؽޫKMO}Yx 9? .Xre;>'}sޢѩS?7o<7iӦnyڴiNJuCz]3gTb/Y Oe}X/ؗ>v˸\O$K_,`@ -+tw/GSQl fEKٙ@_IMM??<H׿毄ҳ\p9\_'?iqݷo­9~(/T_w c@EK=>_ K+Ps:;VYʞ-uvd;~ twy`ooǹDCR(ӦM1cƼy.뮻n͚5tl+/V>>_Wie˗F{Fg (JY,TΧS/F$ոwdV1R11QeE[Q O$AuZ(l3^a.yӗB~  B~}IO9OUƝ$ITX]$IbjH J27~>ŜM7[E((ĔS)e2o(͢N $r2U)ubJPT7b;mTJ٬(} n* 愖҇|F5D.[P_W_r/%li3rSN:th޽7o, ^oٲEK7b\YE``w%]RKf~#ދRIR8+n&OukX@i ;=#%A_LH!"9Q%h渽VYK4&KoWݹs|I"z'ϝ;'Ϭѥy:9p.QJ5C}KGn\Pw}魾EKK 0Lc/"Y!Jp<ƯE/ɭDT.\'Yo9sDUK.?<_L_>yLovI]6t%=e0RnX$dݽw}}| YbCSm,|B_/q[nzꩉ~Z"]$IEmS_wyaVUVz{nDKflˢGVam3d|\ i$&)ąr4ALd,K򥙹xN2y>S)1ŇƫQ,@ ( (LMImh<.ȩZw¼tjE.̟ @6g>X}Y8DA.s$Q VYaAI0\q)r}Ĕq,Ԩ|YeZDt>HV͊ܭijӯxCh)}Z739}LkQEK٩Dn喯듓Dtĉ?qܹsoϞ=yӧ[5k|To߾/~?.ٳg/Q Ν{m(2ԗ`/9>v–:eS\Ah)IIk)ѹssS#IR;~O)¬_nY,W<;9ФΙMDUUUU_zuNT*U%!%AA:ɧԕl0J8Au Mن63d }Y\|HGsfL hO_vaARo$\9ɰ|L>!pUkG~Z{Y}_%Qf1((XgbvXsysWe-ŅDfϞk|P^zuq3f>gΜٽ{/W,X}q+G9w~jƍbzyz}Kїs7=f,%H行acD["nU9U"|`8=0$Lc+ ݝ+lSxB_%Zg$/ ,^x!__}w}+ݻwڵ---555ͷ~={/K_N|3}i mYK=>N;/KRHT[[:0_* F@d SsXrww2Z$R)ޒ8~qg/= #bw rݫ˟n~LL &:'%).U#\ڠr F11 X;fqxYz}y9Y+؂Q}ɒ%wq?;@_X2՗]_.Si/Aw  ?^ƸJp0T?îo8I}0 ytvvdcMn3oDyWo^7bZ2W"2+V lך1cƷoo+ KdG䩴v IDAT} \.!=+@_|;.#w\, I}0 ҇[Ӡ'mMKeޗ8חM~q̙o~|B՗ַ̘ x& qL (0!"?B~i j6?Ï(ͯ"~H*|/JU?'R;*qJ@ʗ=Y_ G9 [.z1f4T|gİG#Ψz{rv\_U<$Z|4asD,T})gQJ}u1})8q? (A*BRdLݔ$k^=ƜごIqE5 2bbq0UPQ)S>}) } < &>Dݽw} f2 +R_][xYr/0K_F%:#}i"9ܝ[dfO7}dkK 3<ޗKDͱ҇+/Ay8xBNCDcl6[ oC~4 /y7KEVn9^_kLE_ f1f~<}}ef;t܉D+`=}ID#59Eo4{ Mx@_zZ 3_ e9^QʌΎU2ؔ,}?;;;*P!8D}I@m.E.՗S|zh0kH FHOǸl(rܘ_QH1q %c2FD9e}rDMMJV򲺺Z}9m4u}Y[WUUƔQ,ufiQ|SF]?ؗ }|5> ## 3_KAb:ɇ -XDG(D&],VUexA_>PVXw "6e&ٻb:jF$EPg^_5 @O N_]-;}}LIvoq'ޗKPn ռy%=vy]N>6z_zF@r#>!8ƒW) $7=/١$뮘m{ԗ&OGOK<馁}\i/ L(7OCv!8==TKIחDt-ӗhSsH՗Dda0W/MJ%䎁}\i/ 2{ݗٷL}=1l&) W@_~3gK Jz1"< +ĪcD$SST_A04@DȈ1F˿/S08`mD?l,xrT*ċEi6$T&(:ɣ 1hÍڝ: d`> e%kW:!B_yɁX I)}侟~ӝ,ogp~K"~zٿwO.6ӗhWizy r\Nwޗ %&$lD Fb-je$ߑk &7<203茶$TJ4tm% (1ܴ`f%`m0-}bY}ȱ6ĄXpiQ\lAmصnG꟫|fɔ_`З `9'!u1t,yK"Z6aɬj&0K4.Ĩo bgf6f 8Hcbf`F3J'XKOy}I6>]fCЗ `@~J۝3zYx~KW^nQAoP_} @@͖3@}Ɉ1OIE/=V99,4 $L(["Id:;, ƬY[A/$%7foK41ⱴk<;U L%N#A # *" $Bc0J )QHbC)Q(RB*%pvannuѧy GyWgLrRFAKoKOA_v99.Җ} q0$OggG6o R@_||e[nQY{_xgpdOĈ S?L,LAj&f R3&1RwHb9 *fԃ ϸ bBq j3$@ srY!# .SCu`hݧ.&W1)+ BY_q29$HjK=aTY5)w1=5a2J#]y |Kl}${ ?1ЗVB_ &K'a>PX (0urKn0*o})7L׮Eӗ>40.ex˾9yۚkr[%s`vq-J.q] L3WX,$'/4"}_}h~Cdz㡝x`0̉$u{ŷ5ܦaT ROa˻З|)f;@k2>EoYreF_BC_zZ ]RBB_r> O$)}pQ erEUR>k-1?x-A_\_ZvnlYRkp֗e/bJLGO0OQ Q)9|K p25+:6^W٩S(*LۄrC9{'%WYe-'PD҇ƒS`cء/#ї2_yuM}p[/@_W_2oЗ ԗm͵.%#m+1wLԚJ˘ͳ/p/o_J_-calx5qf֜TntrYZG%᭸L L_:{]җ L"Idl~C%&~Em?ۊ{B_M_ڼu@_I_WO<7oJ eb{u]e~yTЗ `@UJR`8#lV'_뀘2o{9MЗЗq2rW}c3}yۍ76PKPF@_B_T2ó  "> .g$]KN$#y[G/)R)erzY:{ۍ [}OtCḶY8@L ȥѤIn0& Y>nKD\‡t*YwLlЗ|tImhMc0kKcbЗЗ@yUJ۱佛" &.5#ID{C19f}<Kl{h:}i}gKˣ/QQV@_B_Fb\>!%H0PYDҧǒ*]_yۧ?~ <ӻpv2"}YghpuY,%R/} |A_F/J:h1z,e*L8"ICv4ܮCoB_r}Gv|o?|8A_/ʴہ E_W犯ƥv3C_Z2+v⋗(CCX=Vb'L8JCDcm1 }o.jkr%1Ա#o;?SG'KחgǦNw{g/%c^/ 0M)S<}aW].雹hFWo~4yӗF1@G4їV&ƅظNMhWhy(8C/*$EҀ B_2-X466FbFHQЗ nT "%檞̾_9gl~ЃЗ/n vUM>6Y;nN WTLFxo1q3t#Y)\7Ouk,Qg #bL4ص{#'Lю_# +]WH~wBa@Y0;zV|ծEmD4mXFD;ř-ҙfZ`WJ|Ɩ-7o/=\yɳ$}YK擿#F6.|/AJ$)}:;;uw$fc}z??x'緌3Ξ9vijvr9``2툒V-upW&GxUˉ,bXk-o?Kw$KVSve2_GZ~Ҡ/n<(rPJCDV8K+|g "N8~A+#b!mW}yۍ-ЗЗ﷧ ~e\sЗ &T.Qd2ֻЗ.[ܹSk>i1/Acq=pKӭb?ae8%N_2E$G`XS`ڂ6iMnW@~=oЗǨ W)1]]2. ljņ򷄑qN"$1x>D1If:C>Tҵ} *SXz{SЗ-V,jk2Зῠ;wuWN; 㧊mq8,-s~Dx%N32ޗޗI- J`\ی$Lhte,9mժ0F24$K]%%ps_B_B_X-З &T4rJoՈL{,HXrK0(xg'K m<(/W^9U-pׇrv//t3C_B_xA_zJ^廾bm澀]Jt"YG=ۗ!7-*K'ۗS[% A_E#?e[s][K][s–慾 O_́TѪ΄3hX З+χ$Nqw /\K.DH ЗžTadCb3_$,al7t\9w|E"Iԏ$~*XbEQ̮ aЗ n`cn"P/Xw Zc?&KKKKZ_rJ}ɛI37d4cϊ }:Xv_t|20yize\ޗ~I @f4wv,y%tYKs} } }i[]З _tK{8$O"TTagd~GKX`0X斓DwXrK?ЗЗЗ} E  K_eҮ L"LCv#ٳ%2ہtt0A_B_B_:8ג+ $K}M6HEMQ;f|ou魦0u-B_JoUI9uv.'ْCri!5 rʘ2v @C)}{, h/A( ,s/}G֘:36OtW]+~;Xv;ЗЗ&KKḰ$>w!W,'tMv۶OlQʑ#G^&+ NmK/"5, <{zf{!2nA_B_ KKKꂾޗKk/V߲Fk\޾{V~c^}գG(8ye:z}11ÿNxSYWWGD<}4L:ݺfL{ uЗ H`08%>c{7Ԩa} }ix.///m+ ai<fR}yqX7^c,N_/`wʩە?|З )`pJR؎%^1ЗЗҶ/A@_:,CbP*ݖ^]={NLLPD}tnu l}~B_ -A"wB }j@@@_\C_V:ЗE?/9Ęo( ӭdwXtwQnc8%VnO.'q\N*rY@ЖqAQI~K0yJ"9h1A_)9З=З%9OZC>A7d}/[nǎqH,TR4֦BWTeL7 &wҧwͷ{}iXKKKs }Y@_c}K" YẺnGD_ l=~xpWqˤS*U=7}ݝ)}/lVL(=@<0eGPCȭ[LЗvJaa///mϵRLЗ^_ѡC#oҫW߲qc^Hˋ_Rj-=4t + %/} k.fPNQ^Y;߉̇XuЗJ } } }i{З)C}RllK1nݶm $)˙&EnZdaոrҠ/`9/!OṅXuЗJ } } }i{/חqhN֖^fuнAI[c/HbL_$)'MMMib`R!rҠ/`H)} `QTЗvJaa///m5%_/Br xygxx#Kf31Ƹ[4& T_╸l5kVP } b`NgUX%_^rKW;%Ұ0ЗЗЗx!D})q# tйK0!q#y$%L%\J=zcyS`Qc} } }\/z\^@_بL37uDЗ z@tv0==X[Nwv&~h&QЗvJaa///m5%T$Fmv/TJl۶]>}z8GĈ $xV/A`(Sfrr2<NɁ4, %%J՗2˖-ۻw>}jgE-գ^}X?L^ mBBhn$+tGVe90t!`O )ynjRVn)+.FϹl\t]gʲ̝&raY.ܯƜ!r#綬E_1|^LzVp%A_F>MS5mO)/Nkڲ~Z-}  ɳoރ;vhע]K9܋җl:K !VbR.kr~aY _\/.g/[LU_J})PSBLzi;UG:/Yk[yVs[tGRV\ ) R <a! HO)U}YXYnBɴ(5}v%E!t_R~ڡ/eYb҂'B3*#V=ޡtפr-4!ċz:ΈM]Qq/ח&/ه*/IK.~æM-|!4LW.]tKF@M)}З_З='n_?148I_:11{}uc=7˿y0*PZn"K+I;KW_*r b8mֆ3b8Eҗk9CC9]t- 9ΜghGD_B,icӗQ<r&/g*@d} ՃS/C˹2Ml>>0ؓ]'nsWeYṌF_ڎZ:5^ҞaeEuCfaC!vQ&מ/`07oj eX}ͦ'&'&r}i/@Ml&җtrzaC*VkB^We$})էRu鳑:=bc膡CCzE7ltrւ\^ z4矚TYC_B`@=)}:J_N?²ù\}l{&sKQ}i!iZ?s˜ϣ/eV-}Kma1[ok qV4rه뫱/u.0]Ǒk GR XΘaKh0P':mJҗBӗїM/Kltil1 }Q_zt,ǾԄO2Kbr붭\xQVkzznrТjKc5hTθ ?1V>Kh0P?qJ媾G_ѧR/4Z_6p=W&%"})ٳ۱s4P_V3:6R<jw"`$`@]i)}З{ї%e%ї#m:}Yr2,3} ] 2FקܿmժG/c} L3m4җg}vfCCV</e%2,f.gn&ML_Ra IDAT&rɉ:Mq:@Lh0^c*o&٬oDm/KF@N}Z%b{ڷHQ/HKWїBZ?U^uWJ3bXWZ9^( زuГO>Vjn+5z21 èQj64SXQH`0Yطo,7oD/ғbu9!BV Kkޭ -6>/5o76wk M:(iQ*{ϔ#,̳7pL=0/m`:^їkkMkQ:egT_R϶S- B Nӡ/+[Mӗ;p/`@sRSt\ECe< B>?u#nnRbb2&ʓ8&Q b_Bk&>wq?wUK / ??2?偁ǰ<>>( ? ַ׏o:t_zNJ2`}Zlw.9$[o;pg\.qㆮ\K^.Z &4;9xPi;λ?ΚD_&O<77wrWW׻nj?ΑSO=Y+٬u>*O^W|;y9}{sl7w}ݻw|疖[neϞ=~Ѷm۾KOu]I?})o/%*}k2^ ՗%J9ρ9/#^*˸WB%Z &19xP;ﺻ } O|رc?g?׽uO5 ꫯVw $v5^{mOgppp``CEu\ԩS+{kvbb"J_ꫯ~G}ӟC=w믿 zЗuԗЮMq}k)}}}v̚gzzR_E֗<]!,ї4?=x𐫔kSرcs}E A3`K?yR_i8w8s5]vX?OC˷n7S]Uzϝ;w˿K:v$No׿z9s[?q|S׼?fXiƕW^i_矷|yk&*k֯_9??y睶0_}&qM98:QG?ɥ^X399ع}/.["DJL"oQS7nT-e- TcN=Il A)} ;ja0ї?/y E:'?}Uɥ=? Y:8 _o}[}w|gc{/פ}Y7}I69aV$UA_jӦSSS^wwwٳyLk1Q|˚- 2%k l"p׸#={v:ܹc׮qJ;cNݠ%S`@8OC!ޢøHͣ/l޼Yd4: ;ΡFe'?^//iZ /%ۂĞ=_U!' } Z)}<`|-: DR/:Ϗ~ӧOs=9|Ue]/})ϻn/k.rNngЗ/^RFї[%`@?+瞋LH_z?8Fїvؿۿկ޿ 7ɍee |D_Fҗ4e\Q6@_͛"'RqUut՗zkNQ1 }cB뮻}Ry=؇?}C֪_ЗKKǶxd .:y:)Є\Ke&j5)yd8})A_֙wNRW{"oGI0o]]]{a?/˜Ӫ_ЗKKǶx۴ %pY/їmIR|C:`{.;R?җڵr}S122r|֭lrro]?W})Z%j2^~a55@ak@sqj LS6T0Bg*ƒG;wƈ}/C' MD;`/Ozs5?s=PӮj]|w>,'i^㎻G>{l.۴iӫ_w]z׻ A̗?ey5Bї h.iJ-+wi.Z)^n.uluvPX,VsJ?KC7.՟u]7#uMJY]cm}i3[B[jZSN#З &4)XR K%kt%.[|3L5 Dt}{(@_FӗR,/cm*k)#QKA:ЌTD}/BK%eqqS&&#{_ڪwkVڧڠ/kl)}*nx}.K%&4)^~ǝwUN `/M_@Y-oWWկz{{*җK_j ЗjeUe_8~cKMZ_RJiD/g˅Q.(MӴe]s}@1B2 \Ei겔. #}>Yefdǎ;vطo;BĶꀉӋzeزeWg600@u*r*oc]"}i/kgִP9IDžhrGM k),ʢ*4s(m?aLVF} ؗ-Rb05]ӥ_PEOK|ľ"%#`@Sm掚ODqrx ;;&W1: O})\z_ve ޭZ](Y ] O0e`2BMHMhIPe D Y;N_}vy]wuwwS,o5@_ZG vN3 A@_v6LhUwD_:/d֗<\V^Ç>}Z .=yϵ^N)%H_.f}iF_QEA_,u՗ujV;N&sm/ї &^60ї.,W2Y}Y~a ^m/增/%V/m/CXru/] !\(/QTb_J̉Rcˁz6e-Z &]}&J;2}40=r$ 2- W/KMJE%@͛ ))׻i*%Y[WDA_I(jn5㏦iKiM4ˆ fH'h}=ϿRl$msS.n@V\Z]R:IZk8*K:WIJ}cz3%uRFR]0j.WTE)^u J !V Pr.42&8w]dmT _= gtJ"uZ2@Ρׯ )Jze_qRW:Y~HZqB\/m҅K%j I2RI"e }0jznxK>۬,O.9}<W\KђR'A_&/yn{9}?@_B`$znx&7ve *T}@\])4hU}ix+SD_/!A_ӗ.2dnїPk0x8}YJ_}㕥♓K}V՗e+З/sGїE/EsKx,#)S"njW Z &@rHBtR{7^Y*xtq9_tK;їGCa"} RZK@ᆲrD_/!L_=o}YZʙn=1}qel}DrЗZ@_.hE=]"rKR ! ˢ,^.TR_EqY]}їa՘TЗ]f !.EQ_BSFOD_BVE/_˘$[e  G']9sg T*JSO #%G$]GKʼ^by(*Btl#+ϝ-%M#K)4xPHz\jRiE^LUǕB\WڎugEF[Wz\]ҧlK 8RBEa.j)SzW)\Ս5GKٔD$hSHD_B|郾})ЗPK0>:/C*= +^nK wtQit&JDnL={8Yťk"ZAEXBbԼSSN >2F"AS-jE > wj\z)Зuԗ =} R/ `.rЗjg~9CϠa=eZ}zҗQ:WF</!#4tњKi9R/V@_B&@QDar}r珢/}חýK#Knۣ/їI/їO+R/.`0>D/+rdΜ_X[Tc/DЗWugKK%TDЗ})ЗP/0З]Ycr} }rЗuӗ})ЗO+R/`0zB_FӗL9C1;_\)wD_z!7 eL}9ܻԕ^}}Y/})y+n7}/  G*eL}B児sgP%2yi1}K2ŮL+]1%"ݿї})ЗPw0?R/З*#i!@Z1;/ma_&З/e|rחý˥T~}iF_B"o%2iE_ %4 &@T˄].:fHGLUȸd1J:qJ9zDrd}x5ϋ'DqEk\.F, y%-}F_/V@_B)dЗ5ЗqK/k/6q/e`/їm}Zї}+H }D_/5} m/їO+R/`0~B_/їsD_BcoKeӊKh4L*ЗK%2\/J_J)yn{5R/V@_B1 }D_/5%wvї}y& 1 }D_/5} C/ї5- c}D_k%xG_/k[e  &@m^ЗKh%2Hїͯ/%};IYnїI]Kh9LiR/%zD:_KkC_ wsnFZ&erLRG_B+BL_ЗKO|/ї}IKHA_,ˤ.u%(LDЗKO|/ї}IerLRG_BH }}ZЗM/yCnv}Yܢ/ї /%zD:_KkC_$leܷ4 ˚}ԥ [AY&@_KtЗKz%$|NΠ/k[eR:2l& P}i="/%ҵޡ/ї};I;YnїI]˰ &@=}|/ї}ٜR:nR/c@nї -`xr^_:|}|/ї}٤mxMAȘ͠1/E_68oI0ff60sCKeD_;e˧vslЗ}Yܢ/7z_L8uExĆD_/ })}dK fۀZ}༡/ &[npt K/ї! })}q}6nR/k[el;0~l~GЗ*}ZЗMR`j3&kp4C)Z}༡/ &۶nju2}AJE(n }^o̭7[@_2 }٦`0ض` ЗK^ /їEZ?})Qn- R/k[el_0ly׽/=> %28Ke`U_ !)lMA_ e-slpЗm  nwh#@_/{Q/3D_iO>!4M!j/E_68o!6{1p-F/=L M.{Ϸ~}AJE(n }^ZK!Ľ {#}qA_,˸rB@_ !ܜWcDiv0 $1%2B_/]-']6-Kenї ; &@0>0>6})܋}%2H/(t^?~zռg:X/cWh 0~Bl߶uw7̛rw8ĆD_/ })}Y[}ycc>77{ wT>@ 6lĉU꘨E/=A_}("j@ 00愌3M郾D_ "i }t/^em_{|!wm̆ I}Yܢ/&їa+/rjBImLP]]K%ҵޡ/[O_~>"`6?O/їїŅAŋ|hc=^&&aD:&yKtЗ\˰"#GH4L\zm޼CɃa/k[eLЗ+[>A0{zJ}0#i &wKM|xCO ĆD_/ })}Ys})qfohpPo~t]S??<8Y+˚}5 [A<=u`Tv4EϷ1Llٲ٧᣾OlK%2З"xїЗ_{q!y36]J`/k[eLЗ+G>C *`0\_GKe]D_/[O_nڸ{b~gYwYnї1A_Ʈ ɆB 2.D_/C%}zorY~WǏDЗs 2vMgXJAPC~ؿ)`0J)etÎia}ۖ- 'G_/їŅAŋl })x癦a֝4 4TJbQj-<"N8a˚}e l!q0H"4m6鱉-+L8\{7W9t@_NPx8esD_/+//8of&@rer 2v J`2u`W/p/}UzRC_seF_/m?}]/v& $_톉Ynї1A_Ʈ AtCȡ9`q6wW7_!}}Z lz{59>Ʌ̋@_z2v JvnnΧ溑 K 6u0J4Tjm\#/'6\/HPx8esD_/ğ>廷Ƽf9e*J{JSTFʴn+aNHkm~wuVY!f iЗr [AB$sXn> %&&ԝ &@Ul+yA6M32hoBb6vр:%2ҹC_/;U_بFꖾe||||l /xm\qIؙ8b(FB}%/BylE0 }{{;+y9~o;C>E\m&jQ 13qGۈ9)HExqT$r r"%L2DQȋV eGʺܖe# >J ]V;6=^3Т\x0x7;-;2L})З+Hd_vuFG L01oʡ= ~_{mp H} e .@})X7> B*xuz`R#Q4cMBl<9yȑtE  [Jm˲-2e4]wbYk^,oT]c/p1$&#e,З+Hd˭'u ,maͻaiڞ{^ 8m{U>3@s+*euVB`䳲b}\l~ek_BKi뛜840hs ly\WJkBSGB*N5i3wmu&&[֭]^(>@_ /n,ԤOUv{Rm&:҆hڞwB,v9~E;o=o}[78qă>xqL} e .G}4օQu;~߾@˦ oO= 2v*K _AB$;77wȑSwG_""(ru3(rk3uʊm|yyG?c-ԛMW߈1}Ǻ+12BQf*"OS\y:\Q"(bh{B,--Y BǏIF]疥0ulAi/k[e󖐾O>1їSA_ e l$w)&k0MĮ;;wl}T/CdEJi=_t2hc:"cݺu!%P<殩Mї1A_</Kl;3ЙERG B"_X&֢nXTma4 u=#"g9S=D;>OЪ2.4z]W>Q2܊iaF甠iqm"1+n`zQqx0R~F֭+Ȍy;E_L%ܢ/*ex]][ln^a=50=WFm:\MTE2N$Ƭ>/ IDATj3y`*˪\YYYYQhF4WxyV} ~ Lձ\M󰖆+kY{ŢT 2A5VLǏ?O,Π/c@nї [9rXJU\ڶuvÈf0X>W,㔇i10:7iɓv=qb k5IJ"4d$pDl5]Y#;2dU-{9ަ`0syb0;{bv% @nެv6 -:^z #`,ʱ>~'@˱m.5憮nX(rCF 1MeY $bn-Cj2w\=`@Px=n{GZ[C/?žt\V`.s ^q}Qt Vt{L5xLfl뺚Tq!VZ2nvqcM4=Bj*˴絀>l߾MiF!! ̳ @Rtwt !zzzz{z)`Dc˖BMJk5B}Fv&et"(?l0g4/oj4LW_ uw5hNQwY(l8`2\^q0(j/GD:G\h^11h+kjSLϔ)wM|l7t5Ǿ63ؗa E:Q*qLЋbzݩ C'pšhՖSm=7^q0 Xף6ڴ籡&4/Lh^0м y8^1(?nҕea8ŖJf;m=q0rJ] ^ii*/xj겦pɪ!3ZJ=;^ۨq0uL+ S \RTb_J\5-&׼|h*܀ t5jոjTc !Ri%q}Yuפ4mq`0#I)L㕬ie-NHJ/m|DymmmSY3?kGu*uY .)e=a94/Lh^EP->11FIecТWR^ȝ0=~~zu9ow#ġ>CWRjۨ'*f6Mڥn7 뺵Xaz2v8jtQ`0}^ ^i-+r0mmQ;: fe{˜MފBLגw\EZŢLe:> =Rp׆"L3|eoQ- ef-/R'l=,"Qм`0ya9@„x.<`J{ K0E4?Q^㎽4Y ^+AmT5_6rhE^~tNmDvĻ\7zԆWN'B2Qp;h "+tWH.^}R)bECR(0-Nja@ - y`@(rj j!lтT|Sr1B0 &4/h0fk$^U`94/Lh^0мn +!  `@BLh^0м`0y`@u~2IENDB`python-oracledb-1.2.1/samples/tutorial/rowfactory.py000066400000000000000000000032651434177474600227140ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # rowfactory.py (Section 8.1) # ------------------------------------------------------------------------------ import collections import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() cur.execute("select deptno, dname from dept") res = cur.fetchall() print('Array indexes:') for row in res: print(row[0], "->", row[1]) print('Loop target variables:') for c1, c2 in res: print(c1, "->", c2) python-oracledb-1.2.1/samples/tutorial/run_sql_script.py000066400000000000000000000060421434177474600235600ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # run_sql_script.py # ------------------------------------------------------------------------------ import os import sys import getpass # default values DEFAULT_MAIN_USER = "pythondemo" DEFAULT_EDITION_USER = "pythoneditions" DEFAULT_EDITION_NAME = "python_e1" DEFAULT_CONNECT_STRING = "localhost/orclpdb" DEFAULT_DRCP_CONNECT_STRING = "localhost/orclpdb:pooled" def run_sql_script(conn, script_name, **kwargs): statement_parts = [] cursor = conn.cursor() replace_values = [("&" + k + ".", v) for k, v in kwargs.items()] + \ [("&" + k, v) for k, v in kwargs.items()] script_dir = os.path.dirname(os.path.abspath(sys.argv[0])) file_name = os.path.join(script_dir, "sql", script_name + ".sql") print("SQL File Name: ", file_name) for line in open(file_name): if line.strip() == "/": statement = "".join(statement_parts).strip() if statement: for search_value, replace_value in replace_values: statement = statement.replace(search_value, replace_value) try: cursor.execute(statement) except: print("Failed to execute SQL:", statement) raise statement_parts = [] else: statement_parts.append(line) cursor.execute(""" select name, type, line, position, text from dba_errors where owner = upper(:owner) order by name, type, line, position""", owner=kwargs['user']) prev_name = prev_obj_type = None for name, obj_type, line_num, position, text in cursor: if name != prev_name or obj_type != prev_obj_type: print("%s (%s)" % (name, obj_type)) prev_name = name prev_obj_type = obj_type print(" %s/%s %s" % (line_num, position, text)) python-oracledb-1.2.1/samples/tutorial/setup_tutorial.py000066400000000000000000000033631434177474600235770ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # setup_tutorial.py (Setup Section) # ------------------------------------------------------------------------------ import oracledb import db_config import run_sql_script # Connect using the System User ID and password con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) # create sample schemas and defintions for the tutorial print("Setting up the sample tables and other DB objects for the tutorial...") run_sql_script.run_sql_script( con, "setup_tutorial", user=db_config.user, pw=db_config.pw) print("Done.") python-oracledb-1.2.1/samples/tutorial/soda.py000066400000000000000000000043131434177474600214360ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2019, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # soda.py (Section 15.1) # ------------------------------------------------------------------------------ import oracledb import db_config_thick as db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) soda = con.getSodaDatabase() # Explicit metadata is used for maximum version portability metadata = { "keyColumn": { "name": "ID" }, "contentColumn": { "name": "JSON_DOCUMENT", "sqlType": "BLOB" }, "versionColumn": { "name": "VERSION", "method": "UUID" }, "lastModifiedColumn": { "name": "LAST_MODIFIED" }, "creationTimeColumn": { "name": "CREATED_ON" } } collection = soda.createCollection("friends", metadata) content = {'name': 'Jared', 'age': 35, 'address': {'city': 'Melbourne'}} doc = collection.insertOneAndGet(content) key = doc.key doc = collection.find().key(key).getOne() content = doc.getContent() print('Retrieved SODA document dictionary is:') print(content) python-oracledb-1.2.1/samples/tutorial/solutions/000077500000000000000000000000001434177474600221745ustar00rootroot00000000000000python-oracledb-1.2.1/samples/tutorial/solutions/aq-dequeue.py000066400000000000000000000036531434177474600246110ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # aq-dequeue.py (Section 14.1) # ------------------------------------------------------------------------------ import oracledb import decimal import db_config_thick as db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() BOOK_TYPE_NAME = "UDT_BOOK" QUEUE_NAME = "BOOKS" QUEUE_TABLE_NAME = "BOOK_QUEUE_TABLE" # Dequeue the messages books_type = con.gettype(BOOK_TYPE_NAME) queue = con.queue(QUEUE_NAME, books_type) queue.deqoptions.wait = oracledb.DEQ_NO_WAIT queue.deqoptions.visibility = oracledb.DEQ_IMMEDIATE print("\nDequeuing messages...") while True: props = queue.deqone() if not props: break print(props.payload.TITLE) print("\nDone.") python-oracledb-1.2.1/samples/tutorial/solutions/aq-enqueue.py000066400000000000000000000042111434177474600246120ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # aq-enqueue.py (Section 14.1) # ------------------------------------------------------------------------------ import oracledb import decimal import db_config_thick as db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() BOOK_TYPE_NAME = "UDT_BOOK" QUEUE_NAME = "BOOKS" QUEUE_TABLE_NAME = "BOOK_QUEUE_TABLE" # Enqueue a few messages print("Enqueuing messages...") BOOK_DATA = [ ("The Fellowship of the Ring", "Tolkien, J.R.R.", decimal.Decimal("10.99")), ("Harry Potter and the Philosopher's Stone", "Rowling, J.K.", decimal.Decimal("7.99")) ] books_type = con.gettype(BOOK_TYPE_NAME) queue = con.queue(QUEUE_NAME, books_type) for title, authors, price in BOOK_DATA: book = books_type.newobject() book.TITLE = title book.AUTHORS = authors book.PRICE = price print(title) queue.enqone(con.msgproperties(payload=book, expiration=4)) con.commit() python-oracledb-1.2.1/samples/tutorial/solutions/aq-queuestart.py000066400000000000000000000050071434177474600253510ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # aq-queuestart.py (Section 14.1) # ------------------------------------------------------------------------------ import oracledb import decimal import db_config_thick as db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() BOOK_TYPE_NAME = "UDT_BOOK" QUEUE_NAME = "BOOKS" QUEUE_TABLE_NAME = "BOOK_QUEUE_TABLE" # Cleanup cur.execute(f""" begin dbms_aqadm.stop_queue('{QUEUE_NAME}'); dbms_aqadm.drop_queue('{QUEUE_NAME}'); dbms_aqadm.drop_queue_table('{QUEUE_TABLE_NAME}'); execute immediate 'drop type {BOOK_TYPE_NAME}'; exception when others then if sqlcode <> -24010 then raise; end if; end;""") # Create a type print("Creating books type UDT_BOOK...") cur.execute(f""" create type {BOOK_TYPE_NAME} as object ( title varchar2(100), authors varchar2(100), price number(5,2) );""") # Create queue table and queue and start the queue print("Creating queue table...") cur.callproc("dbms_aqadm.create_queue_table", (QUEUE_TABLE_NAME, BOOK_TYPE_NAME)) cur.callproc("dbms_aqadm.create_queue", (QUEUE_NAME, QUEUE_TABLE_NAME)) cur.callproc("dbms_aqadm.start_queue", (QUEUE_NAME,)) python-oracledb-1.2.1/samples/tutorial/solutions/bind_insert.py000066400000000000000000000036711434177474600250550ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # bind_insert.py (Section 4.3) # ------------------------------------------------------------------------------ import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() rows = [(1, "First"), (2, "Second"), (3, "Third"), (4, "Fourth"), (5, "Fifth"), (6, "Sixth"), (6, "Duplicate"), (7, "Seventh")] cur.executemany("insert into mytab(id, data) values (:1, :2)", rows, batcherrors=True) for error in cur.getbatcherrors(): print("Error", error.message.rstrip(), "at row offset", error.offset) # Now query the results back cur2 = con.cursor() cur2.execute('select * from mytab') res = cur2.fetchall() print(res) python-oracledb-1.2.1/samples/tutorial/solutions/bind_sdo.py000066400000000000000000000070721434177474600243350ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # bind_sdo.py (Section 12.1) # ------------------------------------------------------------------------------ import oracledb import db_config_thick as db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() # Create table cur.execute("""begin execute immediate 'drop table testgeometry'; exception when others then if sqlcode <> -942 then raise; end if; end;""") cur.execute("""create table testgeometry ( id number(9) not null, geometry MDSYS.SDO_GEOMETRY not null)""") # Create and populate Oracle objects type_obj = con.gettype("MDSYS.SDO_GEOMETRY") element_info_type_obj = con.gettype("MDSYS.SDO_ELEM_INFO_ARRAY") ordinate_type_obj = con.gettype("MDSYS.SDO_ORDINATE_ARRAY") obj = type_obj.newobject() obj.SDO_GTYPE = 2003 obj.SDO_ELEM_INFO = element_info_type_obj.newobject() obj.SDO_ELEM_INFO.extend([1, 1003, 3]) obj.SDO_ORDINATES = ordinate_type_obj.newobject() obj.SDO_ORDINATES.extend([1, 1, 5, 7]) point_type_obj = con.gettype("MDSYS.SDO_POINT_TYPE") obj.SDO_POINT = point_type_obj.newobject() obj.SDO_POINT.X = 1 obj.SDO_POINT.Y = 2 obj.SDO_POINT.Z = 3 print("Created object", obj) # Add a new row print("Adding row to table...") cur.execute("insert into testgeometry values (1, :objbv)", objbv=obj) print("Row added!") # (Change below here) # Define a function to dump the contents of an Oracle object def dumpobject(obj, prefix=" "): if obj.type.iscollection: print(prefix, "[") for value in obj.aslist(): if isinstance(value, oracledb.Object): dumpobject(value, prefix + " ") else: print(prefix + " ", repr(value)) print(prefix, "]") else: print(prefix, "{") for attr in obj.type.attributes: value = getattr(obj, attr.name) if isinstance(value, oracledb.Object): print(prefix + " " + attr.name + " :") dumpobject(value, prefix + " ") else: print(prefix + " " + attr.name + " :", repr(value)) print(prefix, "}") # Query the row print("Querying row just inserted...") cur.execute("select id, geometry from testgeometry") for (id, obj) in cur: print("Id: ", id) dumpobject(obj) python-oracledb-1.2.1/samples/tutorial/solutions/connect_pool2.py000066400000000000000000000042701434177474600253150ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # connect_pool2.py (Section 2.5) # ------------------------------------------------------------------------------ import oracledb import threading import time import db_config pool = oracledb.create_pool(user=db_config.user, password=db_config.pw, dsn=db_config.dsn + ":pooled", min=2, max=5, increment=1, getmode=oracledb.POOL_GETMODE_WAIT, cclass="PYTHONDEMO", purity=oracledb.PURITY_SELF) # try PURITY_NEW def Query(): con = pool.acquire() cur = con.cursor() for i in range(4): cur.execute("select myseq.nextval from dual") seqval, = cur.fetchone() print("Thread", threading.current_thread().name, "fetched sequence =", seqval) # time.sleep(1) numberOfThreads = 5 threadArray = [] for i in range(numberOfThreads): thread = threading.Thread(name='#'+str(i), target=Query) threadArray.append(thread) # time.sleep(4) thread.start() for t in threadArray: t.join() print("All done!") python-oracledb-1.2.1/samples/tutorial/solutions/db_config.py000066400000000000000000000023361434177474600244640ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ import os dirName = os.path.dirname(os.path.dirname(__file__)) exec(open(os.path.join(dirName, "db_config.py"), "r").read()) python-oracledb-1.2.1/samples/tutorial/solutions/db_config_thick.py000066400000000000000000000023441434177474600256450ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ import os dirName = os.path.dirname(os.path.dirname(__file__)) exec(open(os.path.join(dirName, "db_config_thick.py"), "r").read()) python-oracledb-1.2.1/samples/tutorial/solutions/query-2.py000066400000000000000000000031041434177474600240500ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # query.py (Section 1.4) # ------------------------------------------------------------------------------ import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() cur.execute("select * from dept order by deptno") res = cur.fetchall() for row in res: print(row) cur.close() con.close() python-oracledb-1.2.1/samples/tutorial/solutions/query.py000066400000000000000000000030631434177474600237150ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # query.py (Section 1.3 and 1.4) # ------------------------------------------------------------------------------ import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() cur.execute("select * from dept order by deptno") res = cur.fetchall() for row in res: print(row) python-oracledb-1.2.1/samples/tutorial/solutions/query_many.py000066400000000000000000000032001434177474600247320ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # query_many.py (Section 3.3) # ------------------------------------------------------------------------------ import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() cur.execute("select * from dept order by deptno") num_rows = 3 res = cur.fetchmany(num_rows) print(res) print(res[0]) # first row print(res[0][1]) # second element of first row python-oracledb-1.2.1/samples/tutorial/solutions/query_scroll.py000066400000000000000000000034521434177474600252750ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # query_scroll.py (Section 11.1) # ------------------------------------------------------------------------------ import oracledb import db_config_thick as db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor(scrollable=True) cur.execute("select * from dept order by deptno") cur.scroll(2, mode="absolute") # go to second row print(cur.fetchone()) cur.scroll(-1) # go back one row print(cur.fetchone()) cur.scroll(1) # go to next row print(cur.fetchone()) cur.scroll(mode="first") # go to first row print(cur.fetchone()) python-oracledb-1.2.1/samples/tutorial/solutions/rowfactory.py000066400000000000000000000036351434177474600247540ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # rowfactory.py (Section 8.1) # ------------------------------------------------------------------------------ import collections import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() cur.execute("select deptno, dname from dept") res = cur.fetchall() print('Array indexes:') for row in res: print(row[0], "->", row[1]) print('Loop target variables:') for c1, c2 in res: print(c1, "->", c2) print('Rowfactory:') cur.execute("select deptno, dname from dept") cur.rowfactory = collections.namedtuple("MyClass", ["DeptNumber", "DeptName"]) res = cur.fetchall() for row in res: print(row.DeptNumber, "->", row.DeptName) python-oracledb-1.2.1/samples/tutorial/solutions/soda.py000066400000000000000000000055151434177474600235020ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2019, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # soda.py (Section 15.2) # ------------------------------------------------------------------------------ import oracledb import db_config_thick as db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) soda = con.getSodaDatabase() # Explicit metadata is used for maximum version portability metadata = { "keyColumn": { "name": "ID" }, "contentColumn": { "name": "JSON_DOCUMENT", "sqlType": "BLOB" }, "versionColumn": { "name": "VERSION", "method": "UUID" }, "lastModifiedColumn": { "name": "LAST_MODIFIED" }, "creationTimeColumn": { "name": "CREATED_ON" } } collection = soda.createCollection("friends", metadata) content = {'name': 'Jared', 'age': 35, 'address': {'city': 'Melbourne'}} doc = collection.insertOneAndGet(content) key = doc.key doc = collection.find().key(key).getOne() content = doc.getContent() print('Retrieved SODA document dictionary is:') print(content) my_docs = [ {'name': 'Gerald', 'age': 21, 'address': {'city': 'London'}}, {'name': 'David', 'age': 28, 'address': {'city': 'Melbourne'}}, {'name': 'Shawn', 'age': 20, 'address': {'city': 'San Francisco'}} ] collection.insertMany(my_docs) filter_spec = {"address.city": "Melbourne"} my_documents = collection.find().filter(filter_spec).getDocuments() print('Melbourne people:') for doc in my_documents: print(doc.getContent()["name"]) filter_spec = {'age': {'$lt': 25}} my_documents = collection.find().filter(filter_spec).getDocuments() print('Young people:') for doc in my_documents: print(doc.getContent()["name"]) python-oracledb-1.2.1/samples/tutorial/solutions/subclass.py000066400000000000000000000042431434177474600243700ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # subclass.py (Section 9.2) # ------------------------------------------------------------------------------ import oracledb import db_config class MyConnection(oracledb.Connection): def __init__(self): print("Connecting to database") return super(MyConnection, self).__init__(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) def cursor(self): return MyCursor(self) class MyCursor(oracledb.Cursor): def execute(self, statement, args): print("Executing:", statement) print("Arguments:") for argIndex, arg in enumerate(args): print(" Bind", argIndex + 1, "has value", repr(arg)) return super(MyCursor, self).execute(statement, args) def fetchone(self): print("Fetchone()") return super(MyCursor, self).fetchone() con = MyConnection() cur = con.cursor() cur.execute("select count(*) from emp where deptno = :bv", (10,)) count, = cur.fetchone() print("Number of rows:", count) python-oracledb-1.2.1/samples/tutorial/solutions/type_converter.py000066400000000000000000000034721434177474600256240ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # type_converter.py (Section 6.2) # ------------------------------------------------------------------------------ import oracledb import decimal import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() def ReturnNumbersAsDecimal(cursor, name, defaultType, size, precision, scale): if defaultType == oracledb.NUMBER: return cursor.var(str, 9, cursor.arraysize, outconverter=decimal.Decimal) cur.outputtypehandler = ReturnNumbersAsDecimal for value, in cur.execute("select 0.1 from dual"): print("Value:", value, "* 3 =", value * 3) python-oracledb-1.2.1/samples/tutorial/solutions/type_output.py000066400000000000000000000035711434177474600251550ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # type_output.py (Section 6.1) # ------------------------------------------------------------------------------ import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() print("Standard output...") for row in cur.execute("select * from dept"): print(row) def ReturnNumbersAsStrings(cursor, name, defaultType, size, precision, scale): if defaultType == oracledb.NUMBER: return cursor.var(str, 9, cursor.arraysize) print("Output type handler output...") cur = con.cursor() cur.outputtypehandler = ReturnNumbersAsStrings for row in cur.execute("select * from dept"): print(row) python-oracledb-1.2.1/samples/tutorial/solutions/versions.py000066400000000000000000000031311434177474600244140ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # versions.py (Section 1.5) # ------------------------------------------------------------------------------ import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) print(oracledb.__version__) # two underscores before and after version print("Database version:", con.version) #print("Client version:", oracledb.clientversion()) python-oracledb-1.2.1/samples/tutorial/sql/000077500000000000000000000000001434177474600207345ustar00rootroot00000000000000python-oracledb-1.2.1/samples/tutorial/sql/create_user.sql000066400000000000000000000040571434177474600237640ustar00rootroot00000000000000/*----------------------------------------------------------------------------- * Copyright (c) 2017, 2022, Oracle and/or its affiliates. * * This software is dual-licensed to you under the Universal Permissive License * (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License * 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose * either license.* * * If you elect to accept the software under the Apache License, Version 2.0, * the following applies: * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *---------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------- * create_user.sql (Setup Section) * Creates a database user for the python-oracledb tutorial * For Oracle Autonomous Database, use 'admin' instead of system. * You will be prompted for the new username and the new password to use. * * When you no longer need this user, run drop_user.sql to drop the user * *---------------------------------------------------------------------------*/ create user &user / grant create session, create table, create procedure, create type, create sequence, select any dictionary, unlimited tablespace to &user / begin for r in ( select role from dba_roles where role in ('SODA_APP', 'AQ_ADMINISTRATOR_ROLE') ) loop execute immediate 'grant ' || r.role || ' to &user'; end loop; end; / alter user &user identified by "&pw" / python-oracledb-1.2.1/samples/tutorial/sql/db_config.sql000066400000000000000000000025341434177474600233730ustar00rootroot00000000000000/*----------------------------------------------------------------------------- * Copyright (c) 2017, 2022, Oracle and/or its affiliates. * * This software is dual-licensed to you under the Universal Permissive License * (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License * 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose * either license.* * * If you elect to accept the software under the Apache License, Version 2.0, * the following applies: * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *---------------------------------------------------------------------------*/ -- Default database username def user = "pythondemo" -- Default database connection string def connect_string = "localhost/orclpdb" -- Prompt for the password accept pw char prompt 'Enter database password for &user: ' hide python-oracledb-1.2.1/samples/tutorial/sql/drcp_query.sql000066400000000000000000000033531434177474600236360ustar00rootroot00000000000000/*----------------------------------------------------------------------------- * Copyright (c) 2017, 2022, Oracle and/or its affiliates. * * This software is dual-licensed to you under the Universal Permissive License * (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License * 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose * either license.* * * If you elect to accept the software under the Apache License, Version 2.0, * the following applies: * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *---------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------- * drcp_query.sql (Section 2.4 and 2.5) *---------------------------------------------------------------------------*/ set echo off verify off feedback off linesize 80 pagesize 1000 accept pw char prompt 'Enter database password for SYSTEM: ' hide accept connect_string char prompt 'Enter database connection string: ' -- Connect to the CDB to see pool statistics connect system/&pw@&connect_string col cclass_name format a40 -- Some DRCP pool statistics select cclass_name, num_requests, num_hits, num_misses from v$cpool_cc_stats; exit python-oracledb-1.2.1/samples/tutorial/sql/drop_user.sql000066400000000000000000000037641434177474600234710ustar00rootroot00000000000000/*----------------------------------------------------------------------------- * Copyright (c) 2017, 2022, Oracle and/or its affiliates. * * This software is dual-licensed to you under the Universal Permissive License * (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License * 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose * either license.* * * If you elect to accept the software under the Apache License, Version 2.0, * the following applies: * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *---------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------- * drop_user.sql (Setup Section) * Drops the database user used for the python-oracledb tutorial * * Substitute your actual password and connection string. * For Oracle Autonomous Database, use 'admin' instead of system. * You will be prompted for the user to drop. *---------------------------------------------------------------------------*/ begin dbms_aqadm.stop_queue('BOOKS'); dbms_aqadm.drop_queue('BOOKS'); dbms_aqadm.drop_queue_table('BOOK_QUEUE_TABLE'); exception when others then if sqlcode <> -24010 then raise; end if; end; / begin for r in ( select username from dba_users where username in (upper('&user')) ) loop execute immediate 'drop user ' || r.username || ' cascade'; end loop; end; / python-oracledb-1.2.1/samples/tutorial/sql/setup_tutorial.sql000066400000000000000000000130601434177474600245400ustar00rootroot00000000000000/*----------------------------------------------------------------------------- * Copyright (c) 2017, 2022, Oracle and/or its affiliates. * * This software is dual-licensed to you under the Universal Permissive License * (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License * 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose * either license.* * * If you elect to accept the software under the Apache License, Version 2.0, * the following applies: * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *---------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------- * setup_tutorial.sql (Setup Section) * Creates the tables, sequence etc. used by the python-oracledb tutorial. *---------------------------------------------------------------------------*/ begin execute immediate 'drop table emp'; exception when others then if sqlcode <> -942 then raise; end if; end; / create table emp (empno number(4) not null, ename varchar2(10), job varchar2(9), mgr number(4), hiredate date, sal number(7, 2), comm number(7, 2), deptno number(2)) / insert into emp values (7369, 'SMITH', 'CLERK', 7902, to_date('17-DEC-1980', 'DD-MON-YYYY'), 800, NULL, 20) / insert into emp values (7499, 'ALLEN', 'SALESMAN', 7698, to_date('20-FEB-1981', 'DD-MON-YYYY'), 1600, 300, 30) / insert into emp values (7521, 'WARD', 'SALESMAN', 7698, to_date('22-FEB-1981', 'DD-MON-YYYY'), 1250, 500, 30) / insert into emp values (7566, 'JONES', 'MANAGER', 7839, to_date('2-APR-1981', 'DD-MON-YYYY'), 2975, NULL, 20) / insert into emp values (7654, 'MARTIN', 'SALESMAN', 7698, to_date('28-SEP-1981', 'DD-MON-YYYY'), 1250, 1400, 30) / insert into emp values (7698, 'BLAKE', 'MANAGER', 7839, to_date('1-MAY-1981', 'DD-MON-YYYY'), 2850, NULL, 30) / insert into emp values (7782, 'CLARK', 'MANAGER', 7839, to_date('9-JUN-1981', 'DD-MON-YYYY'), 2450, NULL, 10) / insert into emp values (7788, 'SCOTT', 'ANALYST', 7566, to_date('09-DEC-1982', 'DD-MON-YYYY'), 3000, NULL, 20) / insert into emp values (7839, 'KING', 'PRESIDENT', NULL, to_date('17-NOV-1981', 'DD-MON-YYYY'), 5000, NULL, 10) / insert into emp values (7844, 'TURNER', 'SALESMAN', 7698, to_date('8-SEP-1981', 'DD-MON-YYYY'), 1500, 0, 30) / insert into emp values (7876, 'ADAMS', 'CLERK', 7788, to_date('12-JAN-1983', 'DD-MON-YYYY'), 1100, NULL, 20) / insert into emp values (7900, 'JAMES', 'CLERK', 7698, to_date('3-DEC-1981', 'DD-MON-YYYY'), 950, NULL, 30) / insert into emp values (7902, 'FORD', 'ANALYST', 7566, to_date('3-DEC-1981', 'DD-MON-YYYY'), 3000, NULL, 20) / insert into emp values (7934, 'MILLER', 'CLERK', 7782, to_date('23-JAN-1982', 'DD-MON-YYYY'), 1300, NULL, 10) / begin execute immediate 'drop table dept'; exception when others then if sqlcode <> -942 then raise; end if; end; / create table dept (deptno number(2), dname varchar2(14), loc varchar2(13) ) / insert into dept values (10, 'ACCOUNTING', 'NEW YORK') / insert into dept values (20, 'RESEARCH', 'DALLAS') / insert into dept values (30, 'SALES', 'CHICAGO') / insert into dept values (40, 'OPERATIONS', 'BOSTON') / commit / -- Table for clob.py and clob_string.py begin execute immediate 'drop table testclobs'; exception when others then if sqlcode <> -942 then raise; end if; end; / create table testclobs ( id number not null, myclob clob not null ) / -- Sequence for connect_pool.py begin execute immediate 'drop sequence myseq'; exception when others then if sqlcode <> -2289 then raise; end if; end; / create sequence myseq / -- Table for bind_insert.py begin execute immediate 'drop table mytab'; exception when others then if sqlcode not in (-00942) then raise; end if; end; / create table mytab (id number, data varchar2(20), constraint my_pk primary key (id)) / --Table for query_arraysize.py begin execute immediate 'drop table bigtab'; exception when others then if sqlcode not in (-00942) then raise; end if; end; / create table bigtab (mycol varchar2(20)) / begin for i in 1..20000 loop insert into bigtab (mycol) values (dbms_random.string('A',20)); end loop; end; / commit / -- Table for plsql_func.py begin execute immediate 'drop table ptab'; exception when others then if sqlcode not in (-00942) then raise; end if; end; / create table ptab (mydata varchar(20), myid number) / -- PL/SQL function for plsql_func.py create or replace function myfunc(d_p in varchar2, i_p in number) return number as begin insert into ptab (mydata, myid) values (d_p, i_p); return (i_p * 2); end; / --PL/SQL procedure for plsql_proc.py create or replace procedure myproc(v1_p in number, v2_p out number) as begin v2_p := v1_p * 2; end; / python-oracledb-1.2.1/samples/tutorial/subclass.py000066400000000000000000000033201434177474600223240ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # subclass.py (Section 9.1 and 9.2) # ------------------------------------------------------------------------------ import oracledb import db_config class MyConnection(oracledb.Connection): def __init__(self): print("Connecting to database") return super(MyConnection, self).__init__(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) con = MyConnection() cur = con.cursor() cur.execute("select count(*) from emp where deptno = :bv", (10,)) count, = cur.fetchone() print("Number of rows:", count) python-oracledb-1.2.1/samples/tutorial/type_converter.py000066400000000000000000000030601434177474600235560ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # type_converter.py (Section 6.2) # ------------------------------------------------------------------------------ import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() for value, in cur.execute("select 0.1 from dual"): print("Value:", value, "* 3 =", value * 3) python-oracledb-1.2.1/samples/tutorial/type_input.py000066400000000000000000000071031434177474600227100ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # type_input.py (Section 6.3) # ------------------------------------------------------------------------------ import oracledb import db_config import json con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() # Create table cur.execute("""begin execute immediate 'drop table BuildingTable'; exception when others then if sqlcode <> -942 then raise; end if; end;""") cur.execute("""create table BuildingTable ( ID number(9) not null, BuildingDetails varchar2(400), constraint TestTempTable_pk primary key (ID))""") # Create a Python class for a Building class Building(object): def __init__(self, building_id, description, num_floors): self.building_id = building_id self.description = description self.num_floors = num_floors def __repr__(self): return "" % (self.building_id, self.description) def __eq__(self, other): if isinstance(other, Building): return other.building_id == self.building_id \ and other.description == self.description \ and other.num_floors == self.num_floors return NotImplemented def to_json(self): return json.dumps(self.__dict__) @classmethod def from_json(cls, value): result = json.loads(value) return cls(**result) # Convert a Python building object to SQL JSON type that can be read as a string def building_in_converter(value): return value.to_json() def input_type_handler(cursor, value, num_elements): if isinstance(value, Building): return cursor.var(oracledb.STRING, arraysize=num_elements, inconverter=building_in_converter) building = Building(1, "The First Building", 5) # Python object cur.execute("truncate table BuildingTable") cur.inputtypehandler = input_type_handler cur.execute("insert into BuildingTable (ID, BuildingDetails) values (:1, :2)", (building.building_id, building)) con.commit() # Query the row print("Querying the row just inserted...") cur.execute("select ID, BuildingDetails from BuildingTable") for (int_col, string_col) in cur: print("Building ID:", int_col) print("Building Details in JSON format:", string_col) python-oracledb-1.2.1/samples/tutorial/type_input_named_obj.py000066400000000000000000000077111434177474600247130ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # type_input_named_obj.py (Section 13.1) # ------------------------------------------------------------------------------ import oracledb import db_config_thick as db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() # Create table cur.execute("""begin execute immediate 'drop table testgeometry'; exception when others then if sqlcode <> -942 then raise; end if; end;""") cur.execute("""create table testgeometry ( id number(9) not null, geometry MDSYS.SDO_GEOMETRY not null)""") # Create a Python class for an SDO class mySDO(object): def __init__(self, gtype, elemInfo, ordinates): self.gtype = gtype self.elemInfo = elemInfo self.ordinates = ordinates # Get Oracle type information obj_type = con.gettype("MDSYS.SDO_GEOMETRY") element_info_type_obj = con.gettype("MDSYS.SDO_ELEM_INFO_ARRAY") ordinate_type_obj = con.gettype("MDSYS.SDO_ORDINATE_ARRAY") # Convert a Python object to MDSYS.SDO_GEOMETRY def SDOInConverter(value): obj = obj_type.newobject() obj.SDO_GTYPE = value.gtype obj.SDO_ELEM_INFO = element_info_type_obj.newobject() obj.SDO_ELEM_INFO.extend(value.elemInfo) obj.SDO_ORDINATES = ordinate_type_obj.newobject() obj.SDO_ORDINATES.extend(value.ordinates) return obj def SDOInputTypeHandler(cursor, value, numElements): if isinstance(value, mySDO): return cursor.var(oracledb.OBJECT, arraysize=numElements, inconverter=SDOInConverter, typename=obj_type.name) sdo = mySDO(2003, [1, 1003, 3], [1, 1, 5, 7]) # Python object cur.inputtypehandler = SDOInputTypeHandler cur.execute("insert into testgeometry values (:1, :2)", (1, sdo)) # Define a function to dump the contents of an Oracle object def dumpobject(obj, prefix=" "): if obj.type.iscollection: print(prefix, "[") for value in obj.aslist(): if isinstance(value, oracledb.Object): dumpobject(value, prefix + " ") else: print(prefix + " ", repr(value)) print(prefix, "]") else: print(prefix, "{") for attr in obj.type.attributes: value = getattr(obj, attr.name) if isinstance(value, oracledb.Object): print(prefix + " " + attr.name + " :") dumpobject(value, prefix + " ") else: print(prefix + " " + attr.name + " :", repr(value)) print(prefix, "}") # Query the row print("Querying row just inserted...") cur.execute("select id, geometry from testgeometry") for (id, obj) in cur: print("Id: ", id) dumpobject(obj) python-oracledb-1.2.1/samples/tutorial/type_output.py000066400000000000000000000030441434177474600231110ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # type_output.py (Section 6.1) # ------------------------------------------------------------------------------ import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() print("Standard output...") for row in cur.execute("select * from dept"): print(row) python-oracledb-1.2.1/samples/tutorial/type_output_named_obj.py000066400000000000000000000076761434177474600251260ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # type_output_named_obj.py (Section 13.2) # ------------------------------------------------------------------------------ import oracledb import db_config_thick as db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) cur = con.cursor() # Create table cur.execute("""begin execute immediate 'drop table testgeometry'; exception when others then if sqlcode <> -942 then raise; end if; end;""") cur.execute("""create table testgeometry ( id number(9) not null, geometry MDSYS.SDO_GEOMETRY not null)""") # Create a Python class for an SDO class mySDO(object): def __init__(self, gtype, elemInfo, ordinates): self.gtype = gtype self.elemInfo = elemInfo self.ordinates = ordinates # Get Oracle type information obj_type = con.gettype("MDSYS.SDO_GEOMETRY") element_info_type_obj = con.gettype("MDSYS.SDO_ELEM_INFO_ARRAY") ordinate_type_obj = con.gettype("MDSYS.SDO_ORDINATE_ARRAY") # Convert a Python object to MDSYS.SDO_GEOMETRY def SDOInConverter(value): obj = obj_type.newobject() obj.SDO_GTYPE = value.gtype obj.SDO_ELEM_INFO = element_info_type_obj.newobject() obj.SDO_ELEM_INFO.extend(value.elemInfo) obj.SDO_ORDINATES = ordinate_type_obj.newobject() obj.SDO_ORDINATES.extend(value.ordinates) return obj def SDOInputTypeHandler(cursor, value, numElements): if isinstance(value, mySDO): return cursor.var(oracledb.OBJECT, arraysize=numElements, inconverter=SDOInConverter, typename=obj_type.name) # Convert a MDSYS.SDO_GEOMETRY DB Object to Python object def SDOOutConverter(DBobj): return mySDO(int(DBobj.SDO_GTYPE), DBobj.SDO_ELEM_INFO.aslist(), DBobj.SDO_ORDINATES.aslist()) def SDOOutputTypeHandler(cursor, name, default_type, size, precision, scale): if default_type == oracledb.DB_TYPE_OBJECT: return cursor.var(obj_type, arraysize=cursor.arraysize, outconverter=SDOOutConverter) sdo = mySDO(2003, [1, 1003, 3], [1, 1, 5, 7]) # Python object cur.inputtypehandler = SDOInputTypeHandler cur.execute("insert into testgeometry values (:1, :2)", (1, sdo)) cur.outputtypehandler = SDOOutputTypeHandler # Query the SDO Table row print("Querying the Spatial Data Object(SDO) Table using the Output Type Handler...") print("----------------------------------------------------------------------------") cur.execute("select id, geometry from testgeometry") for (id, obj) in cur: print("SDO ID:", id) print("SDO GYTPE:", obj.gtype) print("SDO ELEMINFO:", obj.elemInfo) print("SDO_ORDINATES:", obj.ordinates) python-oracledb-1.2.1/samples/tutorial/versions.py000066400000000000000000000027751434177474600223720ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # versions.py (Section 1.5) # ------------------------------------------------------------------------------ import oracledb import db_config con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn) print(oracledb.__version__) # two underscores before and after version python-oracledb-1.2.1/samples/type_handlers_json_strings.py000066400000000000000000000105021434177474600243050ustar00rootroot00000000000000# ----------------------------------------------------------------------------- # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # type_handlers_json_strings.py # # Demonstrates the use of input and output type handlers as well as variable # input and output converters. These methods can be used to extend # python-oracledb in many ways. # # This script differs from type_handlers_objects.py in that it shows the # binding and querying of JSON strings as Python objects for both # python-oracledb thin and thick mode. #------------------------------------------------------------------------------ import json import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) class Building: def __init__(self, building_id, description, num_floors): self.building_id = building_id self.description = description self.num_floors = num_floors def __repr__(self): return "" % (self.building_id, self.description) def __eq__(self, other): if isinstance(other, Building): return other.building_id == self.building_id \ and other.description == self.description \ and other.num_floors == self.num_floors return NotImplemented def to_json(self): return json.dumps(self.__dict__) @classmethod def from_json(cls, value): result = json.loads(value) return cls(**result) def building_in_converter(value): return value.to_json() def input_type_handler(cursor, value, num_elements): if isinstance(value, Building): return cursor.var(oracledb.STRING, arraysize=num_elements, inconverter=building_in_converter) def output_type_handler(cursor, name, default_type, size, precision, scale): if default_type == oracledb.STRING: return cursor.var(default_type, arraysize=cursor.arraysize, outconverter=Building.from_json) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) with connection.cursor() as cursor: buildings = [ Building(1, "The First Building", 5), Building(2, "The Second Building", 87), Building(3, "The Third Building", 12) ] # Insert building data (python object) as a JSON string cursor.inputtypehandler = input_type_handler for building in buildings: cursor.execute("insert into BuildingsAsJsonStrings values (:1, :2)", (building.building_id, building)) # fetch the building data as a JSON string print("NO OUTPUT TYPE HANDLER:") for row in cursor.execute(""" select * from BuildingsAsJsonStrings order by BuildingId"""): print(row) print() with connection.cursor() as cursor: # fetch the building data as python objects cursor.outputtypehandler = output_type_handler print("WITH OUTPUT TYPE HANDLER:") for row in cursor.execute(""" select * from BuildingsAsJsonStrings order by BuildingId"""): print(row) print() python-oracledb-1.2.1/samples/type_handlers_objects.py000066400000000000000000000104271434177474600232220ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, 2022, Oracle and/or its affiliates. # # Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. # # Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, # Canada. All rights reserved. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # type_handlers_objects.py # # Demonstrates the use of input and output type handlers as well as variable # input and output converters. These methods can be used to extend # python-oracledb in many ways. This script demonstrates the binding and # querying of SQL objects as Python objects. #------------------------------------------------------------------------------ import datetime import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) obj_type = connection.gettype("UDT_BUILDING") class Building: def __init__(self, building_id, description, num_floors, date_built): self.building_id = building_id self.description = description self.num_floors = num_floors self.date_built = date_built def __repr__(self): return "" % (self.building_id, self.description) def building_in_converter(value): obj = obj_type.newobject() obj.BUILDINGID = value.building_id obj.DESCRIPTION = value.description obj.NUMFLOORS = value.num_floors obj.DATEBUILT = value.date_built return obj def building_out_converter(obj): return Building(int(obj.BUILDINGID), obj.DESCRIPTION, int(obj.NUMFLOORS), obj.DATEBUILT) def input_type_handler(cursor, value, num_elements): if isinstance(value, Building): return cursor.var(obj_type, arraysize=num_elements, inconverter=building_in_converter) def output_type_handler(cursor, name, default_type, size, precision, scale): if default_type == oracledb.OBJECT: return cursor.var(obj_type, arraysize=cursor.arraysize, outconverter=building_out_converter) buildings = [ Building(1, "The First Building", 5, datetime.date(2007, 5, 18)), Building(2, "The Second Building", 87, datetime.date(2010, 2, 7)), Building(3, "The Third Building", 12, datetime.date(2005, 6, 19)), ] with connection.cursor() as cursor: cursor.inputtypehandler = input_type_handler for building in buildings: cursor.execute("insert into BuildingsAsObjects values (:1, :2)", (building.building_id, building)) print("NO OUTPUT TYPE HANDLER:") for row in cursor.execute(""" select * from BuildingsAsObjects order by BuildingId"""): print(row) print() with connection.cursor() as cursor: cursor.outputtypehandler = output_type_handler print("WITH OUTPUT TYPE HANDLER:") for row in cursor.execute(""" select * from BuildingsAsObjects order by BuildingId"""): print(row) print() python-oracledb-1.2.1/samples/universal_rowids.py000066400000000000000000000062551434177474600222530ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2017, 2022, Oracle and/or its affiliates. # # Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. # # Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, # Canada. All rights reserved. # # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # universal_rowids.py # # Demonstrates the use of universal rowids. Universal rowids are used to # identify rows in index organized tables. #------------------------------------------------------------------------------ import datetime import oracledb import sample_env # determine whether to use python-oracledb thin mode or thick mode if not sample_env.get_is_thin(): oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client()) DATA = [ (1, "String #1", datetime.datetime(2017, 4, 4)), (2, "String #2", datetime.datetime(2017, 4, 5)), (3, "A" * 250, datetime.datetime(2017, 4, 6)) ] connection = oracledb.connect(user=sample_env.get_main_user(), password=sample_env.get_main_password(), dsn=sample_env.get_connect_string()) with connection.cursor() as cursor: # truncate table so sample can be rerun print("Truncating table...") cursor.execute("truncate table TestUniversalRowids") # populate table with a few rows print("Populating table...") for row in DATA: print("Inserting", row) cursor.execute("insert into TestUniversalRowids values (:1, :2, :3)", row) connection.commit() # fetch the rowids from the table cursor.execute("select rowid from TestUniversalRowids") rowids = [r for r, in cursor] # fetch each of the rows given the rowid for rowid in rowids: print("-" * 79) print("Rowid:", rowid) cursor.execute(""" select IntCol, StringCol, DateCol from TestUniversalRowids where rowid = :rid""", {"rid": rowid}) int_col, string_col, dateCol = cursor.fetchone() print("IntCol:", int_col) print("StringCol:", string_col) print("DateCol:", dateCol) python-oracledb-1.2.1/setup.cfg000066400000000000000000000031241434177474600164470ustar00rootroot00000000000000[metadata] name = oracledb version = attr: oracledb.version.__version__ description = Python interface to Oracle Database long_description = file: README.md long_description_content_type = text/markdown keywords = Oracle, database author = Anthony Tuininga author_email = anthony.tuininga@oracle.com url = https://oracle.github.io/python-oracledb project_urls = Installation = https://python-oracledb.readthedocs.io/en/latest/user_guide/installation.html Samples = https://github.com/oracle/python-oracledb/tree/main/samples Documentation = http://python-oracledb.readthedocs.io Release Notes = https://python-oracledb.readthedocs.io/en/latest/release_notes.html#releasenotes Issues = https://github.com/oracle/python-oracledb/issues Source = https://github.com/oracle/python-oracledb license = Apache and/or UPL license_files = LICENSE.txt THIRD_PARTY_LICENSES.txt NOTICE.txt classifiers = Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: Universal Permissive License (UPL) License :: OSI Approved :: Apache Software License Natural Language :: English Operating System :: OS Independent Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython Programming Language :: Cython Topic :: Database [options] zip_safe = false python_requires = >=3.6 setup_requires = cython install_requires = cryptography>=3.2.1 test_suite = tests packages = find: package_dir = =src [options.packages.find] where = src [options.package_data] * = py.typed python-oracledb-1.2.1/setup.py000066400000000000000000000076261434177474600163530ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ import os import platform import sys import sysconfig from setuptools import setup, Extension # base source directory source_dir = os.path.join("src", "oracledb") # determine the base implementation dependent source files (included) impl_dir = os.path.join(source_dir, "impl", "base") base_depends = [os.path.join(impl_dir, n) \ for n in sorted(os.listdir(impl_dir)) if n.endswith(".pyx")] base_pxd = os.path.join(source_dir, "base_impl.pxd") base_depends.append(base_pxd) # determine the thick mode dependent source files (included) impl_dir = os.path.join(source_dir, "impl", "thick") source_dirs = [ os.path.join(impl_dir, "odpi", "src"), os.path.join(impl_dir, "odpi", "include") ] thick_depends = [os.path.join(d, n) \ for d in source_dirs for n in sorted(os.listdir(d)) \ if n.endswith(".c") or n.endswith(".h")] thick_depends.extend(os.path.join(impl_dir, n) \ for n in sorted(os.listdir(impl_dir)) if n.endswith(".pyx") or n.endswith(".pxd")) thick_depends.append(base_pxd) # determine the thin mode dependent source files (included) impl_dir = os.path.join(source_dir, "impl", "thin") thin_depends = [os.path.join(impl_dir, n) \ for n in sorted(os.listdir(impl_dir)) if n.endswith(".pyx") or n.endswith(".pxi") or n.endswith(".pxd")] thin_depends.append(base_pxd) # if the platform is macOS: # - target the minimim OS version that current Python packages work with. # (Use 'otool -l /path/to/python' and look for 'version' in the # LC_VERSION_MIN_MACOSX section) # - add argument required for cross-compilation for both x86_64 and arm64 # architectures if the python interpreter is a universal2 version. extra_compile_args = [] if sys.platform == "darwin": extra_compile_args.extend(["-mmacosx-version-min=10.9"]) if "universal2" in sysconfig.get_platform(): if platform.machine() == "x86_64": target = "arm64-apple-macos" else: target = "x86_64-apple-macos" extra_compile_args.extend(["-target", target]) setup( ext_modules=[ Extension("oracledb.base_impl", sources=["src/oracledb/base_impl.pyx"], depends=base_depends, extra_compile_args=extra_compile_args), Extension("oracledb.thin_impl", sources=["src/oracledb/thin_impl.pyx"], depends=thin_depends, extra_compile_args=extra_compile_args), Extension("oracledb.thick_impl", sources=["src/oracledb/thick_impl.pyx"], include_dirs=["src/oracledb/impl/thick/odpi/include"], depends=thick_depends, extra_compile_args=extra_compile_args) ] ) python-oracledb-1.2.1/src/000077500000000000000000000000001434177474600154155ustar00rootroot00000000000000python-oracledb-1.2.1/src/oracledb/000077500000000000000000000000001434177474600171705ustar00rootroot00000000000000python-oracledb-1.2.1/src/oracledb/__init__.py000066400000000000000000000074511434177474600213100ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # __init__.py # # Package initialization module. #------------------------------------------------------------------------------ import sys from .version import __version__ from .constants import * from .exceptions import * from .errors import _Error from .defaults import defaults from .connection import connect, Connection from .cursor import Cursor from .pool import create_pool, ConnectionPool from .connect_params import ConnectParams from .pool_params import PoolParams from .lob import LOB from .dbobject import DbObject, DbObjectType from .var import Var from .constructors import * from .dsn import makedsn from .driver_mode import is_thin_mode from .base_impl import * from .thick_impl import clientversion, init_oracle_client package = sys.modules[__name__] init_base_impl(package) thick_impl.init_thick_impl(package) thin_impl.init_thin_impl(package) del package # future object used for managing backwards incompatible changes class Future: def __getattr__(self, name): return None def __setattr__(self, name, value): pass __future__ = Future() # remove unnecessary symbols del exceptions, errors, connection, pool, constants del constructors, base_impl, thick_impl, thin_impl, utils # general aliases (for backwards compatibility) ObjectType = DbObjectType Object = DbObject SessionPool = ConnectionPool version = __version__ # aliases for database types (for backwards compatibility) BFILE = DB_TYPE_BFILE BLOB = DB_TYPE_BLOB BOOLEAN = DB_TYPE_BOOLEAN CLOB = DB_TYPE_CLOB CURSOR = DB_TYPE_CURSOR FIXED_CHAR = DB_TYPE_CHAR FIXED_NCHAR = DB_TYPE_NCHAR INTERVAL = DB_TYPE_INTERVAL_DS LONG_BINARY = DB_TYPE_LONG_RAW LONG_STRING = DB_TYPE_LONG NATIVE_INT = DB_TYPE_BINARY_INTEGER NATIVE_FLOAT = DB_TYPE_BINARY_DOUBLE NCHAR = DB_TYPE_NVARCHAR OBJECT = DB_TYPE_OBJECT NCLOB = DB_TYPE_NCLOB TIMESTAMP = DB_TYPE_TIMESTAMP # aliases for authhentication modes (for backwards compatibility) DEFAULT_AUTH = AUTH_MODE_DEFAULT SYSASM = AUTH_MODE_SYSASM SYSBKP = AUTH_MODE_SYSBKP SYSDBA = AUTH_MODE_SYSDBA SYSDGD = AUTH_MODE_SYSDGD SYSKMT = AUTH_MODE_SYSKMT SYSOPER = AUTH_MODE_SYSOPER SYSRAC = AUTH_MODE_SYSRAC PRELIM_AUTH = AUTH_MODE_PRELIM # aliases for pool "get" modes (for backwards compatibility) SPOOL_ATTRVAL_WAIT = POOL_GETMODE_WAIT SPOOL_ATTRVAL_NOWAIT = POOL_GETMODE_NOWAIT SPOOL_ATTRVAL_FORCEGET = POOL_GETMODE_FORCEGET SPOOL_ATTRVAL_TIMEDWAIT = POOL_GETMODE_TIMEDWAIT # aliases for purity (for backwards compatibility) ATTR_PURITY_DEFAULT = PURITY_DEFAULT ATTR_PURITY_NEW = PURITY_NEW ATTR_PURITY_SELF = PURITY_SELF # aliases for subscription protocols (for backwards compatibility) SUBSCR_PROTO_OCI = SUBSCR_PROTO_CALLBACK python-oracledb-1.2.1/src/oracledb/aq.py000066400000000000000000000451031434177474600201460ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # aq.py # # Contains the classes used for handling Advanced Queuing (AQ): Queue, # DeqOptions, EnqOptions and MessageProperties. #------------------------------------------------------------------------------ import datetime from . import connection as connection_module from typing import Union, List from . import errors, exceptions from .dbobject import DbObject, DbObjectType class Queue: @classmethod def _from_impl(cls, connection, impl): queue = cls.__new__(cls) queue._connection = connection queue._deq_options = DeqOptions._from_impl(impl.deq_options_impl) queue._enq_options = EnqOptions._from_impl(impl.enq_options_impl) queue._payload_type = None queue._impl = impl return queue def _verify_message(self, message: "MessageProperties") -> None: """ Internal method used for verifying a message. """ if not isinstance(message, MessageProperties): raise TypeError("expecting MessageProperties object") if message.payload is None: errors._raise_err(errors.ERR_MESSAGE_HAS_NO_PAYLOAD) @property def connection(self) -> "connection_module.Connection": """ Returns the connection on which the queue was created. """ return self._connection def deqmany(self, max_num_messages: int) -> list: """ Dequeues up to the specified number of messages from the queue and returns a list of these messages. """ message_impls = self._impl.deq_many(max_num_messages) return [MessageProperties._from_impl(impl) for impl in message_impls] def deqMany(self, max_num_messages: int) -> List["MessageProperties"]: """ Deprecated: use deqmany() instead. """ return self.deqmany(max_num_messages) def deqone(self) -> Union["MessageProperties", None]: """ Dequeues at most one message from the queue and returns it. If no message is dequeued, None is returned. """ message_impl = self._impl.deq_one() if message_impl is not None: return MessageProperties._from_impl(message_impl) def deqOne(self) -> Union["MessageProperties", None]: """ Deprecated: use deqone() instead. """ return self.deqone() @property def deqoptions(self) -> "DeqOptions": """ Returns the options that will be used when dequeuing messages from the queue. """ return self._deq_options @property def deqOptions(self) -> "DeqOptions": """ Deprecated: use deqoptions instead. """ return self.deqoptions def enqmany(self, messages: list) -> None: """ Enqueues multiple messages into the queue. The messages parameter must be a sequence containing message property objects which have all had their payload attribute set to a value that the queue supports. Warning: calling this function in parallel on different connections acquired from the same pool may fail due to Oracle bug 29928074. Ensure that this function is not run in parallel, use standalone connections or connections from different pools, or make multiple calls to enqOne() instead. The function Queue.deqMany() call is not affected. """ for message in messages: self._verify_message(message) message_impls = [m._impl for m in messages] self._impl.enq_many(message_impls) def enqMany(self, messages: list) -> None: """ Deprecated: use enqmany() instead. """ return self.enqmany(messages) def enqone(self, message: "MessageProperties") -> None: """ Enqueues a single message into the queue. The message must be a message property object which has had its payload attribute set to a value that the queue supports. """ self._verify_message(message) self._impl.enq_one(message._impl) def enqOne(self, message: "MessageProperties") -> None: """ Deprecated: use enqone() instead. """ return self.enqone(message) @property def enqoptions(self) -> "EnqOptions": """ Returns the options that will be used when enqueuing messages into the queue. """ return self._enq_options @property def enqOptions(self) -> "EnqOptions": """ Deprecated: use enqoptions() instead. """ return self.enqoptions @property def name(self) -> str: """ Returns the name of the queue. """ return self._impl.name @property def payload_type(self) -> Union[DbObjectType, None]: """ Returns the object type for payloads that can be enqueued and dequeued. If using a raw queue, this returns the value None. """ if self._payload_type is None: if self._impl.is_json: self._payload_type = "JSON" elif self._impl.payload_type is not None: self._payload_type = \ DbObjectType._from_impl(self._impl.payload_type) return self._payload_type @property def payloadType(self) -> Union[DbObjectType, None]: """ Deprecated: use payload_type instead. """ return self.payload_type class DeqOptions: @classmethod def _from_impl(cls, impl): options = cls.__new__(cls) options._impl = impl return options @property def condition(self) -> str: """ Specifies a boolean expression similar to the where clause of a SQL query. The boolean expression can include conditions on message properties, user data properties and PL/SQL or SQL functions. The default is to have no condition specified. """ return self._impl.get_condition() @condition.setter def condition(self, value: str) -> None: self._impl.set_condition(value) @property def consumername(self) -> str: """ Specifies the name of the consumer. Only messages matching the consumer name will be accessed. If the queue is not set up for multiple consumers this attribute should not be set. The default is to have no consumer name specified. """ return self._impl.get_consumer_name() @consumername.setter def consumername(self, value: str) -> None: self._impl.set_consumer_name(value) @property def correlation(self) -> str: """ Specifies the correlation identifier of the message to be dequeued. Special pattern-matching characters, such as the percent sign (%) and the underscore (_), can be used. If multiple messages satisfy the pattern, the order of dequeuing is indeterminate. The default is to have no correlation specified. """ return self._impl.get_correlation() @correlation.setter def correlation(self, value: str) -> None: self._impl.set_correlation(value) @property def deliverymode(self) -> None: """ Specifies what types of messages should be dequeued. It should be one of the values MSG_PERSISTENT (default), MSG_BUFFERED or MSG_PERSISTENT_OR_BUFFERED. """ raise AttributeError("deliverymode can only be written") @deliverymode.setter def deliverymode(self, value: int) -> None: self._impl.set_delivery_mode(value) @property def mode(self) -> int: """ Specifies the locking behaviour associated with the dequeue operation. It should be one of the values DEQ_BROWSE, DEQ_LOCKED, DEQ_REMOVE (default), or DEQ_REMOVE_NODATA. """ return self._impl.get_mode() @mode.setter def mode(self, value: int) -> None: self._impl.set_mode(value) @property def msgid(self) -> bytes: """ Specifies the identifier of the message to be dequeued. The default is to have no message identifier specified. """ return self._impl.get_message_id() @msgid.setter def msgid(self, value: bytes) -> None: self._impl.set_message_id(value) @property def navigation(self) -> int: """ Specifies the position of the message that is retrieved. It should be one of the values DEQ_FIRST_MSG, DEQ_NEXT_MSG (default), or DEQ_NEXT_TRANSACTION. """ return self._impl.get_navigation() @navigation.setter def navigation(self, value: int) -> None: self._impl.set_navigation(value) @property def transformation(self) -> str: """ Specifies the name of the transformation that must be applied after the message is dequeued from the database but before it is returned to the calling application. The transformation must be created using dbms_transform. The default is to have no transformation specified. """ return self._impl.get_transformation() @transformation.setter def transformation(self, value: str) -> None: self._impl.set_transformation(value) @property def visibility(self) -> int: """ Specifies the transactional behavior of the dequeue request. It should be one of the values DEQ_ON_COMMIT (default) or DEQ_IMMEDIATE. This attribute is ignored when using the DEQ_BROWSE mode. Note the value of autocommit is always ignored. """ return self._impl.get_visibility() @visibility.setter def visibility(self, value: int) -> None: self._impl.set_visibility(value) @property def wait(self) -> int: """ Specifies the time to wait, in seconds, for a message matching the search criteria to become available for dequeuing. One of the values DEQ_NO_WAIT or DEQ_WAIT_FOREVER can also be used. The default is DEQ_WAIT_FOREVER. """ return self._impl.get_wait() @wait.setter def wait(self, value: int) -> None: self._impl.set_wait(value) class EnqOptions: @classmethod def _from_impl(cls, impl): options = cls.__new__(cls) options._impl = impl return options @property def deliverymode(self) -> int: """ Specifies what type of messages should be enqueued. It should be one of the values MSG_PERSISTENT (default) or MSG_BUFFERED. """ raise AttributeError("deliverymode can only be written") @deliverymode.setter def deliverymode(self, value: int) -> None: self._impl.set_delivery_mode(value) @property def transformation(self) -> str: """ Specifies the name of the transformation that must be applied before the message is enqueued into the database. The transformation must be created using dbms_transform. The default is to have no transformation specified. """ return self._impl.get_transformation() @transformation.setter def transformation(self, value: str) -> None: self._impl.set_transformation(value) @property def visibility(self) -> int: """ Specifies the transactional behavior of the enqueue request. It should be one of the values ENQ_ON_COMMIT (default) or ENQ_IMMEDIATE. Note the value of autocommit is ignored. """ return self._impl.get_visibility() @visibility.setter def visibility(self, value: int) -> None: self._impl.set_visibility(value) class MessageProperties: _recipients = [] @classmethod def _from_impl(cls, impl): props = cls.__new__(cls) props._impl = impl return props @property def attempts(self) -> int: """ Specifies the number of attempts that have been made to dequeue the message. """ return self._impl.get_num_attempts() @property def correlation(self) -> str: """ Specifies the correlation used when the message was enqueued. """ return self._impl.get_correlation() @correlation.setter def correlation(self, value: str) -> None: self._impl.set_correlation(value) @property def delay(self) -> int: """ Specifies the number of seconds to delay an enqueued message. Any integer is acceptable but the constant MSG_NO_DELAY can also be used indicating that the message is available for immediate dequeuing. """ return self._impl.get_delay() @delay.setter def delay(self, value: int) -> None: self._impl.set_delay(value) @property def deliverymode(self) -> int: """ Specifies the type of message that was dequeued. It will be one of the values MSG_PERSISTENT or MSG_BUFFERED. """ return self._impl.get_delivery_mode() @property def enqtime(self) -> datetime.datetime: """ Specifies the time that the message was enqueued. """ return self._impl.get_enq_time() @property def exceptionq(self) -> str: """ Specifies the name of the queue to which the message is moved if it cannot be processed successfully. Messages are moved if the number of unsuccessful dequeue attempts has exceeded the maximum number of retries or if the message has expired. All messages in the exception queue are in the MSG_EXPIRED state. The default value is the name of the exception queue associated with the queue table. """ return self._impl.get_exception_queue() @exceptionq.setter def exceptionq(self, value: str) -> None: self._impl.set_exception_queue(value) @property def expiration(self) -> int: """ Specifies, in seconds, how long the message is available for dequeuing. This attribute is an offset from the delay attribute. Expiration processing requires the queue monitor to be running. Any integer is accepted but the constant MSG_NO_EXPIRATION can also be used indicating that the message never expires. """ return self._impl.get_expiration() @expiration.setter def expiration(self, value: int) -> None: self._impl.set_expiration(value) @property def msgid(self) -> bytes: """ Specifies the id of the message in the last queue that enqueued or dequeued this message. If the message has never been dequeued or enqueued, the value will be `None`. """ return self._impl.get_message_id() @property def payload(self) -> Union[bytes, DbObject]: """ Specifies the payload that will be enqueued or the payload that was dequeued when using a queue. When enqueuing, the value is checked to ensure that it conforms to the type expected by that queue. For RAW queues, the value can be a bytes object or a string. If the value is a string it will be converted to bytes in the encoding UTF-8. """ return self._impl.payload @payload.setter def payload(self, value: object) -> None: if isinstance(value, DbObject): self._impl.set_payload_object(value._impl) elif not isinstance(value, (str, bytes)): self._impl.set_payload_json(value) else: if isinstance(value, str): value_bytes = value.encode() elif isinstance(value, bytes): value_bytes = value self._impl.set_payload_bytes(value_bytes) self._impl.payload = value @property def priority(self) -> int: """ Specifies the priority of the message. A smaller number indicates a higher priority. The priority can be any integer, including negative numbers. The default value is zero. """ return self._impl.get_priority() @priority.setter def priority(self, value: int) -> None: self._impl.set_priority(value) @property def recipients(self) -> list: """ A list of recipient names can be associated with a message at the time a message is enqueued. This allows a limited set of recipients to dequeue each message. The recipient list associated with the message overrides the queue subscriber list, if there is one. The recipient names need not be in the subscriber list but can be, if desired. To dequeue a message, the consumername attribute can be set to one of the recipient names. The original message recipient list is not available on dequeued messages. All recipients have to dequeue a message before it gets removed from the queue. Subscribing to a queue is like subscribing to a magazine: each subscriber can dequeue all the messages placed into a specific queue, just as each magazine subscriber has access to all its articles. Being a recipient, however, is like getting a letter: each recipient is a designated target of a particular message. """ return self._recipients @recipients.setter def recipients(self, value: list) -> None: self._impl.set_recipients(value) self._recipients = value @property def state(self) -> int: """ Specifies the state of the message at the time of the dequeue. It will be one of the values MSG_WAITING, MSG_READY, MSG_PROCESSED or MSG_EXPIRED. """ return self._impl.get_state() python-oracledb-1.2.1/src/oracledb/base_impl.pxd000066400000000000000000000345211434177474600216450ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # base_impl.pxd # # Cython definition file defining the base classes from which the thick and # thin implementations derive their classes. #------------------------------------------------------------------------------ # cython: language_level=3 from libc.stdint cimport int8_t, int16_t, int32_t, int64_t from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t cdef enum: NUM_TYPE_FLOAT = 0 NUM_TYPE_INT = 1 NUM_TYPE_DECIMAL = 2 NUM_TYPE_STR = 3 cdef enum: DB_TYPE_NUM_BFILE = 2020 DB_TYPE_NUM_BINARY_DOUBLE = 2008 DB_TYPE_NUM_BINARY_FLOAT = 2007 DB_TYPE_NUM_BINARY_INTEGER = 2009 DB_TYPE_NUM_BLOB = 2019 DB_TYPE_NUM_BOOLEAN = 2022 DB_TYPE_NUM_CHAR = 2003 DB_TYPE_NUM_CLOB = 2017 DB_TYPE_NUM_CURSOR = 2021 DB_TYPE_NUM_DATE = 2011 DB_TYPE_NUM_INTERVAL_DS = 2015 DB_TYPE_NUM_INTERVAL_YM = 2016 DB_TYPE_NUM_JSON = 2027 DB_TYPE_NUM_LONG_NVARCHAR = 2031 DB_TYPE_NUM_LONG_RAW = 2025 DB_TYPE_NUM_LONG_VARCHAR = 2024 DB_TYPE_NUM_NCHAR = 2004 DB_TYPE_NUM_NCLOB = 2018 DB_TYPE_NUM_NUMBER = 2010 DB_TYPE_NUM_NVARCHAR = 2002 DB_TYPE_NUM_OBJECT = 2023 DB_TYPE_NUM_RAW = 2006 DB_TYPE_NUM_ROWID = 2005 DB_TYPE_NUM_TIMESTAMP = 2012 DB_TYPE_NUM_TIMESTAMP_LTZ = 2014 DB_TYPE_NUM_TIMESTAMP_TZ = 2013 DB_TYPE_NUM_UROWID = 2030 DB_TYPE_NUM_VARCHAR = 2001 cdef class ApiType: cdef: readonly str name tuple dbtypes cdef class DbType: cdef: readonly uint32_t num readonly str name readonly uint32_t default_size uint32_t _buffer_size_factor str _ora_name uint8_t _ora_type_num uint8_t _csfrm @staticmethod cdef DbType _from_num(uint32_t num) @staticmethod cdef DbType _from_ora_name(str name) @staticmethod cdef DbType _from_ora_type_and_csfrm(uint8_t ora_type_num, uint8_t csfrm) cdef class Address: cdef: public str host public uint32_t port public str protocol public str https_proxy public uint32_t https_proxy_port cdef str build_connect_string(self) cdef class AddressList: cdef: public list addresses bint source_route bint load_balance int lru_index cdef str build_connect_string(self) cdef class Description: cdef: public list address_lists public bint source_route public bint load_balance public int lru_index public uint32_t expire_time public uint32_t retry_count public uint32_t retry_delay public double tcp_connect_timeout public str service_name public str server_type public str sid public str cclass public uint32_t purity public bint ssl_server_dn_match public str ssl_server_cert_dn public str wallet_location cdef str _build_duration_str(self, double value) cdef str build_connect_string(self, str cid=*) cdef class DescriptionList: cdef: public list descriptions bint source_route bint load_balance int lru_index cdef str build_connect_string(self) cdef class TnsnamesFile: cdef: str file_name int mtime dict entries cdef class ConnectParamsImpl: cdef: public str config_dir public str user public str proxy_user public bint events public bint externalauth public uint32_t mode public str edition public list appcontext public str tag public bint matchanytag public list shardingkey public list supershardingkey public uint32_t stmtcachesize public bint disable_oob public DescriptionList description_list uint64_t _external_handle public str debug_jdwp object access_token_callback object access_token_expires Description _default_description Address _default_address bytearray _password bytearray _password_obfuscator bytearray _new_password bytearray _new_password_obfuscator bytearray _wallet_password bytearray _wallet_password_obfuscator bytearray _token bytearray _token_obfuscator bytearray _private_key bytearray _private_key_obfuscator bint _has_components cdef int _check_credentials(self) except -1 cdef int _copy(self, ConnectParamsImpl other_params) except -1 cdef bytes _get_new_password(self) cdef bytearray _get_obfuscator(self, str secret_value) cdef bytes _get_password(self) cdef str _get_private_key(self) cdef TnsnamesFile _get_tnsnames_file(self) cdef str _get_token(self) cdef object _get_token_expires(self, str token) cdef str _get_wallet_password(self) cdef int _parse_connect_string(self, str connect_string) except -1 cdef int _process_connect_descriptor(self, dict args) except -1 cdef int _set_access_token(self, object val, int error_num) except -1 cdef int _set_access_token_param(self, object val) except -1 cdef int _set_new_password(self, str password) except -1 cdef int _set_password(self, str password) except -1 cdef int _set_wallet_password(self, str password) except -1 cdef bytearray _xor_bytes(self, bytearray a, bytearray b) cdef class PoolParamsImpl(ConnectParamsImpl): cdef: public uint32_t min public uint32_t max public uint32_t increment public type connectiontype public uint32_t getmode public bint homogeneous public uint32_t timeout public uint32_t wait_timeout public uint32_t max_lifetime_session public object session_callback public uint32_t max_sessions_per_shard public bint soda_metadata_cache public int ping_interval cdef class BaseConnImpl: cdef: readonly str username readonly str dsn public object inputtypehandler public object outputtypehandler public bint autocommit public bint invoke_session_callback cdef object _check_value(self, DbType dbtype, BaseDbObjectTypeImpl objtype, object value, bint* is_ok) cdef class BasePoolImpl: cdef: readonly str dsn readonly bint homogeneous readonly uint32_t increment readonly uint32_t min readonly uint32_t max readonly str username readonly str name ConnectParamsImpl connect_params cdef class BaseCursorImpl: cdef: readonly str statement readonly uint64_t rowcount public uint32_t arraysize public uint32_t prefetchrows public object inputtypehandler public object outputtypehandler public object rowfactory public bint scrollable public list fetch_vars public list fetch_var_impls public list bind_vars public type bind_style public dict bind_vars_by_name uint32_t _buffer_rowcount uint32_t _buffer_index uint32_t _fetch_array_size bint _more_rows_to_fetch cdef int _bind_values(self, object cursor, object type_handler, object params, uint32_t num_rows, uint32_t row_num, bint defer_type_assignment) except -1 cdef int _bind_values_by_name(self, object cursor, object type_handler, dict params, uint32_t num_rows, uint32_t row_num, bint defer_type_assignment) except -1 cdef int _bind_values_by_position(self, object cursor, object type_handler, object params, uint32_t num_rows, uint32_t row_num, bint defer_type_assignment) except -1 cdef int _close(self, bint in_del) except -1 cdef int _create_fetch_var(self, object conn, object cursor, object type_handler, ssize_t pos, FetchInfo fetch_info) except -1 cdef object _create_row(self) cdef BaseVarImpl _create_var_impl(self, object conn) cdef int _fetch_rows(self, object cursor) except -1 cdef BaseConnImpl _get_conn_impl(self) cdef object _get_input_type_handler(self) cdef object _get_output_type_handler(self) cdef int _init_fetch_vars(self, uint32_t num_columns) except -1 cdef bint _is_plsql(self) cdef int _perform_binds(self, object conn, uint32_t num_execs) except -1 cdef int _reset_bind_vars(self, uint32_t num_rows) except -1 cdef int _verify_var(self, object var) except -1 cdef class FetchInfo: cdef: int16_t _precision int16_t _scale uint32_t _buffer_size uint32_t _size bint _nulls_allowed str _name DbType _dbtype BaseDbObjectTypeImpl _objtype cdef class BaseVarImpl: cdef: readonly str name readonly int16_t precision readonly int16_t scale readonly uint32_t num_elements readonly object inconverter readonly object outconverter readonly uint32_t size readonly uint32_t buffer_size readonly bint bypass_decode readonly bint is_array readonly bint nulls_allowed public uint32_t num_elements_in_array readonly DbType dbtype readonly BaseDbObjectTypeImpl objtype BaseConnImpl _conn_impl int _preferred_num_type FetchInfo _fetch_info bint _is_value_set cdef int _bind(self, object conn, BaseCursorImpl cursor, uint32_t num_execs, object name, uint32_t pos) except -1 cdef int _check_and_set_scalar_value(self, uint32_t pos, object value, bint* was_set) except -1 cdef int _check_and_set_value(self, uint32_t pos, object value, bint* was_set) except -1 cdef int _finalize_init(self) except -1 cdef list _get_array_value(self) cdef object _get_scalar_value(self, uint32_t pos) cdef int _on_reset_bind(self, uint32_t num_rows) except -1 cdef int _resize(self, uint32_t new_size) except -1 cdef int _set_scalar_value(self, uint32_t pos, object value) except -1 cdef int _set_num_elements_in_array(self, uint32_t num_elements) except -1 cdef int _set_type_info_from_type(self, object typ) except -1 cdef int _set_type_info_from_value(self, object value, bint is_plsql) except -1 cdef class BaseLobImpl: cdef: readonly DbType dbtype cdef class BaseDbObjectTypeImpl: cdef: readonly str schema readonly str name readonly str package_name readonly list attrs readonly bint is_collection readonly dict attrs_by_name readonly DbType element_dbtype readonly BaseDbObjectTypeImpl element_objtype readonly BaseConnImpl _conn_impl cdef class BaseDbObjectAttrImpl: cdef: readonly str name readonly DbType dbtype readonly BaseDbObjectTypeImpl objtype cdef class BaseDbObjectImpl: cdef: readonly BaseDbObjectTypeImpl type cdef class BaseSodaDbImpl: cdef: object _conn cdef class BaseSodaCollImpl: cdef: readonly str name cdef class BaseSodaDocImpl: pass cdef class BaseSodaDocCursorImpl: pass cdef class BaseQueueImpl: cdef: readonly str name readonly BaseDbObjectTypeImpl payload_type readonly BaseDeqOptionsImpl deq_options_impl readonly BaseEnqOptionsImpl enq_options_impl readonly bint is_json cdef class BaseDeqOptionsImpl: pass cdef class BaseEnqOptionsImpl: pass cdef class BaseMsgPropsImpl: cdef: public object payload cdef class BaseSubscrImpl: cdef: readonly object callback readonly object connection readonly uint32_t namespace readonly str name readonly uint32_t protocol readonly str ip_address readonly uint32_t port readonly uint32_t timeout readonly uint32_t operations readonly uint32_t qos readonly uint64_t id readonly uint8_t grouping_class readonly uint32_t grouping_value readonly uint8_t grouping_type readonly bint client_initiated cdef class BindVar: cdef: object var BaseVarImpl var_impl object name ssize_t pos bint has_value cdef int _create_var_from_type(self, object conn, BaseCursorImpl cursor_impl, object value) except -1 cdef int _create_var_from_value(self, object conn, BaseCursorImpl cursor_impl, object value, uint32_t num_elements) except -1 cdef int _set_by_type(self, object conn, BaseCursorImpl cursor_impl, object typ) except -1 cdef int _set_by_value(self, object conn, BaseCursorImpl cursor_impl, object cursor, object value, object type_handler, uint32_t row_num, uint32_t num_elements, bint defer_type_assignment) except -1 cdef object get_exception_class(int32_t code) python-oracledb-1.2.1/src/oracledb/base_impl.pyx000066400000000000000000000124341434177474600216710ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # base_impl.pyx # # Cython file for the base implementation that the thin and thick # implementations use. #------------------------------------------------------------------------------ # cython: language_level=3 cimport cython cimport cpython cimport cpython.datetime as cydatetime from libc.stdint cimport int8_t, int16_t, int32_t, int64_t from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t import base64 import datetime import decimal import json import os import re import secrets import sys cydatetime.import_datetime() include "impl/base/types.pyx" from . import constants, errors, exceptions, utils from .defaults import defaults cdef type PY_TYPE_BOOL = bool cdef type PY_TYPE_CURSOR cdef type PY_TYPE_DATE = datetime.date cdef type PY_TYPE_DATETIME = datetime.datetime cdef type PY_TYPE_DECIMAL = decimal.Decimal cdef type PY_TYPE_DB_OBJECT cdef type PY_TYPE_DB_OBJECT_TYPE cdef type PY_TYPE_LOB cdef type PY_TYPE_TIMEDELTA = datetime.timedelta cdef type PY_TYPE_VAR cdef int32_t* INTEGRITY_ERROR_CODES = [ 1, # unique constraint violated 1400, # cannot insert NULL 1438, # value larger than specified precision 2290, # check constraint violated 2291, # integrity constraint violated - parent key not found 2292, # integrity constraint violated - child record found 21525, # attribute or collection element violated its constraints 40479, # internal JSON serializer error 0 ] cdef int32_t* INTERFACE_ERROR_CODES = [ 24422, 0 ] cdef int32_t* OPERATIONAL_ERROR_CODES = [ 22, # invalid session ID; access denied 378, # buffer pools cannot be created as specified 600, # internal error code 602, # internal programming exception 603, # ORACLE server session terminated by fatal error 604, # error occurred at recursive SQL level 609, # could not attach to incoming connection 1012, # not logged on 1013, # user requested cancel of current operation 1033, # ORACLE initialization or shutdown in progress 1034, # ORACLE not available 1041, # internal error. hostdef extension doesn't exist 1043, # user side memory corruption 1089, # immediate shutdown or close in progress 1090, # shutdown in progress - connection is not permitted 1092, # ORACLE instance terminated. Disconnection forced 3111, # break received on communication channel 3113, # end-of-file on communication channel 3114, # not connected to ORACLE 3122, # attempt to close ORACLE-side window on user side 3135, # connection lost contact 12153, # TNS:not connected 12203, # TNS:unable to connect to destination 12500, # TNS:listener failed to start a dedicated server process 12571, # TNS:packet writer failure 27146, # post/wait initialization failed 28511, # lost RPC connection to heterogeneous remote agent 0 ] cdef int is_code_in_array(int32_t code, int32_t *ptr): cdef int ix = 0 while ptr[ix] != 0: if ptr[ix] == code: return 1 ix += 1 return 0 cdef object get_exception_class(int32_t code): if is_code_in_array(code, INTEGRITY_ERROR_CODES): return exceptions.IntegrityError if is_code_in_array(code, OPERATIONAL_ERROR_CODES): return exceptions.OperationalError if is_code_in_array(code, INTERFACE_ERROR_CODES): return exceptions.InterfaceError return exceptions.DatabaseError include "impl/base/utils.pyx" include "impl/base/connect_params.pyx" include "impl/base/pool_params.pyx" include "impl/base/connection.pyx" include "impl/base/pool.pyx" include "impl/base/cursor.pyx" include "impl/base/var.pyx" include "impl/base/bind_var.pyx" include "impl/base/dbobject.pyx" include "impl/base/lob.pyx" include "impl/base/soda.pyx" include "impl/base/queue.pyx" include "impl/base/subscr.pyx" python-oracledb-1.2.1/src/oracledb/connect_params.py000066400000000000000000000737711434177474600225550ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # connect_params.py # # Contains the ConnectParams class used for managing the parameters required to # establish a connection to the database. # # *** NOTICE *** This file is generated from a template and should not be # modified directly. See build_from_template.py in the utils subdirectory for # more information. #------------------------------------------------------------------------------ import functools from typing import Union, Callable import oracledb from . import base_impl, constants, errors, utils class ConnectParams: """ Contains all parameters used for establishing a connection to the database. """ __module__ = oracledb.__name__ __slots__ = ["_impl"] _impl_class = base_impl.ConnectParamsImpl @utils.params_initer def __init__(self, *, user: str=None, proxy_user: str=None, password: str=None, newpassword: str=None, wallet_password: str=None, access_token: Union[str, tuple, Callable]=None, host: str=None, port: int=1521, protocol: str="tcp", https_proxy: str=None, https_proxy_port: int=0, service_name: str=None, sid: str=None, server_type: str=None, cclass: str=None, purity: int=oracledb.PURITY_DEFAULT, expire_time: int=0, retry_count: int=0, retry_delay: int=0, tcp_connect_timeout: float=60.0, ssl_server_dn_match: bool=True, ssl_server_cert_dn: str=None, wallet_location: str=None, events: bool=False, externalauth: bool=False, mode: int=oracledb.AUTH_MODE_DEFAULT, disable_oob: bool=False, stmtcachesize: int=oracledb.defaults.stmtcachesize, edition: str=None, tag: str=None, matchanytag: bool=False, config_dir: str=oracledb.defaults.config_dir, appcontext: list=None, shardingkey: list=None, supershardingkey: list=None, debug_jdwp: str=None, handle: int=0, threaded: bool=True, encoding: str=None, nencoding: str=None ): """ All parameters are optional. A brief description of each parameter follows: - user: the name of the user to connect to (default: None) - proxy_user: the name of the proxy user to connect to. If this value is not specified, it will be parsed out of user if user is in the form "user[proxy_user]" (default: None) - password: the password for the user (default: None) - newpassword: the new password for the user. The new password will take effect immediately upon a successful connection to the database (default: None) - wallet_password: the password to use to decrypt the wallet, if it is encrypted. This value is only used in thin mode (default: None) - access_token: expected to be a string or a 2-tuple or a callable. If it is a string, it specifies an Azure AD OAuth2 token used for Open Authorization (OAuth 2.0) token based authentication. If it is a 2-tuple, it specifies the token and private key strings used for Oracle Cloud Infrastructure (OCI) Identity and Access Management (IAM) token based authentication. If it is a callable, it returns either a string or a 2-tuple used for OAuth 2.0 or OCI IAM token based authentication and is useful when the pool needs to expand and create new connections but the current authentication token has expired (default: None) - host: the name or IP address of the machine hosting the database or the database listener (default: None) - port: the port number on which the database listener is listening (default: 1521) - protocol: one of the strings "tcp" or "tcps" indicating whether to use unencrypted network traffic or encrypted network traffic (TLS) (default: "tcp") - https_proxy: the name or IP address of a proxy host to use for tunneling secure connections (default: None) - https_proxy_port: the port on which to communicate with the proxy host (default: 0) - service_name: the service name of the database (default: None) - sid: the system identifier (SID) of the database. Note using a service_name instead is recommended (default: None) - server_type: the type of server connection that should be established. If specified, it should be one of "dedicated", "shared" or "pooled" (default: None) - cclass: connection class to use for Database Resident Connection Pooling (DRCP) (default: None) - purity: purity to use for Database Resident Connection Pooling (DRCP) (default: oracledb.PURITY_DEFAULT) - expire_time: an integer indicating the number of minutes between the sending of keepalive probes. If this parameter is set to a value greater than zero it enables keepalive (default: 0) - retry_count: the number of times that a connection attempt should be retried before the attempt is terminated (default: 0) - retry_delay: the number of seconds to wait before making a new connection attempt (default: 0) - tcp_connect_timeout: a float indicating the maximum number of seconds to wait for establishing a connection to the database host (default: 60.0) - ssl_server_dn_match: boolean indicating whether the server certificate distinguished name (DN) should be matched in addition to the regular certificate verification that is performed. Note that if the ssl_server_cert_dn parameter is not privided, host name matching is performed instead (default: True) - ssl_server_cert_dn: the distinguished name (DN) which should be matched with the server. This value is ignored if the ssl_server_dn_match parameter is not set to the value True. If specified this value is used for any verfication. Otherwise the hostname will be used. (default: None) - wallet_location: the directory where the wallet can be found. In thin mode this must be the directory containing the PEM-encoded wallet file ewallet.pem. In thick mode this must be the directory containing the file cwallet.sso (default: None) - events: boolean specifying whether events mode should be enabled. This value is only used in thick mode and is needed for continuous query notification and high availability event notifications (default: False) - externalauth: a boolean indicating whether to use external authentication (default: False) - mode: authorization mode to use. For example oracledb.AUTH_MODE_SYSDBA (default: oracledb.AUTH_MODE_DEFAULT) - disable_oob: boolean indicating whether out-of-band breaks should be disabled. This value is only used in thin mode. It has no effect on Windows which does not support this functionality (default: False) - stmtcachesize: identifies the initial size of the statement cache (default: oracledb.defaults.stmtcachesize) - edition: edition to use for the connection. This parameter cannot be used simultaneously with the cclass parameter (default: None) - tag: identifies the type of connection that should be returned from a pool. This value is only used in thick mode (default: None) - matchanytag: boolean specifying whether any tag can be used when acquiring a connection from the pool. This value is only used in thick mode. (default: False) - config_dir: directory in which the optional tnsnames.ora configuration file is located. This value is only used in thin mode. For thick mode use the config_dir parameter of init_oracle_client() (default: oracledb.defaults.config_dir) - appcontext: application context used by the connection. It should be a list of 3-tuples (namespace, name, value) and each entry in the tuple should be a string. This value is only used in thick mode (default: None) - shardingkey: a list of strings, numbers, bytes or dates that identify the database shard to connect to. This value is only used in thick mode (default: None) - supershardingkey: a list of strings, numbers, bytes or dates that identify the database shard to connect to. This value is only used in thick mode (default: None) - debug_jdwp: a string with the format "host=;port=" that specifies the host and port of the PL/SQL debugger. This value is only used in thin mode. For thick mode set the ORA_DEBUG_JDWP environment variable (default: None) - handle: an integer representing a pointer to a valid service context handle. This value is only used in thick mode. It should be used with extreme caution (default: 0) """ pass def __repr__(self): return self.__class__.__qualname__ + "(" + \ f"user={self.user!r}, " + \ f"proxy_user={self.proxy_user!r}, " + \ f"host={self.host!r}, " + \ f"port={self.port!r}, " + \ f"protocol={self.protocol!r}, " + \ f"https_proxy={self.https_proxy!r}, " + \ f"https_proxy_port={self.https_proxy_port!r}, " + \ f"service_name={self.service_name!r}, " + \ f"sid={self.sid!r}, " + \ f"server_type={self.server_type!r}, " + \ f"cclass={self.cclass!r}, " + \ f"purity={self.purity!r}, " + \ f"expire_time={self.expire_time!r}, " + \ f"retry_count={self.retry_count!r}, " + \ f"retry_delay={self.retry_delay!r}, " + \ f"tcp_connect_timeout={self.tcp_connect_timeout!r}, " + \ f"ssl_server_dn_match={self.ssl_server_dn_match!r}, " + \ f"ssl_server_cert_dn={self.ssl_server_cert_dn!r}, " + \ f"wallet_location={self.wallet_location!r}, " + \ f"events={self.events!r}, " + \ f"externalauth={self.externalauth!r}, " + \ f"mode={self.mode!r}, " + \ f"disable_oob={self.disable_oob!r}, " + \ f"stmtcachesize={self.stmtcachesize!r}, " + \ f"edition={self.edition!r}, " + \ f"tag={self.tag!r}, " + \ f"matchanytag={self.matchanytag!r}, " + \ f"config_dir={self.config_dir!r}, " + \ f"appcontext={self.appcontext!r}, " + \ f"shardingkey={self.shardingkey!r}, " + \ f"supershardingkey={self.supershardingkey!r}, " + \ f"debug_jdwp={self.debug_jdwp!r}" + \ ")" def _address_attr(f): """ Helper function used to get address level attributes. """ @functools.wraps(f) def wrapped(self): values = [getattr(a, f.__name__) \ for a in self._impl._get_addresses()] return values if len(values) > 1 else values[0] return wrapped def _description_attr(f): """ Helper function used to get description level attributes. """ @functools.wraps(f) def wrapped(self): values = [getattr(d, f.__name__) \ for d in self._impl.description_list.descriptions] return values if len(values) > 1 else values[0] return wrapped @property def appcontext(self) -> list: """ Application context used by the connection. It should be a list of 3-tuples (namespace, name, value) and each entry in the tuple should be a string. This value is only used in thick mode. """ return self._impl.appcontext @property @_description_attr def cclass(self) -> Union[list, str]: """ Connection class to use for Database Resident Connection Pooling (DRCP). """ return self._impl.cclass @property def config_dir(self) -> str: """ Directory in which the optional tnsnames.ora configuration file is located. This value is only used in thin mode. For thick mode use the config_dir parameter of init_oracle_client(). """ return self._impl.config_dir @property def debug_jdwp(self) -> str: """ A string with the format "host=;port=" that specifies the host and port of the PL/SQL debugger. This value is only used in thin mode. For thick mode set the ORA_DEBUG_JDWP environment variable. """ return self._impl.debug_jdwp @property def disable_oob(self) -> bool: """ Boolean indicating whether out-of-band breaks should be disabled. This value is only used in thin mode. It has no effect on Windows which does not support this functionality. """ return self._impl.disable_oob @property def edition(self) -> str: """ Edition to use for the connection. This parameter cannot be used simultaneously with the cclass parameter. """ return self._impl.edition @property def events(self) -> bool: """ Boolean specifying whether events mode should be enabled. This value is only used in thick mode and is needed for continuous query notification and high availability event notifications. """ return self._impl.events @property @_description_attr def expire_time(self) -> Union[list, int]: """ An integer indicating the number of minutes between the sending of keepalive probes. If this parameter is set to a value greater than zero it enables keepalive. """ return self._impl.expire_time @property def externalauth(self) -> bool: """ A boolean indicating whether to use external authentication. """ return self._impl.externalauth @property @_address_attr def host(self) -> Union[list, str]: """ The name or IP address of the machine hosting the database or the database listener. """ return self._impl.host @property @_address_attr def https_proxy(self) -> Union[list, str]: """ The name or IP address of a proxy host to use for tunneling secure connections. """ return self._impl.https_proxy @property @_address_attr def https_proxy_port(self) -> Union[list, int]: """ The port on which to communicate with the proxy host. """ return self._impl.https_proxy_port @property def matchanytag(self) -> bool: """ Boolean specifying whether any tag can be used when acquiring a connection from the pool. This value is only used in thick mode.. """ return self._impl.matchanytag @property def mode(self) -> int: """ Authorization mode to use. For example oracledb.AUTH_MODE_SYSDBA. """ return self._impl.mode @property @_address_attr def port(self) -> Union[list, int]: """ The port number on which the database listener is listening. """ return self._impl.port @property @_address_attr def protocol(self) -> Union[list, str]: """ One of the strings "tcp" or "tcps" indicating whether to use unencrypted network traffic or encrypted network traffic (TLS). """ return self._impl.protocol @property def proxy_user(self) -> str: """ The name of the proxy user to connect to. If this value is not specified, it will be parsed out of user if user is in the form "user[proxy_user]". """ return self._impl.proxy_user @property @_description_attr def purity(self) -> Union[list, int]: """ Purity to use for Database Resident Connection Pooling (DRCP). """ return self._impl.purity @property @_description_attr def retry_count(self) -> Union[list, int]: """ The number of times that a connection attempt should be retried before the attempt is terminated. """ return self._impl.retry_count @property @_description_attr def retry_delay(self) -> Union[list, int]: """ The number of seconds to wait before making a new connection attempt. """ return self._impl.retry_delay @property @_description_attr def server_type(self) -> Union[list, str]: """ The type of server connection that should be established. If specified, it should be one of "dedicated", "shared" or "pooled". """ return self._impl.server_type @property @_description_attr def service_name(self) -> Union[list, str]: """ The service name of the database. """ return self._impl.service_name @property def shardingkey(self) -> list: """ A list of strings, numbers, bytes or dates that identify the database shard to connect to. This value is only used in thick mode. """ return self._impl.shardingkey @property @_description_attr def sid(self) -> Union[list, str]: """ The system identifier (SID) of the database. Note using a service_name instead is recommended. """ return self._impl.sid @property @_description_attr def ssl_server_cert_dn(self) -> Union[list, str]: """ The distinguished name (DN) which should be matched with the server. This value is ignored if the ssl_server_dn_match parameter is not set to the value True. If specified this value is used for any verfication. Otherwise the hostname will be used.. """ return self._impl.ssl_server_cert_dn @property @_description_attr def ssl_server_dn_match(self) -> Union[list, bool]: """ Boolean indicating whether the server certificate distinguished name (DN) should be matched in addition to the regular certificate verification that is performed. Note that if the ssl_server_cert_dn parameter is not privided, host name matching is performed instead. """ return self._impl.ssl_server_dn_match @property def stmtcachesize(self) -> int: """ Identifies the initial size of the statement cache. """ return self._impl.stmtcachesize @property def supershardingkey(self) -> list: """ A list of strings, numbers, bytes or dates that identify the database shard to connect to. This value is only used in thick mode. """ return self._impl.supershardingkey @property def tag(self) -> str: """ Identifies the type of connection that should be returned from a pool. This value is only used in thick mode. """ return self._impl.tag @property @_description_attr def tcp_connect_timeout(self) -> Union[list, float]: """ A float indicating the maximum number of seconds to wait for establishing a connection to the database host. """ return self._impl.tcp_connect_timeout @property def user(self) -> str: """ The name of the user to connect to. """ return self._impl.user @property @_description_attr def wallet_location(self) -> Union[list, str]: """ The directory where the wallet can be found. In thin mode this must be the directory containing the PEM-encoded wallet file ewallet.pem. In thick mode this must be the directory containing the file cwallet.sso. """ return self._impl.wallet_location def copy(self) -> "ConnectParams": """ Creates a copy of the parameters and returns it. """ params = ConnectParams.__new__(ConnectParams) params._impl = self._impl.copy() return params def get_connect_string(self) -> str: """ Returns a connect string generated from the parameters. """ return self._impl.get_connect_string() def parse_connect_string(self, connect_string: str) -> None: """ Parses the connect string into its components and stores the parameters. The connect string could be an Easy Connect string, name-value pairs or a simple alias which is looked up in tnsnames.ora. Any parameters found in the connect string override any currently stored values. """ self._impl.parse_connect_string(connect_string) @utils.params_setter def set(self, *, user: str=None, proxy_user: str=None, password: str=None, newpassword: str=None, wallet_password: str=None, access_token: Union[str, tuple, Callable]=None, host: str=None, port: int=None, protocol: str=None, https_proxy: str=None, https_proxy_port: int=None, service_name: str=None, sid: str=None, server_type: str=None, cclass: str=None, purity: int=None, expire_time: int=None, retry_count: int=None, retry_delay: int=None, tcp_connect_timeout: float=None, ssl_server_dn_match: bool=None, ssl_server_cert_dn: str=None, wallet_location: str=None, events: bool=None, externalauth: bool=None, mode: int=None, disable_oob: bool=None, stmtcachesize: int=None, edition: str=None, tag: str=None, matchanytag: bool=None, config_dir: str=None, appcontext: list=None, shardingkey: list=None, supershardingkey: list=None, debug_jdwp: str=None, handle: int=None, threaded: bool=None, encoding: str=None, nencoding: str=None ): """ All parameters are optional. A brief description of each parameter follows: - user: the name of the user to connect to - proxy_user: the name of the proxy user to connect to. If this value is not specified, it will be parsed out of user if user is in the form "user[proxy_user]" - password: the password for the user - newpassword: the new password for the user. The new password will take effect immediately upon a successful connection to the database - wallet_password: the password to use to decrypt the wallet, if it is encrypted. This value is only used in thin mode - access_token: expected to be a string or a 2-tuple or a callable. If it is a string, it specifies an Azure AD OAuth2 token used for Open Authorization (OAuth 2.0) token based authentication. If it is a 2-tuple, it specifies the token and private key strings used for Oracle Cloud Infrastructure (OCI) Identity and Access Management (IAM) token based authentication. If it is a callable, it returns either a string or a 2-tuple used for OAuth 2.0 or OCI IAM token based authentication and is useful when the pool needs to expand and create new connections but the current authentication token has expired - host: the name or IP address of the machine hosting the database or the database listener - port: the port number on which the database listener is listening - protocol: one of the strings "tcp" or "tcps" indicating whether to use unencrypted network traffic or encrypted network traffic (TLS) - https_proxy: the name or IP address of a proxy host to use for tunneling secure connections - https_proxy_port: the port on which to communicate with the proxy host - service_name: the service name of the database - sid: the system identifier (SID) of the database. Note using a service_name instead is recommended - server_type: the type of server connection that should be established. If specified, it should be one of "dedicated", "shared" or "pooled" - cclass: connection class to use for Database Resident Connection Pooling (DRCP) - purity: purity to use for Database Resident Connection Pooling (DRCP) - expire_time: an integer indicating the number of minutes between the sending of keepalive probes. If this parameter is set to a value greater than zero it enables keepalive - retry_count: the number of times that a connection attempt should be retried before the attempt is terminated - retry_delay: the number of seconds to wait before making a new connection attempt - tcp_connect_timeout: a float indicating the maximum number of seconds to wait for establishing a connection to the database host - ssl_server_dn_match: boolean indicating whether the server certificate distinguished name (DN) should be matched in addition to the regular certificate verification that is performed. Note that if the ssl_server_cert_dn parameter is not privided, host name matching is performed instead - ssl_server_cert_dn: the distinguished name (DN) which should be matched with the server. This value is ignored if the ssl_server_dn_match parameter is not set to the value True. If specified this value is used for any verfication. Otherwise the hostname will be used. - wallet_location: the directory where the wallet can be found. In thin mode this must be the directory containing the PEM-encoded wallet file ewallet.pem. In thick mode this must be the directory containing the file cwallet.sso - events: boolean specifying whether events mode should be enabled. This value is only used in thick mode and is needed for continuous query notification and high availability event notifications - externalauth: a boolean indicating whether to use external authentication - mode: authorization mode to use. For example oracledb.AUTH_MODE_SYSDBA - disable_oob: boolean indicating whether out-of-band breaks should be disabled. This value is only used in thin mode. It has no effect on Windows which does not support this functionality - stmtcachesize: identifies the initial size of the statement cache - edition: edition to use for the connection. This parameter cannot be used simultaneously with the cclass parameter - tag: identifies the type of connection that should be returned from a pool. This value is only used in thick mode - matchanytag: boolean specifying whether any tag can be used when acquiring a connection from the pool. This value is only used in thick mode. - config_dir: directory in which the optional tnsnames.ora configuration file is located. This value is only used in thin mode. For thick mode use the config_dir parameter of init_oracle_client() - appcontext: application context used by the connection. It should be a list of 3-tuples (namespace, name, value) and each entry in the tuple should be a string. This value is only used in thick mode - shardingkey: a list of strings, numbers, bytes or dates that identify the database shard to connect to. This value is only used in thick mode - supershardingkey: a list of strings, numbers, bytes or dates that identify the database shard to connect to. This value is only used in thick mode - debug_jdwp: a string with the format "host=;port=" that specifies the host and port of the PL/SQL debugger. This value is only used in thin mode. For thick mode set the ORA_DEBUG_JDWP environment variable - handle: an integer representing a pointer to a valid service context handle. This value is only used in thick mode. It should be used with extreme caution """ pass python-oracledb-1.2.1/src/oracledb/connection.py000066400000000000000000001403741434177474600217120ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # connection.py # # Contains the Connection class and the factory method connect() used for # establishing connections to the database. # # *** NOTICE *** This file is generated from a template and should not be # modified directly. See build_from_template.py in the utils subdirectory for # more information. #------------------------------------------------------------------------------ import collections import functools import oracledb from . import __name__ as MODULE_NAME from typing import Any, Callable, Type, Union from . import constants, driver_mode, errors, exceptions, utils from . import base_impl, thick_impl, thin_impl from . import pool as pool_module from .defaults import defaults from .connect_params import ConnectParams from .cursor import Cursor from .lob import LOB from .subscr import Subscription from .aq import Queue, MessageProperties from .soda import SodaDatabase from .dbobject import DbObjectType, DbObject from .base_impl import DB_TYPE_BLOB, DB_TYPE_CLOB, DB_TYPE_NCLOB, DbType # named tuple used for representing global transactions Xid = collections.namedtuple("Xid", ["format_id", "global_transaction_id", "branch_qualifier"]) class Connection: __module__ = MODULE_NAME def __init__(self, dsn: str=None, *, pool: "pool_module.ConnectionPool"=None, params: ConnectParams=None, **kwargs) -> None: """ Constructor for creating a connection to the database. The dsn parameter (data source name) can be a string in the format user/password@connect_string or can simply be the connect string (in which case authentication credentials such as the username and password need to be specified separately). See the documentation on connection strings for more information. The pool parameter is expected to be a pool object and the use of this parameter is the equivalent of calling acquire() on the pool. The params parameter is expected to be of type ConnectParams and contains connection parameters that will be used when establishing the connection. See the documentation on ConnectParams for more information. If this parameter is not specified, the additional keyword parameters will be used to create an instance of ConnectParams. If both the params parameter and additional keyword parameters are specified, the values in the keyword parameters have precedence. Note that if a dsn is also supplied, then in the python-oracledb Thin mode, the values of the parameters specified (if any) within the dsn will override the values passed as additional keyword parameters, which themselves override the values set in the params parameter object. """ # if this variable is not present, exceptions raised during # construction can result in cascading exceptions; the __repr__() # method depends on this variable being present, too, so make it # available first thing self._impl = None # determine if thin mode is being used with driver_mode.get_manager() as mode_mgr: thin = mode_mgr.thin # determine which connection parameters to use if params is None: params_impl = base_impl.ConnectParamsImpl() elif not isinstance(params, ConnectParams): errors._raise_err(errors.ERR_INVALID_CONNECT_PARAMS) else: params_impl = params._impl.copy() if kwargs: params_impl.set(kwargs) if dsn is not None: dsn = params_impl.parse_dsn(dsn, thin) if dsn is None: dsn = params_impl.get_connect_string() # see if connection is being acquired from a pool if pool is None: pool_impl = None elif not isinstance(pool, pool_module.ConnectionPool): message = "pool must be an instance of oracledb.ConnectionPool" raise TypeError(message) else: pool._verify_open() pool_impl = pool._impl # create thin or thick implementation object if thin: if pool is not None: impl = pool_impl.acquire(params_impl) else: impl = thin_impl.ThinConnImpl(dsn, params_impl) impl.connect(params_impl) else: impl = thick_impl.ThickConnImpl(dsn, params_impl) impl.connect(params_impl, pool_impl) self._impl = impl self._version = None # invoke callback, if applicable if impl.invoke_session_callback and pool is not None \ and pool.session_callback is not None \ and callable(pool.session_callback): pool.session_callback(self, params_impl.tag) impl.invoke_session_callback = False def __del__(self): if self._impl is not None: self._impl.close(in_del=True) self._impl = None def __enter__(self): self._verify_connected() return self def __exit__(self, exc_type, exc_value, exc_tb): self.close() def __repr__(self): typ = type(self) cls_name = f"{typ.__module__}.{typ.__qualname__}" if self._impl is None: return f"<{cls_name} disconnected>" elif self.username is None: return f"<{cls_name} to externally identified user>" return f"<{cls_name} to {self.username}@{self.dsn}>" def _get_oci_attr(self, handle_type: int, attr_num: int, attr_type: int) -> Any: """ Returns the value of the specified OCI attribute from the internal handle. This is only supported in python-oracledb thick mode and should only be used as directed by Oracle. """ self._verify_connected() return self._impl._get_oci_attr(handle_type, attr_num, attr_type) def _set_oci_attr(self, handle_type: int, attr_num: int, attr_type: int, value: Any) -> None: """ Sets the value of the specified OCI attribute on the internal handle. This is only supported in python-oracledb thick mode and should only be used as directed by Oracle. """ self._verify_connected() self._impl._set_oci_attr(handle_type, attr_num, attr_type, value) def _verify_connected(self) -> None: """ Verifies that the connection is connected to the database. If it is not, an exception is raised. """ if self._impl is None: errors._raise_err(errors.ERR_NOT_CONNECTED) def _verify_xid(self, xid: Xid) -> None: """ Verifies that the supplied xid is of the correct type. """ if not isinstance(xid, Xid): message = "expecting transaction id created with xid()" raise TypeError(message) @property def action(self) -> None: raise AttributeError("action is not readable") @action.setter def action(self, value: str) -> None: """ Specifies the action column in the v$session table. It is a string attribute and cannot be set to None -- use the empty string instead. """ self._verify_connected() self._impl.set_action(value) @property def autocommit(self) -> bool: """ Specifies whether autocommit mode is on or off. When autocommit mode is on, all statements are committed as soon as they have completed executing successfully. """ self._verify_connected() return self._impl.autocommit @autocommit.setter def autocommit(self, value: bool) -> None: self._verify_connected() self._impl.autocommit = value def begin(self, format_id: int=-1, transaction_id: str="", branch_id: str="") -> None: """ Deprecated. Use tpc_begin() instead. """ if format_id != -1: self.tpc_begin(self.xid(format_id, transaction_id, branch_id)) @property def call_timeout(self) -> int: """ Specifies the amount of time (in milliseconds) that a single round-trip to the database may take before a timeout will occur. A value of 0 means that no timeout will take place. """ self._verify_connected() return self._impl.get_call_timeout() @call_timeout.setter def call_timeout(self, value: int) -> None: self._verify_connected() self._impl.set_call_timeout(value) @property def callTimeout(self) -> int: """ Deprecated. Use property call_timeout instead. """ return self.call_timeout @callTimeout.setter def callTimeout(self, value: int) -> None: self._verify_connected() self._impl.set_call_timeout(value) def cancel(self) -> None: """ Break a long-running transaction. """ self._verify_connected() self._impl.cancel() def changepassword(self, old_password: str, new_password: str) -> None: """ Changes the password for the user to which the connection is connected. """ self._verify_connected() self._impl.change_password(old_password, new_password) @property def client_identifier(self) -> None: raise AttributeError("client_identifier is not readable") @client_identifier.setter def client_identifier(self, value: str) -> None: """ Specifies the client_identifier column in the v$session table. """ self._verify_connected() self._impl.set_client_identifier(value) @property def clientinfo(self) -> None: raise AttributeError("clientinfo is not readable") @clientinfo.setter def clientinfo(self, value: str) -> None: """ Specifies the client_info column in the v$session table. """ self._verify_connected() self._impl.set_client_info(value) def close(self) -> None: """ Closes the connection and makes it unusable for further operations. An Error exception will be raised if any operation is attempted with this connection after this method completes successfully. """ self._verify_connected() self._impl.close() self._impl = None def commit(self) -> None: """ Commits any pending transactions to the database. """ self._verify_connected() self._impl.commit() def createlob(self, lob_type: DbType) -> LOB: """ Create and return a new temporary LOB of the specified type. """ self._verify_connected() if lob_type not in (DB_TYPE_CLOB, DB_TYPE_NCLOB, DB_TYPE_BLOB): message = "parameter should be one of oracledb.DB_TYPE_CLOB, " \ "oracledb.DB_TYPE_BLOB or oracledb.DB_TYPE_NCLOB" raise TypeError(message) impl = self._impl.create_temp_lob_impl(lob_type) return LOB._from_impl(impl) @property def current_schema(self) -> str: """ Specifies the current schema for the session. Setting this value is the same as executing the SQL statement "ALTER SESSION SET CURRENT_SCHEMA". The attribute is set (and verified) on the next call that does a round trip to the server. The value is placed before unqualified database objects in SQL statements you then execute. """ self._verify_connected() return self._impl.get_current_schema() @current_schema.setter def current_schema(self, value: str) -> None: self._verify_connected() self._impl.set_current_schema(value) def cursor(self, scrollable: bool=False) -> Cursor: """ Returns a cursor associated with the connection. """ self._verify_connected() return Cursor(self, scrollable) @property def dbop(self) -> None: raise AttributeError("dbop is not readable") @dbop.setter def dbop(self, value: str) -> None: """ Specifies the database operation that is to be monitored. This can be viewed in the DBOP_NAME column of the V$SQL_MONITOR table. """ self._verify_connected() self._impl.set_dbop(value) @property def dsn(self) -> str: """ Specifies the connection string (TNS entry) of the database to which a connection has been established. """ self._verify_connected() return self._impl.dsn @property def econtext_id(self) -> None: raise AttributeError("econtext_id is not readable") @econtext_id.setter def econtext_id(self, value: str) -> None: """ Specifies the execution context id. This value can be found as ecid in the v$session table and econtext_id in the auditing tables. The maximum length is 64 bytes. """ self._verify_connected() self._impl.set_econtext_id(value) @property def edition(self) -> str: """ Specifies the session edition. """ self._verify_connected() return self._impl.get_edition() @property def encoding(self) -> str: """ Specifies the IANA character set name of the character set in use. This is always the value "UTF-8". """ return "UTF-8" @property def external_name(self) -> str: """ Specifies the external name that is used by the connection when logging distributed transactions. """ self._verify_connected() return self._impl.get_external_name() @external_name.setter def external_name(self, value: str) -> None: self._verify_connected() self._impl.set_external_name(value) def getSodaDatabase(self) -> SodaDatabase: """ Return a SODA database object for performing all operations on Simple Oracle Document Access (SODA). """ self._verify_connected() db_impl = self._impl.create_soda_database_impl(self) return SodaDatabase._from_impl(self, db_impl) def gettype(self, name: str) -> DbObjectType: """ Return a type object given its name. This can then be used to create objects which can be bound to cursors created by this connection. """ self._verify_connected() obj_type_impl = self._impl.get_type(self, name) return DbObjectType._from_impl(obj_type_impl) @property def handle(self) -> int: """ Returns the OCI service context handle for the connection. It is primarily provided to facilitate testing the creation of a connection using the OCI service context handle. This property is only relevant to python-oracledb's thick mode. """ self._verify_connected() return self._impl.get_handle() @property def inputtypehandler(self) -> Callable: """ Specifies a method called for each value that is bound to a statement executed on any cursor associated with this connection. The method signature is handler(cursor, value, arraysize) and the return value is expected to be a variable object or None in which case a default variable object will be created. If this attribute is None, the default behavior will take place for all values bound to statements. """ self._verify_connected() return self._impl.inputtypehandler @inputtypehandler.setter def inputtypehandler(self, value: Callable) -> None: self._verify_connected() self._impl.inputtypehandler = value @property def internal_name(self) -> str: """ Specifies the internal name that is used by the connection when logging distributed transactions. """ self._verify_connected() return self._impl.get_internal_name() @internal_name.setter def internal_name(self, value: str) -> None: self._verify_connected() self._impl.set_internal_name(value) def is_healthy(self) -> bool: """ Returns a boolean indicating the health status of a connection. Connections may become unusable in several cases, such as if the network socket is broken, if an Oracle error indicates the connection is unusable, or after receiving a planned down notification from the database. This function is best used before starting a new database request on an existing standalone connection. Pooled connections internally perform this check before returning a connection to the application. If this function returns False, the connection should be not be used by the application and a new connection should be established instead. This function performs a local check. To fully check a connection's health, use ping() which performs a round-trip to the database. """ return self._impl is not None and self._impl.get_is_healthy() @property def ltxid(self) -> bytes: """ Returns the logical transaction id for the connection. It is used within Oracle Transaction Guard as a means of ensuring that transactions are not duplicated. See the Oracle documentation and the provided sample for more information. """ self._verify_connected() return self._impl.get_ltxid() @property def maxBytesPerCharacter(self) -> int: """ Deprecated. Use the constant value 4 instead. """ return 4 @property def module(self) -> None: raise AttributeError("module is not readable") @module.setter def module(self, value: str) -> None: """ Specifies the module column in the v$session table. The maximum length for this string is 48 and if you exceed this length you will get ORA-24960. """ self._verify_connected() self._impl.set_module(value) def msgproperties(self, payload: Union[bytes, DbObject]=None, correlation: str=None, delay: int=None, exceptionq: str=None, expiration: int=None, priority: int=None, recipients: list=None) -> MessageProperties: """ Create and return a message properties object. If the parameters are not None, they act as a shortcut for setting each of the equivalently named properties. """ impl = self._impl.create_msg_props_impl() props = MessageProperties._from_impl(impl) if payload is not None: props.payload = payload if correlation is not None: props.correlation = correlation if delay is not None: props.delay = delay if exceptionq is not None: props.exceptionq = exceptionq if expiration is not None: props.expiration = expiration if priority is not None: props.priority = priority if recipients is not None: props.recipients = recipients return props @property def nencoding(self) -> str: """ Specifies the IANA character set name of the national character set in use. This is always the value "UTF-8". """ return "UTF-8" @property def outputtypehandler(self) -> Callable: """ Specifies a method called for each column that is going to be fetched from any cursor associated with this connection. The method signature is handler(cursor, name, defaultType, length, precision, scale) and the return value is expected to be a variable object or None in which case a default variable object will be created. If this attribute is None, the default behavior will take place for all columns fetched from cursors associated with this connection. """ self._verify_connected() return self._impl.outputtypehandler @outputtypehandler.setter def outputtypehandler(self, value: Callable) -> None: self._verify_connected() self._impl.outputtypehandler = value def ping(self) -> None: """ Pings the database to verify the connection is valid. """ self._verify_connected() self._impl.ping() def prepare(self) -> bool: """ Deprecated. Use tpc_prepare() instead. """ return self.tpc_prepare() def queue(self, name: str, payload_type: Union[DbObjectType, str]=None, *, payloadType: DbObjectType=None) -> Queue: """ Creates and returns a queue which is used to enqueue and dequeue messages in Advanced Queueing (AQ). The name parameter is expected to be a string identifying the queue in which messages are to be enqueued or dequeued. The payload_type parameter, if specified, is expected to be an object type that identifies the type of payload the queue expects. If the string "JSON" is specified, JSON data is enqueued and dequeued. If not specified, RAW data is enqueued and dequeued. """ self._verify_connected() payload_type_impl = None is_json = False if payloadType is not None: if payload_type is not None: errors._raise_err(errors.ERR_DUPLICATED_PARAMETER, deprecated_name="payloadType", new_name="payload_type") payload_type = payloadType if payload_type is not None: if payload_type == "JSON": is_json = True elif not isinstance(payload_type, DbObjectType): raise TypeError("expecting DbObjectType") else: payload_type_impl = payload_type._impl impl = self._impl.create_queue_impl() impl.initialize(self._impl, name, payload_type_impl, is_json) return Queue._from_impl(self, impl) def rollback(self) -> None: """ Rolls back any pending transactions. """ self._verify_connected() self._impl.rollback() def shutdown(self, mode: int=0) -> None: """ Shutdown the database. In order to do this the connection must be connected as SYSDBA or SYSOPER. Two calls must be made unless the mode specified is DBSHUTDOWN_ABORT. """ self._verify_connected() self._impl.shutdown(mode) def startup(self, force: bool=False, restrict: bool=False, pfile: str=None) -> None: """ Startup the database. This is equivalent to the SQL*Plus command “startup nomount”. The connection must be connected as SYSDBA or SYSOPER with the PRELIM_AUTH option specified for this to work. The pfile parameter, if specified, is expected to be a string identifying the location of the parameter file (PFILE) which will be used instead of the stored parameter file (SPFILE). """ self._verify_connected() self._impl.startup(force, restrict, pfile) @property def stmtcachesize(self) -> int: """ Specifies the size of the statement cache. This value can make a significant difference in performance (up to 100x) if you have a small number of statements that you execute repeatedly. """ self._verify_connected() return self._impl.get_stmt_cache_size() @stmtcachesize.setter def stmtcachesize(self, value: int) -> None: self._verify_connected() self._impl.set_stmt_cache_size(value) def subscribe(self, namespace: int=constants.SUBSCR_NAMESPACE_DBCHANGE, protocol: int=constants.SUBSCR_PROTO_CALLBACK, callback: Callable=None, timeout: int=0, operations: int=constants.OPCODE_ALLOPS, port: int=0, qos: int=constants.SUBSCR_QOS_DEFAULT, ip_address: str=None, grouping_class: int=constants.SUBSCR_GROUPING_CLASS_NONE, grouping_value: int=0, grouping_type: int=constants.SUBSCR_GROUPING_TYPE_SUMMARY, name: str=None, client_initiated: bool=False, *, ipAddress: str=None, groupingClass: int=constants.SUBSCR_GROUPING_CLASS_NONE, groupingValue: int=0, groupingType: int=constants.SUBSCR_GROUPING_TYPE_SUMMARY, clientInitiated: bool=False) -> Subscription: """ Return a new subscription object that receives notification for events that take place in the database that match the given parameters. The namespace parameter specifies the namespace the subscription uses. It can be one of SUBSCR_NAMESPACE_DBCHANGE or SUBSCR_NAMESPACE_AQ. The protocol parameter specifies the protocol to use when notifications are sent. Currently the only valid value is SUBSCR_PROTO_CALLBACK. The callback is expected to be a callable that accepts a single parameter. A message object is passed to this callback whenever a notification is received. The timeout value specifies that the subscription expires after the given time in seconds. The default value of 0 indicates that the subscription never expires. The operations parameter enables filtering of the messages that are sent (insert, update, delete). The default value will send notifications for all operations. This parameter is only used when the namespace is set to SUBSCR_NAMESPACE_DBCHANGE. The port parameter specifies the listening port for callback notifications from the database server. If not specified, an unused port will be selected by the Oracle Client libraries. The qos parameter specifies quality of service options. It should be one or more of the following flags, OR'ed together: SUBSCR_QOS_RELIABLE, SUBSCR_QOS_DEREG_NFY, SUBSCR_QOS_ROWIDS, SUBSCR_QOS_QUERY, SUBSCR_QOS_BEST_EFFORT. The ip_address parameter specifies the IP address (IPv4 or IPv6) in standard string notation to bind for callback notifications from the database server. If not specified, the client IP address will be determined by the Oracle Client libraries. The grouping_class parameter specifies what type of grouping of notifications should take place. Currently, if set, this value can only be set to the value SUBSCR_GROUPING_CLASS_TIME, which will group notifications by the number of seconds specified in the grouping_value parameter. The grouping_type parameter should be one of the values SUBSCR_GROUPING_TYPE_SUMMARY (the default) or SUBSCR_GROUPING_TYPE_LAST. The name parameter is used to identify the subscription and is specific to the selected namespace. If the namespace parameter is SUBSCR_NAMESPACE_DBCHANGE then the name is optional and can be any value. If the namespace parameter is SUBSCR_NAMESPACE_AQ, however, the name must be in the format '' for single consumer queues and ':' for multiple consumer queues, and identifies the queue that will be monitored for messages. The queue name may include the schema, if needed. The client_initiated parameter is used to determine if client initiated connections or server initiated connections (the default) will be established. Client initiated connections are only available in Oracle Client 19.4 and Oracle Database 19.4 and higher. """ self._verify_connected() if ipAddress is not None: if ip_address is not None: errors._raise_err(errors.ERR_DUPLICATED_PARAMETER, deprecated_name="ipAddress", new_name="ip_address") ip_address = ipAddress if groupingClass != constants.SUBSCR_GROUPING_CLASS_NONE: if grouping_class != constants.SUBSCR_GROUPING_CLASS_NONE: errors._raise_err(errors.ERR_DUPLICATED_PARAMETER, deprecated_name="groupingClass", new_name="grouping_class") grouping_class = groupingClass if groupingValue != 0: if grouping_value != 0: errors._raise_err(errors.ERR_DUPLICATED_PARAMETER, deprecated_name="groupingValue", new_name="grouping_value") grouping_value = groupingValue if groupingType != constants.SUBSCR_GROUPING_TYPE_SUMMARY: if grouping_type != constants.SUBSCR_GROUPING_TYPE_SUMMARY: errors._raise_err(errors.ERR_DUPLICATED_PARAMETER, deprecated_name="groupingType", new_name="grouping_type") grouping_type = groupingType if clientInitiated: if client_initiated: errors._raise_err(errors.ERR_DUPLICATED_PARAMETER, deprecated_name="clientInitiated", new_name="client_initiated") client_initiated = clientInitiated impl = self._impl.create_subscr_impl(self, callback, namespace, name, protocol, ip_address, port, timeout, operations, qos, grouping_class, grouping_value, grouping_type, client_initiated) subscr = Subscription._from_impl(impl) impl.subscribe(subscr, self._impl) return subscr @property def tag(self) -> str: """ This property initially contains the actual tag of the session that was acquired from a pool. If the connection was not acquired from a pool or no tagging parameters were specified (tag and matchanytag) when the connection was acquired from the pool, this value will be None. If the value is changed, it must be a string containing name=value pairs like “k1=v1;k2=v2”. If this value is not None when the connection is released back to the pool it will be used to retag the session. This value can be overridden in the call to SessionPool.release(). """ self._verify_connected() return self._impl.tag @tag.setter def tag(self, value: str) -> None: self._verify_connected() self._impl.tag = value @property def thin(self) -> bool: """ Returns a boolean indicating if the connection was established in python-oracledb's thin mode (True) or thick mode (False). """ self._verify_connected() return isinstance(self._impl, thin_impl.ThinConnImpl) @property def tnsentry(self) -> str: """ Deprecated. Use dsn property instead. """ return self.dsn def tpc_begin(self, xid: Xid, flags: int=constants.TPC_BEGIN_NEW, timeout: int=0) -> None: """ Begins a TPC (two-phase commit) transaction with the given transaction id. This method should be called outside of a transaction (i.e. nothing may have executed since the last commit() or rollback() was performed). """ self._verify_connected() self._verify_xid(xid) self._impl.tpc_begin(xid, flags, timeout) def tpc_commit(self, xid: Xid=None, one_phase: bool=False) -> None: """ Prepare the global transaction for commit. Return a boolean indicating if a transaction was actually prepared in order to avoid the error ORA-24756 (transaction does not exist). When called with no arguments, commits a transaction previously prepared with tpc_prepare(). If tpc_prepare() is not called, a single phase commit is performed. A transaction manager may choose to do this if only a single resource is participating in the global transaction. When called with a transaction id, the database commits the given transaction. This form should be called outside of a transaction and is intended for use in recovery. """ self._verify_connected() if xid is not None: self._verify_xid(xid) self._impl.tpc_commit(xid, one_phase) def tpc_end(self, xid: Xid=None, flags: int=constants.TPC_END_NORMAL) -> None: """ Ends (detaches from) a TPC (two-phase commit) transaction. """ self._verify_connected() if xid is not None: self._verify_xid(xid) self._impl.tpc_end(xid, flags) def tpc_forget(self, xid: Xid) -> None: """ Forgets a TPC (two-phase commit) transaction. """ self._verify_connected() self._verify_xid(xid) self._impl.tpc_forget(xid) def tpc_prepare(self, xid: Xid=None) -> bool: """ Prepares a global transaction for commit. After calling this function, no further activity should take place on this connection until either tpc_commit() or tpc_rollback() have been called. A boolean is returned indicating whether a commit is needed or not. If a commit is performed when one is not needed the error ORA-24756: transaction does not exist is raised. """ self._verify_connected() if xid is not None: self._verify_xid(xid) return self._impl.tpc_prepare(xid) def tpc_recover(self) -> list: """ Returns a list of pending transaction ids suitable for use with tpc_commit() or tpc_rollback(). This function requires select privilege on the view DBA_PENDING_TRANSACTIONS. """ with self.cursor() as cursor: cursor.rowfactory = Xid cursor.execute(""" select formatid, globalid, branchid from dba_pending_transactions""") return cursor.fetchall() def tpc_rollback(self, xid: Xid=None) -> None: """ When called with no arguments, rolls back the transaction previously started with tpc_begin(). When called with a transaction id, the database rolls back the given transaction. This form should be called outside of a transaction and is intended for use in recovery. """ self._verify_connected() if xid is not None: self._verify_xid(xid) self._impl.tpc_rollback(xid) def unsubscribe(self, subscr: Subscription) -> None: """ Unsubscribe from events in the database that were originally subscribed to using subscribe(). The connection used to unsubscribe should be the same one used to create the subscription, or should access the same database and be connected as the same user name. """ self._verify_connected() if not isinstance(subscr, Subscription): raise TypeError("expecting subscription") subscr._impl.unsubscribe(self._impl) @property def username(self) -> str: """ Returns the name of the user which established the connection to the database. """ self._verify_connected() return self._impl.username @property def version(self) -> str: """ Returns the version of the database to which the connection has been established. """ if self._version is None: self._verify_connected() self._version = self._impl.get_version() return self._version def xid(self, format_id: int, global_transaction_id: Union[bytes, str], branch_qualifier: Union[bytes, str]) -> Xid: """ Returns a global transaction identifier that can be used with the TPC (two-phase commit) functions. The format_id parameter should be a non-negative 32-bit integer. The global_transaction_id and branch_qualifier parameters should be bytes (or a string which will be UTF-8 encoded to bytes) of no more than 64 bytes. """ return Xid(format_id, global_transaction_id, branch_qualifier) def _connection_factory(f): """ Decorator which checks the validity of the supplied keyword parameters by calling the original function (which does nothing), then creates and returns an instance of the requested Connection class. The base Connection class constructor does not check the validity of the supplied keyword parameters. """ @functools.wraps(f) def connect(dsn: str=None, *, pool: "pool_module.ConnectionPool"=None, conn_class: Type[Connection]=Connection, params: ConnectParams=None, **kwargs) -> Connection: f(dsn=dsn, pool=pool, conn_class=conn_class, params=params, **kwargs) if not issubclass(conn_class, Connection): errors._raise_err(errors.ERR_INVALID_CONN_CLASS) return conn_class(dsn=dsn, pool=pool, params=params, **kwargs) return connect @_connection_factory def connect(dsn: str=None, *, pool: "pool_module.ConnectionPool"=None, conn_class: Type[Connection]=Connection, params: ConnectParams=None, user: str=None, proxy_user: str=None, password: str=None, newpassword: str=None, wallet_password: str=None, access_token: Union[str, tuple, Callable]=None, host: str=None, port: int=1521, protocol: str="tcp", https_proxy: str=None, https_proxy_port: int=0, service_name: str=None, sid: str=None, server_type: str=None, cclass: str=None, purity: int=oracledb.PURITY_DEFAULT, expire_time: int=0, retry_count: int=0, retry_delay: int=0, tcp_connect_timeout: float=60.0, ssl_server_dn_match: bool=True, ssl_server_cert_dn: str=None, wallet_location: str=None, events: bool=False, externalauth: bool=False, mode: int=oracledb.AUTH_MODE_DEFAULT, disable_oob: bool=False, stmtcachesize: int=oracledb.defaults.stmtcachesize, edition: str=None, tag: str=None, matchanytag: bool=False, config_dir: str=oracledb.defaults.config_dir, appcontext: list=None, shardingkey: list=None, supershardingkey: list=None, debug_jdwp: str=None, handle: int=0, threaded: bool=True, encoding: str=None, nencoding: str=None ) -> Connection: """ Factory function which creates a connection to the database and returns it. The dsn parameter (data source name) can be a string in the format user/password@connect_string or can simply be the connect string (in which case authentication credentials such as the username and password need to be specified separately). See the documentation on connection strings for more information. The pool parameter is expected to be a pool object and the use of this parameter is the equivalent of calling pool.acquire(). The conn_class parameter is expected to be Connection or a subclass of Connection. The params parameter is expected to be of type ConnectParams and contains connection parameters that will be used when establishing the connection. See the documentation on ConnectParams for more information. If this parameter is not specified, the additional keyword parameters will be used to create an instance of ConnectParams. If both the params parameter and additional keyword parameters are specified, the values in the keyword parameters have precedence. Note that if a dsn is also supplied, then in the python-oracledb Thin mode, the values of the parameters specified (if any) within the dsn will override the values passed as additional keyword parameters, which themselves override the values set in the params parameter object. The following parameters are all optional. A brief description of each parameter follows: - user: the name of the user to connect to (default: None) - proxy_user: the name of the proxy user to connect to. If this value is not specified, it will be parsed out of user if user is in the form "user[proxy_user]" (default: None) - password: the password for the user (default: None) - newpassword: the new password for the user. The new password will take effect immediately upon a successful connection to the database (default: None) - wallet_password: the password to use to decrypt the wallet, if it is encrypted. This value is only used in thin mode (default: None) - access_token: expected to be a string or a 2-tuple or a callable. If it is a string, it specifies an Azure AD OAuth2 token used for Open Authorization (OAuth 2.0) token based authentication. If it is a 2-tuple, it specifies the token and private key strings used for Oracle Cloud Infrastructure (OCI) Identity and Access Management (IAM) token based authentication. If it is a callable, it returns either a string or a 2-tuple used for OAuth 2.0 or OCI IAM token based authentication and is useful when the pool needs to expand and create new connections but the current authentication token has expired (default: None) - host: the name or IP address of the machine hosting the database or the database listener (default: None) - port: the port number on which the database listener is listening (default: 1521) - protocol: one of the strings "tcp" or "tcps" indicating whether to use unencrypted network traffic or encrypted network traffic (TLS) (default: "tcp") - https_proxy: the name or IP address of a proxy host to use for tunneling secure connections (default: None) - https_proxy_port: the port on which to communicate with the proxy host (default: 0) - service_name: the service name of the database (default: None) - sid: the system identifier (SID) of the database. Note using a service_name instead is recommended (default: None) - server_type: the type of server connection that should be established. If specified, it should be one of "dedicated", "shared" or "pooled" (default: None) - cclass: connection class to use for Database Resident Connection Pooling (DRCP) (default: None) - purity: purity to use for Database Resident Connection Pooling (DRCP) (default: oracledb.PURITY_DEFAULT) - expire_time: an integer indicating the number of minutes between the sending of keepalive probes. If this parameter is set to a value greater than zero it enables keepalive (default: 0) - retry_count: the number of times that a connection attempt should be retried before the attempt is terminated (default: 0) - retry_delay: the number of seconds to wait before making a new connection attempt (default: 0) - tcp_connect_timeout: a float indicating the maximum number of seconds to wait for establishing a connection to the database host (default: 60.0) - ssl_server_dn_match: boolean indicating whether the server certificate distinguished name (DN) should be matched in addition to the regular certificate verification that is performed. Note that if the ssl_server_cert_dn parameter is not privided, host name matching is performed instead (default: True) - ssl_server_cert_dn: the distinguished name (DN) which should be matched with the server. This value is ignored if the ssl_server_dn_match parameter is not set to the value True. If specified this value is used for any verfication. Otherwise the hostname will be used. (default: None) - wallet_location: the directory where the wallet can be found. In thin mode this must be the directory containing the PEM-encoded wallet file ewallet.pem. In thick mode this must be the directory containing the file cwallet.sso (default: None) - events: boolean specifying whether events mode should be enabled. This value is only used in thick mode and is needed for continuous query notification and high availability event notifications (default: False) - externalauth: a boolean indicating whether to use external authentication (default: False) - mode: authorization mode to use. For example oracledb.AUTH_MODE_SYSDBA (default: oracledb.AUTH_MODE_DEFAULT) - disable_oob: boolean indicating whether out-of-band breaks should be disabled. This value is only used in thin mode. It has no effect on Windows which does not support this functionality (default: False) - stmtcachesize: identifies the initial size of the statement cache (default: oracledb.defaults.stmtcachesize) - edition: edition to use for the connection. This parameter cannot be used simultaneously with the cclass parameter (default: None) - tag: identifies the type of connection that should be returned from a pool. This value is only used in thick mode (default: None) - matchanytag: boolean specifying whether any tag can be used when acquiring a connection from the pool. This value is only used in thick mode. (default: False) - config_dir: directory in which the optional tnsnames.ora configuration file is located. This value is only used in thin mode. For thick mode use the config_dir parameter of init_oracle_client() (default: oracledb.defaults.config_dir) - appcontext: application context used by the connection. It should be a list of 3-tuples (namespace, name, value) and each entry in the tuple should be a string. This value is only used in thick mode (default: None) - shardingkey: a list of strings, numbers, bytes or dates that identify the database shard to connect to. This value is only used in thick mode (default: None) - supershardingkey: a list of strings, numbers, bytes or dates that identify the database shard to connect to. This value is only used in thick mode (default: None) - debug_jdwp: a string with the format "host=;port=" that specifies the host and port of the PL/SQL debugger. This value is only used in thin mode. For thick mode set the ORA_DEBUG_JDWP environment variable (default: None) - handle: an integer representing a pointer to a valid service context handle. This value is only used in thick mode. It should be used with extreme caution (default: 0) """ pass python-oracledb-1.2.1/src/oracledb/constants.py000066400000000000000000000076371434177474600215730ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # constants.py # # Contains the constants defined by the package. #------------------------------------------------------------------------------ # mandated DB API constants apilevel = "2.0" threadsafety = 2 paramstyle = "named" # authentication modes AUTH_MODE_DEFAULT = 0 AUTH_MODE_PRELIM = 0x00000008 AUTH_MODE_SYSASM = 0x00008000 AUTH_MODE_SYSBKP = 0x00020000 AUTH_MODE_SYSDBA = 0x00000002 AUTH_MODE_SYSDGD = 0x00040000 AUTH_MODE_SYSKMT = 0x00080000 AUTH_MODE_SYSOPER = 0x00000004 AUTH_MODE_SYSRAC = 0x00100000 # pool "get" modes POOL_GETMODE_WAIT = 0 POOL_GETMODE_NOWAIT = 1 POOL_GETMODE_FORCEGET = 2 POOL_GETMODE_TIMEDWAIT = 3 # purity values PURITY_DEFAULT = 0 PURITY_NEW = 1 PURITY_SELF = 2 # AQ delivery modes MSG_BUFFERED = 2 MSG_PERSISTENT = 1 MSG_PERSISTENT_OR_BUFFERED = 3 # AQ deque modes DEQ_BROWSE = 1 DEQ_LOCKED = 2 DEQ_REMOVE = 3 DEQ_REMOVE_NODATA = 4 # AQ dequeue navigation modes DEQ_FIRST_MSG = 1 DEQ_NEXT_MSG = 3 DEQ_NEXT_TRANSACTION = 2 # AQ dequeue visibility modes DEQ_IMMEDIATE = 1 DEQ_ON_COMMIT = 2 # AQ dequeue wait modes DEQ_NO_WAIT = 0 DEQ_WAIT_FOREVER = 2 ** 32 - 1 # AQ enqueue visibility modes ENQ_IMMEDIATE = 1 ENQ_ON_COMMIT = 2 # AQ message states MSG_EXPIRED = 3 MSG_PROCESSED = 2 MSG_READY = 0 MSG_WAITING = 1 # AQ other constants MSG_NO_DELAY = 0 MSG_NO_EXPIRATION = -1 # shutdown modes DBSHUTDOWN_ABORT = 4 DBSHUTDOWN_FINAL = 5 DBSHUTDOWN_IMMEDIATE = 3 DBSHUTDOWN_TRANSACTIONAL = 1 DBSHUTDOWN_TRANSACTIONAL_LOCAL = 2 # subscription grouping classes SUBSCR_GROUPING_CLASS_NONE = 0 SUBSCR_GROUPING_CLASS_TIME = 1 # subscription grouping types SUBSCR_GROUPING_TYPE_SUMMARY = 1 SUBSCR_GROUPING_TYPE_LAST = 2 # subscription namespaces SUBSCR_NAMESPACE_AQ = 1 SUBSCR_NAMESPACE_DBCHANGE = 2 # subscription protocols SUBSCR_PROTO_HTTP = 3 SUBSCR_PROTO_MAIL = 1 SUBSCR_PROTO_CALLBACK = 0 SUBSCR_PROTO_SERVER = 2 # subscription quality of service SUBSCR_QOS_BEST_EFFORT = 0x10 SUBSCR_QOS_DEFAULT = 0 SUBSCR_QOS_DEREG_NFY = 0x02 SUBSCR_QOS_QUERY = 0x08 SUBSCR_QOS_RELIABLE = 0x01 SUBSCR_QOS_ROWIDS = 0x04 # event types EVENT_AQ = 100 EVENT_DEREG = 5 EVENT_NONE = 0 EVENT_OBJCHANGE = 6 EVENT_QUERYCHANGE = 7 EVENT_SHUTDOWN = 2 EVENT_SHUTDOWN_ANY = 3 EVENT_STARTUP = 1 # operation codes OPCODE_ALLOPS = 0 OPCODE_ALLROWS = 0x01 OPCODE_ALTER = 0x10 OPCODE_DELETE = 0x08 OPCODE_DROP = 0x20 OPCODE_INSERT = 0x02 OPCODE_UPDATE = 0x04 # flags for tpc_begin() TPC_BEGIN_JOIN = 0x00000002 TPC_BEGIN_NEW = 0x00000001 TPC_BEGIN_PROMOTE = 0x00000008 TPC_BEGIN_RESUME = 0x00000004 # flags for tpc_end() TPC_END_NORMAL = 0 TPC_END_SUSPEND = 0x00100000 # basic configuration constants DRIVER_NAME = "python-oracledb" INSTALLATION_URL = "https://python-oracledb.readthedocs.io/en/" \ "latest/user_guide/installation.html" ENCODING = "UTF-8" python-oracledb-1.2.1/src/oracledb/constructors.py000066400000000000000000000055351434177474600223220ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # constructors.py # # Contains the constructors mandated by the Python Database API. #------------------------------------------------------------------------------ import datetime from . import errors # synonyms for the types mandated by the database API Binary = bytes Date = datetime.date Timestamp = datetime.datetime def Time(hour: int, minute: int, second: int) -> None: """ Constructor mandated by the database API for creating a time value. Since Oracle doesn't support time only values, an exception is raised when this method is called. """ errors._raise_err(errors.ERR_TIME_NOT_SUPPORTED) def DateFromTicks(ticks: float) -> datetime.date: """ Constructor mandated by the database API for creating a date value given the number of seconds since the epoch (January 1, 1970). This is equivalent to using datetime.date.fromtimestamp() and that should be used instead. """ return datetime.date.fromtimestamp(ticks) def TimeFromTicks(ticks: float) -> None: """ Constructor mandated by the database API for creating a time value given the number of seconds since the epoch (January 1, 1970). Since Oracle doesn't support time only values, an exception is raised when this method is called. """ errors._raise_err(errors.ERR_TIME_NOT_SUPPORTED) def TimestampFromTicks(ticks: float) -> datetime.datetime: """ Constructor mandated by the database API for creating a timestamp value given the number of seconds since the epoch (January 1, 1970). This is equivalent to using datetime.datetime.fromtimestamp() and that should be used instead. """ return datetime.datetime.fromtimestamp(ticks) python-oracledb-1.2.1/src/oracledb/cursor.py000066400000000000000000001046621434177474600210700ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # cursor.py # # Contains the Cursor class used for executing statements on connections and # fetching results from queries. #------------------------------------------------------------------------------ from typing import Any, Union, Callable from . import __name__ as MODULE_NAME from . import errors, exceptions from . import connection as connection_module from .defaults import defaults from .var import Var from .base_impl import DbType, DB_TYPE_OBJECT from .dbobject import DbObjectType class Cursor: __module__ = MODULE_NAME def __init__(self, connection: "connection_module.Connection", scrollable: bool = False) -> None: self._impl = None self.connection = connection self.statement = None self._set_input_sizes = False self._impl = connection._impl.create_cursor_impl() self._impl.scrollable = scrollable self._impl.arraysize = defaults.arraysize self._impl.prefetchrows = defaults.prefetchrows def __del__(self): if self._impl is not None: self._impl.close(in_del=True) def __enter__(self): self._verify_open() return self def __exit__(self, exc_type, exc_value, exc_tb): self._verify_open() self._impl.close(in_del=True) self._impl = None def __iter__(self): return self def __next__(self): self._verify_fetch() row = self._impl.fetch_next_row(self) if row is not None: return row raise StopIteration def __repr__(self): return f"" def _call(self, name: str, parameters: Union[list, tuple], keyword_parameters: dict, return_value: str=None) -> None: """ Internal method used for generating the PL/SQL block used to call stored procedures. """ # verify parameters if parameters is not None \ and not isinstance(parameters, (list, tuple)): errors._raise_err(errors.ERR_ARGS_MUST_BE_LIST_OR_TUPLE) if keyword_parameters is not None \ and not isinstance(keyword_parameters, dict): errors._raise_err(errors.ERR_KEYWORD_ARGS_MUST_BE_DICT) self._verify_open() # build and execute statement bind_names = [] bind_values = [] statement_parts = ["begin "] if return_value is not None: statement_parts.append(":retval := ") bind_values.append(return_value) statement_parts.append(name + "(") if parameters: bind_values.extend(parameters) bind_names = [":%d" % (i + 1) for i in range(len(parameters))] if keyword_parameters: for arg_name, arg_value in keyword_parameters.items(): bind_values.append(arg_value) bind_names.append(f"{arg_name} => :{len(bind_names) + 1}") statement_parts.append(",".join(bind_names)) statement_parts.append("); end;") statement = "".join(statement_parts) self.execute(statement, bind_values) def _get_oci_attr(self, attr_num: int, attr_type: int) -> Any: """ Returns the value of the specified OCI attribute from the internal handle. This is only supported in python-oracledb's thick mode and should only be used as directed by Oracle. """ self._verify_open() return self._impl._get_oci_attr(attr_num, attr_type) def _prepare(self, statement: str, tag: str=None, cache_statement: bool=True) -> None: """ Internal method used for preparing a statement for execution. """ self._impl.fetch_vars = None if not self._set_input_sizes: self._impl.bind_vars = None self._impl.bind_vars_by_name = None self._impl.bind_style = None self._impl.prepare(statement, tag, cache_statement) self.statement = statement self._impl.rowfactory = None def _set_oci_attr(self, attr_num: int, attr_type: int, value: Any) -> None: """ Sets the value of the specified OCI attribute on the internal handle. This is only supported in python-oracledb's thick mode and should only be used as directed by Oracle. """ self._verify_open() self._impl._set_oci_attr(attr_num, attr_type, value) def _verify_fetch(self) -> None: """ Verifies that fetching is possible from this cursor. """ self._verify_open() if not self._impl.is_query(self): errors._raise_err(errors.ERR_NOT_A_QUERY) def _verify_open(self) -> None: """ Verifies that the cursor is open and the associated connection is connected. If either condition is false an exception is raised. """ if self._impl is None: errors._raise_err(errors.ERR_CURSOR_NOT_OPEN) self.connection._verify_connected() @property def arraysize(self) -> int: """ Tunes the number of rows fetched and buffered by internal calls to the database when fetching rows from SELECT statements and REF CURSORS. The value can drastically affect the performance of a query since it directly affects the number of network round trips between Python and the database. For methods like fetchone() and fetchall() it does not change how many rows are returned to the application. For fetchmany() it is the default number of rows to fetch. Due to the performance benefits, the default value is 100 instead of the 1 that the DB API recommends. This value means that 100 rows are fetched by each internal call to the database. """ self._verify_open() return self._impl.arraysize @arraysize.setter def arraysize(self, value: int) -> None: self._verify_open() self._impl.arraysize = value def arrayvar(self, typ: Union[DbType, DbObjectType, type], value: Union[list, int], size: int=0) -> Var: """ Create an array variable associated with the cursor of the given type and size and return a variable object. The value is either an integer specifying the number of elements to allocate or it is a list and the number of elements allocated is drawn from the size of the list. If the value is a list, the variable is also set with the contents of the list. If the size is not specified and the type is a string or binary, 4000 bytes is allocated. This is needed for passing arrays to PL/SQL (in cases where the list might be empty and the type cannot be determined automatically) or returning arrays from PL/SQL. Array variables can only be used for PL/SQL associative arrays with contiguous keys. For PL/SQL associative arrays with sparsely populated keys or for varrays and nested tables, the DbObject approach needs to be used instead. """ self._verify_open() if isinstance(value, list): num_elements = len(value) elif isinstance(value, int): num_elements = value else: raise TypeError("expecting integer or list of values") var = self._impl.create_var(self.connection, typ, size=size, num_elements=num_elements, is_array=True) if isinstance(value, list): var.setvalue(0, value) return var def bindnames(self) -> list: """ Return the list of bind variable names bound to the statement. Note that a statement must have been prepared first. """ self._verify_open() if self.statement is None: errors._raise_err(errors.ERR_NO_STATEMENT_PREPARED) return self._impl.get_bind_names() @property def bindvars(self) -> list: """ Returns the bind variables used for the last execute. The value will be either a list or a dictionary depending on whether binding was done by position or name. Care should be taken when referencing this attribute. In particular, elements should not be removed or replaced. """ self._verify_open() return self._impl.get_bind_vars() def callfunc(self, name: str, return_type: object, parameters: Union[list, tuple]=None, keyword_parameters: dict=None, *, keywordParameters: dict=None) -> object: """ Call a function with the given name. The return type is specified in the same notation as is required by setinputsizes(). The sequence of parameters must contain one entry for each parameter that the function expects. Any keyword parameters will be included after the positional parameters. The result of the call is the return value of the function. """ var = self.var(return_type) if keywordParameters is not None: if keyword_parameters is not None: errors._raise_err(errors.ERR_DUPLICATED_PARAMETER, deprecated_name="keywordParameters", new_name="keyword_parameters") keyword_parameters = keywordParameters self._call(name, parameters, keyword_parameters, var) return var.getvalue() def callproc(self, name: str, parameters: Union[list, tuple]=None, keyword_parameters: dict=None, *, keywordParameters: dict=None) -> list: """ Call a procedure with the given name. The sequence of parameters must contain one entry for each parameter that the procedure expects. The result of the call is a modified copy of the input sequence. Input parameters are left untouched; output and input/output parameters are replaced with possibly new values. Keyword parameters will be included after the positional parameters and are not returned as part of the output sequence. """ if keywordParameters is not None: if keyword_parameters is not None: errors._raise_err(errors.ERR_DUPLICATED_PARAMETER, deprecated_name="keywordParameters", new_name="keyword_parameters") keyword_parameters = keywordParameters self._call(name, parameters, keyword_parameters) if parameters is None: return [] return [v.get_value(0) for v in self._impl.bind_vars[:len(parameters)]] def close(self) -> None: """ Close the cursor now, rather than whenever __del__ is called. The cursor will be unusable from this point forward; an Error exception will be raised if any operation is attempted with the cursor. """ self._verify_open() self._impl.close() self._impl = None @property def description(self) -> tuple: """ Returns a sequence of 7-item sequences. Each of these sequences contains information describing one result column: (name, type, display_size, internal_size, precision, scale, null_ok). This attribute will be None for operations that do not return rows or if the cursor has not had an operation invoked via the execute() method yet. """ self._verify_open() if self._impl.is_query(self): return self._impl.get_description() def execute(self, statement: Union[str, None], parameters: Union[list, tuple, dict]=None, **keyword_parameters: dict) -> Union["Cursor", None]: """ Execute a statement against the database. Parameters may be passed as a dictionary or sequence or as keyword parameters. If the parameters are a dictionary, the values will be bound by name and if the parameters are a sequence the values will be bound by position. Note that if the values are bound by position, the order of the variables is from left to right as they are encountered in the statement and SQL statements are processed differently than PL/SQL statements. For this reason, it is generally recommended to bind parameters by name instead of by position. Parameters passed as a dictionary are name and value pairs. The name maps to the bind variable name used by the statement and the value maps to the Python value you wish bound to that bind variable. A reference to the statement will be retained by the cursor. If None or the same string object is passed in again, the cursor will execute that statement again without performing a prepare or rebinding and redefining. This is most effective for algorithms where the same statement is used, but different parameters are bound to it (many times). Note that parameters that are not passed in during subsequent executions will retain the value passed in during the last execution that contained them. For maximum efficiency when reusing an statement, it is best to use the setinputsizes() method to specify the parameter types and sizes ahead of time; in particular, None is assumed to be a string of length 1 so any values that are later bound as numbers or dates will raise a TypeError exception. If the statement is a query, the cursor is returned as a convenience to the caller (so it can be used directly as an iterator over the rows in the cursor); otherwise, None is returned. """ # verify parameters if statement is None and self.statement is None: errors._raise_err(errors.ERR_NO_STATEMENT) if keyword_parameters: if parameters: errors._raise_err(errors.ERR_ARGS_AND_KEYWORD_ARGS) parameters = keyword_parameters elif parameters is not None \ and not isinstance(parameters, (list, tuple, dict)): errors._raise_err(errors.ERR_WRONG_EXECUTE_PARAMETERS_TYPE) self._verify_open() impl = self._impl bind_vars = impl.bind_vars bind_style = impl.bind_style prepare_needed = statement and statement != self.statement if not (prepare_needed and not self._set_input_sizes) \ and bind_vars is not None and parameters is not None: if bind_style is dict and not isinstance(parameters, dict) \ or bind_style is not dict and isinstance(parameters, dict): errors._raise_err(errors.ERR_MIXED_POSITIONAL_AND_NAMED_BINDS) # prepare statement, if necessary if prepare_needed: self._prepare(statement) # perform bind and execute self._set_input_sizes = False if parameters is not None: impl.bind_one(self, parameters) impl.execute(self) if impl.fetch_vars is not None: return self def executemany(self, statement: Union[str, None], parameters: Union[list, int], batcherrors: bool=False, arraydmlrowcounts: bool=False) -> None: """ Prepare a statement for execution against a database and then execute it against all parameter mappings or sequences found in the sequence parameters. The statement is managed in the same way as the execute() method manages it. If the size of the buffers allocated for any of the parameters exceeds 2 GB and you are using the thick implementation, you will receive the error “DPI-1015: array size of is too large”, where varies with the size of each element being allocated in the buffer. If you receive this error, decrease the number of elements in the sequence parameters. If there are no parameters, or parameters have previously been bound, the number of iterations can be specified as an integer instead of needing to provide a list of empty mappings or sequences. When true, the batcherrors parameter enables batch error support within Oracle and ensures that the call succeeds even if an exception takes place in one or more of the sequence of parameters. The errors can then be retrieved using getbatcherrors(). When true, the arraydmlrowcounts parameter enables DML row counts to be retrieved from Oracle after the method has completed. The row counts can then be retrieved using getarraydmlrowcounts(). Both the batcherrors parameter and the arraydmlrowcounts parameter can only be true when executing an insert, update, delete or merge statement; in all other cases an error will be raised. For maximum efficiency, it is best to use the setinputsizes() method to specify the parameter types and sizes ahead of time; in particular, None is assumed to be a string of length 1 so any values that are later bound as numbers or dates will raise a TypeError exception. """ # verify parameters if statement is None and self.statement is None: errors._raise_err(errors.ERR_NO_STATEMENT) if not isinstance(parameters, (list, int)): errors._raise_err(errors.ERR_WRONG_EXECUTEMANY_PARAMETERS_TYPE) # prepare statement, if necessary self._verify_open() if statement and statement != self.statement: self._prepare(statement) # perform bind and execute self._set_input_sizes = False if isinstance(parameters, int): num_execs = parameters else: num_execs = len(parameters) if num_execs > 0: self._impl.bind_many(self, parameters) self._impl.executemany(self, num_execs, bool(batcherrors), bool(arraydmlrowcounts)) def fetchall(self) -> list: """ Fetch all (remaining) rows of a query result, returning them as a list of tuples. An empty list is returned if no more rows are available. Note that the cursor’s arraysize attribute can affect the performance of this operation, as internally reads from the database are done in batches corresponding to the arraysize. An exception is raised if the previous call to execute() did not produce any result set or no call was issued yet. """ self._verify_fetch() result = [] fetch_next_row = self._impl.fetch_next_row while True: row = fetch_next_row(self) if row is None: break result.append(row) return result def fetchmany(self, size: int=None, numRows: int=None) -> list: """ Fetch the next set of rows of a query result, returning a list of tuples. An empty list is returned if no more rows are available. Note that the cursor’s arraysize attribute can affect the performance of this operation. The number of rows to fetch is specified by the parameter (the second parameter is retained for backwards compatibility and should not be used). If it is not given, the cursor’s arraysize attribute determines the number of rows to be fetched. If the number of rows available to be fetched is fewer than the amount requested, fewer rows will be returned. An exception is raised if the previous call to execute() did not produce any result set or no call was issued yet. """ self._verify_fetch() if size is None: if numRows is not None: size = numRows else: size = self._impl.arraysize elif numRows is not None: errors._raise_err(errors.ERR_DUPLICATED_PARAMETER, deprecated_name="numRows", new_name="size") result = [] fetch_next_row = self._impl.fetch_next_row while len(result) < size: row = fetch_next_row(self) if row is None: break result.append(row) return result def fetchone(self) -> object: """ Fetch the next row of a query result set, returning a single tuple or None when no more data is available. An exception is raised if the previous call to execute() did not produce any result set or no call was issued yet. """ self._verify_fetch() return self._impl.fetch_next_row(self) @property def fetchvars(self) -> list: """ Specifies the list of variables created for the last query that was executed on the cursor. Care should be taken when referencing this attribute. In particular, elements should not be removed or replaced. """ self._verify_open() return self._impl.fetch_vars def getarraydmlrowcounts(self) -> list: """ Return the DML row counts after a call to executemany() with arraydmlrowcounts enabled. This will return a list of integers corresponding to the number of rows affected by the DML statement for each element of the array passed to executemany(). """ self._verify_open() return self._impl.get_array_dml_row_counts() def getbatcherrors(self) -> list: """ Return the exceptions that took place after a call to executemany() with batcherrors enabled. This will return a list of Error objects, one error for each iteration that failed. The offset can be determined by looking at the offset attribute of the error object. """ self._verify_open() return self._impl.get_batch_errors() def getimplicitresults(self) -> list: """ Return a list of cursors which correspond to implicit results made available from a PL/SQL block or procedure without the use of OUT ref cursor parameters. The PL/SQL block or procedure opens the cursors and marks them for return to the client using the procedure dbms_sql.return_result. Cursors returned in this fashion should not be closed. They will be closed automatically by the parent cursor when it is closed. Closing the parent cursor will invalidate the cursors returned by this method. """ self._verify_open() return self._impl.get_implicit_results(self.connection) @property def inputtypehandler(self) -> Callable: """ Specifies a method called for each value that is bound to a statement executed on this cursor. The method signature is handler(cursor, value, arraysize) and the return value is expected to be a variable object or None in which case a default variable object will be created. If this attribute is None, the default behavior will take place for all values bound to statements. """ self._verify_open() return self._impl.inputtypehandler @inputtypehandler.setter def inputtypehandler(self, value: Callable) -> None: self._verify_open() self._impl.inputtypehandler = value @property def lastrowid(self) -> str: """ Returns the rowid of the last row modified by the cursor. If no row was modified by the last operation performed on the cursor, the value None is returned. """ self._verify_open() return self._impl.get_lastrowid() @property def outputtypehandler(self) -> Callable: """ Specifies a method called for each column that is going to be fetched from this cursor. The method signature is handler(cursor, name, defaultType, length, precision, scale) and the return value is expected to be a variable object or None in which case a default variable object will be created. If this attribute is None, the default behavior will take place for all columns fetched from this cursor. """ self._verify_open() return self._impl.outputtypehandler @outputtypehandler.setter def outputtypehandler(self, value: Callable) -> None: self._verify_open() self._impl.outputtypehandler = value def parse(self, statement: str) -> None: """ This can be used to parse a statement without actually executing it (this step is done automatically by Oracle when a statement is executed). """ self._verify_open() self._prepare(statement) self._impl.parse(self) @property def prefetchrows(self) -> int: """ Used to tune the number of rows fetched when a SELECT statement is executed. This value can reduce the number of round-trips to the database that are required to fetch rows but at the cost of additional memory. Setting this value to 0 can be useful when the timing of fetches must be explicitly controlled. """ self._verify_open() return self._impl.prefetchrows @prefetchrows.setter def prefetchrows(self, value: int) -> None: self._verify_open() self._impl.prefetchrows = value def prepare(self, statement: str, tag: str=None, cache_statement: bool=True) -> None: """ This can be used before a call to execute() to define the statement that will be executed. When this is done, the prepare phase will not be performed when the call to execute() is made with None or the same string object as the statement. If the tag parameter is specified and the cache_statement parameter is True, the statement will be returned to the statement cache with the given tag. If the cache_statement parameter is False, the statement will be removed from the statement cache (if it was found there) or will simply not be cached. See the Oracle documentation for more information about the statement cache. """ self._verify_open() self._prepare(statement, tag, cache_statement) @property def rowcount(self) -> int: """ This read-only attribute specifies the number of rows that have currently been fetched from the cursor (for select statements), that have been affected by the operation (for insert, update, delete and merge statements), or the number of successful executions of the statement (for PL/SQL statements). """ if self._impl is not None and self.connection._impl is not None: return self._impl.rowcount return -1 @property def rowfactory(self) -> Callable: """ Specifies a method to call for each row that is retrieved from the database. Ordinarily a tuple is returned for each row but if this attribute is set, the method is called with the tuple that would normally be returned, and the result of the method is returned instead. """ self._verify_open() return self._impl.rowfactory @rowfactory.setter def rowfactory(self, value: Callable) -> None: self._verify_open() self._impl.rowfactory = value def scroll(self, value: int=0, mode: str="relative") -> None: """ Scroll the cursor in the result set to a new position according to the mode. If mode is “relative” (the default value), the value is taken as an offset to the current position in the result set. If set to “absolute”, value states an absolute target position. If set to “first”, the cursor is positioned at the first row and if set to “last”, the cursor is set to the last row in the result set. An error is raised if the mode is “relative” or “absolute” and the scroll operation would position the cursor outside of the result set. """ self._verify_open() self._impl.scroll(self.connection, value, mode) @property def scrollable(self) -> bool: """ Specifies whether the cursor can be scrolled or not. By default, cursors are not scrollable, as the server resources and response times are greater than for nonscrollable cursors. This attribute is checked and the corresponding mode set in Oracle when calling the method execute(). """ self._verify_open() return self._impl.scrollable @scrollable.setter def scrollable(self, value: bool) -> None: self._verify_open() self._impl.scrollable = value def setinputsizes(self, *args: tuple, **kwargs: dict) -> Union[list, dict]: """ This can be used before a call to execute(), callfunc() or callproc() to predefine memory areas for the operation’s parameters. Each parameter should be a type object corresponding to the input that will be used or it should be an integer specifying the maximum length of a string parameter. Use keyword parameters when binding by name and positional parameters when binding by position. The singleton None can be used as a parameter when using positional parameters to indicate that no space should be reserved for that position. """ if args and kwargs: errors._raise_err(errors.ERR_ARGS_AND_KEYWORD_ARGS) elif args or kwargs: self._verify_open() self._impl.setinputsizes(self.connection, args, kwargs) self._set_input_sizes = True return self._impl.get_bind_vars() return [] def setoutputsize(self, size: int, column: int=0) -> None: """ Sets a column buffer size for fetches of long columns. However python-oracledb does not require it so this method does nothing. """ pass def var(self, typ: Union[DbType, DbObjectType, type], size: int=0, arraysize: int=1, inconverter: Callable=None, outconverter: Callable=None, typename: str=None, encoding_errors: str=None, bypass_decode: bool=False, *, encodingErrors: str=None) -> "Var": """ Create a variable with the specified characteristics. This method was designed for use with PL/SQL in/out variables where the length or type cannot be determined automatically from the Python object passed in or for use in input and output type handlers defined on cursors or connections. The typ parameter specifies the type of data that should be stored in the variable. This should be one of the database type constants, DB API constants, an object type returned from the method Connection.gettype() or one of the following Python types: Python Type Database Type bool DB_TYPE_BOOLEAN bytes DB_TYPE_RAW datetime.date DB_TYPE_DATE datetime.datetime DB_TYPE_DATE datetime.timedelta DB_TYPE_INTERVAL_DS decimal.Decimal DB_TYPE_NUMBER float DB_TYPE_NUMBER int DB_TYPE_NUMBER str DB_TYPE_VARCHAR The size parameter specifies the length of string and raw variables and is ignored in all other cases. If not specified for string and raw variables, the value 4000 is used. The arraysize parameter specifies the number of elements the variable will have. If not specified the bind array size (usually 1) is used. When a variable is created in an output type handler this parameter should be set to the cursor’s array size. The inconverter and outconverter parameters specify methods used for converting values to/from the database. More information can be found in the section on variable objects. The typename parameter specifies the name of a SQL object type and must be specified when using type DB_TYPE_OBJECT unless the type object was passed directly as the first parameter. The encoding_errors parameter specifies what should happen when decoding byte strings fetched from the database into strings. It should be one of the values noted in the builtin decode function. The bypass_decode parameter, if specified, should be passed as a boolean value. Passing a True value causes values of database types DB_TYPE_VARCHAR, DB_TYPE_CHAR, DB_TYPE_NVARCHAR, DB_TYPE_NCHAR and DB_TYPE_LONG to be returned as bytes instead of str, meaning that oracledb doesn't do any decoding. """ self._verify_open() if typename is not None: typ = self.connection.gettype(typename) elif typ is DB_TYPE_OBJECT: errors._raise_err(errors.ERR_MISSING_TYPE_NAME_FOR_OBJECT_VAR) if encodingErrors is not None: if encoding_errors is not None: errors._raise_err(errors.ERR_DUPLICATED_PARAMETER, deprecated_name="encodingErrors", new_name="encoding_errors") encoding_errors = encodingErrors return self._impl.create_var(self.connection, typ, size, arraysize, inconverter, outconverter, encoding_errors, bypass_decode) python-oracledb-1.2.1/src/oracledb/dbobject.py000066400000000000000000000271161434177474600213250ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # dbobject.py # # Contains the classes used for managing database objects and the database # object type metadata: DbObject, DbObjectType and DbObjectAttr. #------------------------------------------------------------------------------ from typing import Sequence, Union from . import errors from . import __name__ as MODULE_NAME from .base_impl import DbType class DbObject: __module__ = MODULE_NAME def __getattr__(self, name): attr_impl = self._impl.type.attrs_by_name[name] return self._impl.get_attr_value(attr_impl) def __repr__(self): return f"" def __setattr__(self, name, value): if name == "_impl" or name == "_type": super().__setattr__(name, value) else: attr_impl = self._impl.type.attrs_by_name[name] self._impl.set_attr_value(attr_impl, value) def _ensure_is_collection(self): """ Ensures that the object refers to a collection. If not, an exception is raised. """ if not self.type.iscollection: errors._raise_err(errors.ERR_OBJECT_IS_NOT_A_COLLECTION, name=self.type._get_full_name()) @classmethod def _from_impl(cls, impl): obj = cls.__new__(cls) obj._impl = impl obj._type = None return obj def append(self, element: object) -> None: """ Append an element to the collection object. If no elements exist in the collection, this creates an element at index 0; otherwise, it creates an element immediately following the highest index available in the collection. """ self._impl.append(element) def asdict(self) -> dict: """ Return a dictionary where the collection’s indexes are the keys and the elements are its values. """ self._ensure_is_collection() result = {} ix = self._impl.get_first_index() while ix is not None: result[ix] = self._impl.get_element_by_index(ix) ix = self._impl.get_next_index(ix) return result def aslist(self) -> list: """ Return a list of each of the collection’s elements in index order. """ self._ensure_is_collection() result = [] ix = self._impl.get_first_index() while ix is not None: result.append(self._impl.get_element_by_index(ix)) ix = self._impl.get_next_index(ix) return result def copy(self) -> "DbObject": """ Create a copy of the object and return it. """ copied_impl = self._impl.copy() return DbObject._from_impl(copied_impl) def delete(self, index: int) -> None: """ Delete the element at the specified index of the collection. If the element does not exist or is otherwise invalid, an error is raised. Note that the indices of the remaining elements in the collection are not changed. In other words, the delete operation creates holes in the collection. """ self._ensure_is_collection() self._impl.delete_by_index(index) def exists(self, index: int) -> bool: """ Return True or False indicating if an element exists in the collection at the specified index. """ self._ensure_is_collection() return self._impl.exists_by_index(index) def extend(self, seq: list) -> None: """ Append all of the elements in the sequence to the collection. This is the equivalent of performing append() for each element found in the sequence. """ self._ensure_is_collection() for value in seq: self.append(value) def first(self) -> int: """ Return the index of the first element in the collection. If the collection is empty, None is returned. """ self._ensure_is_collection() return self._impl.get_first_index() def getelement(self, index: int) -> object: """ Return the element at the specified index of the collection. If no element exists at that index, an exception is raised. """ self._ensure_is_collection() return self._impl.get_element_by_index(index) def last(self) -> int: """ Return the index of the last element in the collection. If the collection is empty, None is returned. """ self._ensure_is_collection() return self._impl.get_last_index() def next(self, index: int) -> int: """ Return the index of the next element in the collection following the specified index. If there are no elements in the collection following the specified index, None is returned. """ self._ensure_is_collection() return self._impl.get_next_index(index) def prev(self, index: int) -> int: """ Return the index of the element in the collection preceding the specified index. If there are no elements in the collection preceding the specified index, None is returned. """ self._ensure_is_collection() return self._impl.get_prev_index(index) def setelement(self, index: int, value: object) -> None: """ Set the value in the collection at the specified index to the given value. """ self._ensure_is_collection() self._impl.set_element_by_index(index, value) def size(self) -> int: """ Return the number of elements in the collection. """ self._ensure_is_collection() return self._impl.get_size() def trim(self, num: int) -> None: """ Remove the specified number of elements from the end of the collection. """ self._ensure_is_collection() self._impl.trim(num) @property def type(self) -> "DbObjectType": """ Returns an ObjectType corresponding to the type of the object. """ if self._type is None: self._type = DbObjectType._from_impl(self._impl.type) return self._type class DbObjectAttr: __module__ = MODULE_NAME def __repr__(self): return f"" @classmethod def _from_impl(cls, impl): attr = cls.__new__(cls) attr._impl = impl attr._type = None return attr @property def name(self) -> str: """ This read-only attribute returns the name of the attribute. """ return self._impl.name @property def type(self) -> Union["DbObjectType", DbType]: """ This read-only attribute returns the type of the attribute. This will be an Oracle Object Type if the variable binds Oracle objects; otherwise, it will be one of the database type constants. """ if self._type is None: if self._impl.objtype is not None: self._type = DbObjectType._from_impl(self._impl.objtype) else: self._type = self._impl.dbtype return self._type class DbObjectType: __module__ = MODULE_NAME def __call__(self, value=None): return self.newobject(value) def __eq__(self, other): if isinstance(other, DbObjectType): return other._impl == self._impl return NotImplemented def __repr__(self): return f"" @classmethod def _from_impl(cls, impl): typ = cls.__new__(cls) typ._impl = impl typ._attributes = None typ._element_type = None return typ def _get_full_name(self): """ Returns the full name of the type. """ if self.package_name is not None: return f"{self.schema}.{self.package_name}.{self.name}" return f"{self.schema}.{self.name}" @property def attributes(self) -> list: """ This read-only attribute returns a list of the attributes that make up the object type. """ if self._attributes is None: self._attributes = [DbObjectAttr._from_impl(i) \ for i in self._impl.attrs] return self._attributes @property def iscollection(self) -> bool: """ This read-only attribute returns a boolean indicating if the object type refers to a collection or not. """ return self._impl.is_collection @property def name(self) -> str: """ This read-only attribute returns the name of the type. """ return self._impl.name @property def element_type(self) -> Union["DbObjectType", DbType]: """ This read-only attribute returns the type of elements found in collections of this type, if iscollection is True; otherwise, it returns None. If the collection contains objects, this will be another object type; otherwise, it will be one of the database type constants. """ if self._element_type is None: if self._impl.element_objtype is not None: typ_impl = self._impl.element_objtype self._element_type = DbObjectType._from_impl(typ_impl) else: self._element_type = self._impl.element_dbtype return self._element_type def newobject(self, value: Sequence=None) -> DbObject: """ Return a new Oracle object of the given type. This object can then be modified by setting its attributes and then bound to a cursor for interaction with Oracle. If the object type refers to a collection, a sequence may be passed and the collection will be initialized with the items in that sequence. """ obj_impl = self._impl.create_new_object() obj = DbObject._from_impl(obj_impl) if value is not None: obj.extend(value) return obj @property def package_name(self) -> str: """ This read-only attribute returns the name of the package containing the PL/SQL type or None if the type is not a PL/SQL type. """ return self._impl.package_name @property def schema(self) -> str: """ This read-only attribute returns the name of the schema that owns the type. """ return self._impl.schema python-oracledb-1.2.1/src/oracledb/defaults.py000066400000000000000000000034431434177474600213550ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # defaults.py # # Contains the Defaults class used for managing default values used throughout # the module. #------------------------------------------------------------------------------ import os from . import __name__ as MODULE_NAME class Defaults: """ Identifies the default values used by the driver. """ __module__ = MODULE_NAME def __init__(self) -> None: self.arraysize = 100 self.stmtcachesize = 20 self.config_dir = os.environ.get("TNS_ADMIN") self.fetch_lobs = True self.fetch_decimals = False self.prefetchrows = 2 defaults = Defaults() python-oracledb-1.2.1/src/oracledb/driver_mode.py000066400000000000000000000124331434177474600220440ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022 Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # driver_mode.py # # Contains a simple method for checking and returning which mode the driver is # currently using. The driver only supports creating connections and pools with # either the thin implementation or the thick implementation, not both # simultaneously. #------------------------------------------------------------------------------ import threading from . import errors # The DriverModeHandler class is used to manage which mode the driver is using. # # The "thin_mode" flag contains the current state: # None: neither thick nor thin implementation has been used yet # False: thick implementation is being used # True: thin implementation is being used # # The "requested_thin_mode" flag is set to the mode that is being requested: # False: thick implementation is being initialized # True: thin implementation is being initialized class DriverModeManager: """ Manages the mode the driver is using. The "thin_mode" flag contains the current state: None: neither thick nor thin implementation has been used yet False: thick implementation is being used True: thin implementation is being used The "requested_thin_mode" is set to the mode that is being requested, but only while initialization is taking place (otherwise, it contains the value None): False: thick implementation is being initialized True: thin implementation is being initialized The condition is used to ensure that only one thread is performing initialization. """ def __init__(self): self.thin_mode = None self.requested_thin_mode = None self.condition = threading.Condition() def __enter__(self): return self def __exit__(self, exc_type, exc_value, exc_tb): with self.condition: if exc_type is None and exc_value is None and exc_tb is None \ and self.requested_thin_mode is not None: self.thin_mode = self.requested_thin_mode self.requested_thin_mode = None self.condition.notify() @property def thin(self): if self.requested_thin_mode is not None: return self.requested_thin_mode return self.thin_mode manager = DriverModeManager() def get_manager(requested_thin_mode=None): """ Returns the manager, but only after ensuring that no other threads are attempting to initialize the mode. NOTE: the current implementation of the driver only requires requested_thin_mode to be set when initializing the thick mode; for this reason the error raised is specified about a thin mode connection already being created. If this assumption changes, a new error message will be required. """ with manager.condition: if manager.thin_mode is None: if manager.requested_thin_mode is not None: manager.condition.wait() if manager.thin_mode is None: if requested_thin_mode is None: manager.requested_thin_mode = True else: manager.requested_thin_mode = requested_thin_mode elif requested_thin_mode is not None \ and requested_thin_mode != manager.thin_mode: errors._raise_err(errors.ERR_THIN_CONNECTION_ALREADY_CREATED) return manager def is_thin_mode() -> bool: """ Return a boolean specifying whether the driver is using thin mode (True) or thick mode (False). Immediately after python-oracledb is imported, this function will return True indicating that python-oracledb defaults to Thin mode. If oracledb.init_oracle_client() is called successfully, then a subsequent call to is_thin_mode() will return False indicating that Thick mode is enabled. Once the first standalone connection or connection pool is created succesfully, or a call to oracledb.init_oracle_client() is made successfully, then python-oracledb's mode is fixed and the value returned by is_thin_mode() will never change for the lifetime of the process. """ if manager.thin_mode is not None: return manager.thin_mode return True python-oracledb-1.2.1/src/oracledb/dsn.py000066400000000000000000000060721434177474600203330ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # dsn.py # # Contains makedsn(), a method available for backwards compatibility with # cx_Oracle. Use of the ConnectParams class or the keyword arguments to # connect() and create_pool() is recommended instead. #------------------------------------------------------------------------------ from . import errors def _check_arg(name: str, value: str) -> None: """ Checks the argument to ensure that it does not contain (, ) or = as these characters are not permitted within connect strings. """ if "(" in value or ")" in value or "=" in value: errors._raise_err(errors.ERR_INVALID_MAKEDSN_ARG, name=name) def makedsn(host: str, port: int, sid: str=None, service_name: str=None, region:str=None, sharding_key: str=None, super_sharding_key: str=None) -> str: """ Return a string suitable for use as the dsn parameter for connect(). This string is identical to the strings that are defined in the tnsnames.ora file. """ connect_data_parts = [] _check_arg("host", host) if sid is not None: _check_arg("sid", sid) connect_data_parts.append(f"(SID={sid})") if service_name is not None: _check_arg("service_name", service_name) connect_data_parts.append(f"(SERVICE_NAME={service_name})") if region is not None: _check_arg("region", region) connect_data_parts.append(f"(REGION={region})") if sharding_key is not None: _check_arg("sharding_key", sharding_key) connect_data_parts.append(f"(SHARDING_KEY={sharding_key})") if super_sharding_key is not None: _check_arg("super_sharding_key", super_sharding_key) connect_data_parts.append(f"(SUPER_SHARDING_KEY={super_sharding_key})") connect_data = "".join(connect_data_parts) return f"(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST={host})" \ f"(PORT={port}))(CONNECT_DATA={connect_data}))" python-oracledb-1.2.1/src/oracledb/errors.py000066400000000000000000000502251434177474600210620ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # errors.py # # Contains the _Error class and all of the errors that are raised explicitly by # the package. Oracle Database errors and ODPI-C errors (when using thick mode) # are only referenced here if they are transformed into package specific # errors. #------------------------------------------------------------------------------ import re from . import exceptions class _Error: """ Error class which is used for all errors that are raised by the driver. """ def __init__(self, message: str=None, context: str=None, isrecoverable: bool=False, iswarning: bool=False, code: int=0, offset: int=0) -> None: self.message = message self.context = context self.isrecoverable = isrecoverable self.iswarning = iswarning self.code = code self.offset = offset self.is_session_dead = False self.full_code = "" self._make_adjustments() def _make_adjustments(self): """ Make adjustments to the error, if needed, and calculate the full_code attribute. """ if self.message is not None: pos = self.message.find(":") if pos > 0: self.full_code = self.message[:pos] if self.code != 0 or self.full_code.startswith("DPI-"): args = {} if self.code != 0: driver_error_info = ERR_ORACLE_ERROR_XREF.get(self.code) else: error_num = int(self.full_code[4:]) driver_error_info = ERR_DPI_ERROR_XREF.get(error_num) if driver_error_info is not None: if isinstance(driver_error_info, tuple): driver_error_num, pattern = driver_error_info args = re.search(pattern, self.message).groupdict() else: driver_error_num = driver_error_info if driver_error_num == ERR_CONNECTION_CLOSED: self.is_session_dead = True driver_error = _get_error_text(driver_error_num, **args) self.message = f"{driver_error}\n{self.message}" self.full_code = f"{ERR_PREFIX}-{driver_error_num:04}" def __str__(self): return self.message def _get_error_text(error_num: int, **args) -> str: """ Return the error text for the driver specific error number. """ message_format = ERR_MESSAGE_FORMATS.get(error_num) if message_format is None: message_format = "missing error {error_num}" args = dict(error_num=error_num) error_num = ERR_MISSING_ERROR message = message_format.format(**args) return f"{ERR_PREFIX}-{error_num:04}: {message}" def _raise_err(error_num: int, context_error_message: str=None, cause: Exception=None, **args) -> None: """ Raises a driver specific exception from the specified error number and supplied arguments. """ message = _get_error_text(error_num, **args) if context_error_message is not None: message = f"{message}\n{context_error_message}" exc_type = ERR_EXCEPTION_TYPES[error_num // 1000] raise exc_type(_Error(message)) from cause def _raise_from_string(exc_type: Exception, message: str) -> None: """ Raises an exception from a given string. This ensures that an _Error object is created for all exceptions that are raised. """ raise exc_type(_Error(message)) from None # prefix used for all error messages ERR_PREFIX = "DPY" # error numbers that result in InterfaceError ERR_MISSING_ERROR = 1000 ERR_NOT_CONNECTED = 1001 ERR_POOL_NOT_OPEN = 1002 ERR_NOT_A_QUERY = 1003 ERR_NO_STATEMENT_EXECUTED = 1004 ERR_POOL_HAS_BUSY_CONNECTIONS = 1005 ERR_CURSOR_NOT_OPEN = 1006 # error numbers that result in ProgrammingError ERR_MESSAGE_HAS_NO_PAYLOAD = 2000 ERR_NO_STATEMENT = 2001 ERR_NO_STATEMENT_PREPARED = 2002 ERR_WRONG_EXECUTE_PARAMETERS_TYPE = 2003 ERR_WRONG_EXECUTEMANY_PARAMETERS_TYPE = 2004 ERR_ARGS_AND_KEYWORD_ARGS = 2005 ERR_MIXED_POSITIONAL_AND_NAMED_BINDS = 2006 ERR_EXPECTING_TYPE = 2007 ERR_WRONG_OBJECT_TYPE = 2008 ERR_WRONG_SCROLL_MODE = 2009 ERR_MIXED_ELEMENT_TYPES = 2010 ERR_WRONG_ARRAY_DEFINITION = 2011 ERR_ARGS_MUST_BE_LIST_OR_TUPLE = 2012 ERR_KEYWORD_ARGS_MUST_BE_DICT = 2013 ERR_DUPLICATED_PARAMETER = 2014 ERR_EXPECTING_VAR = 2015 ERR_INCORRECT_VAR_ARRAYSIZE = 2016 ERR_LIBRARY_ALREADY_INITIALIZED = 2017 ERR_WALLET_FILE_MISSING = 2018 ERR_THIN_CONNECTION_ALREADY_CREATED = 2019 ERR_INVALID_MAKEDSN_ARG = 2020 ERR_INIT_ORACLE_CLIENT_NOT_CALLED = 2021 ERR_INVALID_OCI_ATTR_TYPE = 2022 ERR_INVALID_CONN_CLASS = 2023 ERR_INVALID_CONNECT_PARAMS = 2025 ERR_INVALID_POOL_CLASS = 2026 ERR_INVALID_POOL_PARAMS = 2027 ERR_EXPECTING_LIST_FOR_ARRAY_VAR = 2028 ERR_HTTPS_PROXY_REQUIRES_TCPS = 2029 ERR_INVALID_LOB_OFFSET = 2030 ERR_INVALID_ACCESS_TOKEN_PARAM = 2031 ERR_INVALID_ACCESS_TOKEN_RETURNED = 2032 ERR_EXPIRED_ACCESS_TOKEN = 2033 ERR_ACCESS_TOKEN_REQUIRES_TCPS = 2034 ERR_INVALID_OBJECT_TYPE_NAME = 2035 ERR_OBJECT_IS_NOT_A_COLLECTION = 2036 ERR_MISSING_TYPE_NAME_FOR_OBJECT_VAR = 2037 # error numbers that result in NotSupportedError ERR_TIME_NOT_SUPPORTED = 3000 ERR_FEATURE_NOT_SUPPORTED = 3001 ERR_PYTHON_VALUE_NOT_SUPPORTED = 3002 ERR_PYTHON_TYPE_NOT_SUPPORTED = 3003 ERR_UNSUPPORTED_TYPE_SET = 3004 ERR_ARRAYS_OF_ARRAYS = 3005 ERR_ORACLE_TYPE_NOT_SUPPORTED = 3006 ERR_DB_TYPE_NOT_SUPPORTED = 3007 ERR_UNSUPPORTED_INBAND_NOTIFICATION = 3008 ERR_SELF_BIND_NOT_SUPPORTED = 3009 ERR_SERVER_VERSION_NOT_SUPPORTED = 3010 ERR_NCHAR_CS_NOT_SUPPORTED = 3012 ERR_UNSUPPORTED_PYTHON_TYPE_FOR_DB_TYPE = 3013 ERR_LOB_OF_WRONG_TYPE = 3014 ERR_UNSUPPORTED_VERIFIER_TYPE = 3015 ERR_NO_CRYPTOGRAPHY_PACKAGE = 3016 ERR_ORACLE_TYPE_NAME_NOT_SUPPORTED = 3017 ERR_TDS_TYPE_NOT_SUPPORTED = 3018 # error numbers that result in DatabaseError ERR_TNS_ENTRY_NOT_FOUND = 4000 ERR_NO_CREDENTIALS = 4001 ERR_COLUMN_TRUNCATED = 4002 ERR_ORACLE_NUMBER_NO_REPR = 4003 ERR_INVALID_NUMBER = 4004 ERR_POOL_NO_CONNECTION_AVAILABLE = 4005 ERR_ARRAY_DML_ROW_COUNTS_NOT_ENABLED = 4006 ERR_INCONSISTENT_DATATYPES = 4007 ERR_INVALID_BIND_NAME = 4008 ERR_WRONG_NUMBER_OF_POSITIONAL_BINDS = 4009 ERR_MISSING_BIND_VALUE = 4010 ERR_CONNECTION_CLOSED = 4011 ERR_NUMBER_WITH_INVALID_EXPONENT = 4012 ERR_NUMBER_STRING_OF_ZERO_LENGTH = 4013 ERR_NUMBER_STRING_TOO_LONG = 4014 ERR_NUMBER_WITH_EMPTY_EXPONENT = 4015 ERR_CONTENT_INVALID_AFTER_NUMBER = 4016 ERR_INVALID_CONNECT_DESCRIPTOR = 4017 ERR_CANNOT_PARSE_CONNECT_STRING = 4018 ERR_INVALID_REDIRECT_DATA = 4019 ERR_INVALID_PROTOCOL = 4021 ERR_INVALID_POOL_PURITY = 4022 ERR_CALL_TIMEOUT_EXCEEDED = 4024 ERR_INVALID_REF_CURSOR = 4025 ERR_TNS_NAMES_FILE_MISSING = 4026 ERR_NO_CONFIG_DIR = 4027 ERR_INVALID_SERVER_TYPE = 4028 # error numbers that result in InternalError ERR_MESSAGE_TYPE_UNKNOWN = 5000 ERR_BUFFER_LENGTH_INSUFFICIENT = 5001 ERR_INTEGER_TOO_LARGE = 5002 ERR_UNEXPECTED_NEGATIVE_INTEGER = 5003 ERR_UNEXPECTED_DATA = 5004 ERR_UNEXPECTED_REFUSE = 5005 ERR_UNEXPECTED_END_OF_DATA = 5006 ERR_UNEXPECTED_XML_TYPE = 5007 # error numbers that result in OperationalError ERR_LISTENER_REFUSED_CONNECTION = 6000 ERR_INVALID_SERVICE_NAME = 6001 ERR_INVALID_SERVER_CERT_DN = 6002 ERR_INVALID_SID = 6003 ERR_PROXY_FAILURE = 6004 ERR_CONNECTION_FAILED = 6005 # Oracle error number cross reference ERR_ORACLE_ERROR_XREF = { 28: ERR_CONNECTION_CLOSED, 600: ERR_CONNECTION_CLOSED, 1005: ERR_NO_CREDENTIALS, 22303: (ERR_INVALID_OBJECT_TYPE_NAME, 'type "(?P[^"]*"."[^"]*)"'), 24422: ERR_POOL_HAS_BUSY_CONNECTIONS, 24349: ERR_ARRAY_DML_ROW_COUNTS_NOT_ENABLED, 24459: ERR_POOL_NO_CONNECTION_AVAILABLE, 24496: ERR_POOL_NO_CONNECTION_AVAILABLE, 24338: ERR_INVALID_REF_CURSOR, } # ODPI-C error number cross reference ERR_DPI_ERROR_XREF = { 1010: ERR_CONNECTION_CLOSED, 1043: ERR_INVALID_NUMBER, 1044: ERR_ORACLE_NUMBER_NO_REPR, 1067: (ERR_CALL_TIMEOUT_EXCEEDED, "call timeout of (?P\d+) ms"), 1080: ERR_CONNECTION_CLOSED, } # error message exception types (multiples of 1000) ERR_EXCEPTION_TYPES = { 1: exceptions.InterfaceError, 2: exceptions.ProgrammingError, 3: exceptions.NotSupportedError, 4: exceptions.DatabaseError, 5: exceptions.InternalError, 6: exceptions.OperationalError } # error message formats ERR_MESSAGE_FORMATS = { ERR_ACCESS_TOKEN_REQUIRES_TCPS: 'access_token requires use of the tcps protocol', ERR_ARGS_MUST_BE_LIST_OR_TUPLE: 'arguments must be a list or tuple', ERR_ARGS_AND_KEYWORD_ARGS: 'expecting positional arguments or keyword arguments, not both', ERR_ARRAY_DML_ROW_COUNTS_NOT_ENABLED: 'array DML row counts mode is not enabled', ERR_ARRAYS_OF_ARRAYS: 'arrays of arrays are not supported', ERR_BUFFER_LENGTH_INSUFFICIENT: 'internal error: buffer of length {actual_buffer_len} ' 'insufficient to hold {required_buffer_len} bytes', ERR_CALL_TIMEOUT_EXCEEDED: 'call timeout of {timeout} ms exceeded', ERR_CANNOT_PARSE_CONNECT_STRING: 'cannot parse connect string "{data}"', ERR_COLUMN_TRUNCATED: 'column truncated to {col_value_len} {unit}. ' 'Untruncated was {actual_len}', ERR_CONNECTION_FAILED: 'cannot connect to database. Connection failed with "{exception}"', ERR_CONTENT_INVALID_AFTER_NUMBER: 'invalid number (content after number)', ERR_CURSOR_NOT_OPEN: 'cursor is not open', ERR_DB_TYPE_NOT_SUPPORTED: 'database type "{name}" is not supported', ERR_DUPLICATED_PARAMETER: '"{deprecated_name}" and "{new_name}" cannot be specified together', ERR_EXPECTING_LIST_FOR_ARRAY_VAR: 'expecting list when setting array variables', ERR_EXPECTING_TYPE: 'expected a type', ERR_EXPECTING_VAR: 'type handler should return None or the value returned by a call ' 'to cursor.var()', ERR_EXPIRED_ACCESS_TOKEN: 'access token has expired', ERR_FEATURE_NOT_SUPPORTED: '{feature} is only supported in python-oracledb {driver_type} mode', ERR_HTTPS_PROXY_REQUIRES_TCPS: 'https_proxy requires use of the tcps protocol', ERR_INCONSISTENT_DATATYPES: 'cannot convert from data type {input_type} to {output_type}', ERR_INCORRECT_VAR_ARRAYSIZE: 'variable array size of {var_arraysize} is ' 'too small (should be at least {required_arraysize})', ERR_INIT_ORACLE_CLIENT_NOT_CALLED: 'init_oracle_client() must be called first', ERR_INTEGER_TOO_LARGE: 'internal error: read integer of length {length} when expecting ' 'integer of no more than length {max_length}', ERR_INVALID_ACCESS_TOKEN_PARAM: 'invalid access token: value must be a string (for OAuth), a ' '2-tuple containing the token and private key strings (for IAM), ' 'or a callable that returns a string or 2-tuple', ERR_INVALID_ACCESS_TOKEN_RETURNED: 'invalid access token returned from callable: value must be a ' 'string (for OAuth) or a 2-tuple containing the token and private ' 'key strings (for IAM)', ERR_INVALID_BIND_NAME: 'no bind placeholder named ":{name}" was found in the SQL text', ERR_INVALID_CONN_CLASS: 'invalid connection class', ERR_INVALID_CONNECT_DESCRIPTOR: 'invalid connect descriptor "{data}"', ERR_INVALID_CONNECT_PARAMS: 'invalid connection params', ERR_INVALID_LOB_OFFSET: 'LOB offset must be greater than zero', ERR_INVALID_MAKEDSN_ARG: '"{name}" argument contains invalid values', ERR_INVALID_NUMBER: 'invalid number', ERR_INVALID_OBJECT_TYPE_NAME: 'invalid object type name: "{name}"', ERR_INVALID_OCI_ATTR_TYPE: 'invalid OCI attribute type {attr_type}', ERR_INVALID_POOL_CLASS: 'invalid connection pool class', ERR_INVALID_POOL_PARAMS: 'invalid pool params', ERR_INVALID_POOL_PURITY: 'invalid DRCP purity {purity}', ERR_INVALID_PROTOCOL: 'invalid protocol "{protocol}"', ERR_INVALID_REDIRECT_DATA: 'invalid redirect data {data}', ERR_INVALID_REF_CURSOR: 'invalid REF CURSOR: never opened in PL/SQL', ERR_INVALID_SERVER_CERT_DN: 'cannot connect to database. The distinguished name (DN) on ' 'the server certificate does not match the expected value', ERR_INVALID_SERVER_TYPE: 'invalid server_type: {server_type}', ERR_INVALID_SERVICE_NAME: 'cannot connect to database. Service "{service_name}" is not ' 'registered with the listener at host "{host}" port {port}. ' '(Similar to ORA-12514)', ERR_INVALID_SID: 'cannot connect to database. SID "{sid}" is not registered ' 'with the listener at host "{host}" port {port}. ' '(Similar to ORA-12505)', ERR_KEYWORD_ARGS_MUST_BE_DICT: '"keyword_parameters" argument must be a dict', ERR_LIBRARY_ALREADY_INITIALIZED: 'init_oracle_client() was already called with different arguments', ERR_LISTENER_REFUSED_CONNECTION: 'cannot connect to database. Listener refused connection. ' '(Similar to ORA-{error_code})', ERR_LOB_OF_WRONG_TYPE: 'LOB is of type {actual_type_name} but must be of type ' '{expected_type_name}', ERR_MESSAGE_HAS_NO_PAYLOAD: 'message has no payload', ERR_MESSAGE_TYPE_UNKNOWN: 'internal error: unknown protocol message type {message_type}', ERR_MISSING_BIND_VALUE: 'a bind variable replacement value for placeholder ":{name}" was ' 'not provided', ERR_MISSING_TYPE_NAME_FOR_OBJECT_VAR: 'no object type specified for object variable', ERR_MIXED_ELEMENT_TYPES: 'element {element} is not the same data type as previous elements', ERR_MIXED_POSITIONAL_AND_NAMED_BINDS: 'positional and named binds cannot be intermixed', ERR_NCHAR_CS_NOT_SUPPORTED: 'national character set id {charset_id} is not supported by ' 'python-oracledb in thin mode', ERR_NO_CONFIG_DIR: 'no configuration directory to search for tnsnames.ora', ERR_NO_CREDENTIALS: 'no credentials specified', ERR_NO_CRYPTOGRAPHY_PACKAGE: 'python-oracledb thin mode cannot be used because the ' 'cryptography package is not installed', ERR_NO_STATEMENT: 'no statement specified and no prior statement prepared', ERR_NO_STATEMENT_EXECUTED: 'no statement executed', ERR_NO_STATEMENT_PREPARED: 'statement must be prepared first', ERR_NOT_A_QUERY: 'the executed statement does not return rows', ERR_NOT_CONNECTED: 'not connected to database', ERR_NUMBER_STRING_OF_ZERO_LENGTH: 'invalid number: zero length string', ERR_NUMBER_STRING_TOO_LONG: 'invalid number: string too long', ERR_NUMBER_WITH_EMPTY_EXPONENT: 'invalid number: empty exponent', ERR_NUMBER_WITH_INVALID_EXPONENT: 'invalid number: invalid exponent', ERR_OBJECT_IS_NOT_A_COLLECTION: 'object {name} is not a collection', ERR_ORACLE_NUMBER_NO_REPR: 'value cannot be represented as an Oracle number', ERR_ORACLE_TYPE_NAME_NOT_SUPPORTED: 'Oracle data type name "{name}" is not supported', ERR_ORACLE_TYPE_NOT_SUPPORTED: 'Oracle data type {num} is not supported', ERR_POOL_HAS_BUSY_CONNECTIONS: 'connection pool cannot be closed because connections are busy', ERR_POOL_NO_CONNECTION_AVAILABLE: 'timed out waiting for the connection pool to return a connection', ERR_POOL_NOT_OPEN: 'connection pool is not open', ERR_PROXY_FAILURE: 'network proxy failed: response was {response}', ERR_PYTHON_TYPE_NOT_SUPPORTED: 'Python type {typ} is not supported', ERR_PYTHON_VALUE_NOT_SUPPORTED: 'Python value of type "{type_name}" is not supported', ERR_SELF_BIND_NOT_SUPPORTED: 'binding to self is not supported', ERR_CONNECTION_CLOSED: 'the database or network closed the connection', ERR_SERVER_VERSION_NOT_SUPPORTED: 'connections to this database server version are not supported ' 'by python-oracledb in thin mode', ERR_TDS_TYPE_NOT_SUPPORTED: 'Oracle TDS data type {num} is not supported', ERR_THIN_CONNECTION_ALREADY_CREATED: 'python-oracledb thick mode cannot be used because a thin mode ' 'connection has already been created', ERR_TIME_NOT_SUPPORTED: 'Oracle Database does not support time only variables', ERR_TNS_ENTRY_NOT_FOUND: 'cannot connect to database. Unable to find "{name}" in ' '{file_name}', ERR_TNS_NAMES_FILE_MISSING: 'cannot connect to database. File tnsnames.ora not found in ' '{config_dir}', ERR_UNEXPECTED_DATA: 'unexpected data received: {data}', ERR_UNEXPECTED_END_OF_DATA: 'unexpected end of data: want {num_bytes_wanted} bytes but ' 'only {num_bytes_available} bytes are available', ERR_UNEXPECTED_NEGATIVE_INTEGER: 'internal error: read a negative integer when expecting a ' 'positive integer', ERR_UNEXPECTED_REFUSE: 'the listener refused the connection but an unexpected error ' 'format was returned', ERR_UNEXPECTED_XML_TYPE: 'unexpected XMLType with flag {flag}', ERR_UNSUPPORTED_INBAND_NOTIFICATION: 'unsupported in-band notification with error number {err_num}', ERR_UNSUPPORTED_PYTHON_TYPE_FOR_DB_TYPE: 'unsupported Python type {py_type_name} for database type ' '{db_type_name}', ERR_UNSUPPORTED_TYPE_SET: 'type {db_type_name} does not support being set', ERR_UNSUPPORTED_VERIFIER_TYPE: 'password verifier type 0x{verifier_type:x} is not supported by ' 'python-oracledb in thin mode', ERR_WALLET_FILE_MISSING: 'cannot connect to database. Wallet file {name} was not found', ERR_WRONG_ARRAY_DEFINITION: 'expecting a list of two elements [type, numelems]', ERR_WRONG_EXECUTE_PARAMETERS_TYPE: 'expecting a dictionary, list or tuple, or keyword args', ERR_WRONG_EXECUTEMANY_PARAMETERS_TYPE: '"parameters" argument should be a list of sequences or ' 'dictionaries, or an integer specifying the number of ' 'times to execute the statement', ERR_WRONG_NUMBER_OF_POSITIONAL_BINDS: '{expected_num} positional bind values are required but ' '{actual_num} were provided', ERR_WRONG_OBJECT_TYPE: 'found object of type "{actual_schema}.{actual_name}" when ' 'expecting object of type "{expected_schema}.{expected_name}"', ERR_WRONG_SCROLL_MODE: 'scroll mode must be relative, absolute, first or last', } python-oracledb-1.2.1/src/oracledb/exceptions.py000066400000000000000000000034231434177474600217250ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # exceptions.py # # Contains the exception classes mandated by the Python Database API. #------------------------------------------------------------------------------ class Warning(Exception): pass class Error(Exception): pass class DatabaseError(Error): pass class DataError(DatabaseError): pass class IntegrityError(DatabaseError): pass class InterfaceError(Error): pass class InternalError(DatabaseError): pass class NotSupportedError(DatabaseError): pass class OperationalError(DatabaseError): pass class ProgrammingError(DatabaseError): pass python-oracledb-1.2.1/src/oracledb/impl/000077500000000000000000000000001434177474600201315ustar00rootroot00000000000000python-oracledb-1.2.1/src/oracledb/impl/base/000077500000000000000000000000001434177474600210435ustar00rootroot00000000000000python-oracledb-1.2.1/src/oracledb/impl/base/bind_var.pyx000066400000000000000000000162551434177474600234020ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # bind_var.pyx # # Cython file defining the BindVar implementation class (embedded in # base_impl.pyx). #------------------------------------------------------------------------------ @cython.freelist(20) cdef class BindVar: cdef int _create_var_from_type(self, object conn, BaseCursorImpl cursor_impl, object value) except -1: """ Creates a variable given type information. This may be supplied as a list of size 2 (type, num_elements) which internally creates an array variable, or as an integer which internally creates a string of the given length, or as a database type, API type or Python type. """ cdef BaseVarImpl var_impl var_impl = cursor_impl._create_var_impl(conn) var_impl.num_elements = 1 if isinstance(value, list): if len(value) != 2: errors._raise_err(errors.ERR_WRONG_ARRAY_DEFINITION) var_impl._set_type_info_from_type(value[0]) var_impl.num_elements = value[1] var_impl.is_array = True elif isinstance(value, int): var_impl.dbtype = DB_TYPE_VARCHAR var_impl.size = value else: var_impl._set_type_info_from_type(value) var_impl._finalize_init() self.var_impl = var_impl if isinstance(value, PY_TYPE_DB_OBJECT_TYPE): self.var = PY_TYPE_VAR._from_impl(self.var_impl, value) cdef int _create_var_from_value(self, object conn, BaseCursorImpl cursor_impl, object value, uint32_t num_elements) except -1: """ Creates a variable using the value as a template. """ cdef: bint is_plsql = cursor_impl._is_plsql() BaseVarImpl var_impl var_impl = cursor_impl._create_var_impl(conn) if not isinstance(value, list): var_impl.num_elements = num_elements var_impl._set_type_info_from_value(value, is_plsql) else: var_impl.is_array = True var_impl.num_elements = max(num_elements, len(value)) for element in value: if element is not None: var_impl._set_type_info_from_value(element, is_plsql) if var_impl.dbtype is None: var_impl.dbtype = DB_TYPE_VARCHAR var_impl.size = 1 var_impl._finalize_init() self.var_impl = var_impl cdef int _set_by_type(self, object conn, BaseCursorImpl cursor_impl, object typ) except -1: """ Sets the bind variable information given a type. """ if typ is not None: if isinstance(typ, PY_TYPE_VAR): self.var = typ self.var_impl = typ._impl else: self._create_var_from_type(conn, cursor_impl, typ) cdef int _set_by_value(self, object conn, BaseCursorImpl cursor_impl, object cursor, object value, object type_handler, uint32_t row_num, uint32_t num_elements, bint defer_type_assignment) except -1: """ Sets the bind variable information given a value. The row number supplied is used as the offset into the variable value array. Type assignment is deferred for None values if specified. Once a value that is not None is set, an exception is raised for any non-compliant values that are seen after that. """ cdef: bint was_set = True object var # a variable can be set directly in which case nothing further needs to # be done! if isinstance(value, PY_TYPE_VAR): if value is not self.var: self.var = value self.var_impl = value._impl return 0 # if a variable already exists check to see if the value can be set on # that variable; an exception is raised if a value has been previously # set on that bind variable; otherwise, the variable is replaced with a # new one if self.var_impl is not None: if self.has_value: return self.var_impl._check_and_set_value(row_num, value, NULL) self.var_impl._check_and_set_value(row_num, value, &was_set) if was_set: self.has_value = True return 0 self.var_impl = None self.var = None # a new variable needs to be created; if the value is null (None), # however, and type assignment is deferred, nothing to do! if value is None and defer_type_assignment: return 0 # if an input type handler is specified, call it; the input type # handler should return a variable or None; the value None implies # that the default processing should take place just as if no input # type handler was defined if type_handler is not None: var = type_handler(cursor, value, num_elements) if var is not None: if not isinstance(var, PY_TYPE_VAR): errors._raise_err(errors.ERR_EXPECTING_VAR) self.var = var self.var_impl = var._impl self.var_impl._check_and_set_value(row_num, value, NULL) self.has_value = True return 0 # otherwise, if no type handler exists or the type handler returned # the value None, create a new variable deriving type information # from the value that is being set self._create_var_from_value(conn, cursor_impl, value, num_elements) self.var_impl._check_and_set_value(row_num, value, NULL) self.has_value = True def get_value(self, uint32_t pos): """ Internal method for getting the value of a variable at a given position. """ if self.var_impl is not None: return self.var_impl.get_value(pos) python-oracledb-1.2.1/src/oracledb/impl/base/connect_params.pyx000066400000000000000000001057571434177474600246200ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # connect_params.pyx # # Cython file defining the base ConnectParams implementation class (embedded in # base_impl.pyx). #------------------------------------------------------------------------------ # a set of alternative parameter names that are used in connect descriptors # the key is the parameter name used in connect descriptors and the value is # the key used in argument dictionaries (and stored in the parameter objects) ALTERNATIVE_PARAM_NAMES = { "pool_connection_class": "cclass", "pool_purity": "purity", "server": "server_type", "transport_connect_timeout": "tcp_connect_timeout", "my_wallet_directory": "wallet_location" } # a set of parameter names in connect descriptors that are treated as # containers (values are always lists) CONTAINER_PARAM_NAMES = set(( "address_list", "description", "address" )) # regular expression used for determining if a connect string refers to an Easy # Connect string or not EASY_CONNECT_PATTERN = \ "((?P\w+)://)?(?P[^:/]+)(:(?P\d+)?)?/" \ "(?P[^:?]*)(:(?P\w+))?" # dictionary of tnsnames.ora files, indexed by the directory in which the file # is found; the results are cached in order to avoid parsing a file multiple # times; the modification time of the file is checked each time, though, to # ensure that no changes were made since the last time that the file was read # and parsed. _tnsnames_files = {} # internal default values cdef str DEFAULT_PROTOCOL = "tcp" cdef uint32_t DEFAULT_PORT = 1521 cdef double DEFAULT_TCP_CONNECT_TIMEOUT = 60 cdef int _add_container(dict args, str name, object value) except -1: """ Adds a container to the arguments. """ if name == "address" and "address_list" in args: value = dict(address=[value]) name = "address_list" elif name == "address_list" and "address" in args: args[name] = [dict(address=[v]) for v in args["address"]] del args["address"] args.setdefault(name, []).append(value) cdef dict _parse_connect_descriptor(str data, dict args): """ Internal method which parses a connect descriptor containing name-value pairs in the form (KEY = VALUE), where the value can itself be another set of nested name-value pairs. A dictionary is returned containing these key value pairs. """ if data[0] != "(" or data[-1] != ")": errors._raise_err(errors.ERR_INVALID_CONNECT_DESCRIPTOR, data=data) data = data[1:-1] pos = data.find("=") if pos < 0: errors._raise_err(errors.ERR_INVALID_CONNECT_DESCRIPTOR, data=data) name = data[:pos].strip().lower() data = data[pos + 1:].strip() if not data or not data.startswith("("): value = data if value and value[0] == '"' and value[-1] == '"': value = value[1:-1] else: value = {} while data: search_pos = 1 num_opening_parens = 1 num_closing_parens = 0 while num_closing_parens < num_opening_parens: end_pos = data.find(")", search_pos) if end_pos < 0: errors._raise_err(errors.ERR_INVALID_CONNECT_DESCRIPTOR, data=data) num_closing_parens += 1 num_opening_parens += data.count("(", search_pos, end_pos) search_pos = end_pos + 1 _parse_connect_descriptor(data[:end_pos + 1].strip(), value) data = data[end_pos + 1:].strip() name = ALTERNATIVE_PARAM_NAMES.get(name, name) if name in CONTAINER_PARAM_NAMES: _add_container(args, name, value) else: args[name] = value return args cdef class ConnectParamsImpl: def __init__(self): cdef AddressList address_list self.stmtcachesize = defaults.stmtcachesize self.config_dir = defaults.config_dir self._default_description = Description() self._default_address = Address() self.description_list = DescriptionList() self.description_list.descriptions.append(self._default_description) self.debug_jdwp = os.getenv("ORA_DEBUG_JDWP") address_list = AddressList() address_list.addresses.append(self._default_address) self._default_description.address_lists.append(address_list) def set(self, dict args): """ Sets the property values based on the supplied arguments. All values not supplied will be left unchanged. """ self._external_handle = args.get("handle", 0) _set_str_param(args, "user", self) _set_str_param(args, "proxy_user", self) if self.proxy_user is None and self.user is not None: self.parse_user(self.user) self._set_password(args.get("password")) self._set_new_password(args.get("newpassword")) self._set_wallet_password(args.get("wallet_password")) _set_bool_param(args, "events", &self.events) _set_uint_param(args, "mode", &self.mode) _set_str_param(args, "edition", self) _set_str_param(args, "tag", self) _set_bool_param(args, "matchanytag", &self.matchanytag) _set_uint_param(args, "stmtcachesize", &self.stmtcachesize) _set_bool_param(args, "disable_oob", &self.disable_oob) _set_str_param(args, "debug_jdwp", self) _set_str_param(args, "config_dir", self) self.appcontext = args.get("appcontext") self.shardingkey = args.get("shardingkey") self.supershardingkey = args.get("supershardingkey") self._default_description.set_from_connect_data_args(args) self._default_description.set_from_description_args(args) self._default_description.set_from_security_args(args) self._default_address.set_from_args(args) _set_bool_param(args, "externalauth", &self.externalauth) self._set_access_token_param(args.get("access_token")) if args: self._has_components = True cdef int _check_credentials(self) except -1: """ Check to see that credentials have been supplied: either a password or an access token. """ if self._password is None and self._token is None \ and self.access_token_callback is None: errors._raise_err(errors.ERR_NO_CREDENTIALS) cdef int _copy(self, ConnectParamsImpl other_params) except -1: """ Internal method for copying attributes from another set of parameters. """ self.config_dir = other_params.config_dir self.user = other_params.user self.proxy_user = other_params.proxy_user self.events = other_params.events self.externalauth = other_params.externalauth self.mode = other_params.mode self.edition = other_params.edition self.appcontext = other_params.appcontext self.tag = other_params.tag self.matchanytag = other_params.matchanytag self.shardingkey = other_params.shardingkey self.supershardingkey = other_params.supershardingkey self.stmtcachesize = other_params.stmtcachesize self.disable_oob = other_params.disable_oob self.debug_jdwp = other_params.debug_jdwp self.description_list = other_params.description_list self.access_token_callback = other_params.access_token_callback self.access_token_expires = other_params.access_token_expires self._external_handle = other_params._external_handle self._default_description = other_params._default_description self._default_address = other_params._default_address self._password = other_params._password self._password_obfuscator = other_params._password_obfuscator self._new_password = other_params._new_password self._new_password_obfuscator = other_params._new_password_obfuscator self._wallet_password = other_params._wallet_password self._wallet_password_obfuscator = \ other_params._wallet_password_obfuscator self._token = other_params._token self._token_obfuscator = other_params._token_obfuscator self._private_key = other_params._private_key self._private_key_obfuscator = other_params._private_key_obfuscator cdef bytes _get_new_password(self): """ Returns the new password, after removing the obfuscation. """ if self._new_password is not None: return bytes(self._xor_bytes(self._new_password, self._new_password_obfuscator)) cdef bytearray _get_obfuscator(self, str secret_value): """ Return a byte array suitable for obfuscating the specified secret value. """ return bytearray(secrets.token_bytes(len(secret_value.encode()))) cdef bytes _get_password(self): """ Returns the password, after removing the obfuscation. """ if self._password is not None: return bytes(self._xor_bytes(self._password, self._password_obfuscator)) cdef str _get_private_key(self): """ Returns the private key, after removing the obfuscation. """ if self._private_key is not None: return self._xor_bytes(self._private_key, self._private_key_obfuscator).decode() cdef TnsnamesFile _get_tnsnames_file(self): """ Return a tnsnames file, if one is present, or None if one is not. If the file was previously loaded, the modification time is checked and, if unchanged, the previous file is returned. """ cdef TnsnamesFile tnsnames_file if self.config_dir is None: errors._raise_err(errors.ERR_NO_CONFIG_DIR) file_name = os.path.join(self.config_dir, "tnsnames.ora") tnsnames_file = _tnsnames_files.get(self.config_dir) try: stat_info = os.stat(file_name) except: if tnsnames_file is not None: del _tnsnames_files[self.config_dir] errors._raise_err(errors.ERR_TNS_NAMES_FILE_MISSING, config_dir=self.config_dir) if tnsnames_file is not None \ and tnsnames_file.mtime == stat_info.st_mtime: return tnsnames_file tnsnames_file = TnsnamesFile(file_name, stat_info.st_mtime) tnsnames_file.read() _tnsnames_files[self.config_dir] = tnsnames_file return tnsnames_file cdef str _get_token(self): """ Returns the token, after removing the obfuscation. If a callable has been registered and there is no token stored yet, the callable will be invoked with the refresh parameter set to False. If the token returned by the callable is expired, the callable will be invoked a second time with the refresh parameter set to True. If this token is also expired, an exception will be raised. If the stored token has expired and no callable has been registered, an exception will be raised; otherwise, the callable will be invoked with the refresh parameter set to True. """ cdef: object returned_val, current_date = datetime.datetime.utcnow() bint expired if self._token is None and self.access_token_callback is not None: returned_val = self.access_token_callback(False) self._set_access_token(returned_val, errors.ERR_INVALID_ACCESS_TOKEN_RETURNED) expired = self.access_token_expires < current_date if expired and self.access_token_callback is not None: returned_val = self.access_token_callback(True) self._set_access_token(returned_val, errors.ERR_INVALID_ACCESS_TOKEN_RETURNED) expired = self.access_token_expires < current_date if expired: errors._raise_err(errors.ERR_EXPIRED_ACCESS_TOKEN) return self._xor_bytes(self._token, self._token_obfuscator).decode() cdef object _get_token_expires(self, str token): """ Gets the expiry date from the token. """ cdef: str header_seg dict header int num_pad header_seg = token.split(".")[1] num_pad = len(header_seg) % 4 if num_pad != 0: header_seg += '=' * num_pad header = json.loads(base64.b64decode(header_seg)) return datetime.datetime.utcfromtimestamp(header["exp"]) cdef str _get_wallet_password(self): """ Returns the wallet password, after removing the obfuscation. """ if self._wallet_password is not None: return self._xor_bytes(self._wallet_password, self._wallet_password_obfuscator).decode() cdef int _parse_connect_string(self, str connect_string) except -1: """ Internal method for parsing a connect string. """ cdef: TnsnamesFile tnsnames_file Description description Address address dict args = {} str name # if a connect string starts with an opening parenthesis it is assumed # to be a full connect descriptor if connect_string.startswith("("): _parse_connect_descriptor(connect_string, args) self._has_components = True return self._process_connect_descriptor(args) # otherwise, see if the connect string is an EasyConnect string m = re.search(EASY_CONNECT_PATTERN, connect_string) if m is not None: # build up arguments args = m.groupdict() connect_string = connect_string[m.end():] params_pos = connect_string.find("?") if params_pos >= 0: params = connect_string[params_pos + 1:] for part in params.split("&"): name, value = [s.strip() for s in part.split("=", 1)] name = name.lower() name = ALTERNATIVE_PARAM_NAMES.get(name, name) if value.startswith('"') and value.endswith('"'): value = value[1:-1] args[name] = value # create description list address = self._default_address.copy() address.set_from_args(args) description = self._default_description.copy() description.set_from_connect_data_args(args) description.set_from_description_args(args) description.set_from_security_args(args) description.address_lists = [AddressList()] description.address_lists[0].addresses.append(address) self.description_list = DescriptionList() self.description_list.descriptions.append(description) # otherwise, see if the name is a connect alias in a tnsnames.ora # configuration file else: tnsnames_file = self._get_tnsnames_file() name = connect_string connect_string = tnsnames_file.entries.get(name.upper()) if connect_string is None: errors._raise_err(errors.ERR_TNS_ENTRY_NOT_FOUND, name=name, file_name=tnsnames_file.file_name) _parse_connect_descriptor(connect_string, args) self._process_connect_descriptor(args) # mark that object has components self._has_components = True cdef int _process_connect_descriptor(self, dict args) except -1: """ Internal method used for processing the parsed connect descriptor into the set of DescriptionList, Description, AddressList and Address container objects. """ cdef: AddressList address_list Description description Address address self.description_list = DescriptionList() list_args = args.get("description_list") if list_args is not None: self.description_list.set_from_args(list_args) else: list_args = args for desc_args in list_args.get("description", [list_args]): description = self._default_description.copy() description.set_from_description_args(desc_args) self.description_list.descriptions.append(description) sub_args = desc_args.get("connect_data") if sub_args is not None: description.set_from_connect_data_args(sub_args) sub_args = desc_args.get("security") if sub_args is not None: description.set_from_security_args(sub_args) for list_args in desc_args.get("address_list", [desc_args]): address_list = AddressList() address_list.set_from_args(list_args) description.address_lists.append(address_list) for addr_args in list_args.get("address", []): address = self._default_address.copy() address.set_from_args(addr_args) address_list.addresses.append(address) cdef int _set_access_token(self, object val, int error_num) except -1: """ Sets the access token either supplied directly by the user or indirectly via a callback. """ cdef: str token, private_key = None object token_expires if isinstance(val, tuple) and len(val) == 2: token, private_key = val if token is None or private_key is None: errors._raise_err(error_num) elif isinstance(val, str): token = val else: errors._raise_err(error_num) try: token_expires = self._get_token_expires(token) except Exception as e: errors._raise_err(error_num, cause=e) self._token_obfuscator = self._get_obfuscator(token) self._token = self._xor_bytes(bytearray(token.encode()), self._token_obfuscator) if private_key is not None: self._private_key_obfuscator = self._get_obfuscator(private_key) self._private_key = self._xor_bytes(bytearray(private_key.encode()), self._private_key_obfuscator) self.access_token_expires = token_expires cdef int _set_access_token_param(self, object val) except -1: """ Sets the access token parameter. """ if val is not None: if callable(val): self.access_token_callback = val else: self._set_access_token(val, errors.ERR_INVALID_ACCESS_TOKEN_PARAM) cdef int _set_new_password(self, str password) except -1: """ Sets the new password on the instance after first obfuscating it. """ if password is not None: self._new_password_obfuscator = self._get_obfuscator(password) self._new_password = self._xor_bytes(bytearray(password.encode()), self._new_password_obfuscator) cdef int _set_password(self, str password) except -1: """ Sets the password on the instance after first obfuscating it. """ if password is not None: self._password_obfuscator = self._get_obfuscator(password) self._password = self._xor_bytes(bytearray(password.encode()), self._password_obfuscator) cdef int _set_wallet_password(self, str password) except -1: """ Sets the wallet password on the instance after first obfuscating it. """ if password is not None: self._wallet_password_obfuscator = self._get_obfuscator(password) self._wallet_password = \ self._xor_bytes(bytearray(password.encode()), self._wallet_password_obfuscator) cdef bytearray _xor_bytes(self, bytearray a, bytearray b): """ Perform an XOR of two byte arrays as a means of obfuscating a password that is stored on the class. It is assumed that the byte arrays are of the same length. """ cdef: ssize_t length, i bytearray result length = len(a) result = bytearray(length) for i in range(length): result[i] = a[i] ^ b[i] return result def copy(self): """ Creates a copy of the connection parameters and returns it. """ cdef ConnectParamsImpl new_params new_params = ConnectParamsImpl.__new__(ConnectParamsImpl) new_params._copy(self) return new_params def _get_addresses(self): """ Return a list of the stored addresses. """ cdef: AddressList addr_list Description desc Address addr return [addr for desc in self.description_list.descriptions \ for addr_list in desc.address_lists \ for addr in addr_list.addresses] def get_connect_string(self): """ Internal method for getting the connect string. This will either be the connect string supplied to parse_connect_string() or parse_dsn(), or it will be a connect string built up from the components supplied when the object was built. """ if self._has_components: return self.description_list.build_connect_string() def get_full_user(self): """ Internal method used for getting the full user (including any proxy user) which is used in thick mode exlusively and which is used in the repr() methods for Connection and ConnectionPool. """ if self.proxy_user is not None: return f"{self.user}[{self.proxy_user}]" return self.user def parse_connect_string(self, str connect_string): """ Internal method for parsing the connect string. """ connect_string = connect_string.strip() try: self._parse_connect_string(connect_string) except exceptions.Error: raise except Exception as e: errors._raise_err(errors.ERR_CANNOT_PARSE_CONNECT_STRING, cause=e, data=connect_string) def parse_dsn(self, str dsn, bint parse_connect_string): """ Parse a dsn (data source name) string supplied by the user. This can be in the form user/password@connect_string or it can be a simple connect string. The connect string is returned and the user, proxy_user and password values are retained. """ connect_string = dsn pos = dsn.rfind("@") if pos >= 0: credentials = dsn[:pos] connect_string = dsn[pos + 1:] pos = credentials.find("/") if pos >= 0: user = credentials[:pos] self._set_password(credentials[pos + 1:]) else: user = credentials self.parse_user(user) if parse_connect_string: self.parse_connect_string(connect_string) return connect_string def parse_user(self, str user): """ Parses a user string into its component parts, if applicable. The user string may be in the form user[proxy_user] or it may simply be a simple user string. """ start_pos = user.find("[") if start_pos > 0 and user.endswith("]"): self.proxy_user = user[start_pos + 1:-1] self.user = user[:start_pos] else: self.user = user cdef class Address: """ Internal class used to hold parameters for an address used to create a connection to the database. """ def __init__(self): self.protocol = DEFAULT_PROTOCOL self.port = DEFAULT_PORT cdef str build_connect_string(self): """ Build a connect string from the components. """ parts = [f"(PROTOCOL={self.protocol})", f"(HOST={self.host})", f"(PORT={self.port})"] if self.https_proxy is not None: parts.append(f"(HTTPS_PROXY={self.https_proxy})") if self.https_proxy_port != 0: parts.append(f"(HTTPS_PROXY_PORT={self.https_proxy_port})") return f'(ADDRESS={"".join(parts)})' def copy(self): """ Creates a copy of the address and returns it. """ cdef Address address = Address.__new__(Address) address.host = self.host address.port = self.port address.protocol = self.protocol address.https_proxy = self.https_proxy address.https_proxy_port = self.https_proxy_port return address @classmethod def from_args(cls, dict args): """ Creates an address and sets the arguments before returning it. This is used within connect descriptors containing address lists. """ address = cls() address.set_from_args(args) return address def set_from_args(self, dict args): """ Sets parameter values from an argument dictionary or an (ADDRESS) node in a connect descriptor. """ _set_str_param(args, "host", self) _set_uint_param(args, "port", &self.port) _set_protocol_param(args, "protocol", self) _set_str_param(args, "https_proxy", self) _set_uint_param(args, "https_proxy_port", &self.https_proxy_port) cdef class AddressList: """ Internal class used to hold address list parameters and a list of addresses used to create connections to the database. """ def __init__(self): self.addresses = [] cdef str build_connect_string(self): """ Build a connect string from the components. """ cdef Address a parts = [a.build_connect_string() for a in self.addresses] return f'(ADDRESS_LIST={"".join(parts)})' def set_from_args(self, dict args): """ Set paramter values from an argument dictionary or an (ADDRESS_LIST) node in a connect descriptor. """ _set_bool_param(args, "load_balance", &self.load_balance) _set_bool_param(args, "source_route", &self.source_route) cdef class Description: """ Internal class used to hold description parameters. """ def __init__(self): self.address_lists = [] self.tcp_connect_timeout = DEFAULT_TCP_CONNECT_TIMEOUT self.ssl_server_dn_match = True cdef str _build_duration_str(self, double value): """ Build up the value to display for a duration in the connect string. This must be an integer with the units following it. """ cdef int value_int, value_minutes value_int = value if value != value_int: return f"{int(value * 1000)}ms" value_minutes = (value_int // 60) if value_minutes * 60 == value_int: return f"{value_minutes}min" return f"{value_int}" cdef str build_connect_string(self, str cid=None): """ Build a connect string from the components. """ cdef: str connect_data, security, temp list parts, address_lists AddressList a # build connect data segment parts = [] if self.service_name is not None: parts.append(f"(SERVICE_NAME={self.service_name})") elif self.sid is not None: parts.append(f"(SID={self.sid})") if self.server_type is not None: parts.append(f"(SERVER={self.server_type})") if self.cclass is not None: parts.append(f"(POOL_CONNECTION_CLASS={self.cclass})") if self.purity != 0: parts.append(f"(POOL_PURITY={self.purity})") if cid is not None: parts.append(f"(CID={cid})") connect_data = f'(CONNECT_DATA={"".join(parts)})' # build security segment, if applicable parts = [] if self.ssl_server_dn_match: parts.append("(SSL_SERVER_DN_MATCH=ON)") if self.ssl_server_cert_dn is not None: parts.append(f"(SSL_SERVER_CERT_DN={self.ssl_server_cert_dn})") if self.wallet_location is not None: parts.append(f"(MY_WALLET_DIRECTORY={self.wallet_location})") security = f'(SECURITY={"".join(parts)})' # build connect string parts = [] if self.load_balance: parts.append("(LOAD_BALANCE=ON)") if self.source_route: parts.append("(SOURCE_ROUTE=ON)") if self.retry_count != 0: parts.append(f"(RETRY_COUNT={self.retry_count})") if self.retry_delay != 0: parts.append(f"(RETRY_DELAY={self.retry_delay})") if self.expire_time != 0: parts.append(f"(EXPIRE_TIME={self.expire_time})") if self.tcp_connect_timeout != DEFAULT_TCP_CONNECT_TIMEOUT: temp = self._build_duration_str(self.tcp_connect_timeout) parts.append(f"(TRANSPORT_CONNECT_TIMEOUT={temp})") address_lists = [a.build_connect_string() for a in self.address_lists] parts.extend(address_lists) parts.append(connect_data) parts.append(security) return f'(DESCRIPTION={"".join(parts)})' def copy(self): """ Creates a copy of the description (except for the address lists) and returns it. """ cdef Description description = Description.__new__(Description) description.address_lists = [] description.service_name = self.service_name description.sid = self.sid description.server_type = self.server_type description.cclass = self.cclass description.purity = self.purity description.expire_time = self.expire_time description.load_balance = self.load_balance description.source_route = self.source_route description.retry_count = self.retry_count description.retry_delay = self.retry_delay description.tcp_connect_timeout = self.tcp_connect_timeout description.ssl_server_dn_match = self.ssl_server_dn_match description.ssl_server_cert_dn = self.ssl_server_cert_dn description.wallet_location = self.wallet_location return description def set_from_connect_data_args(self, dict args): """ Set parameter values from an argument dictionary or a (CONNECT_DATA) node in a connect descriptor. """ _set_str_param(args, "service_name", self) _set_str_param(args, "sid", self) _set_server_type_param(args, "server_type", self) _set_str_param(args, "cclass", self) _set_purity_param(args, "purity", &self.purity) def set_from_description_args(self, dict args): """ Set parameter values from an argument dictionary or a (DESCRIPTION) node in a connect descriptor. """ cdef Address address _set_uint_param(args, "expire_time", &self.expire_time) _set_bool_param(args, "load_balance", &self.load_balance) _set_bool_param(args, "source_route", &self.source_route) _set_uint_param(args, "retry_count", &self.retry_count) _set_uint_param(args, "retry_delay", &self.retry_delay) _set_duration_param(args, "tcp_connect_timeout", &self.tcp_connect_timeout) def set_from_security_args(self, dict args): """ Set parameter values from an argument dictionary or a (SECURITY) node in a connect descriptor. """ _set_bool_param(args, "ssl_server_dn_match", &self.ssl_server_dn_match) _set_str_param(args, "ssl_server_cert_dn", self) _set_str_param(args, "wallet_location", self) cdef class DescriptionList: """ Internal class used to hold description list parameters and a list of descriptions. """ def __init__(self): self.descriptions = [] cdef str build_connect_string(self): """ Build a connect string from the components. """ cdef: Description d list parts parts = [d.build_connect_string() for d in self.descriptions] if len(parts) == 1: return parts[0] return f'(DESCIPTION_LIST={"".join(parts)})' def set_from_args(self, dict args): """ Set paramter values from an argument dictionary or a (DESCRIPTION_LIST) node in a connect descriptor. """ _set_bool_param(args, "load_balance", &self.load_balance) _set_bool_param(args, "source_route", &self.source_route) cdef class TnsnamesFile: """ Internal class used to parse and retain connect descriptor entries found in a tnsnames.ora file. """ def __init__(self, str file_name, int mtime): self.file_name = file_name self.mtime = mtime self.entries = {} def read(self): """ Read and parse the file and retain the connect descriptors found inside the file. """ with open(self.file_name) as f: entry_names = None for line in f: line = line.strip() pos = line.find("#") if pos >= 0: line = line[:pos] if not line: continue if entry_names is None: pos = line.find("=") if pos < 0: continue entry_names = [s.strip() for s in line[:pos].split(",")] entry_lines = [] num_parens = 0 line = line[pos+1:].strip() if line: num_parens += line.count("(") - line.count(")") entry_lines.append(line) if entry_lines and num_parens <= 0: descriptor = "".join(entry_lines) for name in entry_names: self.entries[name.upper()] = descriptor entry_names = None python-oracledb-1.2.1/src/oracledb/impl/base/connection.pyx000066400000000000000000000267651434177474600237640ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # connection.pyx # # Cython file defining the base Connection implementation class (embedded in # base_impl.pyx). #------------------------------------------------------------------------------ cdef class BaseConnImpl: def __init__(self, str dsn, ConnectParamsImpl params): self.dsn = dsn self.username = params.user cdef object _check_value(self, DbType dbtype, BaseDbObjectTypeImpl objtype, object value, bint* is_ok): """ Checks that the specified Python value is acceptable for the given database type. If the "is_ok" parameter is passed as NULL, an exception is raised. The value to use is returned (possibly modified from the value passed in). """ cdef: uint32_t db_type_num BaseLobImpl lob_impl # null values are always accepted if value is None: return value # check to see if the Python value is accepted and perform any # necessary adjustments db_type_num = dbtype.num if db_type_num in (DB_TYPE_NUM_NUMBER, DB_TYPE_NUM_BINARY_INTEGER, DB_TYPE_NUM_BINARY_DOUBLE, DB_TYPE_NUM_BINARY_FLOAT): if isinstance(value, (PY_TYPE_BOOL, int, float, PY_TYPE_DECIMAL)): if db_type_num in (DB_TYPE_NUM_BINARY_FLOAT, DB_TYPE_NUM_BINARY_DOUBLE): return float(value) elif db_type_num == DB_TYPE_NUM_BINARY_INTEGER \ or cpython.PyBool_Check(value): return int(value) return value elif db_type_num in (DB_TYPE_NUM_CHAR, DB_TYPE_NUM_VARCHAR, DB_TYPE_NUM_NCHAR, DB_TYPE_NUM_NVARCHAR, DB_TYPE_NUM_LONG_VARCHAR, DB_TYPE_NUM_LONG_NVARCHAR): if isinstance(value, bytes): return ( value).decode() elif isinstance(value, str): return value elif db_type_num in (DB_TYPE_NUM_RAW, DB_TYPE_NUM_LONG_RAW): if isinstance(value, str): return ( value).encode() elif isinstance(value, bytes): return value elif db_type_num in (DB_TYPE_NUM_DATE, DB_TYPE_NUM_TIMESTAMP, DB_TYPE_NUM_TIMESTAMP_LTZ, DB_TYPE_NUM_TIMESTAMP_TZ): if cydatetime.PyDateTime_Check(value) \ or cydatetime.PyDate_Check(value): return value elif db_type_num == DB_TYPE_NUM_INTERVAL_DS: if isinstance(value, PY_TYPE_TIMEDELTA): return value elif db_type_num in (DB_TYPE_NUM_CLOB, DB_TYPE_NUM_NCLOB, DB_TYPE_NUM_BLOB): if isinstance(value, PY_TYPE_LOB): lob_impl = value._impl if lob_impl.dbtype is not dbtype: if is_ok != NULL: is_ok[0] = False return value errors._raise_err(errors.ERR_LOB_OF_WRONG_TYPE, actual_type_name=lob_impl.dbtype.name, expected_type_name=dbtype.name) return value elif isinstance(value, (bytes, str)): if db_type_num == DB_TYPE_NUM_BLOB: if isinstance(value, str): value = value.encode() elif isinstance(value, bytes): value = value.decode() lob_impl = self.create_temp_lob_impl(dbtype) if value: lob_impl.write(value, 1) return PY_TYPE_LOB._from_impl(lob_impl) elif db_type_num == DB_TYPE_NUM_OBJECT: if isinstance(value, PY_TYPE_DB_OBJECT): if value._impl.type != objtype: if is_ok != NULL: is_ok[0] = False return value errors._raise_err(errors.ERR_WRONG_OBJECT_TYPE, actual_schema=value.type.schema, actual_name=value.type.name, expected_schema=objtype.schema, expected_name=objtype.name) return value elif db_type_num == DB_TYPE_NUM_CURSOR: if isinstance(value, PY_TYPE_CURSOR): return value elif db_type_num == DB_TYPE_NUM_BOOLEAN: return bool(value) elif db_type_num == DB_TYPE_NUM_JSON: return value else: if is_ok != NULL: is_ok[0] = False return value errors._raise_err(errors.ERR_UNSUPPORTED_TYPE_SET, db_type_name=dbtype.name) # the Python value was not considered acceptable if is_ok != NULL: is_ok[0] = False return value errors._raise_err(errors.ERR_UNSUPPORTED_PYTHON_TYPE_FOR_DB_TYPE, py_type_name=type(value).__name__, db_type_name=dbtype.name) @utils.CheckImpls("getting a connection OCI attribute") def _get_oci_attr(self, uint32_t handle_type, uint32_t attr_num, uint32_t attr_type): pass @utils.CheckImpls("setting a connection OCI attribute") def _set_oci_attr(self, uint32_t handle_type, uint32_t attr_num, uint32_t attr_type, object value): pass @utils.CheckImpls("aborting a currently executing statement") def cancel(self): pass @utils.CheckImpls("changing a password") def change_password(self, old_password, new_password): pass @utils.CheckImpls("checking if the connection is healthy") def get_is_healthy(self): pass @utils.CheckImpls("closing a connection") def close(self, in_del=False): pass @utils.CheckImpls("committing a transaction") def commit(self): pass @utils.CheckImpls("creating a cursor") def create_cursor_impl(self): pass @utils.CheckImpls("creating a queue") def create_queue_impl(self): pass @utils.CheckImpls("creating a SODA database object") def create_soda_database_impl(self, conn): pass @utils.CheckImpls("creating a subscription") def create_subscr_impl(self, object conn, object callback, uint32_t namespace, str name, uint32_t protocol, str ip_address, uint32_t port, uint32_t timeout, uint32_t operations, uint32_t qos, uint8_t grouping_class, uint32_t grouping_value, uint8_t grouping_type, bint client_initiated): pass @utils.CheckImpls("creating a temporary LOB") def create_temp_lob_impl(self, DbType dbtype): pass @utils.CheckImpls("getting the call timeout") def get_call_timeout(self): pass @utils.CheckImpls("getting the current schema") def get_current_schema(self): pass @utils.CheckImpls("getting the edition") def get_edition(self): pass @utils.CheckImpls("getting the external name") def get_external_name(self): pass @utils.CheckImpls("getting the OCI service context handle") def get_handle(self): pass @utils.CheckImpls("getting the internal name") def get_internal_name(self): pass @utils.CheckImpls("getting the logical transaction id") def get_ltxid(self): pass @utils.CheckImpls("getting the statement cache size") def get_stmt_cache_size(self): pass @utils.CheckImpls("getting an object type") def get_type(self, object conn, str name): pass @utils.CheckImpls("getting the database version") def get_version(self): pass @utils.CheckImpls("pinging the database") def ping(self): pass @utils.CheckImpls("rolling back a transaction") def rollback(self): pass @utils.CheckImpls("setting the action") def set_action(self, value): pass @utils.CheckImpls("setting the call timeout") def set_call_timeout(self, value): pass @utils.CheckImpls("setting the client identifier") def set_client_identifier(self, value): pass @utils.CheckImpls("setting the client info") def set_client_info(self, value): pass @utils.CheckImpls("setting the current schema") def set_current_schema(self, value): pass @utils.CheckImpls("setting the database operation") def set_dbop(self, value): pass @utils.CheckImpls("setting the execution context id") def set_econtext_id(self, value): pass @utils.CheckImpls("setting the external name") def set_external_name(self, value): pass @utils.CheckImpls("setting the internal name") def set_internal_name(self, value): pass @utils.CheckImpls("setting the module") def set_module(self, value): pass @utils.CheckImpls("setting the statement cache size") def set_stmt_cache_size(self, value): pass @utils.CheckImpls("shutting down the database") def shutdown(self, uint32_t mode): pass @utils.CheckImpls("starting up the database") def startup(self, bint force, bint restrict, str pfile): pass @utils.CheckImpls("starting a TPC (two-phase commit) transaction") def tpc_begin(self, xid, uint32_t flags, uint32_t timeout): pass @utils.CheckImpls("committing a TPC (two-phase commit) transaction") def tpc_commit(self, xid, bint one_phase): pass @utils.CheckImpls("ending a TPC (two-phase commit) transaction") def tpc_end(self, xid, uint32_t flags): pass @utils.CheckImpls("forgetting a TPC (two-phase commit) transaction") def tpc_forget(self, xid): pass @utils.CheckImpls("preparing a TPC (two-phase commit) transaction") def tpc_prepare(self, xid): pass @utils.CheckImpls("rolling back a TPC (two-phase commit) transaction") def tpc_rollback(self, xid): pass python-oracledb-1.2.1/src/oracledb/impl/base/cursor.pyx000066400000000000000000000450071434177474600231300ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # cursor.pyx # # Cython file defining the base Cursor implementation class (embedded in # base_impl.pyx). #------------------------------------------------------------------------------ cdef class BaseCursorImpl: @cython.boundscheck(False) @cython.wraparound(False) cdef int _bind_values(self, object cursor, object type_handler, object params, uint32_t num_rows, uint32_t row_num, bint defer_type_assignment) except -1: """ Internal method used for binding values. """ if self.bind_vars is None: self.bind_vars = [] if isinstance(params, dict): if self.bind_style is None: self.bind_style = dict self.bind_vars_by_name = {} elif self.bind_style is not dict: errors._raise_err(errors.ERR_MIXED_POSITIONAL_AND_NAMED_BINDS) self._bind_values_by_name(cursor, type_handler, params, num_rows, row_num, defer_type_assignment) elif isinstance(params, (list, tuple)): if self.bind_style is None: self.bind_style = list elif self.bind_style is not list: errors._raise_err(errors.ERR_MIXED_POSITIONAL_AND_NAMED_BINDS) self._bind_values_by_position(cursor, type_handler, params, num_rows, row_num, defer_type_assignment) else: errors._raise_err(errors.ERR_WRONG_EXECUTEMANY_PARAMETERS_TYPE) @cython.boundscheck(False) @cython.wraparound(False) cdef int _bind_values_by_name(self, object cursor, object type_handler, dict params, uint32_t num_rows, uint32_t row_num, bint defer_type_assignment) except -1: """ Internal method used for binding values by name. """ cdef: BindVar bind_var object conn ssize_t pos conn = cursor.connection for name, value in params.items(): bind_var = self.bind_vars_by_name.get(name) if bind_var is None: pos = len(self.bind_vars_by_name) if pos < len(self.bind_vars): bind_var = self.bind_vars[pos] else: bind_var = BindVar.__new__(BindVar) self.bind_vars.append(bind_var) bind_var.name = name self.bind_vars_by_name[name] = bind_var bind_var._set_by_value(conn, self, cursor, value, type_handler, row_num, num_rows, defer_type_assignment) @cython.boundscheck(False) @cython.wraparound(False) cdef int _bind_values_by_position(self, object cursor, object type_handler, object params, uint32_t num_rows, uint32_t row_num, bint defer_type_assignment) except -1: """ Internal method used for binding values by position. """ cdef: BindVar bind_var object conn ssize_t i conn = cursor.connection for i, value in enumerate(params): if i < len(self.bind_vars): bind_var = self.bind_vars[i] else: bind_var = BindVar.__new__(BindVar) bind_var.pos = i + 1 self.bind_vars.append(bind_var) bind_var._set_by_value(conn, self, cursor, value, type_handler, row_num, num_rows, defer_type_assignment) cdef int _close(self, bint in_del) except -1: """ Internal method for closing the cursor. """ raise NotImplementedError() @cython.boundscheck(False) @cython.wraparound(False) cdef int _create_fetch_var(self, object conn, object cursor, object type_handler, ssize_t pos, FetchInfo fetch_info) except -1: """ Create the fetch variable for the given position and fetch information. The output type handler is consulted, if present, to make any necessary adjustments. """ cdef: BaseVarImpl var_impl uint32_t db_type_num object var # if an output type handler is specified, call it; the output type # handler should return a variable or None; the value None implies that # the default processing should take place just as if no output type # handler was defined if type_handler is not None: var = type_handler(cursor, fetch_info._name, fetch_info._dbtype, fetch_info._size, fetch_info._precision, fetch_info._scale) if var is not None: self._verify_var(var) var_impl = var._impl var_impl._fetch_info = fetch_info self.fetch_vars[pos] = var self.fetch_var_impls[pos] = var_impl return 0 # otherwise, create a new variable using the provided fetch information var_impl = self._create_var_impl(conn) var_impl.num_elements = self._fetch_array_size var_impl.dbtype = fetch_info._dbtype var_impl.objtype = fetch_info._objtype var_impl.name = fetch_info._name var_impl.size = fetch_info._size var_impl.precision = fetch_info._precision var_impl.scale = fetch_info._scale var_impl.nulls_allowed = fetch_info._nulls_allowed var_impl._fetch_info = fetch_info # adjust the variable based on the defaults specified by the user, if # applicable db_type_num = var_impl.dbtype.num if db_type_num == DB_TYPE_NUM_NUMBER: if defaults.fetch_decimals: var_impl._preferred_num_type = NUM_TYPE_DECIMAL elif var_impl.scale == 0 \ or (var_impl.scale == -127 and var_impl.precision == 0): var_impl._preferred_num_type = NUM_TYPE_INT elif not defaults.fetch_lobs: if db_type_num == DB_TYPE_NUM_BLOB: var_impl.dbtype = DB_TYPE_LONG_RAW elif db_type_num == DB_TYPE_NUM_CLOB: var_impl.dbtype = DB_TYPE_LONG elif db_type_num == DB_TYPE_NUM_NCLOB: var_impl.dbtype = DB_TYPE_LONG_NVARCHAR # finalize variable and store in arrays var_impl._finalize_init() self.fetch_var_impls[pos] = var_impl self.fetch_vars[pos] = PY_TYPE_VAR._from_impl(var_impl) @cython.boundscheck(False) @cython.wraparound(False) cdef object _create_row(self): """ Internal method for creating a row from the fetched data. """ cdef: Py_ssize_t i, num_vars BaseVarImpl var_impl object row, value num_vars = cpython.PyList_GET_SIZE(self.fetch_var_impls) row = cpython.PyTuple_New(num_vars) for i in range(num_vars): var_impl = self.fetch_var_impls[i] value = var_impl._get_scalar_value(self._buffer_index) cpython.Py_INCREF(value) cpython.PyTuple_SET_ITEM(row, i, value) if self.rowfactory is not None: row = self.rowfactory(*row) self._buffer_index += 1 self._buffer_rowcount -= 1 self.rowcount += 1 return row cdef BaseVarImpl _create_var_impl(self, object conn): """ Internal method for creating a variable. """ raise NotImplementedError() cdef int _fetch_rows(self, object cursor) except -1: """ Internal method used for fetching rows from a cursor. """ raise NotImplementedError() cdef BaseConnImpl _get_conn_impl(self): """ Internal method used to return the connection implementation associated with the cursor implementation. """ raise NotImplementedError() cdef object _get_input_type_handler(self): """ Return the input type handler to use for the cursor. If one is not directly defined on the cursor then the one defined on the connection is used instead. """ cdef BaseConnImpl conn_impl if self.inputtypehandler is not None: return self.inputtypehandler conn_impl = self._get_conn_impl() return conn_impl.inputtypehandler @utils.CheckImpls("getting a cursor OCI attribute") def _get_oci_attr(self, uint32_t attr_num, uint32_t attr_type): pass cdef object _get_output_type_handler(self): """ Return the output type handler to use for the cursor. If one is not directly defined on the cursor then the one defined on the connection is used instead. """ cdef BaseConnImpl conn_impl if self.outputtypehandler is not None: return self.outputtypehandler conn_impl = self._get_conn_impl() return conn_impl.outputtypehandler cdef int _init_fetch_vars(self, uint32_t num_columns) except -1: """ Initializes the fetch variable lists in preparation for creating the fetch variables used in fetching rows from the database. """ self.fetch_vars = [None] * num_columns self.fetch_var_impls = [None] * num_columns cdef bint _is_plsql(self): """ Internal method that indicates whether the currently prepared statement is a PL/SQL statement or not. """ raise NotImplementedError() cdef int _perform_binds(self, object conn, uint32_t num_execs) except -1: """ Perform all binds on the cursor. """ cdef: BindVar bind_var ssize_t i for i, bind_var in enumerate(self.bind_vars): bind_var.var_impl._bind(conn, self, num_execs, bind_var.name, bind_var.pos) cdef int _reset_bind_vars(self, uint32_t num_rows) except -1: """ Reset all of the existing bind variables. If any bind variables don't have enough space to store the number of rows specified, expand and then reinitialize that bind variable. """ cdef: BindVar bind_var ssize_t i if self.bind_vars is not None: for i in range(len(self.bind_vars)): bind_var = self.bind_vars[i] if bind_var.var_impl is not None: bind_var.var_impl._on_reset_bind(num_rows) bind_var.has_value = False @utils.CheckImpls("setting a cursor OCI attribute") def _set_oci_attr(self, uint32_t attr_num, uint32_t attr_type, object value): pass cdef int _verify_var(self, object var) except -1: """ Internal method used for verifying if an outputtypehandler returns a valid var object. """ if not isinstance(var, PY_TYPE_VAR): errors._raise_err(errors.ERR_EXPECTING_VAR) if self.arraysize > var.num_elements: errors._raise_err(errors.ERR_INCORRECT_VAR_ARRAYSIZE, var_arraysize=var.num_elements, required_arraysize=self.arraysize) def bind_many(self, object cursor, list parameters): """ Internal method used for binding multiple rows of data. """ cdef: bint defer_type_assignment ssize_t i, num_rows object params_row type_handler = self._get_input_type_handler() num_rows = len(parameters) self._reset_bind_vars(num_rows) for i, params_row in enumerate(parameters): defer_type_assignment = (i < num_rows - 1) self._bind_values(cursor, type_handler, params_row, num_rows, i, defer_type_assignment) def bind_one(self, cursor, parameters): """ Internal method used for binding a single row of data. """ cdef: bint defer_type_assignment = False uint32_t row_num = 0, num_rows = 1 ssize_t num_bind_vars, pos object name, value BindVar bind_var dict dict_params type_handler = self._get_input_type_handler() self._reset_bind_vars(num_rows) self._bind_values(cursor, type_handler, parameters, num_rows, row_num, defer_type_assignment) def close(self, bint in_del=False): """ Closes the cursor and makes it unusable for further operations. """ self.bind_vars = None self.bind_vars_by_name = None self.bind_style = None self.fetch_vars = None self._close(in_del) def create_var(self, object conn, object typ, uint32_t size=0, uint32_t num_elements=1, object inconverter=None, object outconverter=None, str encoding_errors=None, bint bypass_decode=False, bint is_array=False): cdef BaseVarImpl var_impl var_impl = self._create_var_impl(conn) var_impl._set_type_info_from_type(typ) var_impl.size = size var_impl.num_elements = num_elements var_impl.inconverter = inconverter var_impl.outconverter = outconverter var_impl.bypass_decode = bypass_decode var_impl.is_array = is_array var_impl._finalize_init() return PY_TYPE_VAR._from_impl(var_impl) @utils.CheckImpls("executing a statement") def execute(self, cursor): pass @utils.CheckImpls("executing a statement in batch") def executemany(self, cursor, num_execs, batcherrors, arraydmlrowcounts): pass def fetch_next_row(self, cursor): """ Internal method used for fetching the next row from a cursor. """ if self._buffer_rowcount == 0 and self._more_rows_to_fetch: self._fetch_rows(cursor) if self._buffer_rowcount > 0: return self._create_row() @utils.CheckImpls("getting a list of array DML row counts") def get_array_dml_row_counts(self): pass @utils.CheckImpls("getting a list of batch errors") def get_batch_errors(self): pass @utils.CheckImpls("getting a list of bind variable names") def get_bind_names(self): pass def get_bind_vars(self): """ Return a list (when binding by position) or a dictionary (when binding by name) of the bind variables associated with the cursor. """ cdef: BindVar bind_var ssize_t i if self.bind_vars is None: return [] for bind_var in self.bind_vars: if bind_var.var is None and bind_var.var_impl is not None: bind_var.var = PY_TYPE_VAR._from_impl(bind_var.var_impl) if self.bind_style is list: return [bind_var.var for bind_var in self.bind_vars] return dict([(bind_var.name, bind_var.var) \ for bind_var in self.bind_vars]) def get_description(self): """ Internal method for populating the cursor description. """ return [v._impl.get_description() for v in self.fetch_vars] @utils.CheckImpls("getting implicit results from PL/SQL") def get_implicit_results(self, connection): pass @utils.CheckImpls("getting the rowid of the row last modified") def get_lastrowid(self): pass @utils.CheckImpls("determining if the cursor last executed a query") def is_query(self, cursor): pass @utils.CheckImpls("parsing a statement without executing it") def parse(self, cursor): pass @utils.CheckImpls("preparing a statement") def prepare(self, str statement, str tag, bint cache_statement): pass @utils.CheckImpls("scrolling a scrollable cursor") def scroll(self, conn, value, mode): """ Scrolls a scrollable cursor. """ pass def setinputsizes(self, object conn, tuple args, dict kwargs): """ Sets type information for bind variables in advance of executing a statement (and binding values). """ cdef: object name, value BindVar bind_var ssize_t pos self.bind_vars = [] if kwargs: self.bind_style = dict self.bind_vars_by_name = {} for name, value in kwargs.items(): bind_var = BindVar.__new__(BindVar) self.bind_vars.append(bind_var) self.bind_vars_by_name[name] = bind_var bind_var._set_by_type(conn, self, value) bind_var.name = name else: self.bind_style = list for pos, value in enumerate(args): bind_var = BindVar.__new__(BindVar) self.bind_vars.append(bind_var) bind_var._set_by_type(conn, self, value) bind_var.pos = pos + 1 python-oracledb-1.2.1/src/oracledb/impl/base/dbobject.pyx000066400000000000000000000113101434177474600233550ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # dbobject.pyx # # Cython file defining the base DbObjectType, DbObjectAttr and DbObject # implementation classes (embedded in base_impl.pyx). #------------------------------------------------------------------------------ cdef class BaseDbObjectImpl: def append(self, object value): """ Appends a value to the collection after first checking to see if the value is acceptable. """ cdef BaseConnImpl conn_impl = self.type._conn_impl value = conn_impl._check_value(self.type.element_dbtype, self.type.element_objtype, value, NULL) self.append_checked(value) @utils.CheckImpls("appending a value to a collection") def append_checked(self, object value): pass @utils.CheckImpls("creating a copy of an object") def copy(self): pass @utils.CheckImpls("deleting an element in a collection") def delete_by_index(self, int32_t index): pass @utils.CheckImpls("determining if an entry exists in a collection") def exists_by_index(self, int32_t index): pass @utils.CheckImpls("getting an attribute value") def get_attr_value(self, BaseDbObjectAttrImpl attr): pass @utils.CheckImpls("getting an element of a collection") def get_element_by_index(self, int32_t index): pass @utils.CheckImpls("getting the first index of a collection") def get_first_index(self): pass @utils.CheckImpls("getting the last index of a collection") def get_last_index(self): pass @utils.CheckImpls("getting the next index of a collection") def get_next_index(self, int32_t index): pass @utils.CheckImpls("getting the previous index of a collection") def get_prev_index(self, int32_t index): pass @utils.CheckImpls("getting the size of a collection") def get_size(self): pass def set_attr_value(self, BaseDbObjectAttrImpl attr, object value): """ Sets the attribute value after first checking to see if the value is acceptable. """ cdef BaseConnImpl conn_impl = self.type._conn_impl value = conn_impl._check_value(attr.dbtype, attr.objtype, value, NULL) self.set_attr_value_checked(attr, value) @utils.CheckImpls("setting an attribute value") def set_attr_value_checked(self, BaseDbObjectAttrImpl attr, object value): pass def set_element_by_index(self, int32_t index, object value): """ Sets the element value after first checking to see if the value is acceptable. """ cdef BaseConnImpl conn_impl = self.type._conn_impl value = conn_impl._check_value(self.type.element_dbtype, self.type.element_objtype, value, NULL) self.set_element_by_index_checked(index, value) @utils.CheckImpls("setting an element of a collection") def set_element_by_index_checked(self, int32_t index, object value): pass @utils.CheckImpls("trimming elements from a collection") def trim(self, int32_t num_to_trim): pass cdef class BaseDbObjectAttrImpl: pass cdef class BaseDbObjectTypeImpl: def __eq__(self, other): if isinstance(other, BaseDbObjectTypeImpl): return other._conn_impl is self._conn_impl \ and other.schema == self.schema \ and other.name == self.name return NotImplemented @utils.CheckImpls("creating a new object") def create_new_object(self): pass python-oracledb-1.2.1/src/oracledb/impl/base/lob.pyx000066400000000000000000000052651434177474600223710ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # lob.pyx # # Cython file defining the base Lob implementation class (embedded in # base_impl.pyx). #------------------------------------------------------------------------------ cdef class BaseLobImpl: @utils.CheckImpls("closing a LOB") def close(self): pass @utils.CheckImpls("checking if a BFILE exists") def file_exists(self): pass @utils.CheckImpls("freeing the lob object") def free_lob(self): pass @utils.CheckImpls("getting the chunk size of a LOB") def get_chunk_size(self): pass @utils.CheckImpls("getting the file name and directory alias of a BFILE") def get_file_name(self): pass @utils.CheckImpls("getting whether a LOB is open or not") def get_is_open(self): pass @utils.CheckImpls("getting the maximum amount that can be read from a LOB") def get_max_amount(self): pass @utils.CheckImpls("getting the size of a LOB") def get_size(self): pass @utils.CheckImpls("opening a LOB") def open(self): pass @utils.CheckImpls("reading data from a LOB") def read(self, uint64_t offset, uint64_t amount): pass @utils.CheckImpls("setting the file name an directory alias of a BFILE") def set_file_name(self, str dir_alias, str name): pass @utils.CheckImpls("trimming a LOB") def trim(self, uint64_t new_size): pass @utils.CheckImpls("writing data to a LOB") def write(self, object value, uint64_t offset): pass python-oracledb-1.2.1/src/oracledb/impl/base/pool.pyx000066400000000000000000000101671434177474600225630ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # pool.pyx # # Cython file defining the base Pool implementation class (embedded in # base_impl.pyx). #------------------------------------------------------------------------------ cdef class BasePoolImpl: @utils.CheckImpls("acquiring a connection from a pool") def acquire(self, str user, str password, str cclass, uint32_t purity, str tag, bint matchanytag, list shardingkey, list supershardingkey): pass @utils.CheckImpls("closing a pool") def close(self, bint force): pass @utils.CheckImpls("dropping a connection from a pool") def drop(self, conn_impl): pass @utils.CheckImpls("getting the number of busy connections in a pool") def get_busy_count(self): pass @utils.CheckImpls("getting the 'get' mode of a pool") def get_getmode(self): pass @utils.CheckImpls("getting the maximum lifetime of a connection in a pool") def get_max_lifetime_session(self): pass @utils.CheckImpls("getting the maximum sessions per shard in a pool") def get_max_sessions_per_shard(self): pass @utils.CheckImpls("getting the number of connections open in a pool") def get_open_count(self): pass @utils.CheckImpls("getting the ping interval of a pool") def get_ping_interval(self): pass @utils.CheckImpls("getting whether the SODA metadata cache is enabled") def get_soda_metadata_cache(self): pass @utils.CheckImpls("getting the size of the statement cache in a pool") def get_stmt_cache_size(self): pass @utils.CheckImpls("getting the timeout for idle connections in a pool") def get_timeout(self): pass @utils.CheckImpls("getting the wait timeout for a pool") def get_wait_timeout(self): pass @utils.CheckImpls("reconfiguring a pool") def reconfigure(self, uint32_t min, uint32_t max, uint32_t increment): pass @utils.CheckImpls("setting the 'get' mode of a pool") def set_getmode(self, uint8_t value): pass @utils.CheckImpls("setting the maximum lifetime of a connection a pool") def set_max_lifetime_session(self, uint32_t value): pass @utils.CheckImpls("setting the maximum sessions per shard") def set_max_sessions_per_shard(self, uint32_t value): pass @utils.CheckImpls("setting the ping interval") def set_ping_interval(self, int value): pass @utils.CheckImpls("setting whether the SODA metadata cache is enabled") def set_soda_metadata_cache(self, bint value): pass @utils.CheckImpls("setting the size of the statement cache in a pool") def set_stmt_cache_size(self, uint32_t value): pass @utils.CheckImpls("setting the timeout for idle connections in a pool") def set_timeout(self, uint32_t value): pass @utils.CheckImpls("setting the wait timeout for a pool") def set_wait_timeout(self, uint32_t value): pass python-oracledb-1.2.1/src/oracledb/impl/base/pool_params.pyx000066400000000000000000000105041434177474600241210ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # pool_params.pyx # # Cython file defining the base PoolParams implementation class (embedded in # base_impl.pyx). #------------------------------------------------------------------------------ cdef class PoolParamsImpl(ConnectParamsImpl): def __init__(self): ConnectParamsImpl.__init__(self) self.min = 1 self.max = 2 self.increment = 1 self.getmode = constants.POOL_GETMODE_WAIT self.homogeneous = True self.ping_interval = 60 cdef int _copy(self, ConnectParamsImpl other_params) except -1: """ Internal method for copying attributes from another set of parameters. """ cdef PoolParamsImpl pool_params = other_params ConnectParamsImpl._copy(self, other_params) self.min = pool_params.min self.max = pool_params.max self.increment = pool_params.increment self.connectiontype = pool_params.connectiontype self.getmode = pool_params.getmode self.homogeneous = pool_params.homogeneous self.timeout = pool_params.timeout self.wait_timeout = pool_params.wait_timeout self.max_lifetime_session = pool_params.max_lifetime_session self.session_callback = pool_params.session_callback self.max_sessions_per_shard = pool_params.max_sessions_per_shard self.soda_metadata_cache = pool_params.soda_metadata_cache self.ping_interval = pool_params.ping_interval def copy(self): """ Creates a copy of the connection parameters and returns it. """ cdef PoolParamsImpl new_params new_params = PoolParamsImpl.__new__(PoolParamsImpl) new_params._copy(self) return new_params def set(self, dict args): """ Sets the property values based on the supplied arguments. All values not supplied will be left unchanged. """ ConnectParamsImpl.set(self, args) _set_uint_param(args, "min", &self.min) _set_uint_param(args, "max", &self.max) _set_uint_param(args, "increment", &self.increment) self.connectiontype = args.get("connectiontype") _set_uint_param(args, "getmode", &self.getmode) _set_bool_param(args, "homogeneous", &self.homogeneous) _set_uint_param(args, "timeout", &self.timeout) _set_uint_param_with_deprecated_name(args, "wait_timeout", "waitTimeout", &self.wait_timeout) _set_uint_param_with_deprecated_name(args, "max_lifetime_session", "maxLifetimeSession", &self.max_lifetime_session) _set_obj_param_with_deprecated_name(args, "session_callback", "sessionCallback", self) _set_uint_param_with_deprecated_name(args, "max_sessions_per_shard", "maxSessionsPerShard", &self.max_sessions_per_shard) _set_bool_param(args, "soda_metadata_cache", &self.soda_metadata_cache) _set_int_param(args, "ping_interval", &self.ping_interval) python-oracledb-1.2.1/src/oracledb/impl/base/queue.pyx000066400000000000000000000143531434177474600227370ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # queue.pyx # # Cython file defining the base Queue implementation class (embedded in # base_impl.pyx). #------------------------------------------------------------------------------ cdef class BaseQueueImpl: @utils.CheckImpls("dequeuing multiple messages") def deq_many(self, uint32_t max_num_messages): pass @utils.CheckImpls("dequeuing a single message") def deq_one(self): pass @utils.CheckImpls("enqueueing multiple messages") def enq_many(self, list props_impls): pass @utils.CheckImpls("enqueuing a single message") def enq_one(self, BaseMsgPropsImpl props_impl): pass @utils.CheckImpls("initializing a queue") def initialize(self, BaseConnImpl conn_impl, str name, BaseDbObjectImpl payload_type, bint is_json): pass cdef class BaseDeqOptionsImpl: @utils.CheckImpls("getting the condition") def get_condition(self): pass @utils.CheckImpls("getting the consumer name") def get_consumer_name(self): pass @utils.CheckImpls("getting the correlation") def get_correlation(self): pass @utils.CheckImpls("getting the message id") def get_message_id(self): pass @utils.CheckImpls("getting the mode") def get_mode(self): pass @utils.CheckImpls("getting the navigation") def get_navigation(self): pass @utils.CheckImpls("getting the transformation") def get_transformation(self): pass @utils.CheckImpls("getting the visibility") def get_visibility(self): pass @utils.CheckImpls("getting the wait time") def get_wait(self): pass @utils.CheckImpls("setting the condition") def set_condition(self, str value): pass @utils.CheckImpls("setting the consumer name") def set_consumer_name(self, str value): pass @utils.CheckImpls("setting the correlation") def set_correlation(self, str value): pass @utils.CheckImpls("setting the delivery mode") def set_delivery_mode(self, uint16_t value): pass @utils.CheckImpls("setting the mode") def set_mode(self, uint32_t value): pass @utils.CheckImpls("setting the message id") def set_message_id(self, bytes value): pass @utils.CheckImpls("setting the navigation") def set_navigation(self, uint32_t value): pass @utils.CheckImpls("setting the transformation") def set_transformation(self, str value): pass @utils.CheckImpls("setting the visibility") def set_visibility(self, uint32_t value): pass @utils.CheckImpls("setting the wait time") def set_wait(self, uint32_t value): pass cdef class BaseEnqOptionsImpl: @utils.CheckImpls("getting the transformation") def get_transformation(self): pass @utils.CheckImpls("getting the visibility") def get_visibility(self): pass @utils.CheckImpls("setting the delivery mode") def set_delivery_mode(self, uint16_t value): pass @utils.CheckImpls("setting the transformation") def set_transformation(self, str value): pass @utils.CheckImpls("setting the visibility") def set_visibility(self, uint32_t value): pass cdef class BaseMsgPropsImpl: @utils.CheckImpls("getting the number of attempts") def get_num_attempts(self): pass @utils.CheckImpls("getting the correlation") def get_correlation(self): pass @utils.CheckImpls("getting the delay") def get_delay(self): pass @utils.CheckImpls("getting the delivery mode") def get_delivery_mode(self): pass @utils.CheckImpls("getting the enqueue time") def get_enq_time(self): pass @utils.CheckImpls("getting the name of the exception queue") def get_exception_queue(self): pass @utils.CheckImpls("getting the expiration") def get_expiration(self): pass @utils.CheckImpls("getting the message id") def get_message_id(self): pass @utils.CheckImpls("getting the priority") def get_priority(self): pass @utils.CheckImpls("getting the message state") def get_state(self): pass @utils.CheckImpls("setting the correlation") def set_correlation(self, str value): pass @utils.CheckImpls("setting the delay") def set_delay(self, int32_t value): pass @utils.CheckImpls("setting the name of the exception queue") def set_exception_queue(self, str value): pass @utils.CheckImpls("setting the expiration") def set_expiration(self, int32_t value): pass @utils.CheckImpls("setting the payload from bytes") def set_payload_bytes(self, bytes value): pass @utils.CheckImpls("setting the payload from a database object") def set_payload_object(self, BaseDbObjectImpl value): pass @utils.CheckImpls("setting the priority") def set_priority(self, int32_t value): pass @utils.CheckImpls("setting recipients list") def set_recipients(self, list value): pass python-oracledb-1.2.1/src/oracledb/impl/base/soda.pyx000066400000000000000000000110611434177474600225320ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # soda.pyx # # Cython file defining the base SODA implementation classes (embedded in # base_impl.pyx). #------------------------------------------------------------------------------ cdef class BaseSodaDbImpl: @utils.CheckImpls("creating a SODA collection") def create_collection(self, str name, str metadata, bint map_mode): pass @utils.CheckImpls("creating a SODA document") def create_document(self, bytes content, str key, str media_type): pass @utils.CheckImpls("getting a list of SODA collection names") def get_collection_names(self, str start_name, uint32_t limit): pass @utils.CheckImpls("opening a SODA collection") def open_collection(self, str name): pass cdef class BaseSodaCollImpl: @utils.CheckImpls("creating an index on a SODA collection") def create_index(self, str spec): pass @utils.CheckImpls("dropping a SODA collection") def drop(self): pass @utils.CheckImpls("dropping an index on a SODA collection") def drop_index(self, str name, bint force): pass @utils.CheckImpls("getting the count of documents in a SODA collection") def get_count(self, object op): pass @utils.CheckImpls("getting a cursor for documents in a SODA collection") def get_cursor(self, object op): pass @utils.CheckImpls("getting the data guide for a SODA collection") def get_data_guide(self): pass @utils.CheckImpls("getting the metadata of a SODA collection") def get_metadata(self): pass @utils.CheckImpls("getting a document from a SODA collection") def get_one(self, object op): pass @utils.CheckImpls("inserting multiple documents into a SODA collection") def insert_many(self, list documents, str hint, bint return_docs): pass @utils.CheckImpls("inserting a single document into a SODA collection") def insert_one(self, BaseSodaDocImpl doc, str hint, bint return_doc): pass @utils.CheckImpls("removing documents from a SODA collection") def remove(self, object op): pass @utils.CheckImpls("replacing a document in a SODA collection") def replace_one(self, BaseSodaDocImpl doc_impl, bint return_doc): pass @utils.CheckImpls("saving a document in a SODA collection") def save(self, BaseSodaDocImpl doc, str hint, bint return_doc): pass @utils.CheckImpls("truncating a SODA collection") def truncate(self): pass cdef class BaseSodaDocImpl: @utils.CheckImpls("getting the content of a SODA document") def get_content(self): pass @utils.CheckImpls("getting the created on date of a SODA document") def get_created_on(self): pass @utils.CheckImpls("getting the key of a SODA document") def get_key(self): pass @utils.CheckImpls("getting the last modified date of a SODA document") def get_last_modified(self): pass @utils.CheckImpls("getting the media type of a SODA document") def get_media_type(self): pass @utils.CheckImpls("getting the version of a SODA document") def get_version(self): pass cdef class BaseSodaDocCursorImpl: @utils.CheckImpls("closing a SODA document cursor") def close(self): pass @utils.CheckImpls("getting the next document from a SODA document cursor") def get_next_doc(self): pass python-oracledb-1.2.1/src/oracledb/impl/base/subscr.pyx000066400000000000000000000036151434177474600231130ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # subscr.pyx # # Cython file defining the base Subscription implementation class (embedded in # base_impl.pyx). #------------------------------------------------------------------------------ cdef class BaseSubscrImpl: @utils.CheckImpls("registering a query on a subscription") def register_query(self, str sql, object args): pass @utils.CheckImpls("creating a subscription") def subscribe(self, object subscr, BaseConnImpl conn_impl): pass @utils.CheckImpls("destroying a subscription") def unsubscribe(self, BaseConnImpl conn_impl): pass cdef class Message: pass cdef class MessageQuery: pass cdef class MessageRow: pass cdef class MessageTable: pass python-oracledb-1.2.1/src/oracledb/impl/base/types.pyx000066400000000000000000000202511434177474600227510ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # types.pyx # # Cython file defining the API types mandated by the Python Database API as # well as the database types specific to Oracle (embedded in base_impl.pyx). #------------------------------------------------------------------------------ cdef class ApiType: def __init__(self, name, *dbtypes): self.name = name self.dbtypes = dbtypes def __eq__(self, other): if isinstance(other, DbType): return other in self.dbtypes return NotImplemented def __hash__(self): return hash(self.name) def __reduce__(self): return self.name def __repr__(self): return f"" cdef dict db_type_by_num = {} cdef dict db_type_by_ora_name = {} cdef dict db_type_by_ora_type_num = {} cdef class DbType: def __init__(self, num, name, ora_name, ora_type_num, default_size=0, csfrm=0, buffer_size_factor=0): cdef uint16_t ora_type_key = csfrm * 256 + ora_type_num self.num = num self.name = name self.default_size = default_size self._ora_name = ora_name self._ora_type_num = ora_type_num self._csfrm = csfrm self._buffer_size_factor = buffer_size_factor db_type_by_num[num] = self db_type_by_ora_name[ora_name] = self db_type_by_ora_type_num[ora_type_key] = self def __reduce__(self): return self.name def __repr__(self): return f"" @staticmethod cdef DbType _from_num(uint32_t num): try: return db_type_by_num[num] except KeyError: pass errors._raise_err(errors.ERR_ORACLE_TYPE_NOT_SUPPORTED, num=num) @staticmethod cdef DbType _from_ora_name(str name): try: return db_type_by_ora_name[name] except KeyError: pass errors._raise_err(errors.ERR_ORACLE_TYPE_NAME_NOT_SUPPORTED, name=name) @staticmethod cdef DbType _from_ora_type_and_csfrm(uint8_t ora_type_num, uint8_t csfrm): cdef uint16_t ora_type_key = csfrm * 256 + ora_type_num try: return db_type_by_ora_type_num[ora_type_key] except KeyError: pass errors._raise_err(errors.ERR_ORACLE_TYPE_NOT_SUPPORTED, num=ora_type_num) # database types DB_TYPE_BFILE = DbType(DB_TYPE_NUM_BFILE, "DB_TYPE_BFILE", "BFILE", 114) DB_TYPE_BINARY_DOUBLE = DbType(DB_TYPE_NUM_BINARY_DOUBLE, "DB_TYPE_BINARY_DOUBLE", "BINARY_DOUBLE", 101, buffer_size_factor=8) DB_TYPE_BINARY_FLOAT = DbType(DB_TYPE_NUM_BINARY_FLOAT, "DB_TYPE_BINARY_FLOAT", "BINARY_FLOAT", 100, buffer_size_factor=4) DB_TYPE_BINARY_INTEGER = DbType(DB_TYPE_NUM_BINARY_INTEGER, "DB_TYPE_BINARY_INTEGER", "BINARY_INTEGER", 3, buffer_size_factor=22) DB_TYPE_BLOB = DbType(DB_TYPE_NUM_BLOB, "DB_TYPE_BLOB", "BLOB", 113, buffer_size_factor=112) DB_TYPE_BOOLEAN = DbType(DB_TYPE_NUM_BOOLEAN, "DB_TYPE_BOOLEAN", "BOOLEAN", 252, buffer_size_factor=4) DB_TYPE_CHAR = DbType(DB_TYPE_NUM_CHAR, "DB_TYPE_CHAR", "CHAR", 96, 2000, csfrm=1, buffer_size_factor=4) DB_TYPE_CLOB = DbType(DB_TYPE_NUM_CLOB, "DB_TYPE_CLOB", "CLOB", 112, csfrm=1, buffer_size_factor=112) DB_TYPE_CURSOR = DbType(DB_TYPE_NUM_CURSOR, "DB_TYPE_CURSOR", "CURSOR", 102, buffer_size_factor=4) DB_TYPE_DATE = DbType(DB_TYPE_NUM_DATE, "DB_TYPE_DATE", "DATE", 12, buffer_size_factor=7) DB_TYPE_INTERVAL_DS = DbType(DB_TYPE_NUM_INTERVAL_DS, "DB_TYPE_INTERVAL_DS", "INTERVAL DAY TO SECOND", 183, buffer_size_factor=11) DB_TYPE_INTERVAL_YM = DbType(DB_TYPE_NUM_INTERVAL_YM, "DB_TYPE_INTERVAL_YM", "INTERVAL YEAR TO MONTH", 182) DB_TYPE_JSON = DbType(DB_TYPE_NUM_JSON, "DB_TYPE_JSON", "JSON", 119) DB_TYPE_LONG = DbType(DB_TYPE_NUM_LONG_VARCHAR, "DB_TYPE_LONG", "LONG", 8, csfrm=1, buffer_size_factor=2147483647) DB_TYPE_LONG_NVARCHAR = DbType(DB_TYPE_NUM_LONG_NVARCHAR, "DB_TYPE_LONG_NVARCHAR", "LONG NVARCHAR", 8, csfrm=2, buffer_size_factor=2147483647) DB_TYPE_LONG_RAW = DbType(DB_TYPE_NUM_LONG_RAW, "DB_TYPE_LONG_RAW", "LONG RAW", 24, buffer_size_factor=2147483647) DB_TYPE_NCHAR = DbType(DB_TYPE_NUM_NCHAR, "DB_TYPE_NCHAR", "NCHAR", 96, 2000, csfrm=2, buffer_size_factor=4) DB_TYPE_NCLOB = DbType(DB_TYPE_NUM_NCLOB, "DB_TYPE_NCLOB", "NCLOB", 112, csfrm=2, buffer_size_factor=112) DB_TYPE_NUMBER = DbType(DB_TYPE_NUM_NUMBER, "DB_TYPE_NUMBER", "NUMBER", 2, buffer_size_factor=22) DB_TYPE_NVARCHAR = DbType(DB_TYPE_NUM_NVARCHAR, "DB_TYPE_NVARCHAR", "NVARCHAR2", 1, 4000, csfrm=2, buffer_size_factor=4) DB_TYPE_OBJECT = DbType(DB_TYPE_NUM_OBJECT, "DB_TYPE_OBJECT", "OBJECT", 109) DB_TYPE_RAW = DbType(DB_TYPE_NUM_RAW, "DB_TYPE_RAW", "RAW", 23, 4000, buffer_size_factor=1) DB_TYPE_ROWID = DbType(DB_TYPE_NUM_ROWID, "DB_TYPE_ROWID", "ROWID", 11, buffer_size_factor=18) DB_TYPE_TIMESTAMP = DbType(DB_TYPE_NUM_TIMESTAMP, "DB_TYPE_TIMESTAMP", "TIMESTAMP", 180, buffer_size_factor=11) DB_TYPE_TIMESTAMP_LTZ = DbType(DB_TYPE_NUM_TIMESTAMP_LTZ, "DB_TYPE_TIMESTAMP_LTZ", "TIMESTAMP WITH LOCAL TZ", 231, buffer_size_factor=11) DB_TYPE_TIMESTAMP_TZ = DbType(DB_TYPE_NUM_TIMESTAMP_TZ, "DB_TYPE_TIMESTAMP_TZ", "TIMESTAMP WITH TZ", 181, buffer_size_factor=13) DB_TYPE_UROWID = DbType(DB_TYPE_NUM_UROWID, "DB_TYPE_UROWID", "UROWID", 208) DB_TYPE_VARCHAR = DbType(DB_TYPE_NUM_VARCHAR, "DB_TYPE_VARCHAR", "VARCHAR2", 1, 4000, csfrm=1, buffer_size_factor=4) # additional aliases db_type_by_ora_name["DOUBLE PRECISION"] = DB_TYPE_NUMBER db_type_by_ora_name["FLOAT"] = DB_TYPE_NUMBER db_type_by_ora_name["INTEGER"] = DB_TYPE_NUMBER db_type_by_ora_name["PL/SQL BOOLEAN"] = DB_TYPE_BOOLEAN db_type_by_ora_name["PL/SQL BINARY INTEGER"] = DB_TYPE_BINARY_INTEGER db_type_by_ora_name["PL/SQL PLS INTEGER"] = DB_TYPE_BINARY_INTEGER db_type_by_ora_name["REAL"] = DB_TYPE_NUMBER db_type_by_ora_name["SMALLINT"] = DB_TYPE_NUMBER # DB API types BINARY = ApiType("BINARY", DB_TYPE_RAW, DB_TYPE_LONG_RAW) DATETIME = ApiType("DATETIME", DB_TYPE_DATE, DB_TYPE_TIMESTAMP, DB_TYPE_TIMESTAMP_LTZ, DB_TYPE_TIMESTAMP_TZ) NUMBER = ApiType("NUMBER", DB_TYPE_NUMBER, DB_TYPE_BINARY_DOUBLE, DB_TYPE_BINARY_FLOAT, DB_TYPE_BINARY_INTEGER) ROWID = ApiType("ROWID", DB_TYPE_ROWID, DB_TYPE_UROWID) STRING = ApiType("STRING", DB_TYPE_VARCHAR, DB_TYPE_NVARCHAR, DB_TYPE_CHAR, DB_TYPE_NCHAR, DB_TYPE_LONG) python-oracledb-1.2.1/src/oracledb/impl/base/utils.pyx000066400000000000000000000207061434177474600227520ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # utils.pyx # # Cython file defining utility methods (embedded in base_impl.pyx). #------------------------------------------------------------------------------ cdef int _set_bool_param(dict args, str name, bint *out_val) except -1: """ Sets a boolean parameter to the value provided in the dictionary. This can be a case-insenstive string matching on/off, yes/no or true/false (such as when parsed from a connect string). It can also be a directly passed argument which will be explicitly converted to a boolean value. """ in_val = args.get(name) if in_val is not None: if isinstance(in_val, str): out_val[0] = (in_val.strip().lower() in ("on", "yes", "true")) else: out_val[0] = bool(in_val) cdef int _set_duration_param(dict args, str name, double *out_val) except -1: """ Sets a duration parameter to the value provided in the dictionary. This can be a string (such as when parsed from a connect string) containing a floating point value followed by an otional unit: ms (milliseconds), sec (seconds) or min (minutes). It can also be a directly passed argument which will be explicitly converted to a floating point value. """ in_val = args.get(name) if in_val is not None: if isinstance(in_val, str): in_val = in_val.strip().lower() if in_val.endswith("sec"): out_val[0] = float(in_val[:-3].strip()) elif in_val.endswith("ms"): out_val[0] = float(in_val[:-2].strip()) / 1000 elif in_val.endswith("min"): out_val[0] = float(in_val[:-3].strip()) * 60 else: out_val[0] = float(in_val.strip()) else: out_val[0] = float(in_val) cdef int _set_int_param(dict args, str name, int* out_val) except -1: """ Sets an integer parameter to the value provided in the dictionary. This can be a string (such as when parsed from a connect string). It can also be a directly passed argument which will be explicitly converted to an integer value. """ in_val = args.get(name) if in_val is not None: out_val[0] = int(in_val) cdef int _set_uint_param(dict args, str name, uint32_t* out_val) except -1: """ Sets an unsigned integer parameter to the value provided in the dictionary. This can be a string (such as when parsed from a connect string). It can also be a directly passed argument which will be explicitly converted to an integer value. """ in_val = args.get(name) if in_val is not None: out_val[0] = int(in_val) cdef int _set_uint_param_with_deprecated_name(dict args, str name, str deprecated_name, uint32_t* out_val) except -1: """ Similar to _set_uint_param() but also checks to see if the deprecated parameter name has been used. """ in_val = args.get(name) deprecated_val = args.get(deprecated_name) if in_val is not None and deprecated_val is not None: errors._raise_err(errors.ERR_DUPLICATED_PARAMETER, deprecated_name=deprecated_name, new_name=name) elif in_val is not None: out_val[0] = int(in_val) elif deprecated_val is not None: out_val[0] = int(deprecated_val) cdef int _set_obj_param_with_deprecated_name(dict args, str name, str deprecated_name, object target) except -1: """ Sets an object parameter to the value provided in the dictionary. If a value is specified it is set directly on the target. """ in_val = args.get(name) deprecated_val = args.get(deprecated_name) if in_val is not None and deprecated_val is not None: errors._raise_err(errors.ERR_DUPLICATED_PARAMETER, deprecated_name=deprecated_name, new_name=name) elif in_val is not None: setattr(target, name, in_val) elif deprecated_val is not None: setattr(target, name, deprecated_val) cdef int _set_protocol_param(dict args, str name, object target) except -1: """ Sets a protocol parameter to the value provided in the dictionary. This must be one of "tcp" or "tcps" currently. If it is not one of these values an error is raised. If a value is specified and meets the criteria it is set directly on the target (since strings are treated as Python objects). """ in_val = args.get(name) if in_val is not None: in_val = in_val.lower() if in_val not in ("tcp", "tcps"): errors._raise_err(errors.ERR_INVALID_PROTOCOL, protocol=in_val) setattr(target, name, in_val) cdef int _set_purity_param(dict args, str name, uint32_t* out_val) except -1: """ Sets a purity parameter to the value provided in the dictionary. This must be one of "new" or "self" currently (or the equivalent constants, if specified directly). If it is not one of these values an error is raised. """ in_val = args.get(name) if in_val is not None: if isinstance(in_val, str): in_val = in_val.lower() if in_val == "new": out_val[0] = constants.PURITY_NEW elif in_val == "self": out_val[0] = constants.PURITY_SELF else: errors._raise_err(errors.ERR_INVALID_POOL_PURITY, purity=in_val) else: if in_val not in (constants.PURITY_SELF, constants.PURITY_NEW, constants.PURITY_DEFAULT): errors._raise_err(errors.ERR_INVALID_POOL_PURITY, purity=in_val) out_val[0] = in_val cdef int _set_server_type_param(dict args, str name, object target) except -1: """ Sets a server type parameter to the value provided in the dictionary. This must be one of "dedicated", "pooled" or "shared" currently. If it is not one of these values an error is raised. If a value is specified and meets the criteria it is set directly on the target (since strings are treated as Python objects). """ in_val = args.get(name) if in_val is not None: in_val = in_val.lower() if in_val not in ("dedicated", "pooled", "shared"): errors._raise_err(errors.ERR_INVALID_SERVER_TYPE, server_type=in_val) setattr(target, name, in_val) cdef int _set_str_param(dict args, str name, object target) except -1: """ Sets a string parameter to the value provided in the dictionary. If a value is specified it is set directly on the target (since strings are treated as Python objects). """ in_val = args.get(name) if in_val is not None: setattr(target, name, str(in_val)) def init_base_impl(package): """ Initializes globals after the package has been completely initialized. This is to avoid circular imports and eliminate the need for global lookups. """ global PY_TYPE_CURSOR, PY_TYPE_DB_OBJECT, PY_TYPE_DB_OBJECT_TYPE global PY_TYPE_LOB, PY_TYPE_VAR PY_TYPE_CURSOR = package.Cursor PY_TYPE_DB_OBJECT = package.DbObject PY_TYPE_DB_OBJECT_TYPE = package.DbObjectType PY_TYPE_LOB = package.LOB PY_TYPE_VAR = package.Var python-oracledb-1.2.1/src/oracledb/impl/base/var.pyx000066400000000000000000000313021434177474600223740ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # var.pyx # # Cython file defining the base Variable implementation class (embedded in # base_impl.pyx). #------------------------------------------------------------------------------ cdef class BaseVarImpl: cdef int _bind(self, object conn, BaseCursorImpl cursor, uint32_t num_execs, object name, uint32_t pos) except -1: """ Binds a variable to the cursor. """ raise NotImplementedError() cdef int _check_and_set_scalar_value(self, uint32_t pos, object value, bint* was_set) except -1: """ Sets a scalar value in the variable at the given position, but first checks the type of Python value to see if it is acceptable. The value may be modified by the in converter (if one has been set) or adjusted to be acceptable (for some cases). If the was_set pointer is NULL, an exception is raised when the Python value is found to be unacceptable; otherwise, the flag is cleared if the Python value is unacceptable. """ cdef uint32_t size # call in converter, if applicable if self.inconverter is not None: value = self.inconverter(value) # check the value and verify it is acceptable value = self._conn_impl._check_value(self.dbtype, self.objtype, value, was_set) if was_set != NULL and not was_set[0]: return 0 # resize variable, if applicable if value is not None and self.dbtype.default_size != 0: size = len(value) if size > self.size: self._resize(size) # set value self._set_scalar_value(pos, value) self._is_value_set = True cdef int _check_and_set_value(self, uint32_t pos, object value, bint* was_set) except -1: """ Sets the value in the variable at the given position, but first checks the type of Python value to see if it is acceptable. """ cdef: uint32_t i, num_elements_in_array object element_value # scalar variables can be checked directly if not self.is_array: return self._check_and_set_scalar_value(pos, value, was_set) # array variables must have a list supplied to them if not isinstance(value, list): if was_set != NULL: was_set[0] = False return 0 errors._raise_err(errors.ERR_EXPECTING_LIST_FOR_ARRAY_VAR) # the size of the array must be sufficient to hold all of the # elements num_elements_in_array = len( value) if num_elements_in_array > self.num_elements: if was_set != NULL: was_set[0] = False return 0 errors._raise_err(errors.ERR_INCORRECT_VAR_ARRAY_SIZE, var_arraysize=self.num_elements, required_arraysize=num_elements_in_array) # check and set each of the element's values for i, element_value in enumerate( value): self._check_and_set_scalar_value(i, element_value, was_set) if was_set != NULL and not was_set[0]: return 0 self._set_num_elements_in_array(num_elements_in_array) cdef int _finalize_init(self) except -1: """ Internal method that finalizes initialization of the variable. """ if self.dbtype.default_size > 0: if self.size == 0: self.size = self.dbtype.default_size self.buffer_size = self.size * self.dbtype._buffer_size_factor else: self.buffer_size = self.dbtype._buffer_size_factor if self.num_elements == 0: self.num_elements = 1 cdef list _get_array_value(self): """ Internal method to return the value of the array. """ raise NotImplementedError() cdef object _get_scalar_value(self, uint32_t pos): """ Internal method to return the value of the variable at the given position. """ raise NotImplementedError() cdef int _on_reset_bind(self, uint32_t num_rows) except -1: """ Called when the bind variable is being reset, just prior to performing a bind operation. """ if self.num_elements < num_rows: self.num_elements = num_rows self._finalize_init() cdef int _resize(self, uint32_t new_size) except -1: """ Resize the variable to the new size provided. """ self.size = new_size self.buffer_size = new_size * self.dbtype._buffer_size_factor cdef int _set_scalar_value(self, uint32_t pos, object value) except -1: """ Set the value of the variable at the given position. At this point it is assumed that all checks have been performed! """ raise NotImplementedError() cdef int _set_num_elements_in_array(self, uint32_t num_elements) except -1: """ Sets the number of elements in the array. """ self.num_elements_in_array = num_elements cdef int _set_type_info_from_type(self, object typ) except -1: """ Sets the type and size of the variable given a Python type. """ cdef ApiType apitype if isinstance(typ, DbType): self.dbtype = typ elif isinstance(typ, ApiType): apitype = typ self.dbtype = apitype.dbtypes[0] elif isinstance(typ, PY_TYPE_DB_OBJECT_TYPE): self.dbtype = DB_TYPE_OBJECT self.objtype = typ._impl elif not isinstance(typ, type): errors._raise_err(errors.ERR_EXPECTING_TYPE) elif typ is int: self.dbtype = DB_TYPE_NUMBER self._preferred_num_type = NUM_TYPE_INT elif typ is float: self.dbtype = DB_TYPE_NUMBER self._preferred_num_type = NUM_TYPE_FLOAT elif typ is str: self.dbtype = DB_TYPE_VARCHAR elif typ is bytes: self.dbtype = DB_TYPE_RAW elif typ is PY_TYPE_DECIMAL: self.dbtype = DB_TYPE_NUMBER self._preferred_num_type = NUM_TYPE_DECIMAL elif typ is PY_TYPE_BOOL: self.dbtype = DB_TYPE_BOOLEAN elif typ is PY_TYPE_DATE: self.dbtype = DB_TYPE_DATE elif typ is PY_TYPE_DATETIME: self.dbtype = DB_TYPE_TIMESTAMP elif typ is PY_TYPE_TIMEDELTA: self.dbtype = DB_TYPE_INTERVAL_DS else: errors._raise_err(errors.ERR_PYTHON_TYPE_NOT_SUPPORTED, typ=typ) cdef int _set_type_info_from_value(self, object value, bint is_plsql) except -1: """ Sets the type and size of the variable given a Python value. This method is called once for scalars and once per element in a list for array values. If a different type is detected an error is raised. """ cdef: int preferred_num_type = NUM_TYPE_FLOAT BaseDbObjectTypeImpl objtype = None DbType dbtype = None uint32_t size = 0 if value is None: dbtype = DB_TYPE_VARCHAR size = 1 elif isinstance(value, PY_TYPE_BOOL): dbtype = DB_TYPE_BOOLEAN if is_plsql else DB_TYPE_BINARY_INTEGER elif isinstance(value, str): size = len(value) dbtype = DB_TYPE_VARCHAR elif isinstance(value, bytes): size = len(value) dbtype = DB_TYPE_RAW elif isinstance(value, int): dbtype = DB_TYPE_NUMBER preferred_num_type = NUM_TYPE_INT elif isinstance(value, float): dbtype = DB_TYPE_NUMBER preferred_num_type = NUM_TYPE_FLOAT elif isinstance(value, PY_TYPE_DECIMAL): dbtype = DB_TYPE_NUMBER preferred_num_type = NUM_TYPE_DECIMAL elif isinstance(value, (PY_TYPE_DATE, PY_TYPE_DATETIME)): dbtype = DB_TYPE_DATE elif isinstance(value, PY_TYPE_TIMEDELTA): dbtype = DB_TYPE_INTERVAL_DS elif isinstance(value, PY_TYPE_DB_OBJECT): dbtype = DB_TYPE_OBJECT objtype = value.type._impl elif isinstance(value, PY_TYPE_LOB): dbtype = value.type elif isinstance(value, PY_TYPE_CURSOR): dbtype = DB_TYPE_CURSOR else: errors._raise_err(errors.ERR_PYTHON_VALUE_NOT_SUPPORTED, type_name=type(value).__name__) if self.dbtype is None: self.dbtype = dbtype self.objtype = objtype self._preferred_num_type = preferred_num_type elif dbtype is not self.dbtype or objtype is not self.objtype: errors._raise_err(errors.ERR_MIXED_ELEMENT_TYPES, element=value) if size > self.size: self.size = size def get_description(self): """ Return a 7-tuple containing information about the variable: (name, type, display_size, internal_size, precision, scale, null_ok). """ cdef: uint32_t display_size = 0, size_in_bytes = 0 FetchInfo fetch_info = self._fetch_info DbType dbtype = fetch_info._dbtype object precision, scale if fetch_info._size > 0: display_size = fetch_info._size size_in_bytes = fetch_info._buffer_size elif dbtype is DB_TYPE_DATE \ or dbtype is DB_TYPE_TIMESTAMP \ or dbtype is DB_TYPE_TIMESTAMP_LTZ \ or dbtype is DB_TYPE_TIMESTAMP_TZ: display_size = 23 elif dbtype is DB_TYPE_BINARY_FLOAT \ or dbtype is DB_TYPE_BINARY_DOUBLE \ or dbtype is DB_TYPE_BINARY_INTEGER \ or dbtype is DB_TYPE_NUMBER: if fetch_info._precision: display_size = fetch_info._precision + 1 if fetch_info._scale > 0: display_size += fetch_info._scale + 1 else: display_size = 127 if fetch_info._precision or fetch_info._scale: precision = fetch_info._precision scale = fetch_info._scale else: scale = precision = None return (fetch_info._name, dbtype, display_size or None, size_in_bytes or None, precision, scale, fetch_info._nulls_allowed) def get_all_values(self): """ Internal method for returning an array of all of the values stored in the variable. """ cdef uint32_t i if self.is_array: return self._get_array_value() return [self._get_scalar_value(i) for i in range(self.num_elements)] def get_value(self, uint32_t pos): """ Internal method for getting the value of a variable. """ if self.is_array: return self._get_array_value() if pos >= self.num_elements: raise IndexError("position out of range") return self._get_scalar_value(pos) def set_value(self, uint32_t pos, object value): """ Internal method for setting a variable's value at the specified position. """ if self.is_array: if pos > 0: errors._raise_err(errors.ERR_ARRAYS_OF_ARRAYS) elif pos >= self.num_elements: raise IndexError("position out of range") self._check_and_set_value(pos, value, NULL) python-oracledb-1.2.1/src/oracledb/impl/thick/000077500000000000000000000000001434177474600212335ustar00rootroot00000000000000python-oracledb-1.2.1/src/oracledb/impl/thick/buffer.pyx000066400000000000000000000043201434177474600232450ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # buffer.pyx # # Cython file defining the StringBuffer class, used for transforming Python # "str" and "bytes" objects into the C buffer representation required by ODPI-C # (embedded in thick_impl.pyx). #------------------------------------------------------------------------------ @cython.freelist(20) cdef class StringBuffer: cdef: bytes obj char *ptr uint32_t length uint32_t size_in_chars cdef int set_value(self, value) except -1: if value is None: self.obj = None self.ptr = NULL self.length = self.size_in_chars = 0 return 0 elif isinstance(value, str): self.obj = ( value).encode() self.size_in_chars = len( value) elif isinstance(value, bytes): self.obj = value self.size_in_chars = len( value) else: raise TypeError("expecting string or bytes") self.ptr = self.obj self.length = len(self.obj) python-oracledb-1.2.1/src/oracledb/impl/thick/connection.pyx000066400000000000000000000625421434177474600241450ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # connection.pyx # # Cython file defining the thick Connection implementation class (embedded in # thick_impl.pyx). #------------------------------------------------------------------------------ ctypedef int (*dpiConnSetTextAttrFunc)(dpiConn*, const char*, uint32_t) nogil cdef class ConnectionParams: cdef: bytes dsn bytes username bytes password bytes cclass bytes new_password bytes edition bytes tag bytes token bytes private_key const char *dsn_ptr const char *username_ptr const char *password_ptr const char *cclass_ptr const char *new_password_ptr const char *edition_ptr const char *tag_ptr const char *token_ptr const char *private_key_ptr uint32_t dsn_len uint32_t username_len uint32_t password_len uint32_t cclass_len uint32_t new_password_len uint32_t edition_len uint32_t tag_len uint32_t token_len uint32_t private_key_len uint32_t num_app_context list app_context_bytes dpiAppContext *app_context uint32_t num_sharding_key_columns dpiShardingKeyColumn *sharding_key_columns uint32_t num_super_sharding_key_columns dpiShardingKeyColumn *super_sharding_key_columns def __dealloc__(self): if self.app_context is not NULL: cpython.PyMem_Free(self.app_context) if self.sharding_key_columns is not NULL: cpython.PyMem_Free(self.sharding_key_columns) if self.super_sharding_key_columns is not NULL: cpython.PyMem_Free(self.super_sharding_key_columns) cdef int _process_context_str(self, str value, const char **ptr, uint32_t *length) except -1: cdef bytes temp temp = value.encode() self.app_context_bytes.append(temp) ptr[0] = temp length[0] = len(temp) cdef process_appcontext(self, list entries): cdef: object namespace, name, value dpiAppContext *entry ssize_t num_bytes bytes temp uint32_t i self.app_context_bytes = [] self.num_app_context = len(entries) num_bytes = self.num_app_context * sizeof(dpiAppContext) self.app_context = cpython.PyMem_Malloc(num_bytes) for i in range(self.num_app_context): if not isinstance(entries[i], tuple) or len(entries[i]) != 3: raise TypeError("appcontext should be a list of 3-tuples") namespace, name, value = entries[i] entry = &self.app_context[i] self._process_context_str(namespace, &entry.namespaceName, &entry.namespaceNameLength) self._process_context_str(name, &entry.name, &entry.nameLength) self._process_context_str(value, &entry.value, &entry.valueLength) @cython.freelist(8) cdef class ThickXid: cdef: StringBuffer global_transaction_id_buf StringBuffer branch_qualifier_buf dpiXid* xid_ptr dpiXid xid_buf def __init__(self, xid): if xid is not None: self.global_transaction_id_buf = StringBuffer() self.global_transaction_id_buf.set_value(xid.global_transaction_id) self.branch_qualifier_buf = StringBuffer() self.branch_qualifier_buf.set_value(xid.branch_qualifier) self.xid_buf.formatId = xid.format_id self.xid_buf.globalTransactionId = \ self.global_transaction_id_buf.ptr self.xid_buf.globalTransactionIdLength = \ self.global_transaction_id_buf.length self.xid_buf.branchQualifier = \ self.branch_qualifier_buf.ptr self.xid_buf.branchQualifierLength = \ self.branch_qualifier_buf.length self.xid_ptr = &self.xid_buf cdef class ThickConnImpl(BaseConnImpl): cdef: dpiConn *_handle bint _is_external public str tag def __dealloc__(self): if self._handle != NULL: dpiConn_release(self._handle) def _get_oci_attr(self, uint32_t handle_type, uint32_t attr_num, uint32_t attr_type): """ Internal method for getting the value of an OCI attribute on the connection. """ cdef: dpiDataBuffer value uint32_t value_len if dpiConn_getOciAttr(self._handle, handle_type, attr_num, &value, &value_len) < 0: _raise_from_odpi() return _convert_oci_attr_to_python(attr_type, &value, value_len) def _set_oci_attr(self, uint32_t handle_type, uint32_t attr_num, uint32_t attr_type, object value): """ Internal method for setting the value of an OCI attribute on the connection. """ cdef: StringBuffer str_buf = StringBuffer() void *oci_value = NULL dpiDataBuffer oci_buf uint32_t oci_len = 0 _convert_python_to_oci_attr(value, attr_type, str_buf, &oci_buf, &oci_value, &oci_len) if dpiConn_setOciAttr(self._handle, handle_type, attr_num, oci_value, oci_len) < 0: _raise_from_odpi() cdef int _set_text_attr(self, dpiConnSetTextAttrFunc func, str value) except -1: cdef: uint32_t value_length const char *value_ptr bytes value_bytes value_bytes = value.encode() value_ptr = value_bytes value_length = len(value_bytes) if func(self._handle, value_ptr, value_length) < 0: _raise_from_odpi() def cancel(self): cdef int status with nogil: status = dpiConn_breakExecution(self._handle) if status < 0: _raise_from_odpi() def change_password(self, str old_password, str new_password): cdef: bytes username_bytes, old_password_bytes, new_password_bytes uint32_t username_len = 0, old_password_len = 0 const char *old_password_ptr = NULL const char *new_password_ptr = NULL const char *username_ptr = NULL uint32_t new_password_len = 0 int status if self.username is not None: username_bytes = self.username.encode() username_ptr = username_bytes username_len = len(username_bytes) old_password_bytes = old_password.encode() old_password_ptr = old_password_bytes old_password_len = len(old_password_bytes) new_password_bytes = new_password.encode() new_password_ptr = new_password_bytes new_password_len = len(new_password_bytes) with nogil: status = dpiConn_changePassword(self._handle, username_ptr, username_len, old_password_ptr, old_password_len, new_password_ptr, new_password_len) if status < 0: _raise_from_odpi() def get_is_healthy(self): cdef bint is_healthy if dpiConn_getIsHealthy(self._handle, &is_healthy) < 0: _raise_from_odpi() return is_healthy def close(self, bint in_del=False): cdef: uint32_t mode = DPI_MODE_CONN_CLOSE_DEFAULT const char *tag_ptr = NULL uint32_t tag_length = 0 bytes tag_bytes int status if in_del and self._is_external: return 0 if self.tag is not None: mode = DPI_MODE_CONN_CLOSE_RETAG tag_bytes = self.tag.encode() tag_ptr = tag_bytes tag_length = len(tag_bytes) with nogil: status = dpiConn_close(self._handle, mode, tag_ptr, tag_length) if status == DPI_SUCCESS: dpiConn_release(self._handle) self._handle = NULL if status < 0 and not in_del: _raise_from_odpi() def commit(self): cdef int status with nogil: status = dpiConn_commit(self._handle) if status < 0: _raise_from_odpi() def connect(self, ConnectParamsImpl user_params, ThickPoolImpl pool_impl): cdef: str full_user, cclass, token, private_key bytes password_bytes, new_password_bytes dpiCommonCreateParams common_params dpiConnCreateParams conn_params ConnectParamsImpl pool_params dpiAccessToken access_token ConnectionParams params int status # if the connection is part of the pool, get the pool creation params if pool_impl is not None: pool_params = pool_impl.connect_params self.username = pool_impl.username self.dsn = pool_impl.dsn # set up connection parameters params = ConnectionParams() password_bytes = user_params._get_password() new_password_bytes = user_params._get_new_password() full_user = user_params.get_full_user() if full_user is not None: params.username = full_user.encode() params.username_ptr = params.username params.username_len = len(params.username) if password_bytes is not None: params.password = password_bytes params.password_ptr = params.password params.password_len = len(params.password) if self.dsn is not None: params.dsn = self.dsn.encode() params.dsn_ptr = params.dsn params.dsn_len = len(params.dsn) if pool_impl is None \ or user_params._default_description.cclass is not None: cclass = user_params._default_description.cclass else: cclass = pool_params._default_description.cclass if cclass is not None: params.cclass = cclass.encode() params.cclass_ptr = params.cclass params.cclass_len = len(params.cclass) if new_password_bytes is not None: params.new_password = new_password_bytes params.new_password_ptr = params.new_password params.new_password_len = len(params.new_password) if user_params.edition is not None: params.edition = user_params.edition.encode() params.edition_ptr = params.edition params.edition_len = len(params.edition) if user_params.tag is not None: params.tag = user_params.tag.encode() params.tag_ptr = params.tag params.tag_len = len(params.tag) if user_params.appcontext: params.process_appcontext(user_params.appcontext) if user_params._token is not None \ or user_params.access_token_callback is not None: token = user_params._get_token() private_key = user_params._get_private_key() params.token = token.encode() params.token_ptr = params.token params.token_len = len(params.token) if private_key is not None: params.private_key = private_key.encode() params.private_key_ptr = params.private_key params.private_key_len = len(params.private_key) # set up common creation parameters if dpiContext_initCommonCreateParams(driver_context, &common_params) < 0: _raise_from_odpi() common_params.createMode |= DPI_MODE_CREATE_THREADED if user_params.events: common_params.createMode |= DPI_MODE_CREATE_EVENTS if user_params.edition is not None: common_params.edition = params.edition_ptr common_params.editionLength = params.edition_len if params.token is not None: access_token.token = params.token_ptr access_token.tokenLength = params.token_len access_token.privateKey = params.private_key_ptr access_token.privateKeyLength = params.private_key_len common_params.accessToken = &access_token # set up connection specific creation parameters if dpiContext_initConnCreateParams(driver_context, &conn_params) < 0: _raise_from_odpi() if params.username_len == 0 and params.password_len == 0: conn_params.externalAuth = 1 else: conn_params.externalAuth = user_params.externalauth if params.cclass is not None: conn_params.connectionClass = params.cclass_ptr conn_params.connectionClassLength = params.cclass_len if new_password_bytes is not None: conn_params.newPassword = params.new_password_ptr conn_params.newPasswordLength = params.new_password_len if user_params.appcontext: conn_params.appContext = params.app_context conn_params.numAppContext = params.num_app_context if user_params.tag is not None: conn_params.tag = params.tag_ptr conn_params.tagLength = params.tag_len if user_params._external_handle != 0: conn_params.externalHandle = user_params._external_handle self._is_external = True if pool_impl is not None: conn_params.pool = pool_impl._handle common_params.stmtCacheSize = user_params.stmtcachesize conn_params.authMode = user_params.mode conn_params.matchAnyTag = user_params.matchanytag if user_params._default_description.purity != constants.PURITY_DEFAULT: conn_params.purity = user_params._default_description.purity elif pool_impl is not None: conn_params.purity = pool_params._default_description.purity # perform connection with nogil: status = dpiConn_create(driver_context, params.username_ptr, params.username_len, params.password_ptr, params.password_len, params.dsn_ptr, params.dsn_len, &common_params, &conn_params, &self._handle) if status < 0: _raise_from_odpi() # determine if session callback should be invoked; this takes place if # the connection is newly created by the pool or if the requested tag # does not match the actual tag if (conn_params.outNewSession \ or conn_params.outTagLength != params.tag_len \ or (params.tag_len > 0 \ and conn_params.outTag[:conn_params.outTagLength] != \ params.tag_ptr[:conn_params.outTagLength])): self.invoke_session_callback = True # set tag property, if applicable if conn_params.outTagLength > 0: self.tag = conn_params.outTag[:conn_params.outTagLength].decode() def create_cursor_impl(self): return ThickCursorImpl.__new__(ThickCursorImpl, self) def create_msg_props_impl(self): cdef ThickMsgPropsImpl impl impl = ThickMsgPropsImpl.__new__(ThickMsgPropsImpl) impl._conn_impl = self if dpiConn_newMsgProps(self._handle, &impl._handle) < 0: _raise_from_odpi() return impl def create_queue_impl(self): return ThickQueueImpl.__new__(ThickQueueImpl) def create_soda_database_impl(self, conn): cdef ThickSodaDbImpl impl = ThickSodaDbImpl.__new__(ThickSodaDbImpl) impl._conn = conn if dpiConn_getSodaDb(self._handle, &impl._handle) < 0: _raise_from_odpi() return impl def create_subscr_impl(self, object conn, object callback, uint32_t namespace, str name, uint32_t protocol, str ip_address, uint32_t port, uint32_t timeout, uint32_t operations, uint32_t qos, uint8_t grouping_class, uint32_t grouping_value, uint8_t grouping_type, bint client_initiated): cdef ThickSubscrImpl impl = ThickSubscrImpl.__new__(ThickSubscrImpl) impl.connection = conn impl.callback = callback impl.namespace = namespace impl.name = name impl.protocol = protocol impl.ip_address = ip_address impl.port = port impl.timeout = timeout impl.operations = operations impl.qos = qos impl.grouping_class = grouping_class impl.grouping_value = grouping_value impl.grouping_type = grouping_type impl.client_initiated = client_initiated return impl def create_temp_lob_impl(self, DbType dbtype): return ThickLobImpl._create(self, dbtype, NULL) def get_call_timeout(self): cdef uint32_t value if dpiConn_getCallTimeout(self._handle, &value) < 0: _raise_from_odpi() return value def get_current_schema(self): cdef: uint32_t value_length const char *value if dpiConn_getCurrentSchema(self._handle, &value, &value_length) < 0: _raise_from_odpi() if value is not NULL: return value[:value_length].decode() def get_edition(self): cdef: uint32_t value_length const char *value if dpiConn_getEdition(self._handle, &value, &value_length) < 0: _raise_from_odpi() if value is not NULL: return value[:value_length].decode() def get_external_name(self): cdef: uint32_t value_length const char *value if dpiConn_getExternalName(self._handle, &value, &value_length) < 0: _raise_from_odpi() if value is not NULL: return value[:value_length].decode() def get_handle(self): cdef void* handle if dpiConn_getHandle(self._handle, &handle) < 0: _raise_from_odpi() return handle def get_internal_name(self): cdef: uint32_t value_length const char *value if dpiConn_getInternalName(self._handle, &value, &value_length) < 0: _raise_from_odpi() if value is not NULL: return value[:value_length].decode() def get_ltxid(self): cdef: uint32_t value_length const char *value if dpiConn_getLTXID(self._handle, &value, &value_length) < 0: _raise_from_odpi() return value[:value_length] def get_stmt_cache_size(self): cdef uint32_t value if dpiConn_getStmtCacheSize(self._handle, &value) < 0: _raise_from_odpi() return value def get_type(self, object conn, str name): cdef: dpiObjectType *handle const char *name_ptr uint32_t name_len bytes name_bytes int status name_bytes = name.encode() name_ptr = name_bytes name_len = len(name_bytes) with nogil: status = dpiConn_getObjectType(self._handle, name_ptr, name_len, &handle) if status < 0: _raise_from_odpi() try: return ThickDbObjectTypeImpl._from_handle(self, handle) finally: dpiObjectType_release(handle) def get_version(self): cdef: dpiVersionInfo version_info int status with nogil: status = dpiConn_getServerVersion(self._handle, NULL, NULL, &version_info) if status < 0: _raise_from_odpi() return "%d.%d.%d.%d.%d" % \ (version_info.versionNum, version_info.releaseNum, version_info.updateNum, version_info.portReleaseNum, version_info.portUpdateNum) def set_action(self, str value): self._set_text_attr(dpiConn_setAction, value) def set_call_timeout(self, uint32_t value): if dpiConn_setCallTimeout(self._handle, value) < 0: _raise_from_odpi() def set_client_identifier(self, str value): self._set_text_attr(dpiConn_setClientIdentifier, value) def set_client_info(self, str value): self._set_text_attr(dpiConn_setClientInfo, value) def set_current_schema(self, str value): self._set_text_attr(dpiConn_setCurrentSchema, value) def set_dbop(self, str value): self._set_text_attr(dpiConn_setDbOp, value) def ping(self): cdef int status with nogil: status = dpiConn_ping(self._handle) if status < 0: _raise_from_odpi() def rollback(self): cdef int status with nogil: status = dpiConn_rollback(self._handle) if status < 0: _raise_from_odpi() def set_econtext_id(self, value): self._set_text_attr(dpiConn_setEcontextId, value) def set_external_name(self, str value): self._set_text_attr(dpiConn_setExternalName, value) def set_internal_name(self, str value): self._set_text_attr(dpiConn_setInternalName, value) def set_module(self, str value): self._set_text_attr(dpiConn_setModule, value) def set_stmt_cache_size(self, uint32_t value): if dpiConn_setStmtCacheSize(self._handle, value) < 0: _raise_from_odpi() def shutdown(self, uint32_t mode): cdef int status with nogil: status = dpiConn_shutdownDatabase(self._handle, mode) if status < 0: _raise_from_odpi() def startup(self, bint force, bint restrict, str pfile): cdef: uint32_t mode, pfile_length = 0 const char *pfile_ptr = NULL bytes temp int status mode = DPI_MODE_STARTUP_DEFAULT if force: mode |= DPI_MODE_STARTUP_FORCE if restrict: mode |= DPI_MODE_STARTUP_RESTRICT if pfile is not None: temp = pfile.encode() pfile_ptr = temp pfile_length = len(pfile_ptr) with nogil: status = dpiConn_startupDatabaseWithPfile(self._handle, pfile_ptr, pfile_length, mode) if status < 0: _raise_from_odpi() def tpc_begin(self, xid, uint32_t flags, uint32_t timeout): cdef: ThickXid thick_xid = ThickXid(xid) int status with nogil: status = dpiConn_tpcBegin(self._handle, thick_xid.xid_ptr, timeout, flags) if status < 0: _raise_from_odpi() def tpc_commit(self, xid, bint one_phase): cdef: ThickXid thick_xid = ThickXid(xid) int status with nogil: status = dpiConn_tpcCommit(self._handle, thick_xid.xid_ptr, one_phase) if status < 0: _raise_from_odpi() def tpc_end(self, xid, uint32_t flags): cdef: ThickXid thick_xid = ThickXid(xid) int status with nogil: status = dpiConn_tpcEnd(self._handle, thick_xid.xid_ptr, flags) if status < 0: _raise_from_odpi() def tpc_forget(self, xid): cdef: ThickXid thick_xid = ThickXid(xid) int status with nogil: status = dpiConn_tpcForget(self._handle, thick_xid.xid_ptr) if status < 0: _raise_from_odpi() def tpc_prepare(self, xid): cdef: ThickXid thick_xid = ThickXid(xid) bint commit_needed int status with nogil: status = dpiConn_tpcPrepare(self._handle, thick_xid.xid_ptr, &commit_needed) if status < 0: _raise_from_odpi() return commit_needed def tpc_rollback(self, xid): cdef: ThickXid thick_xid = ThickXid(xid) int status with nogil: status = dpiConn_tpcRollback(self._handle, thick_xid.xid_ptr) if status < 0: _raise_from_odpi() python-oracledb-1.2.1/src/oracledb/impl/thick/cursor.pyx000066400000000000000000000464241434177474600233240ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # cursor.pyx # # Cython file defining the thick Cursor implementation class (embedded in # thick_impl.pyx). #------------------------------------------------------------------------------ cdef class ThickCursorImpl(BaseCursorImpl): cdef: ThickConnImpl _conn_impl bint _is_implicit_cursor bint _fixup_ref_cursor dpiStmtInfo _stmt_info dpiStmt *_handle str _tag def __cinit__(self, conn_impl): self._conn_impl = conn_impl def __dealloc__(self): if self._handle != NULL: dpiStmt_release(self._handle) cdef int _close(self, bint in_del) except -1: """ Internal method for closing the cursor. """ if self._handle != NULL: if not in_del and self._conn_impl._handle != NULL \ and not self._is_implicit_cursor: if dpiStmt_close(self._handle, NULL, 0) < 0: _raise_from_odpi() dpiStmt_release(self._handle) self._handle = NULL cdef BaseVarImpl _create_var_impl(self, object conn): """ Internal method for creating a variable. """ cdef ThickVarImpl var_impl = ThickVarImpl.__new__(ThickVarImpl) var_impl._conn = conn var_impl._conn_impl = self._conn_impl var_impl._buf = StringBuffer.__new__(StringBuffer) return var_impl cdef int _define_var(self, object conn, object cursor, object type_handler, ssize_t pos) except -1: """ Internal method that creates the variable using the query info (unless an output type handler has been specified) and then performs the define in ODPI-C. """ cdef: ThickDbObjectTypeImpl typ_impl dpiDataTypeInfo *type_info ThickConnImpl conn_impl dpiQueryInfo query_info ThickVarImpl var_impl FetchInfo fetch_info # build FetchInfo based on query info provided by ODPI-C if dpiStmt_getQueryInfo(self._handle, pos + 1, &query_info) < 0: _raise_from_odpi() type_info = &query_info.typeInfo fetch_info = FetchInfo() fetch_info._dbtype = DbType._from_num(type_info.oracleTypeNum) if fetch_info._dbtype.num == DPI_ORACLE_TYPE_INTERVAL_YM: errors._raise_err(errors.ERR_DB_TYPE_NOT_SUPPORTED, name=fetch_info._dbtype.name) if type_info.sizeInChars > 0: fetch_info._size = type_info.sizeInChars else: fetch_info._size = type_info.clientSizeInBytes fetch_info._buffer_size = type_info.clientSizeInBytes fetch_info._name = query_info.name[:query_info.nameLength].decode() fetch_info._scale = type_info.scale + type_info.fsPrecision fetch_info._precision = type_info.precision fetch_info._nulls_allowed = query_info.nullOk if type_info.objectType != NULL: typ_impl = ThickDbObjectTypeImpl._from_handle(self._conn_impl, type_info.objectType) fetch_info._objtype = typ_impl # create variable and call define in ODPI-C self._create_fetch_var(conn, cursor, type_handler, pos, fetch_info) var_impl = self.fetch_var_impls[pos] var_impl._create_handle() if dpiStmt_define(self._handle, pos + 1, var_impl._handle) < 0: _raise_from_odpi() cdef int _fetch_rows(self, object cursor) except -1: """ Internal method for fetching rows from a cursor. """ cdef: uint32_t temp_buffer_row_index, num_rows_in_buffer bint more_rows_to_fetch ThickVarImpl var_impl int status with nogil: status = dpiStmt_fetchRows(self._handle, self._fetch_array_size, &temp_buffer_row_index, &num_rows_in_buffer, &more_rows_to_fetch) if status < 0: _raise_from_odpi() self._buffer_index = 0 self._buffer_rowcount = num_rows_in_buffer self._more_rows_to_fetch = more_rows_to_fetch cdef BaseConnImpl _get_conn_impl(self): """ Internal method used to return the connection implementation associated with the cursor implementation. """ return self._conn_impl cdef bint _is_plsql(self): return self._stmt_info.isPLSQL def _get_oci_attr(self, uint32_t attr_num, uint32_t attr_type): """ Internal method for getting the value of an OCI attribute on the cursor. """ cdef: dpiDataBuffer value uint32_t value_len if dpiStmt_getOciAttr(self._handle, attr_num, &value, &value_len) < 0: _raise_from_odpi() return _convert_oci_attr_to_python(attr_type, &value, value_len) cdef int _perform_define(self, object cursor, uint32_t num_query_cols) except -1: """ Internal method for performing defines. At this point it is assumed that the statement executed was in fact a query. """ cdef: ThickCursorImpl cursor_impl = cursor._impl object var, type_handler, conn ThickVarImpl var_impl ssize_t i # initialize fetching variables; these are used to reduce the number of # times that the GIL is released/acquired -- as there is a significant # amount of overhead in doing so self._buffer_rowcount = 0 self._more_rows_to_fetch = True # if fetch variables already exist, nothing to do (the same statement # is being executed and therefore all defines have already been # performed if self.fetch_vars is not None: return 0 # populate list to contain fetch variables that are created self._fetch_array_size = self.arraysize self._init_fetch_vars(num_query_cols) type_handler = self._get_output_type_handler() conn = cursor.connection for i in range(num_query_cols): self._define_var(conn, cursor, type_handler, i) def _set_oci_attr(self, uint32_t attr_num, uint32_t attr_type, object value): """ Internal method for setting the value of an OCI attribute on the cursor. """ cdef: StringBuffer str_buf = StringBuffer() void *oci_value = NULL dpiDataBuffer oci_buf uint32_t oci_len = 0 _convert_python_to_oci_attr(value, attr_type, str_buf, &oci_buf, &oci_value, &oci_len) if dpiStmt_setOciAttr(self._handle, attr_num, oci_value, oci_len) < 0: _raise_from_odpi() cdef int _transform_binds(self) except -1: cdef: ThickVarImpl var_impl uint32_t num_elements BindVar bind_var if self.bind_vars is not None: for bind_var in self.bind_vars: var_impl = bind_var.var_impl if var_impl.is_array: if dpiVar_getNumElementsInArray(var_impl._handle, &num_elements) < 0: _raise_from_odpi() var_impl.num_elements_in_array = num_elements def execute(self, cursor): """ Internal method for executing a statement. """ cdef: uint32_t mode, num_query_cols uint64_t rowcount = 0 int status if self.bind_vars is not None: self._perform_binds(cursor.connection, 1) if self._conn_impl.autocommit: mode = DPI_MODE_EXEC_COMMIT_ON_SUCCESS else: mode = DPI_MODE_EXEC_DEFAULT with nogil: status = dpiStmt_execute(self._handle, mode, &num_query_cols) if status == DPI_SUCCESS and not self._stmt_info.isPLSQL: status = dpiStmt_getRowCount(self._handle, &rowcount) if status < 0: _raise_from_odpi() self.rowcount = rowcount if num_query_cols > 0: self._perform_define(cursor, num_query_cols) elif self._stmt_info.isReturning or self._stmt_info.isPLSQL: self._transform_binds() def executemany(self, cursor, num_execs, batcherrors, arraydmlrowcounts): """ Internal method for executing a statement multiple times. """ cdef: uint32_t mode, num_execs_int = num_execs dpiErrorInfo error_info uint64_t rowcount = 0 int status if self._conn_impl.autocommit: mode = DPI_MODE_EXEC_COMMIT_ON_SUCCESS else: mode = DPI_MODE_EXEC_DEFAULT if arraydmlrowcounts: mode |= DPI_MODE_EXEC_ARRAY_DML_ROWCOUNTS if batcherrors: mode |= DPI_MODE_EXEC_BATCH_ERRORS if self.bind_vars is not None: self._perform_binds(cursor.connection, num_execs_int) if num_execs_int > 0: with nogil: status = dpiStmt_executeMany(self._handle, mode, num_execs_int) if status < 0: dpiContext_getError(driver_context, &error_info) if not self._stmt_info.isPLSQL: dpiStmt_getRowCount(self._handle, &rowcount) self.rowcount = rowcount if status < 0: _raise_from_info(&error_info) if self._stmt_info.isReturning or self._stmt_info.isPLSQL: self._transform_binds() def get_array_dml_row_counts(self): """ Internal method for returning a list of array DML row counts for the last operation executed. """ cdef: uint32_t num_row_count, i uint64_t *rowcount int status status = dpiStmt_getRowCounts(self._handle, &num_row_count, &rowcount) if status < 0: _raise_from_odpi() result = [] for i in range(num_row_count): result.append(rowcount[i]) return result def get_batch_errors(self): """ Internal method for returning a list of batch errors. """ cdef: uint32_t num_errors, i dpiErrorInfo *errors if dpiStmt_getBatchErrorCount(self._handle, &num_errors) < 0: _raise_from_odpi() if num_errors == 0: return [] errors = \ cpython.PyMem_Malloc(num_errors * sizeof(dpiErrorInfo)) try: if dpiStmt_getBatchErrors(self._handle, num_errors, errors) < 0: _raise_from_odpi() result = cpython.PyList_New(num_errors) for i in range(num_errors): error = _create_new_from_info(&errors[i]) cpython.Py_INCREF(error) cpython.PyList_SET_ITEM(result, i, error) finally: cpython.PyMem_Free(errors) return result def get_bind_names(self): cdef: uint32_t *name_lengths = NULL const char **names = NULL uint32_t num_binds, i ssize_t num_bytes list result if dpiStmt_getBindCount(self._handle, &num_binds) < 0: _raise_from_odpi() if num_binds == 0: return [] try: num_bytes = num_binds * sizeof(char*) names = cpython.PyMem_Malloc(num_bytes) num_bytes = num_binds * sizeof(uint32_t) name_lengths = cpython.PyMem_Malloc(num_bytes) if dpiStmt_getBindNames(self._handle, &num_binds, names, name_lengths) < 0: _raise_from_odpi() result = [None] * num_binds for i in range(num_binds): result[i] = names[i][:name_lengths[i]].decode() return result finally: if names: cpython.PyMem_Free(names) if name_lengths: cpython.PyMem_Free(name_lengths) def get_implicit_results(self, connection): cdef: ThickCursorImpl child_cursor_impl object child_cursor dpiStmt *child_stmt list result = [] if self._handle == NULL: errors._raise_err(errors.ERR_NO_STATEMENT_EXECUTED) while True: if dpiStmt_getImplicitResult(self._handle, &child_stmt) < 0: _raise_from_odpi() if child_stmt == NULL: break child_cursor = connection.cursor() child_cursor_impl = child_cursor._impl child_cursor_impl._handle = child_stmt child_cursor_impl._fixup_ref_cursor = True child_cursor_impl._is_implicit_cursor = True result.append(child_cursor) return result def get_lastrowid(self): """ Internal method for returning the rowid of the row that was last modified by an operation. """ cdef: uint32_t rowid_length const char *rowid_ptr dpiRowid *rowid if self._handle is not NULL: if dpiStmt_getLastRowid(self._handle, &rowid) < 0: _raise_from_odpi() if rowid: if dpiRowid_getStringValue(rowid, &rowid_ptr, &rowid_length) < 0: _raise_from_odpi() return rowid_ptr[:rowid_length].decode() def is_query(self, cursor): cdef uint32_t num_query_cols if self._fixup_ref_cursor: self._fetch_array_size = self.arraysize if dpiStmt_setFetchArraySize(self._handle, self._fetch_array_size) < 0: _raise_from_odpi() if dpiStmt_getNumQueryColumns(self._handle, &num_query_cols) < 0: _raise_from_odpi() self._perform_define(cursor, num_query_cols) self._fixup_ref_cursor = False return self.fetch_vars is not None def parse(self, cursor): """ Internal method for parsing a statement. """ cdef: uint32_t mode, num_query_cols int status if self._stmt_info.isQuery: mode = DPI_MODE_EXEC_DESCRIBE_ONLY else: mode = DPI_MODE_EXEC_PARSE_ONLY with nogil: status = dpiStmt_execute(self._handle, mode, &num_query_cols) if status < 0: _raise_from_odpi() if num_query_cols > 0: self._perform_define(cursor, num_query_cols) def prepare(self, str statement, str tag, bint cache_statement): """ Internal method used for preparing statements for execution. """ cdef: uint32_t statement_bytes_length, tag_bytes_length = 0 ThickConnImpl conn_impl = self._conn_impl bytes statement_bytes, tag_bytes const char *tag_ptr = NULL const char *statement_ptr int status self.statement = statement statement_bytes = statement.encode() statement_ptr = statement_bytes statement_bytes_length = len(statement_bytes) if tag is not None: self._tag = tag tag_bytes = tag.encode() tag_bytes_length = len(tag_bytes) tag_ptr = tag_bytes with nogil: if self._handle != NULL: dpiStmt_release(self._handle) self._handle = NULL status = dpiConn_prepareStmt(conn_impl._handle, self.scrollable, statement_ptr, statement_bytes_length, tag_ptr, tag_bytes_length, &self._handle) if status == DPI_SUCCESS and not cache_statement: status = dpiStmt_deleteFromCache(self._handle) if status == DPI_SUCCESS: status = dpiStmt_getInfo(self._handle, &self._stmt_info) if status == DPI_SUCCESS and self._stmt_info.isQuery: status = dpiStmt_setFetchArraySize(self._handle, self.arraysize) if status == DPI_SUCCESS \ and self.prefetchrows != DPI_DEFAULT_PREFETCH_ROWS: status = dpiStmt_setPrefetchRows(self._handle, self.prefetchrows) if status < 0: _raise_from_odpi() def scroll(self, object conn, int32_t offset, object mode): cdef: uint32_t temp_buffer_row_index = 0, num_rows_in_buffer = 0 bint more_rows_to_fetch = False ThickVarImpl var_impl uint32_t int_mode = 0 int status if mode == "relative": int_mode = DPI_MODE_FETCH_RELATIVE elif mode == "absolute": int_mode = DPI_MODE_FETCH_ABSOLUTE elif mode == "first": int_mode = DPI_MODE_FETCH_FIRST elif mode == "last": int_mode = DPI_MODE_FETCH_LAST else: errors._raise_err(errors.ERR_WRONG_SCROLL_MODE) with nogil: status = dpiStmt_scroll(self._handle, int_mode, offset, 0 - self._buffer_rowcount) if status == 0: status = dpiStmt_fetchRows(self._handle, self._fetch_array_size, &temp_buffer_row_index, &num_rows_in_buffer, &more_rows_to_fetch) if status == 0: status = dpiStmt_getRowCount(self._handle, &self.rowcount) if status < 0: _raise_from_odpi() self._buffer_index = temp_buffer_row_index self._buffer_rowcount = num_rows_in_buffer self._more_rows_to_fetch = more_rows_to_fetch self.rowcount -= num_rows_in_buffer python-oracledb-1.2.1/src/oracledb/impl/thick/dbobject.pyx000066400000000000000000000331471434177474600235610ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # dbobject.pyx # # Cython file defining the thick DbObjectType, DbObjectAttr and DbObject # implementation classes (embedded in thick_impl.pyx). #------------------------------------------------------------------------------ cdef class ThickDbObjectImpl(BaseDbObjectImpl): cdef: dpiObject *_handle def __dealloc__(self): if self._handle != NULL: dpiObject_release(self._handle) cdef int _convert_from_python(self, object value, DbType dbtype, ThickDbObjectTypeImpl objtype, dpiData *data, StringBuffer buf): """ Internal method for converting a value from Python to the value required by ODPI-C. """ if value is None: data.isNull = 1 else: data.isNull = 0 _convert_from_python(value, dbtype, objtype, &data.value, buf) def append_checked(self, object value): """ Internal method for appending a value to a collection object. """ cdef: StringBuffer buf = StringBuffer() ThickDbObjectTypeImpl objtype dpiData data objtype = self.type self._convert_from_python(value, objtype.element_dbtype, objtype.element_objtype, &data, buf) if dpiObject_appendElement(self._handle, objtype._element_native_type_num, &data) < 0: _raise_from_odpi() def copy(self): """ Internal method for creating a copy of an object. """ cdef ThickDbObjectImpl copied_impl copied_impl = ThickDbObjectImpl.__new__(ThickDbObjectImpl) if dpiObject_copy(self._handle, &copied_impl._handle) < 0: _raise_from_odpi() copied_impl.type = self.type return copied_impl def delete_by_index(self, int32_t index): """ Internal method for deleting an entry from a collection that is indexed by integers. """ if dpiObject_deleteElementByIndex(self._handle, index) < 0: _raise_from_odpi() def exists_by_index(self, int32_t index): """ Internal method for determining if an entry exists in a collection that is indexed by integers. """ cdef bint exists if dpiObject_getElementExistsByIndex(self._handle, index, &exists) < 0: _raise_from_odpi() return exists def get_attr_value(self, ThickDbObjectAttrImpl attr): """ Internal method for getting an attribute value. """ cdef: char number_as_string_buffer[200] ThickDbObjectTypeImpl type_impl dpiData data if attr._native_type_num == DPI_NATIVE_TYPE_BYTES \ and attr.dbtype.num == DPI_ORACLE_TYPE_NUMBER: data.value.asBytes.ptr = number_as_string_buffer data.value.asBytes.length = sizeof(number_as_string_buffer) data.value.asBytes.encoding = NULL if dpiObject_getAttributeValue(self._handle, attr._handle, attr._native_type_num, &data) < 0: _raise_from_odpi() if data.isNull: return None type_impl = self.type return _convert_to_python(type_impl._conn_impl, attr.dbtype, attr.objtype, &data.value) def get_element_by_index(self, int32_t index): """ Internal method for getting an entry from a collection that is indexed by integers. """ cdef: char number_as_string_buffer[200] ThickDbObjectTypeImpl objtype dpiData data objtype = self.type if objtype._element_native_type_num == DPI_NATIVE_TYPE_BYTES \ and objtype.element_dbtype.num == DPI_ORACLE_TYPE_NUMBER: data.value.asBytes.ptr = number_as_string_buffer data.value.asBytes.length = sizeof(number_as_string_buffer) data.value.asBytes.encoding = NULL if dpiObject_getElementValueByIndex(self._handle, index, objtype._element_native_type_num, &data) < 0: _raise_from_odpi() if data.isNull: return None return _convert_to_python(objtype._conn_impl, objtype.element_dbtype, objtype.element_objtype, &data.value) def get_first_index(self): """ Internal method for getting the first index from a collection that is indexed by integers. """ cdef: int32_t index bint exists if dpiObject_getFirstIndex(self._handle, &index, &exists) < 0: _raise_from_odpi() if exists: return index def get_last_index(self): """ Internal method for getting the last index from a collection that is indexed by integers. """ cdef: int32_t index bint exists if dpiObject_getLastIndex(self._handle, &index, &exists) < 0: _raise_from_odpi() if exists: return index def get_next_index(self, int32_t index): """ Internal method for getting the next index from a collection that is indexed by integers. """ cdef: int32_t next_index bint exists if dpiObject_getNextIndex(self._handle, index, &next_index, &exists) < 0: _raise_from_odpi() if exists: return next_index def get_prev_index(self, int32_t index): """ Internal method for getting the next index from a collection that is indexed by integers. """ cdef: int32_t prev_index bint exists if dpiObject_getPrevIndex(self._handle, index, &prev_index, &exists) < 0: _raise_from_odpi() if exists: return prev_index def get_size(self): """ Internal method for getting the size of a collection. """ cdef int32_t size if dpiObject_getSize(self._handle, &size) < 0: _raise_from_odpi() return size def set_attr_value_checked(self, ThickDbObjectAttrImpl attr, object value): """ Internal method for setting an attribute value. """ cdef: StringBuffer buf = StringBuffer() uint32_t native_type_num dpiData data self._convert_from_python(value, attr.dbtype, attr.objtype, &data, buf) native_type_num = attr._native_type_num if native_type_num == DPI_NATIVE_TYPE_LOB \ and not isinstance(value, PY_TYPE_LOB): native_type_num = DPI_NATIVE_TYPE_BYTES if dpiObject_setAttributeValue(self._handle, attr._handle, native_type_num, &data) < 0: _raise_from_odpi() def set_element_by_index_checked(self, int32_t index, object value): """ Internal method for setting an entry in a collection that is indexed by integers. """ cdef: StringBuffer buf = StringBuffer() ThickDbObjectTypeImpl objtype uint32_t native_type_num dpiData data objtype = self.type self._convert_from_python(value, objtype.element_dbtype, objtype.element_objtype, &data, buf) native_type_num = objtype._element_native_type_num if native_type_num == DPI_NATIVE_TYPE_LOB \ and not isinstance(value, PY_TYPE_LOB): native_typeNum = DPI_NATIVE_TYPE_BYTES if dpiObject_setElementValueByIndex(self._handle, index, native_type_num, &data) < 0: _raise_from_odpi() def trim(self, int32_t num_to_trim): """ Internal method for trimming a number of entries from a collection. """ if dpiObject_trim(self._handle, num_to_trim) < 0: _raise_from_odpi() cdef class ThickDbObjectAttrImpl(BaseDbObjectAttrImpl): cdef: dpiObjectAttr *_handle uint32_t _native_type_num def __dealloc__(self): if self._handle != NULL: dpiObjectAttr_release(self._handle) @staticmethod cdef ThickDbObjectAttrImpl _from_handle(ThickConnImpl conn_impl, dpiObjectAttr *handle): """ Create a new DbObjectAttr implementation object given an ODPI-C handle. """ cdef: ThickDbObjectAttrImpl impl dpiObjectType *typ_handle dpiObjectAttrInfo info impl = ThickDbObjectAttrImpl.__new__(ThickDbObjectAttrImpl) impl._handle = handle if dpiObjectAttr_getInfo(handle, &info) < 0: _raise_from_odpi() impl.name = info.name[:info.nameLength].decode() impl.dbtype = DbType._from_num(info.typeInfo.oracleTypeNum) if info.typeInfo.objectType: typ_handle = info.typeInfo.objectType impl.objtype = ThickDbObjectTypeImpl._from_handle(conn_impl, typ_handle) impl._native_type_num = _get_native_type_num(impl.dbtype) return impl cdef class ThickDbObjectTypeImpl(BaseDbObjectTypeImpl): cdef: dpiObjectType *_handle uint32_t _element_native_type_num def __dealloc__(self): if self._handle != NULL: dpiObjectType_release(self._handle) @staticmethod cdef ThickDbObjectTypeImpl _from_handle(ThickConnImpl conn_impl, dpiObjectType *handle): """ Create a new DbObjectType implementation object given an ODPI-C handle. """ cdef: dpiObjectAttr **attributes = NULL ThickDbObjectTypeImpl impl, temp ThickDbObjectAttrImpl attr_impl dpiObjectTypeInfo info ssize_t num_bytes DbType dbtype uint16_t i object typ impl = ThickDbObjectTypeImpl.__new__(ThickDbObjectTypeImpl) if dpiObjectType_addRef(handle) < 0: _raise_from_odpi() impl._conn_impl = conn_impl impl._handle = handle if dpiObjectType_getInfo(impl._handle, &info) < 0: _raise_from_odpi() impl.schema = info.schema[:info.schemaLength].decode() impl.name = info.name[:info.nameLength].decode() if info.packageNameLength > 0: impl.package_name = \ info.packageName[:info.packageNameLength].decode() impl.is_collection = info.isCollection if impl.is_collection: dbtype = DbType._from_num(info.elementTypeInfo.oracleTypeNum) impl.element_dbtype = dbtype if info.elementTypeInfo.objectType != NULL: handle = info.elementTypeInfo.objectType temp = ThickDbObjectTypeImpl._from_handle(conn_impl, handle) impl.element_objtype = temp impl._element_native_type_num = \ _get_native_type_num(impl.element_dbtype) impl.attrs_by_name = {} impl.attrs = [None] * info.numAttributes try: num_bytes = info.numAttributes * sizeof(dpiObjectAttr*) attributes = cpython.PyMem_Malloc(num_bytes) if dpiObjectType_getAttributes(impl._handle, info.numAttributes, attributes) < 0: _raise_from_odpi() for i in range(info.numAttributes): attr_impl = ThickDbObjectAttrImpl._from_handle(conn_impl, attributes[i]) impl.attrs[i] = attr_impl impl.attrs_by_name[attr_impl.name] = attr_impl finally: if attributes != NULL: cpython.PyMem_Free(attributes) return impl def create_new_object(self): """ Internal method for creating a new object. """ cdef ThickDbObjectImpl obj_impl obj_impl = ThickDbObjectImpl.__new__(ThickDbObjectImpl) obj_impl.type = self if dpiObjectType_createObject(self._handle, &obj_impl._handle) < 0: _raise_from_odpi() return obj_impl python-oracledb-1.2.1/src/oracledb/impl/thick/json.pyx000066400000000000000000000221621434177474600227510ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # json.pyx # # Cython file defining methods and classes used for processing JSON data # (embedded in thick_impl.pyx). #------------------------------------------------------------------------------ cdef class JsonBuffer: cdef: dpiJsonNode _top_node dpiDataBuffer _top_node_buf list _buffers def __dealloc__(self): _free_node(&self._top_node) cdef int _add_buf(self, object value, char **ptr, uint32_t *length) except -1: cdef StringBuffer buf = StringBuffer() buf.set_value(value) if self._buffers is None: self._buffers = [] self._buffers.append(buf) ptr[0] = buf.ptr length[0] = buf.length cdef int _populate_array_node(self, dpiJsonNode *node, list value) except -1: cdef: dpiJsonArray *array object child_value uint32_t i node.oracleTypeNum = DPI_ORACLE_TYPE_JSON_ARRAY node.nativeTypeNum = DPI_NATIVE_TYPE_JSON_ARRAY array = &node.value.asJsonArray array.numElements = len(value) array.elements = _calloc(array.numElements, sizeof(dpiJsonNode)) array.elementValues = _calloc(array.numElements, sizeof(dpiDataBuffer)) for i, child_value in enumerate(value): array.elements[i].value = &array.elementValues[i] self._populate_node(&array.elements[i], child_value) cdef int _populate_obj_node(self, dpiJsonNode *node, dict value) except -1: cdef: object child_key, child_value dpiJsonObject *obj uint32_t i node.oracleTypeNum = DPI_ORACLE_TYPE_JSON_OBJECT node.nativeTypeNum = DPI_NATIVE_TYPE_JSON_OBJECT obj = &node.value.asJsonObject obj.numFields = len(value) obj.fieldNames = _calloc(obj.numFields, sizeof(char*)) obj.fieldNameLengths = _calloc(obj.numFields, sizeof(uint32_t)) obj.fields = _calloc(obj.numFields, sizeof(dpiJsonNode)) obj.fieldValues = _calloc(obj.numFields, sizeof(dpiDataBuffer)) i = 0 for child_key, child_value in value.items(): obj.fields[i].value = &obj.fieldValues[i] self._add_buf(child_key, &obj.fieldNames[i], &obj.fieldNameLengths[i]) self._populate_node(&obj.fields[i], child_value) i += 1 cdef int _populate_node(self, dpiJsonNode *node, object value) except -1: cdef: dpiTimestamp *timestamp dpiIntervalDS *interval int seconds if value is None: node.oracleTypeNum = DPI_ORACLE_TYPE_NONE node.nativeTypeNum = DPI_NATIVE_TYPE_NULL elif isinstance(value, list): self._populate_array_node(node, value) elif isinstance(value, dict): self._populate_obj_node(node, value) elif isinstance(value, str): node.oracleTypeNum = DPI_ORACLE_TYPE_VARCHAR node.nativeTypeNum = DPI_NATIVE_TYPE_BYTES self._add_buf(value, &node.value.asBytes.ptr, &node.value.asBytes.length) elif isinstance(value, bytes): node.oracleTypeNum = DPI_ORACLE_TYPE_RAW node.nativeTypeNum = DPI_NATIVE_TYPE_BYTES self._add_buf(value, &node.value.asBytes.ptr, &node.value.asBytes.length) elif isinstance(value, bool): node.oracleTypeNum = DPI_ORACLE_TYPE_BOOLEAN node.nativeTypeNum = DPI_NATIVE_TYPE_BOOLEAN node.value.asBoolean = value elif isinstance(value, (int, PY_TYPE_DECIMAL)): node.oracleTypeNum = DPI_ORACLE_TYPE_NUMBER node.nativeTypeNum = DPI_NATIVE_TYPE_BYTES self._add_buf(str(value), &node.value.asBytes.ptr, &node.value.asBytes.length) elif isinstance(value, float): node.oracleTypeNum = DPI_ORACLE_TYPE_NUMBER node.nativeTypeNum = DPI_NATIVE_TYPE_DOUBLE node.value.asDouble = value elif isinstance(value, PY_TYPE_DATETIME): node.oracleTypeNum = DPI_ORACLE_TYPE_TIMESTAMP node.nativeTypeNum = DPI_NATIVE_TYPE_TIMESTAMP memset(&node.value.asTimestamp, 0, sizeof(node.value.asTimestamp)) timestamp = &node.value.asTimestamp timestamp.year = cydatetime.datetime_year(value) timestamp.month = cydatetime.datetime_month(value) timestamp.day = cydatetime.datetime_day(value) timestamp.hour = cydatetime.datetime_hour(value) timestamp.minute = cydatetime.datetime_minute(value) timestamp.second = cydatetime.datetime_second(value) timestamp.fsecond = cydatetime.datetime_microsecond(value) * 1000 elif isinstance(value, PY_TYPE_DATE): node.oracleTypeNum = DPI_ORACLE_TYPE_DATE node.nativeTypeNum = DPI_NATIVE_TYPE_TIMESTAMP memset(&node.value.asTimestamp, 0, sizeof(node.value.asTimestamp)) timestamp = &node.value.asTimestamp timestamp = &node.value.asTimestamp timestamp.year = cydatetime.date_year(value) timestamp.month = cydatetime.date_month(value) timestamp.day = cydatetime.date_day(value) elif isinstance(value, datetime.timedelta): node.oracleTypeNum = DPI_ORACLE_TYPE_INTERVAL_DS node.nativeTypeNum = DPI_NATIVE_TYPE_INTERVAL_DS interval = &node.value.asIntervalDS seconds = cydatetime.timedelta_seconds(value) interval.days = cydatetime.timedelta_days(value) interval.hours = seconds // 3600 seconds = seconds % 3600 interval.minutes = seconds // 60 interval.seconds = seconds % 60 interval.fseconds = cydatetime.timedelta_microseconds(value) * 1000 else: errors._raise_err(errors.ERR_PYTHON_TYPE_NOT_SUPPORTED, typ=type(value)) cdef int from_object(self, object value) except -1: self._top_node.value = &self._top_node_buf self._populate_node(&self._top_node, value) cdef void* _calloc(size_t num_elements, size_t size) except NULL: cdef: size_t num_bytes = num_elements * size void *ptr ptr = cpython.PyMem_Malloc(num_bytes) memset(ptr, 0, num_bytes) return ptr cdef void _free_node(dpiJsonNode *node): cdef: dpiJsonArray *array dpiJsonObject *obj uint32_t i if node.nativeTypeNum == DPI_NATIVE_TYPE_JSON_ARRAY: array = &node.value.asJsonArray if array.elements != NULL: for i in range(array.numElements): if array.elements[i].value != NULL: _free_node(&array.elements[i]) cpython.PyMem_Free(array.elements) array.elements = NULL if array.elementValues != NULL: cpython.PyMem_Free(array.elementValues) array.elementValues = NULL elif node.nativeTypeNum == DPI_NATIVE_TYPE_JSON_OBJECT: obj = &node.value.asJsonObject if obj.fields != NULL: for i in range(obj.numFields): if obj.fields[i].value != NULL: _free_node(&obj.fields[i]) cpython.PyMem_Free(obj.fields) obj.fields = NULL if obj.fieldNames != NULL: cpython.PyMem_Free(obj.fieldNames) obj.fieldNames = NULL if obj.fieldNameLengths != NULL: cpython.PyMem_Free(obj.fieldNameLengths) obj.fieldNameLengths = NULL if obj.fieldValues != NULL: cpython.PyMem_Free(obj.fieldValues) obj.fieldValues = NULL python-oracledb-1.2.1/src/oracledb/impl/thick/lob.pyx000066400000000000000000000165561434177474600225660ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # lob.pyx # # Cython file defining the thick Lob implementation class (embedded in # thick_impl.pyx). #------------------------------------------------------------------------------ cdef class ThickLobImpl(BaseLobImpl): cdef: dpiLob *_handle @staticmethod cdef ThickLobImpl _create(ThickConnImpl conn_impl, DbType dbtype, dpiLob *handle): cdef: ThickLobImpl impl = ThickLobImpl.__new__(ThickLobImpl) int status impl.dbtype = dbtype if handle == NULL: with nogil: status = dpiConn_newTempLob(conn_impl._handle, dbtype.num, &handle) if status < 0: _raise_from_odpi() elif dpiLob_addRef(handle) < 0: _raise_from_odpi() impl._handle = handle return impl def close(self): """ Internal method for closing a LOB that was opened earlier. """ cdef int status with nogil: status = dpiLob_closeResource(self._handle) if status < 0: _raise_from_odpi() def file_exists(self): """ Internal method for returning whether the file referenced by a BFILE exists. """ cdef: bint exists int status with nogil: status = dpiLob_getFileExists(self._handle, &exists) if status < 0: _raise_from_odpi() return exists def free_lob(self): """ Internal method for releasing the handle """ if self._handle != NULL: dpiLob_release(self._handle) def get_chunk_size(self): """ Internal method for returning the chunk size of the LOB. """ cdef uint32_t chunk_size if dpiLob_getChunkSize(self._handle, &chunk_size) < 0: _raise_from_odpi() return chunk_size def get_file_name(self): """ Internal method for returning a 2-tuple constaining the directory alias and file name of a BFILE type LOB. """ cdef: uint32_t dir_alias_len, file_name_len const char *dir_alias const char *file_name int status with nogil: status = dpiLob_getDirectoryAndFileName(self._handle, &dir_alias, &dir_alias_len, &file_name, &file_name_len) if status < 0: _raise_from_odpi() return (dir_alias[:dir_alias_len].decode(), file_name[:file_name_len].decode()) def get_is_open(self): """ Internal method for returning whether the LOB is open or not. """ cdef: bint is_open int status with nogil: status = dpiLob_getIsResourceOpen(self._handle, &is_open) if status < 0: _raise_from_odpi() return is_open def get_max_amount(self): """ Internal method for returning the maximum amount that can be read. """ return self.get_size() def get_size(self): """ Internal method for returning the size of a LOB. """ cdef uint64_t size if dpiLob_getSize(self._handle, &size) < 0: _raise_from_odpi() return size def open(self): """ Internal method for opening a LOB. """ cdef int status with nogil: status = dpiLob_openResource(self._handle) if status < 0: _raise_from_odpi() def read(self, uint64_t offset, uint64_t amount): """ Internal method for reading a portion (or all) of the data in the LOB. """ cdef: uint64_t buf_size object result char *buf int status if dpiLob_getBufferSize(self._handle, amount, &buf_size) < 0: _raise_from_odpi() buf = cpython.PyMem_Malloc(buf_size) with nogil: status = dpiLob_readBytes(self._handle, offset, amount, buf, &buf_size) try: if status < 0: _raise_from_odpi() result = buf[:buf_size] if self.dbtype.num == DPI_ORACLE_TYPE_CLOB \ or self.dbtype.num == DPI_ORACLE_TYPE_NCLOB: result = result.decode() return result finally: cpython.PyMem_Free(buf) def set_file_name(self, str dir_alias, str name): """ Internal method for setting the directory alias and file name associated with a BFILE LOB. """ cdef: bytes dir_alias_bytes, name_bytes uint32_t dir_alias_len, name_len const char *dir_alias_ptr const char *name_ptr int status dir_alias_bytes = dir_alias.encode() dir_alias_ptr = dir_alias_bytes dir_alias_len = len(dir_alias_bytes) name_bytes = name.encode() name_ptr = name_bytes name_len = len(name_bytes) with nogil: status = dpiLob_setDirectoryAndFileName(self._handle, dir_alias_ptr, dir_alias_len, name_ptr, name_len) if status < 0: _raise_from_odpi() def trim(self, uint64_t new_size): """ Internal method for trimming the data in the LOB to the new size """ cdef int status with nogil: status = dpiLob_trim(self._handle, new_size) if status < 0: _raise_from_odpi() def write(self, object value, uint64_t offset): """ Internal method for writing data to the LOB object. """ cdef: StringBuffer buf = StringBuffer() int status buf.set_value(value) with nogil: status = dpiLob_writeBytes(self._handle, offset, buf.ptr, buf.length) if status < 0: _raise_from_odpi() python-oracledb-1.2.1/src/oracledb/impl/thick/odpi/000077500000000000000000000000001434177474600221665ustar00rootroot00000000000000python-oracledb-1.2.1/src/oracledb/impl/thick/odpi.pxd000066400000000000000000001143411434177474600227070ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # odpi.pxd # # Cython definition file for the constants, functions and structures found in # ODPI-C used by the thick implementation classes (embedded in thick_impl.pyx). #------------------------------------------------------------------------------ from libc.stdint cimport int8_t, int16_t, int32_t, int64_t from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t cdef enum: PYO_OCI_ATTR_TYPE_STRING = 1 PYO_OCI_ATTR_TYPE_BOOLEAN = 2 PYO_OCI_ATTR_TYPE_UINT8 = 8 PYO_OCI_ATTR_TYPE_UINT16 = 16 PYO_OCI_ATTR_TYPE_UINT32 = 32 PYO_OCI_ATTR_TYPE_UINT64 = 64 cdef extern from "impl/thick/odpi/embed/dpi.c": # version constants unsigned int DPI_MAJOR_VERSION unsigned int DPI_MINOR_VERSION # success/failure constants int DPI_SUCCESS int DPI_FAILURE # defaults enum: DPI_DEFAULT_PREFETCH_ROWS # execution modes enum: DPI_MODE_EXEC_ARRAY_DML_ROWCOUNTS DPI_MODE_EXEC_BATCH_ERRORS DPI_MODE_EXEC_COMMIT_ON_SUCCESS DPI_MODE_EXEC_DEFAULT DPI_MODE_EXEC_DESCRIBE_ONLY DPI_MODE_EXEC_PARSE_ONLY # connection/pool creation modes enum: DPI_MODE_CREATE_DEFAULT DPI_MODE_CREATE_THREADED DPI_MODE_CREATE_EVENTS # connection close modes enum: DPI_MODE_CONN_CLOSE_DEFAULT DPI_MODE_CONN_CLOSE_DROP DPI_MODE_CONN_CLOSE_RETAG # pool close modes enum: DPI_MODE_POOL_CLOSE_DEFAULT DPI_MODE_POOL_CLOSE_FORCE # native types enum: DPI_NATIVE_TYPE_BOOLEAN DPI_NATIVE_TYPE_BYTES DPI_NATIVE_TYPE_DOUBLE DPI_NATIVE_TYPE_FLOAT DPI_NATIVE_TYPE_INT64 DPI_NATIVE_TYPE_INTERVAL_DS DPI_NATIVE_TYPE_JSON DPI_NATIVE_TYPE_JSON_ARRAY DPI_NATIVE_TYPE_JSON_OBJECT DPI_NATIVE_TYPE_LOB DPI_NATIVE_TYPE_NULL DPI_NATIVE_TYPE_OBJECT DPI_NATIVE_TYPE_ROWID DPI_NATIVE_TYPE_STMT DPI_NATIVE_TYPE_TIMESTAMP # Oracle types enum: DPI_ORACLE_TYPE_BFILE DPI_ORACLE_TYPE_BLOB DPI_ORACLE_TYPE_BOOLEAN DPI_ORACLE_TYPE_CHAR DPI_ORACLE_TYPE_CLOB DPI_ORACLE_TYPE_DATE DPI_ORACLE_TYPE_INTERVAL_DS DPI_ORACLE_TYPE_INTERVAL_YM DPI_ORACLE_TYPE_JSON DPI_ORACLE_TYPE_JSON_ARRAY DPI_ORACLE_TYPE_JSON_OBJECT DPI_ORACLE_TYPE_LONG_NVARCHAR DPI_ORACLE_TYPE_LONG_RAW DPI_ORACLE_TYPE_LONG_VARCHAR DPI_ORACLE_TYPE_NATIVE_DOUBLE DPI_ORACLE_TYPE_NATIVE_FLOAT DPI_ORACLE_TYPE_NATIVE_INT DPI_ORACLE_TYPE_NCHAR DPI_ORACLE_TYPE_NCLOB DPI_ORACLE_TYPE_NONE DPI_ORACLE_TYPE_NUMBER DPI_ORACLE_TYPE_NVARCHAR DPI_ORACLE_TYPE_OBJECT DPI_ORACLE_TYPE_RAW DPI_ORACLE_TYPE_ROWID DPI_ORACLE_TYPE_STMT DPI_ORACLE_TYPE_TIMESTAMP DPI_ORACLE_TYPE_TIMESTAMP_LTZ DPI_ORACLE_TYPE_TIMESTAMP_TZ DPI_ORACLE_TYPE_VARCHAR # SODA flags enum: DPI_SODA_FLAGS_DEFAULT DPI_SODA_FLAGS_ATOMIC_COMMIT DPI_SODA_FLAGS_CREATE_COLL_MAP DPI_SODA_FLAGS_INDEX_DROP_FORCE # database startup modes enum: DPI_MODE_STARTUP_DEFAULT DPI_MODE_STARTUP_FORCE DPI_MODE_STARTUP_RESTRICT # fetch modes (for scrolling cursors) enum: DPI_MODE_FETCH_FIRST DPI_MODE_FETCH_LAST DPI_MODE_FETCH_ABSOLUTE DPI_MODE_FETCH_RELATIVE # subscription constants uint32_t DPI_SUBSCR_QOS_QUERY uint32_t DPI_EVENT_OBJCHANGE uint32_t DPI_EVENT_QUERYCHANGE # JSON flags uint32_t DPI_JSON_OPT_NUMBER_AS_STRING # opaque handles ctypedef struct dpiConn: pass ctypedef struct dpiContext: pass ctypedef struct dpiDeqOptions: pass ctypedef struct dpiEnqOptions: pass ctypedef struct dpiJson: pass ctypedef struct dpiLob: pass ctypedef struct dpiMsgProps: pass ctypedef struct dpiObject: pass ctypedef struct dpiObjectAttr: pass ctypedef struct dpiObjectType: pass ctypedef struct dpiPool: pass ctypedef struct dpiQueue: pass ctypedef struct dpiRowid: pass ctypedef struct dpiSodaColl: pass ctypedef struct dpiSodaDb: pass ctypedef struct dpiSodaDoc: pass ctypedef struct dpiSodaDocCursor: pass ctypedef struct dpiStmt: pass ctypedef struct dpiSubscr: pass ctypedef struct dpiVar: pass # function pointer types ctypedef void (*dpiSubscrCallback)(void* context, dpiSubscrMessage *message) ctypedef int (*dpiAccessTokenCallback)(void *context, dpiAccessToken *accessToken) # complex native data types ctypedef struct dpiBytes: char *ptr uint32_t length const char *encoding ctypedef struct dpiIntervalDS: int32_t days int32_t hours int32_t minutes int32_t seconds int32_t fseconds ctypedef struct dpiIntervalYM: int32_t years int32_t months ctypedef struct dpiJsonNode: uint32_t oracleTypeNum uint32_t nativeTypeNum dpiDataBuffer *value ctypedef struct dpiJsonArray: uint32_t numElements dpiJsonNode *elements dpiDataBuffer *elementValues ctypedef struct dpiJsonObject: uint32_t numFields char **fieldNames uint32_t *fieldNameLengths dpiJsonNode *fields dpiDataBuffer *fieldValues ctypedef struct dpiTimestamp: int16_t year uint8_t month uint8_t day uint8_t hour uint8_t minute uint8_t second uint32_t fsecond int8_t tzHourOffset int8_t tzMinuteOffset # public structures ctypedef struct dpiAppContext: const char *namespaceName uint32_t namespaceNameLength const char *name uint32_t nameLength const char *value uint32_t valueLength ctypedef struct dpiCommonCreateParams: uint32_t createMode const char *encoding const char *nencoding const char *edition uint32_t editionLength const char *driverName uint32_t driverNameLength bint sodaMetadataCache uint32_t stmtCacheSize dpiAccessToken *accessToken ctypedef struct dpiConnCreateParams: uint32_t authMode const char *connectionClass uint32_t connectionClassLength uint32_t purity const char *newPassword uint32_t newPasswordLength dpiAppContext *appContext uint32_t numAppContext bint externalAuth void *externalHandle dpiPool *pool const char *tag uint32_t tagLength bint matchAnyTag const char *outTag uint32_t outTagLength bint outTagFound dpiShardingKeyColumn *shardingKeyColumns uint8_t numShardingKeyColumns dpiShardingKeyColumn *superShardingKeyColumns uint8_t numSuperShardingKeyColumns bint outNewSession ctypedef struct dpiContextCreateParams: const char *defaultDriverName const char *defaultEncoding const char *loadErrorUrl const char *oracleClientLibDir const char *oracleClientConfigDir ctypedef union dpiDataBuffer: bint asBoolean uint8_t asUint8 uint16_t asUint16 uint32_t asUint32 int64_t asInt64 uint64_t asUint64 float asFloat double asDouble char *asString void *asRaw dpiBytes asBytes dpiTimestamp asTimestamp dpiIntervalDS asIntervalDS dpiIntervalYM asIntervalYM dpiJson *asJson dpiJsonObject asJsonObject dpiJsonArray asJsonArray dpiLob *asLOB dpiObject *asObject dpiStmt *asStmt dpiRowid *asRowid ctypedef struct dpiData: bint isNull dpiDataBuffer value ctypedef struct dpiDataTypeInfo: uint32_t oracleTypeNum uint32_t defaultNativeTypeNum uint16_t ociTypeCode uint32_t dbSizeInBytes uint32_t clientSizeInBytes uint32_t sizeInChars int16_t precision int8_t scale uint8_t fsPrecision dpiObjectType *objectType ctypedef struct dpiAccessToken: const char *token uint32_t tokenLength const char *privateKey uint32_t privateKeyLength ctypedef struct dpiErrorInfo: int32_t code uint16_t offset16 const char *message uint32_t messageLength const char *encoding const char *fnName const char *action const char *sqlState bint isRecoverable bint isWarning uint32_t offset ctypedef struct dpiObjectAttrInfo: const char *name uint32_t nameLength dpiDataTypeInfo typeInfo ctypedef struct dpiObjectTypeInfo: const char *schema uint32_t schemaLength const char *name uint32_t nameLength bint isCollection dpiDataTypeInfo elementTypeInfo uint16_t numAttributes const char *packageName uint32_t packageNameLength ctypedef struct dpiPoolCreateParams: uint32_t minSessions uint32_t maxSessions uint32_t sessionIncrement int pingInterval int pingTimeout bint homogeneous bint externalAuth uint32_t getMode const char *outPoolName uint32_t outPoolNameLength uint32_t timeout uint32_t waitTimeout uint32_t maxLifetimeSession const char *plsqlFixupCallback uint32_t plsqlFixupCallbackLength uint32_t maxSessionsPerShard dpiAccessTokenCallback accessTokenCallback void *accessTokenCallbackContext ctypedef struct dpiQueryInfo: const char *name uint32_t nameLength dpiDataTypeInfo typeInfo bint nullOk ctypedef struct dpiShardingKeyColumn: uint32_t oracleTypeNum uint32_t nativeTypeNum dpiDataBuffer value ctypedef struct dpiSodaCollNames: uint32_t numNames const char **names uint32_t *nameLengths ctypedef struct dpiSodaOperOptions: uint32_t numKeys const char **keys uint32_t *keyLengths const char *key uint32_t keyLength const char *version uint32_t versionLength const char *filter uint32_t filterLength const char *hint; uint32_t hintLength; uint32_t skip uint32_t limit uint32_t fetchArraySize ctypedef struct dpiStmtInfo: bint isQuery bint isPLSQL bint isDDL bint isDML uint16_t statementType bint isReturning ctypedef struct dpiSubscrCreateParams: uint32_t subscrNamespace uint32_t protocol uint32_t qos uint32_t operations uint32_t portNumber uint32_t timeout const char *name uint32_t nameLength dpiSubscrCallback callback void *callbackContext const char *recipientName uint32_t recipientNameLength const char *ipAddress uint32_t ipAddressLength uint8_t groupingClass uint32_t groupingValue uint8_t groupingType uint64_t outRegId bint clientInitiated ctypedef struct dpiSubscrMessage: uint32_t eventType const char *dbName uint32_t dbNameLength dpiSubscrMessageTable *tables uint32_t numTables dpiSubscrMessageQuery *queries uint32_t numQueries dpiErrorInfo *errorInfo const void *txId uint32_t txIdLength bint registered const char *queueName uint32_t queueNameLength const char *consumerName uint32_t consumerNameLength const void *aqMsgId uint32_t aqMsgIdLength ctypedef struct dpiSubscrMessageQuery: uint64_t id uint32_t operation dpiSubscrMessageTable *tables uint32_t numTables ctypedef struct dpiSubscrMessageRow: uint32_t operation const char *rowid uint32_t rowidLength ctypedef struct dpiSubscrMessageTable: uint32_t operation const char *name uint32_t nameLength dpiSubscrMessageRow *rows uint32_t numRows ctypedef struct dpiVersionInfo: int versionNum int releaseNum int updateNum int portReleaseNum int portUpdateNum uint32_t fullVersionNum ctypedef struct dpiXid: long formatId const char *globalTransactionId uint32_t globalTransactionIdLength const char *branchQualifier uint32_t branchQualifierLength ctypedef struct dpiMsgRecipient: const char *name uint32_t nameLength # functions int dpiConn_breakExecution(dpiConn *conn) nogil int dpiConn_changePassword(dpiConn *conn, const char *userName, uint32_t userNameLength, const char *oldPassword, uint32_t oldPasswordLength, const char *newPassword, uint32_t newPasswordLength) nogil int dpiConn_close(dpiConn *conn, uint32_t mode, const char *tag, uint32_t tagLength) nogil int dpiConn_commit(dpiConn *conn) nogil int dpiConn_create(const dpiContext *context, const char *userName, uint32_t userNameLength, const char *password, uint32_t passwordLength, const char *connectString, uint32_t connectStringLength, const dpiCommonCreateParams *commonParams, dpiConnCreateParams *createParams, dpiConn **conn) nogil int dpiConn_getCurrentSchema(dpiConn *conn, const char **value, uint32_t *valueLength) nogil int dpiConn_getEdition(dpiConn *conn, const char **value, uint32_t *valueLength) nogil int dpiConn_getExternalName(dpiConn *conn, const char **value, uint32_t *valueLength) nogil int dpiConn_getHandle(dpiConn *conn, void **handle) nogil int dpiConn_getInternalName(dpiConn *conn, const char **value, uint32_t *valueLength) nogil int dpiConn_getIsHealthy(dpiConn *conn, bint *isHealthy) nogil int dpiConn_getLTXID(dpiConn *conn, const char **value, uint32_t *valueLength) nogil int dpiConn_getOciAttr(dpiConn *conn, uint32_t handleType, uint32_t attribute, dpiDataBuffer *value, uint32_t *valueLength) nogil int dpiConn_getObjectType(dpiConn *conn, const char *name, uint32_t nameLength, dpiObjectType **objType) nogil int dpiConn_getServerVersion(dpiConn *conn, const char **releaseString, uint32_t *releaseStringLength, dpiVersionInfo *versionInfo) nogil int dpiConn_getSodaDb(dpiConn *conn, dpiSodaDb **db) nogil int dpiConn_getStmtCacheSize(dpiConn *conn, uint32_t *cacheSize) nogil int dpiConn_getCallTimeout(dpiConn *conn, uint32_t *value) nogil int dpiConn_newMsgProps(dpiConn *conn, dpiMsgProps **props) nogil int dpiConn_newQueue(dpiConn *conn, const char *name, uint32_t nameLength, dpiObjectType *payloadType, dpiQueue **queue) nogil int dpiConn_newJson(dpiConn *conn, dpiJson **json) nogil int dpiConn_newJsonQueue(dpiConn *conn, const char *name, uint32_t nameLength, dpiQueue **queue) nogil int dpiConn_newTempLob(dpiConn *conn, uint32_t lobType, dpiLob **lob) nogil int dpiConn_newVar(dpiConn *conn, uint32_t oracleTypeNum, uint32_t nativeTypeNum, uint32_t maxArraySize, uint32_t size, int sizeIsBytes, bint isArray, dpiObjectType *objType, dpiVar **var, dpiData **data) nogil int dpiConn_ping(dpiConn *conn) nogil int dpiConn_prepareStmt(dpiConn *conn, bint scrollable, const char *sql, uint32_t sqlLength, const char *tag, uint32_t tagLength, dpiStmt **stmt) nogil int dpiConn_release(dpiConn *conn) nogil int dpiConn_rollback(dpiConn *conn) nogil int dpiConn_setAction(dpiConn *conn, const char *value, uint32_t valueLength) nogil int dpiConn_setCallTimeout(dpiConn *conn, uint32_t value) nogil int dpiConn_setClientIdentifier(dpiConn *conn, const char *value, uint32_t valueLength) nogil int dpiConn_setClientInfo(dpiConn *conn, const char *value, uint32_t valueLength) nogil int dpiConn_setCurrentSchema(dpiConn *conn, const char *value, uint32_t valueLength) nogil int dpiConn_setDbOp(dpiConn *conn, const char *value, uint32_t valueLength) nogil int dpiConn_setEcontextId(dpiConn *conn, const char *value, uint32_t valueLength) nogil int dpiConn_setExternalName(dpiConn *conn, const char *value, uint32_t valueLength) nogil int dpiConn_setInternalName(dpiConn *conn, const char *value, uint32_t valueLength) nogil int dpiConn_setModule(dpiConn *conn, const char *value, uint32_t valueLength) nogil int dpiConn_setOciAttr(dpiConn *conn, uint32_t handleType, uint32_t attribute, void *value, uint32_t valueLength) nogil int dpiConn_setStmtCacheSize(dpiConn *conn, uint32_t cacheSize) nogil int dpiConn_shutdownDatabase(dpiConn *conn, uint32_t mode) nogil int dpiConn_startupDatabaseWithPfile(dpiConn *conn, const char *pfile, uint32_t pfileLength, uint32_t mode) nogil int dpiConn_subscribe(dpiConn *conn, dpiSubscrCreateParams *params, dpiSubscr **subscr) nogil int dpiConn_tpcBegin(dpiConn *conn, dpiXid *xid, uint32_t transactionTimeout, uint32_t flags) nogil int dpiConn_tpcCommit(dpiConn *conn, dpiXid *xid, bint onePhase) nogil int dpiConn_tpcEnd(dpiConn *conn, dpiXid *xid, uint32_t flags) nogil int dpiConn_tpcForget(dpiConn *conn, dpiXid *xid) nogil int dpiConn_tpcPrepare(dpiConn *conn, dpiXid *xid, bint *commitNeeded) nogil int dpiConn_tpcRollback(dpiConn *conn, dpiXid *xid) nogil int dpiConn_unsubscribe(dpiConn *conn, dpiSubscr *subscr) nogil int dpiContext_createWithParams(unsigned int majorVersion, unsigned int minorVersion, dpiContextCreateParams *params, dpiContext **context, dpiErrorInfo *errorInfo) nogil int dpiContext_getClientVersion(const dpiContext *context, dpiVersionInfo *versionInfo) nogil void dpiContext_getError(const dpiContext *context, dpiErrorInfo *errorInfo) nogil int dpiContext_initCommonCreateParams(const dpiContext *context, dpiCommonCreateParams *params) nogil int dpiContext_initConnCreateParams(const dpiContext *context, dpiConnCreateParams *params) nogil int dpiContext_initPoolCreateParams(const dpiContext *context, dpiPoolCreateParams *params) nogil int dpiContext_initSodaOperOptions(const dpiContext *context, dpiSodaOperOptions *options) nogil int dpiContext_initSubscrCreateParams(const dpiContext *context, dpiSubscrCreateParams *params) nogil int dpiDeqOptions_getCondition(dpiDeqOptions *options, const char **value, uint32_t *valueLength) nogil int dpiDeqOptions_getConsumerName(dpiDeqOptions *options, const char **value, uint32_t *valueLength) nogil int dpiDeqOptions_getCorrelation(dpiDeqOptions *options, const char **value, uint32_t *valueLength) nogil int dpiDeqOptions_getMode(dpiDeqOptions *options, uint32_t *value) nogil int dpiDeqOptions_getMsgId(dpiDeqOptions *options, const char **value, uint32_t *valueLength) nogil int dpiDeqOptions_getNavigation(dpiDeqOptions *options, uint32_t *value) nogil int dpiDeqOptions_getTransformation(dpiDeqOptions *options, const char **value, uint32_t *valueLength) nogil int dpiDeqOptions_getVisibility(dpiDeqOptions *options, uint32_t *value) nogil int dpiDeqOptions_getWait(dpiDeqOptions *options, uint32_t *value) nogil int dpiDeqOptions_release(dpiDeqOptions *options) nogil int dpiDeqOptions_setCondition(dpiDeqOptions *options, const char *value, uint32_t valueLength) nogil int dpiDeqOptions_setConsumerName(dpiDeqOptions *options, const char *value, uint32_t valueLength) nogil int dpiDeqOptions_setCorrelation(dpiDeqOptions *options, const char *value, uint32_t valueLength) nogil int dpiDeqOptions_setDeliveryMode(dpiDeqOptions *options, uint16_t value) nogil int dpiDeqOptions_setMode(dpiDeqOptions *options, uint32_t value) nogil int dpiDeqOptions_setMsgId(dpiDeqOptions *options, const char *value, uint32_t valueLength) nogil int dpiDeqOptions_setNavigation(dpiDeqOptions *options, uint32_t value) nogil int dpiDeqOptions_setTransformation(dpiDeqOptions *options, const char *value, uint32_t valueLength) nogil int dpiDeqOptions_setVisibility(dpiDeqOptions *options, uint32_t value) nogil int dpiDeqOptions_setWait(dpiDeqOptions *options, uint32_t value) nogil int dpiEnqOptions_getTransformation(dpiEnqOptions *options, const char **value, uint32_t *valueLength) nogil int dpiEnqOptions_getVisibility(dpiEnqOptions *options, uint32_t *value) nogil int dpiEnqOptions_release(dpiEnqOptions *options) nogil int dpiEnqOptions_setDeliveryMode(dpiEnqOptions *options, uint16_t value) nogil int dpiEnqOptions_setTransformation(dpiEnqOptions *options, const char *value, uint32_t valueLength) nogil int dpiEnqOptions_setVisibility(dpiEnqOptions *options, uint32_t value) nogil int dpiJson_getValue(dpiJson *json, uint32_t options, dpiJsonNode **topNode) nogil int dpiJson_setValue(dpiJson *json, dpiJsonNode *topNode) int dpiLob_addRef(dpiLob *lob) nogil int dpiLob_closeResource(dpiLob *lob) nogil int dpiLob_getBufferSize(dpiLob *lob, uint64_t sizeInChars, uint64_t *sizeInBytes) nogil int dpiLob_getChunkSize(dpiLob *lob, uint32_t *size) nogil int dpiLob_getDirectoryAndFileName(dpiLob *lob, const char **directoryAlias, uint32_t *directoryAliasLength, const char **fileName, uint32_t *fileNameLength) nogil int dpiLob_getFileExists(dpiLob *lob, bint *exists) nogil int dpiLob_getIsResourceOpen(dpiLob *lob, bint *isOpen) nogil int dpiLob_getSize(dpiLob *lob, uint64_t *size) nogil int dpiLob_openResource(dpiLob *lob) nogil int dpiLob_readBytes(dpiLob *lob, uint64_t offset, uint64_t amount, char *value, uint64_t *valueLength) nogil int dpiLob_release(dpiLob *lob) nogil int dpiLob_setDirectoryAndFileName(dpiLob *lob, const char *directoryAlias, uint32_t directoryAliasLength, const char *fileName, uint32_t fileNameLength) nogil int dpiLob_setFromBytes(dpiLob *lob, const char *value, uint64_t valueLength) nogil int dpiLob_trim(dpiLob *lob, uint64_t newSize) nogil int dpiLob_writeBytes(dpiLob *lob, uint64_t offset, const char *value, uint64_t valueLength) nogil int dpiMsgProps_getNumAttempts(dpiMsgProps *props, int32_t *value) nogil int dpiMsgProps_getCorrelation(dpiMsgProps *props, const char **value, uint32_t *valueLength) nogil int dpiMsgProps_getDelay(dpiMsgProps *props, int32_t *value) nogil int dpiMsgProps_getDeliveryMode(dpiMsgProps *props, uint16_t *value) nogil int dpiMsgProps_getEnqTime(dpiMsgProps *props, dpiTimestamp *value) nogil int dpiMsgProps_getExceptionQ(dpiMsgProps *props, const char **value, uint32_t *valueLength) nogil int dpiMsgProps_getExpiration(dpiMsgProps *props, int32_t *value) nogil int dpiMsgProps_getMsgId(dpiMsgProps *props, const char **value, uint32_t *valueLength) nogil int dpiMsgProps_getOriginalMsgId(dpiMsgProps *props, const char **value, uint32_t *valueLength) nogil int dpiMsgProps_getPayload(dpiMsgProps *props, dpiObject **obj, const char **value, uint32_t *valueLength) nogil int dpiMsgProps_getPayloadJson(dpiMsgProps *props, dpiJson **json) nogil int dpiMsgProps_getPriority(dpiMsgProps *props, int32_t *value) nogil int dpiMsgProps_getState(dpiMsgProps *props, uint32_t *value) nogil int dpiMsgProps_release(dpiMsgProps *props) nogil int dpiMsgProps_setCorrelation(dpiMsgProps *props, const char *value, uint32_t valueLength) nogil int dpiMsgProps_setDelay(dpiMsgProps *props, int32_t value) nogil int dpiMsgProps_setExceptionQ(dpiMsgProps *props, const char *value, uint32_t valueLength) nogil int dpiMsgProps_setExpiration(dpiMsgProps *props, int32_t value) nogil int dpiMsgProps_setOriginalMsgId(dpiMsgProps *props, const char *value, uint32_t valueLength) nogil int dpiMsgProps_setPayloadBytes(dpiMsgProps *props, const char *value, uint32_t valueLength) nogil int dpiMsgProps_setPayloadObject(dpiMsgProps *props, dpiObject *obj) nogil int dpiMsgProps_setPayloadJson(dpiMsgProps *props, dpiJson *json) nogil int dpiMsgProps_setPriority(dpiMsgProps *props, int32_t value) nogil int dpiMsgProps_setRecipients(dpiMsgProps *props, dpiMsgRecipient *recipients, uint32_t numRecipients) nogil int dpiObject_addRef(dpiObject *obj) nogil int dpiObject_appendElement(dpiObject *obj, uint32_t nativeTypeNum, dpiData *value) nogil int dpiObject_copy(dpiObject *obj, dpiObject **copiedObj) nogil int dpiObject_deleteElementByIndex(dpiObject *obj, int32_t index) nogil int dpiObject_getAttributeValue(dpiObject *obj, dpiObjectAttr *attr, uint32_t nativeTypeNum, dpiData *value) nogil int dpiObject_getElementExistsByIndex(dpiObject *obj, int32_t index, bint *exists) nogil int dpiObject_getElementValueByIndex(dpiObject *obj, int32_t index, uint32_t nativeTypeNum, dpiData *value) nogil int dpiObject_getFirstIndex(dpiObject *obj, int32_t *index, bint *exists) nogil int dpiObject_getLastIndex(dpiObject *obj, int32_t *index, bint *exists) nogil int dpiObject_getNextIndex(dpiObject *obj, int32_t index, int32_t *nextIndex, bint *exists) nogil int dpiObject_getPrevIndex(dpiObject *obj, int32_t index, int32_t *prevIndex, bint *exists) nogil int dpiObject_getSize(dpiObject *obj, int32_t *size) nogil int dpiObject_release(dpiObject *obj) nogil int dpiObject_setAttributeValue(dpiObject *obj, dpiObjectAttr *attr, uint32_t nativeTypeNum, dpiData *value) nogil int dpiObject_setElementValueByIndex(dpiObject *obj, int32_t index, uint32_t nativeTypeNum, dpiData *value) nogil int dpiObject_trim(dpiObject *obj, uint32_t numToTrim) nogil int dpiObjectAttr_getInfo(dpiObjectAttr *attr, dpiObjectAttrInfo *info) nogil int dpiObjectAttr_release(dpiObjectAttr *attr) nogil int dpiObjectType_addRef(dpiObjectType *objType) nogil int dpiObjectType_createObject(dpiObjectType *objType, dpiObject **obj) nogil int dpiObjectType_getAttributes(dpiObjectType *objType, uint16_t numAttributes, dpiObjectAttr **attributes) nogil int dpiObjectType_getInfo(dpiObjectType *objType, dpiObjectTypeInfo *info) nogil int dpiObjectType_release(dpiObjectType *objType) nogil int dpiPool_close(dpiPool *pool, uint32_t closeMode) nogil int dpiPool_create(const dpiContext *context, const char *userName, uint32_t userNameLength, const char *password, uint32_t passwordLength, const char *connectString, uint32_t connectStringLength, const dpiCommonCreateParams *commonParams, dpiPoolCreateParams *createParams, dpiPool **pool) nogil int dpiPool_getBusyCount(dpiPool *pool, uint32_t *value) nogil int dpiPool_getGetMode(dpiPool *pool, uint8_t *value) nogil int dpiPool_getMaxLifetimeSession(dpiPool *pool, uint32_t *value) nogil int dpiPool_getMaxSessionsPerShard(dpiPool *pool, uint32_t *value) nogil int dpiPool_getOpenCount(dpiPool *pool, uint32_t *value) nogil int dpiPool_getPingInterval(dpiPool *pool, int *value) nogil int dpiPool_getSodaMetadataCache(dpiPool *pool, bint *enabled) nogil int dpiPool_getStmtCacheSize(dpiPool *pool, uint32_t *cacheSize) nogil int dpiPool_getTimeout(dpiPool *pool, uint32_t *value) nogil int dpiPool_getWaitTimeout(dpiPool *pool, uint32_t *value) nogil int dpiPool_release(dpiPool *pool) nogil int dpiPool_reconfigure(dpiPool *pool, uint32_t minSessions, uint32_t maxSessions, uint32_t sessionIncrement) nogil int dpiPool_setAccessToken(dpiPool *pool, dpiAccessToken *params) nogil int dpiPool_setGetMode(dpiPool *pool, uint8_t value) nogil int dpiPool_setMaxLifetimeSession(dpiPool *pool, uint32_t value) nogil int dpiPool_setMaxSessionsPerShard(dpiPool *pool, uint32_t value) nogil int dpiPool_setPingInterval(dpiPool *pool, int value) nogil int dpiPool_setSodaMetadataCache(dpiPool *pool, bint enabled) nogil int dpiPool_setStmtCacheSize(dpiPool *pool, uint32_t cacheSize) nogil int dpiPool_setTimeout(dpiPool *pool, uint32_t value) nogil int dpiPool_setWaitTimeout(dpiPool *pool, uint32_t value) nogil int dpiQueue_deqMany(dpiQueue *queue, uint32_t *numProps, dpiMsgProps **props) nogil int dpiQueue_deqOne(dpiQueue *queue, dpiMsgProps **props) nogil int dpiQueue_enqMany(dpiQueue *queue, uint32_t numProps, dpiMsgProps **props) nogil int dpiQueue_enqOne(dpiQueue *queue, dpiMsgProps *props) nogil int dpiQueue_getDeqOptions(dpiQueue *queue, dpiDeqOptions **options) nogil int dpiQueue_getEnqOptions(dpiQueue *queue, dpiEnqOptions **options) nogil int dpiQueue_release(dpiQueue *queue) nogil int dpiRowid_getStringValue(dpiRowid *rowid, const char **value, uint32_t *valueLength) int dpiSodaColl_createIndex(dpiSodaColl *coll, const char *indexSpec, uint32_t indexSpecLength, uint32_t flags) nogil int dpiSodaColl_drop(dpiSodaColl *coll, uint32_t flags, bint *isDropped) nogil int dpiSodaColl_dropIndex(dpiSodaColl *coll, const char *name, uint32_t nameLength, uint32_t flags, bint *isDropped) nogil int dpiSodaColl_find(dpiSodaColl *coll, const dpiSodaOperOptions *options, uint32_t flags, dpiSodaDocCursor **cursor) nogil int dpiSodaColl_findOne(dpiSodaColl *coll, const dpiSodaOperOptions *options, uint32_t flags, dpiSodaDoc **doc) nogil int dpiSodaColl_getDataGuide(dpiSodaColl *coll, uint32_t flags, dpiSodaDoc **doc) nogil int dpiSodaColl_getDocCount(dpiSodaColl *coll, const dpiSodaOperOptions *options, uint32_t flags, uint64_t *count) nogil int dpiSodaColl_getMetadata(dpiSodaColl *coll, const char **value, uint32_t *valueLength) nogil int dpiSodaColl_getName(dpiSodaColl *coll, const char **value, uint32_t *valueLength) nogil int dpiSodaColl_insertManyWithOptions(dpiSodaColl *coll, uint32_t numDocs, dpiSodaDoc **docs, dpiSodaOperOptions *options, uint32_t flags, dpiSodaDoc **insertedDocs) nogil int dpiSodaColl_insertOneWithOptions(dpiSodaColl *coll, dpiSodaDoc *doc, dpiSodaOperOptions *options, uint32_t flags, dpiSodaDoc **insertedDoc) nogil int dpiSodaColl_release(dpiSodaColl *coll) nogil int dpiSodaColl_remove(dpiSodaColl *coll, const dpiSodaOperOptions *options, uint32_t flags, uint64_t *count) nogil int dpiSodaColl_replaceOne(dpiSodaColl *coll, const dpiSodaOperOptions *options, dpiSodaDoc *doc, uint32_t flags, bint *replaced, dpiSodaDoc **replacedDoc) nogil int dpiSodaColl_saveWithOptions(dpiSodaColl *coll, dpiSodaDoc *doc, dpiSodaOperOptions *options, uint32_t flags, dpiSodaDoc **savedDoc) nogil int dpiSodaColl_truncate(dpiSodaColl *coll) nogil int dpiSodaDb_createCollection(dpiSodaDb *db, const char *name, uint32_t nameLength, const char *metadata, uint32_t metadataLength, uint32_t flags, dpiSodaColl **coll) nogil int dpiSodaDb_createDocument(dpiSodaDb *db, const char *key, uint32_t keyLength, const char *content, uint32_t contentLength, const char *mediaType, uint32_t mediaTypeLength, uint32_t flags, dpiSodaDoc **doc) nogil int dpiSodaDb_freeCollectionNames(dpiSodaDb *db, dpiSodaCollNames *names) nogil int dpiSodaDb_getCollectionNames(dpiSodaDb *db, const char *startName, uint32_t startNameLength, uint32_t limit, uint32_t flags, dpiSodaCollNames *names) nogil int dpiSodaDb_openCollection(dpiSodaDb *db, const char *name, uint32_t nameLength, uint32_t flags, dpiSodaColl **coll) nogil int dpiSodaDb_release(dpiSodaDb *db) nogil int dpiSodaDoc_getContent(dpiSodaDoc *doc, const char **value, uint32_t *valueLength, const char **encoding) nogil int dpiSodaDoc_getCreatedOn(dpiSodaDoc *doc, const char **value, uint32_t *valueLength) nogil int dpiSodaDoc_getKey(dpiSodaDoc *doc, const char **value, uint32_t *valueLength) nogil int dpiSodaDoc_getLastModified(dpiSodaDoc *doc, const char **value, uint32_t *valueLength) nogil int dpiSodaDoc_getMediaType(dpiSodaDoc *doc, const char **value, uint32_t *valueLength) nogil int dpiSodaDoc_getVersion(dpiSodaDoc *doc, const char **value, uint32_t *valueLength) nogil int dpiSodaDoc_release(dpiSodaDoc *doc) nogil int dpiSodaDocCursor_close(dpiSodaDocCursor *cursor) nogil int dpiSodaDocCursor_getNext(dpiSodaDocCursor *cursor, uint32_t flags, dpiSodaDoc **doc) nogil int dpiSodaDocCursor_release(dpiSodaDocCursor *cursor) nogil int dpiStmt_addRef(dpiStmt *stmt) nogil int dpiStmt_bindByName(dpiStmt *stmt, const char *name, uint32_t nameLength, dpiVar *var) nogil int dpiStmt_bindByPos(dpiStmt *stmt, uint32_t pos, dpiVar *var) nogil int dpiStmt_close(dpiStmt *stmt, const char *tag, uint32_t tagLength) int dpiStmt_define(dpiStmt *stmt, uint32_t pos, dpiVar *var) nogil int dpiStmt_deleteFromCache(dpiStmt *stmt) nogil int dpiStmt_execute(dpiStmt *stmt, uint32_t mode, uint32_t *numQueryColumns) nogil int dpiStmt_executeMany(dpiStmt *stmt, uint32_t mode, uint32_t numIters) nogil int dpiStmt_fetchRows(dpiStmt *stmt, uint32_t maxRows, uint32_t *bufferRowIndex, uint32_t *numRowsFetched, bint *moreRows) nogil int dpiStmt_getBatchErrorCount(dpiStmt *stmt, uint32_t *count) nogil int dpiStmt_getBatchErrors(dpiStmt *stmt, uint32_t numErrors, dpiErrorInfo *errors) nogil int dpiStmt_getBindCount(dpiStmt *stmt, uint32_t *count) nogil int dpiStmt_getBindNames(dpiStmt *stmt, uint32_t *numBindNames, const char **bindNames, uint32_t *bindNameLengths) nogil int dpiStmt_getImplicitResult(dpiStmt *stmt, dpiStmt **implicitResult) nogil int dpiStmt_getNumQueryColumns(dpiStmt *stmt, uint32_t *numQueryColumns) nogil int dpiStmt_getInfo(dpiStmt *stmt, dpiStmtInfo *info) nogil int dpiStmt_getLastRowid(dpiStmt *stmt, dpiRowid **rowid) nogil int dpiStmt_getOciAttr(dpiStmt *stmt, uint32_t attribute, dpiDataBuffer *value, uint32_t *valueLength) nogil int dpiStmt_getQueryInfo(dpiStmt *stmt, uint32_t pos, dpiQueryInfo *info) nogil int dpiStmt_getRowCount(dpiStmt *stmt, uint64_t *count) nogil int dpiStmt_getRowCounts(dpiStmt *stmt, uint32_t *numRowCounts, uint64_t **rowCounts) nogil int dpiStmt_getSubscrQueryId(dpiStmt *stmt, uint64_t *queryId) nogil int dpiStmt_release(dpiStmt *stmt) nogil int dpiStmt_scroll(dpiStmt *stmt, uint32_t mode, int32_t offset, int32_t rowCountOffset) nogil int dpiStmt_setFetchArraySize(dpiStmt *stmt, uint32_t arraySize) nogil int dpiStmt_setOciAttr(dpiStmt *stmt, uint32_t attribute, void *value, uint32_t valueLength) nogil int dpiStmt_setPrefetchRows(dpiStmt *stmt, uint32_t numRows) nogil int dpiSubscr_prepareStmt(dpiSubscr *subscr, const char *sql, uint32_t sqlLength, dpiStmt **stmt) nogil int dpiSubscr_release(dpiSubscr *subscr) nogil int dpiVar_getNumElementsInArray(dpiVar *var, uint32_t *numElements) nogil int dpiVar_getReturnedData(dpiVar *var, uint32_t pos, uint32_t *numElements, dpiData **data) nogil int dpiVar_getSizeInBytes(dpiVar *var, uint32_t *sizeInBytes) nogil int dpiVar_release(dpiVar *var) nogil int dpiVar_setNumElementsInArray(dpiVar *var, uint32_t numElements) nogil int dpiVar_setFromBytes(dpiVar *var, uint32_t pos, const char *value, uint32_t valueLength) nogil int dpiVar_setFromLob(dpiVar *var, uint32_t pos, dpiLob *lob) nogil int dpiVar_setFromObject(dpiVar *var, uint32_t pos, dpiObject *obj) nogil int dpiVar_setFromStmt(dpiVar *var, uint32_t pos, dpiStmt *stmt) nogil python-oracledb-1.2.1/src/oracledb/impl/thick/pool.pyx000066400000000000000000000325761434177474600227630ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # pool.pyx # # Cython file defining the thick Pool implementation class (embedded in # thick_impl.pyx). #------------------------------------------------------------------------------ cdef int _token_callback_handler(void *context, dpiAccessToken *refresh_token) with gil: cdef: ThickPoolImpl pool_impl = context pool_impl._token_handler(refresh_token, pool_impl.connect_params) cdef class ThickPoolImpl(BasePoolImpl): cdef: dpiPool *_handle def __init__(self, str dsn, PoolParamsImpl params): cdef: bytes edition_bytes, user_bytes, password_bytes, dsn_bytes uint32_t password_len = 0, user_len = 0, dsn_len = 0 bytes session_callback_bytes, name_bytes dpiCommonCreateParams common_params dpiPoolCreateParams create_params const char *password_ptr = NULL const char *user_ptr = NULL const char *dsn_ptr = NULL bytes token_bytes, private_key_bytes uint32_t token_len = 0, private_key_len = 0 const char *token_ptr = NULL const char *private_key_ptr = NULL dpiAccessToken access_token str token, private_key int status # save parameters self.connect_params = params self.username = params.user self.dsn = dsn self.min = params.min self.max = params.max self.increment = params.increment self.homogeneous = params.homogeneous # set up token parameters if provided if params._token is not None \ or params.access_token_callback is not None: token = params._get_token() token_bytes = token.encode() token_ptr = token_bytes token_len = len(token_bytes) private_key = params._get_private_key() if private_key is not None: private_key_bytes = private_key.encode() private_key_ptr = private_key_bytes private_key_len = len(private_key_bytes) # set up common creation parameters if dpiContext_initCommonCreateParams(driver_context, &common_params) < 0: _raise_from_odpi() common_params.createMode |= DPI_MODE_CREATE_THREADED if params.events: common_params.createMode |= DPI_MODE_CREATE_EVENTS if params.edition is not None: edition_bytes = params.edition.encode() common_params.edition = edition_bytes common_params.editionLength = len(edition_bytes) if params._token is not None: access_token.token = token_ptr access_token.tokenLength = token_len access_token.privateKey = private_key_ptr access_token.privateKeyLength = private_key_len common_params.accessToken = &access_token # set up pool creation parameters if dpiContext_initPoolCreateParams(driver_context, &create_params) < 0: _raise_from_odpi() create_params.minSessions = self.min create_params.maxSessions = self.max create_params.sessionIncrement = self.increment create_params.homogeneous = self.homogeneous create_params.getMode = params.getmode if params.session_callback is not None \ and not callable(params.session_callback): session_callback_bytes = params.session_callback.encode() create_params.plsqlFixupCallback = session_callback_bytes create_params.plsqlFixupCallbackLength = \ len(session_callback_bytes) if params.access_token_callback is not None: create_params.accessTokenCallback = _token_callback_handler create_params.accessTokenCallbackContext = self create_params.timeout = params.timeout create_params.waitTimeout = params.wait_timeout create_params.maxSessionsPerShard = params.max_sessions_per_shard create_params.maxLifetimeSession = params.max_lifetime_session create_params.pingInterval = params.ping_interval common_params.stmtCacheSize = params.stmtcachesize common_params.sodaMetadataCache = params.soda_metadata_cache create_params.externalAuth = params.externalauth # prepare user, password and DSN for use if self.username is not None: user_bytes = params.get_full_user().encode() user_ptr = user_bytes user_len = len(user_bytes) password_bytes = params._get_password() if password_bytes is not None: password_ptr = password_bytes password_len = len(password_bytes) if self.dsn is not None: dsn_bytes = self.dsn.encode() dsn_ptr = dsn_bytes dsn_len = len(dsn_bytes) # create pool with nogil: status = dpiPool_create(driver_context, user_ptr, user_len, password_ptr, password_len, dsn_ptr, dsn_len, &common_params, &create_params, &self._handle) if status < 0: _raise_from_odpi() name_bytes = create_params.outPoolName[:create_params.outPoolNameLength] self.name = name_bytes.decode() def __dealloc__(self): if self._handle != NULL: dpiPool_release(self._handle) cdef object _token_handler(self, dpiAccessToken *access_token, ConnectParamsImpl params): cdef: str token, private_key bytes token_bytes, private_key_bytes uint32_t token_len = 0, private_key_len = 0 const char *token_ptr = NULL const char *private_key_ptr = NULL token = params._get_token() token_bytes = token.encode() token_ptr = token_bytes token_len = len(token_bytes) private_key = params._get_private_key() if private_key is not None: private_key_bytes = private_key.encode() private_key_ptr = private_key_bytes private_key_len = len(private_key_bytes) access_token.token = token_ptr access_token.tokenLength = token_len access_token.privateKey = private_key_ptr access_token.privateKeyLength = private_key_len def close(self, bint force): """ Internal method for closing the pool. """ cdef: uint32_t close_mode int status close_mode = DPI_MODE_POOL_CLOSE_FORCE if force \ else DPI_MODE_POOL_CLOSE_DEFAULT with nogil: status = dpiPool_close(self._handle, close_mode); if status < 0: _raise_from_odpi() def drop(self, ThickConnImpl conn_impl): """ Internal method for dropping a connection from the pool. """ cdef int status with nogil: status = dpiConn_close(conn_impl._handle, DPI_MODE_CONN_CLOSE_DROP, NULL, 0) if status < 0: _raise_from_odpi() dpiConn_release(conn_impl._handle) conn_impl._handle = NULL def get_busy_count(self): """ Internal method for getting the number of busy connections in the pool. """ cdef uint32_t value if dpiPool_getBusyCount(self._handle, &value) < 0: _raise_from_odpi() return value def get_getmode(self): """ Internal method for getting the method by which connections are acquired from the pool. """ cdef uint8_t value if dpiPool_getGetMode(self._handle, &value) < 0: _raise_from_odpi() return value def get_max_lifetime_session(self): """ Internal method for getting the maximum lifetime of each session. """ cdef uint32_t value if dpiPool_getMaxLifetimeSession(self._handle, &value) < 0: _raise_from_odpi() return value def get_max_sessions_per_shard(self): """ Internal method for getting the maximum sessions per shard in the pool. """ cdef uint32_t value if dpiPool_getMaxSessionsPerShard(self._handle, &value) < 0: _raise_from_odpi() return value def get_open_count(self): """ Internal method for getting the number of connections in the pool. """ cdef uint32_t value if dpiPool_getOpenCount(self._handle, &value) < 0: _raise_from_odpi() return value def get_ping_interval(self): """ Internal method for getting the value of the pool-ping-interval. """ cdef int value if dpiPool_getPingInterval(self._handle, &value) < 0: _raise_from_odpi return value def get_soda_metadata_cache(self): """ Internal method for getting the value of soda metadata cache. """ cdef bint value if dpiPool_getSodaMetadataCache(self._handle, &value) < 0: _raise_from_odpi return value def get_stmt_cache_size(self): """ Internal method for getting the size of the statement cache. """ cdef uint32_t value if dpiPool_getStmtCacheSize(self._handle, &value) < 0: _raise_from_odpi() return value def get_timeout(self): """ Internal method for getting the timeout for idle sessions. """ cdef uint32_t value if dpiPool_getTimeout(self._handle, &value) < 0: _raise_from_odpi() return value def get_wait_timeout(self): """ Internal method for getting the wait timeout for acquiring sessions. """ cdef uint32_t value if dpiPool_getWaitTimeout(self._handle, &value) < 0: _raise_from_odpi() return value def reconfigure(self, uint32_t min, uint32_t max, uint32_t increment): """ Internal method for reconfiguring the size of the pool. """ if dpiPool_reconfigure(self._handle, min, max, increment) < 0: _raise_from_odpi() self.min = min self.max = max self.increment = increment def set_getmode(self, uint8_t value): """ Internal method for setting the method by which connections are acquired from the pool. """ if dpiPool_setGetMode(self._handle, value) < 0: _raise_from_odpi() def set_max_lifetime_session(self, uint32_t value): """ Internal method for setting the maximum lifetime of each session. """ if dpiPool_setMaxLifetimeSession(self._handle, value) < 0: _raise_from_odpi() def set_max_sessions_per_shard(self, uint32_t value): """ Internal method for setting the maximum sessions per shard in the pool. """ if dpiPool_setMaxSessionsPerShard(self._handle, value) < 0: _raise_from_odpi() def set_ping_interval(self, int value): """ Internal method for setting the value of the pool-ping-interval. """ if dpiPool_setPingInterval(self._handle, value) < 0: _raise_from_odpi def set_soda_metadata_cache(self, bint value): """ Internal method for enabling or disabling the soda metadata cache. """ if dpiPool_setSodaMetadataCache(self._handle, value) < 0: _raise_from_odpi def set_stmt_cache_size(self, uint32_t value): """ Internal method for setting the size of the statement cache. """ if dpiPool_setStmtCacheSize(self._handle, value) < 0: _raise_from_odpi() def set_timeout(self, uint32_t value): """ Internal method for setting the timeout for idle sessions. """ if dpiPool_setTimeout(self._handle, value) < 0: _raise_from_odpi() def set_wait_timeout(self, uint32_t value): """ Internal method for setting the wait timeout for acquiring sessions. """ if dpiPool_setWaitTimeout(self._handle, value) < 0: _raise_from_odpi() python-oracledb-1.2.1/src/oracledb/impl/thick/queue.pyx000066400000000000000000000516601434177474600231310ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # queue.pyx # # Cython file defining the thick Queue implementation class (embedded in # thick_impl.pyx). #------------------------------------------------------------------------------ cdef class ThickQueueImpl(BaseQueueImpl): cdef: dpiQueue* _handle ThickConnImpl _conn_impl def __dealloc__(self): if self._handle != NULL: dpiQueue_release(self._handle) def deq_many(self, uint32_t max_num_messages): """ Internal method for dequeuing multiple message from a queue. """ cdef: uint32_t num_props = max_num_messages, processed_props = 0, i dpiMsgProps** handles ThickMsgPropsImpl props ssize_t num_bytes list result int status result = [] num_bytes = num_props * sizeof(dpiMsgProps*) handles = cpython.PyMem_Malloc(num_bytes) try: with nogil: status = dpiQueue_deqMany(self._handle, &num_props, handles) if status < 0: _raise_from_odpi() for i in range(num_props): props = ThickMsgPropsImpl.__new__(ThickMsgPropsImpl) props._handle = handles[i] processed_props += 1 props._initialize(self) result.append(props) finally: for i in range(processed_props, num_props): dpiMsgProps_release(handles[i]) cpython.PyMem_Free(handles) return result def deq_one(self): """ Internal method for dequeuing a single message from a queue. """ cdef: ThickMsgPropsImpl props int status props = ThickMsgPropsImpl.__new__(ThickMsgPropsImpl) with nogil: status = dpiQueue_deqOne(self._handle, &props._handle) if status < 0: _raise_from_odpi() if props._handle != NULL: props._initialize(self) return props def enq_many(self, list props_impls): """ Internal method for enqueuing many messages into a queue. """ cdef: uint32_t i, num_props = 0 ThickMsgPropsImpl props dpiMsgProps **handles ssize_t num_bytes int status num_props = len(props_impls) num_bytes = num_props * sizeof(dpiMsgProps*) handles = cpython.PyMem_Malloc(num_bytes) for i, props in enumerate(props_impls): handles[i] = props._handle with nogil: status = dpiQueue_enqMany(self._handle, num_props, handles) cpython.PyMem_Free(handles) if status < 0: _raise_from_odpi() def enq_one(self, ThickMsgPropsImpl props_impl): """ Internal method for enqueuing a single message into a queue. """ cdef int status with nogil: status = dpiQueue_enqOne(self._handle, props_impl._handle) if status < 0: _raise_from_odpi() def initialize(self, ThickConnImpl conn_impl, str name, ThickDbObjectTypeImpl payload_type, bint is_json): """ Internal method for initializing the queue. """ cdef: ThickDeqOptionsImpl deq_options_impl ThickEnqOptionsImpl enq_options_impl dpiObjectType* type_handle = NULL StringBuffer buf = StringBuffer() self._conn_impl = conn_impl self.is_json = is_json buf.set_value(name) if is_json: if dpiConn_newJsonQueue(conn_impl._handle, buf.ptr, buf.length, &self._handle) < 0: _raise_from_odpi() else: if payload_type is not None: type_handle = payload_type._handle if dpiConn_newQueue(conn_impl._handle, buf.ptr, buf.length, type_handle, &self._handle) < 0: _raise_from_odpi() deq_options_impl = ThickDeqOptionsImpl.__new__(ThickDeqOptionsImpl) if dpiQueue_getDeqOptions(self._handle, &deq_options_impl._handle) < 0: _raise_from_odpi() self.deq_options_impl = deq_options_impl enq_options_impl = ThickEnqOptionsImpl.__new__(ThickEnqOptionsImpl) if dpiQueue_getEnqOptions(self._handle, &enq_options_impl._handle) < 0: _raise_from_odpi() self.enq_options_impl = enq_options_impl self.payload_type = payload_type self.name = name cdef class ThickDeqOptionsImpl(BaseDeqOptionsImpl): cdef: dpiDeqOptions* _handle def __dealloc__(self): if self._handle != NULL: dpiDeqOptions_release(self._handle) def get_condition(self): """ Internal method for getting the condition. """ cdef: uint32_t value_length const char *value if dpiDeqOptions_getCondition(self._handle, &value, &value_length) < 0: _raise_from_odpi() if value != NULL: return value[:value_length].decode() def get_consumer_name(self): """ Internal method for getting the consumer name. """ cdef: uint32_t value_length const char *value if dpiDeqOptions_getConsumerName(self._handle, &value, &value_length) < 0: _raise_from_odpi() if value != NULL: return value[:value_length].decode() def get_correlation(self): """ Internal method for getting the correlation. """ cdef: uint32_t value_length const char *value if dpiDeqOptions_getCorrelation(self._handle, &value, &value_length) < 0: _raise_from_odpi() if value != NULL: return value[:value_length].decode() def get_message_id(self): """ Internal method for getting the message id. """ cdef: uint32_t value_length const char *value if dpiDeqOptions_getMsgId(self._handle, &value, &value_length) < 0: _raise_from_odpi() if value != NULL: return value[:value_length] def get_mode(self): """ Internal method for getting the mode. """ cdef uint32_t value if dpiDeqOptions_getMode(self._handle, &value) < 0: _raise_from_odpi() return value def get_navigation(self): """ Internal method for getting the navigation. """ cdef uint32_t value if dpiDeqOptions_getNavigation(self._handle, &value) < 0: _raise_from_odpi() return value def get_transformation(self): """ Internal method for getting the transformation. """ cdef: uint32_t value_length const char *value if dpiDeqOptions_getTransformation(self._handle, &value, &value_length) < 0: _raise_from_odpi() if value != NULL: return value[:value_length].decode() def get_visibility(self): """ Internal method for getting the visibility. """ cdef uint32_t value if dpiDeqOptions_getVisibility(self._handle, &value) < 0: _raise_from_odpi() return value def get_wait(self): """ Internal method for getting the wait. """ cdef uint32_t value if dpiDeqOptions_getWait(self._handle, &value) < 0: _raise_from_odpi() return value def set_condition(self, str value): """ Internal method for setting the condition. """ cdef StringBuffer buf = StringBuffer() buf.set_value(value) if dpiDeqOptions_setCondition(self._handle, buf.ptr, buf.length) < 0: _raise_from_odpi() def set_consumer_name(self, str value): """ Internal method for setting the consumer name. """ cdef StringBuffer buf = StringBuffer() buf.set_value(value) if dpiDeqOptions_setConsumerName(self._handle, buf.ptr, buf.length) < 0: _raise_from_odpi() def set_correlation(self, str value): """ Internal method for setting the correlation. """ cdef StringBuffer buf = StringBuffer() buf.set_value(value) if dpiDeqOptions_setCorrelation(self._handle, buf.ptr, buf.length) < 0: _raise_from_odpi() def set_delivery_mode(self, uint16_t value): """ Internal method for setting the delivery mode. """ if dpiDeqOptions_setDeliveryMode(self._handle, value) < 0: _raise_from_odpi() def set_mode(self, uint32_t value): """ Internal method for setting the mode. """ if dpiDeqOptions_setMode(self._handle, value) < 0: _raise_from_odpi() def set_message_id(self, bytes value): """ Internal method for setting the message id. """ cdef StringBuffer buf = StringBuffer() buf.set_value(value) if dpiDeqOptions_setMsgId(self._handle, buf.ptr, buf.length) < 0: _raise_from_odpi() def set_navigation(self, uint32_t value): """ Internal method for setting the navigation. """ if dpiDeqOptions_setNavigation(self._handle, value) < 0: _raise_from_odpi() def set_transformation(self, str value): """ Internal method for setting the transformation. """ cdef StringBuffer buf = StringBuffer() buf.set_value(value) if dpiDeqOptions_setTransformation(self._handle, buf.ptr, buf.length) < 0: _raise_from_odpi() def set_visibility(self, uint32_t value): """ Internal method for setting the visibility. """ if dpiDeqOptions_setVisibility(self._handle, value) < 0: _raise_from_odpi() def set_wait(self, uint32_t value): """ Internal method for setting the wait. """ if dpiDeqOptions_setWait(self._handle, value) < 0: _raise_from_odpi() cdef class ThickEnqOptionsImpl(BaseEnqOptionsImpl): cdef: dpiEnqOptions* _handle def __dealloc__(self): if self._handle != NULL: dpiEnqOptions_release(self._handle) def get_transformation(self): """ Internal method for getting the transformation. """ cdef: uint32_t value_length const char *value if dpiEnqOptions_getTransformation(self._handle, &value, &value_length) < 0: _raise_from_odpi() if value != NULL: return value[:value_length].decode() def get_visibility(self): """ Internal method for getting the visibility. """ cdef uint32_t value if dpiEnqOptions_getVisibility(self._handle, &value) < 0: _raise_from_odpi() return value def set_delivery_mode(self, uint16_t value): """ Internal method for setting the delivery mode. """ if dpiEnqOptions_setDeliveryMode(self._handle, value) < 0: _raise_from_odpi() def set_transformation(self, str value): """ Internal method for setting the transformation. """ cdef StringBuffer buf = StringBuffer() buf.set_value(value) if dpiEnqOptions_setTransformation(self._handle, buf.ptr, buf.length) < 0: _raise_from_odpi() def set_visibility(self, uint32_t value): """ Internal method for setting the visibility. """ if dpiEnqOptions_setVisibility(self._handle, value) < 0: _raise_from_odpi() cdef class ThickMsgPropsImpl(BaseMsgPropsImpl): cdef: dpiMsgProps* _handle ThickConnImpl _conn_impl def __dealloc__(self): if self._handle != NULL: dpiMsgProps_release(self._handle) cdef int _initialize(self, ThickQueueImpl queue_impl) except -1: cdef: dpiObject *payload_obj_handle ThickDbObjectImpl obj_impl const char *payload_ptr uint32_t payload_len dpiJsonNode *node dpiJson *json self._conn_impl = queue_impl._conn_impl if queue_impl.is_json: if dpiMsgProps_getPayloadJson(self._handle, &json) < 0: _raise_from_odpi() if dpiJson_getValue(json, DPI_JSON_OPT_NUMBER_AS_STRING, &node) < 0: _raise_from_odpi() self.payload = _convert_from_json_node(node) else: if dpiMsgProps_getPayload(self._handle, &payload_obj_handle, &payload_ptr, &payload_len) < 0: _raise_from_odpi() if payload_obj_handle != NULL: obj_impl = ThickDbObjectImpl.__new__(ThickDbObjectImpl) obj_impl.type = queue_impl.payload_type if dpiObject_addRef(payload_obj_handle) < 0: _raise_from_odpi() obj_impl._handle = payload_obj_handle self.payload = PY_TYPE_DB_OBJECT._from_impl(obj_impl) else: self.payload = payload_ptr[:payload_len] def get_num_attempts(self): """ Internal method for getting the number of attempts made. """ cdef int32_t value if dpiMsgProps_getNumAttempts(self._handle, &value) < 0: _raise_from_odpi() return value def get_correlation(self): """ Internal method for getting the correlation. """ cdef: uint32_t value_length const char *value if dpiMsgProps_getCorrelation(self._handle, &value, &value_length) < 0: _raise_from_odpi() if value != NULL: return value[:value_length].decode() def get_delay(self): """ Internal method for getting the delay. """ cdef int32_t value if dpiMsgProps_getDelay(self._handle, &value) < 0: _raise_from_odpi() return value def get_delivery_mode(self): """ Internal method for getting the delivery mode. """ cdef uint16_t value if dpiMsgProps_getDeliveryMode(self._handle, &value) < 0: _raise_from_odpi() return value def get_enq_time(self): """ Internal method for getting the enqueue time. """ cdef dpiTimestamp timestamp if dpiMsgProps_getEnqTime(self._handle, ×tamp) < 0: _raise_from_odpi() return cydatetime.datetime_new(timestamp.year, timestamp.month, timestamp.day, timestamp.hour, timestamp.minute, timestamp.second, timestamp.fsecond // 1000, None) def get_exception_queue(self): """ Internal method for getting the exception queue. """ cdef: uint32_t value_length const char *value if dpiMsgProps_getExceptionQ(self._handle, &value, &value_length) < 0: _raise_from_odpi() if value != NULL: return value[:value_length].decode() def get_expiration(self): """ Internal method for getting the message expiration. """ cdef int32_t value if dpiMsgProps_getExpiration(self._handle, &value) < 0: _raise_from_odpi() return value def get_message_id(self): """ Internal method for getting the message id. """ cdef: uint32_t value_length const char *value if dpiMsgProps_getMsgId(self._handle, &value, &value_length) < 0: _raise_from_odpi() if value != NULL: return value[:value_length] def get_priority(self): """ Internal method for getting the priority. """ cdef int32_t value if dpiMsgProps_getPriority(self._handle, &value) < 0: _raise_from_odpi() return value def get_state(self): """ Internal method for getting the message state. """ cdef uint32_t value if dpiMsgProps_getState(self._handle, &value) < 0: _raise_from_odpi() return value def set_correlation(self, str value): """ Internal method for setting the correlation. """ cdef StringBuffer buf = StringBuffer() buf.set_value(value) if dpiMsgProps_setCorrelation(self._handle, buf.ptr, buf.length) < 0: _raise_from_odpi() def set_delay(self, int32_t value): """ Internal method for setting the correlation. """ if dpiMsgProps_setDelay(self._handle, value) < 0: _raise_from_odpi() def set_exception_queue(self, str value): """ Internal method for setting the exception queue. """ cdef StringBuffer buf = StringBuffer() buf.set_value(value) if dpiMsgProps_setExceptionQ(self._handle, buf.ptr, buf.length) < 0: _raise_from_odpi() def set_expiration(self, int32_t value): """ Internal method for setting the message expiration. """ if dpiMsgProps_setExpiration(self._handle, value) < 0: _raise_from_odpi() def set_payload_bytes(self, bytes value): """ Internal method for setting the payload from bytes. """ cdef StringBuffer buf = StringBuffer() buf.set_value(value) if dpiMsgProps_setPayloadBytes(self._handle, buf.ptr, buf.length) < 0: _raise_from_odpi() def set_payload_object(self, ThickDbObjectImpl obj_impl): """ Internal method for setting the payload from an object. """ if dpiMsgProps_setPayloadObject(self._handle, obj_impl._handle) < 0: _raise_from_odpi() def set_payload_json(self, object json_val): """ Internal method for setting the payload from a JSON object """ cdef: JsonBuffer json_buf = JsonBuffer() dpiJson *json json_buf.from_object(json_val) if dpiConn_newJson(self._conn_impl._handle, &json) < 0: _raise_from_odpi() if dpiJson_setValue(json, &json_buf._top_node) < 0: _raise_from_odpi() if dpiMsgProps_setPayloadJson(self._handle, json) < 0: _raise_from_odpi() def set_priority(self, int32_t value): """ Internal method for setting the priority. """ if dpiMsgProps_setPriority(self._handle, value) < 0: _raise_from_odpi() def set_recipients(self, list value): """ Internal method for setting the recipients list. """ cdef: dpiMsgRecipient *recipients uint32_t num_recipients ssize_t num_bytes list buffers = [] StringBuffer buf num_recipients = len(value) num_bytes = num_recipients * sizeof(dpiMsgRecipient) recipients = cpython.PyMem_Malloc(num_bytes) try: for i in range(num_recipients): buf = StringBuffer() buf.set_value(value[i]) buffers.append(buf) recipients[i].name = buf.ptr recipients[i].nameLength = buf.length if dpiMsgProps_setRecipients(self._handle, recipients, num_recipients) < 0: _raise_from_odpi() finally: cpython.PyMem_Free(recipients) python-oracledb-1.2.1/src/oracledb/impl/thick/soda.pyx000066400000000000000000000567511434177474600227410ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # soda.pyx # # Cython file defining the thick implementation SODA classes (embedded in # thick_impl.pyx). #------------------------------------------------------------------------------ cdef class ThickSodaDbImpl(BaseSodaDbImpl): cdef: dpiSodaDb* _handle def __dealloc__(self): if self._handle != NULL: dpiSodaDb_release(self._handle) cdef int _get_flags(self, uint32_t* flags) except -1: self._conn._verify_connected() if self._conn.autocommit: flags[0] = DPI_SODA_FLAGS_ATOMIC_COMMIT else: flags[0] = DPI_SODA_FLAGS_DEFAULT def create_collection(self, str name, str metadata, bint map_mode): """ Internal method for creating a collection. """ cdef: StringBuffer metadata_buf = StringBuffer() StringBuffer name_buf = StringBuffer() ThickSodaCollImpl coll_impl uint32_t flags int status name_buf.set_value(name) metadata_buf.set_value(metadata) self._get_flags(&flags) if map_mode: flags |= DPI_SODA_FLAGS_CREATE_COLL_MAP coll_impl = ThickSodaCollImpl.__new__(ThickSodaCollImpl) coll_impl._db_impl = self with nogil: status = dpiSodaDb_createCollection(self._handle, name_buf.ptr, name_buf.length, metadata_buf.ptr, metadata_buf.length, flags, &coll_impl._handle) if status < 0: _raise_from_odpi() coll_impl._get_name() return coll_impl def create_document(self, bytes content, str key, str media_type): """ Internal method for creating a document. """ cdef: StringBuffer media_type_buf = StringBuffer() StringBuffer content_buf = StringBuffer() StringBuffer key_buf = StringBuffer() ThickSodaDocImpl doc_impl content_buf.set_value(content) key_buf.set_value(key) media_type_buf.set_value(media_type) doc_impl = ThickSodaDocImpl.__new__(ThickSodaDocImpl) if dpiSodaDb_createDocument(self._handle, key_buf.ptr, key_buf.length, content_buf.ptr, content_buf.length, media_type_buf.ptr, media_type_buf.length, DPI_SODA_FLAGS_DEFAULT, &doc_impl._handle) < 0: _raise_from_odpi() return doc_impl def get_collection_names(self, str start_name, uint32_t limit): """ Internal method for getting the list of collection names. """ cdef: StringBuffer start_name_buf = StringBuffer() dpiSodaCollNames coll_names uint32_t flags, i list result int status str temp start_name_buf.set_value(start_name) self._get_flags(&flags) with nogil: status = dpiSodaDb_getCollectionNames(self._handle, start_name_buf.ptr, start_name_buf.length, limit, flags, &coll_names) if status < 0: _raise_from_odpi() try: result = cpython.PyList_New(coll_names.numNames) for i in range(coll_names.numNames): temp = coll_names.names[i][:coll_names.nameLengths[i]].decode() cpython.Py_INCREF(temp) cpython.PyList_SET_ITEM(result, i, temp) return result finally: if dpiSodaDb_freeCollectionNames(self._handle, &coll_names) < 0: _raise_from_odpi() def open_collection(self, str name): """ Internal method for opening a collection. """ cdef: StringBuffer name_buf = StringBuffer() ThickSodaCollImpl coll_impl uint32_t flags int status name_buf.set_value(name) self._get_flags(&flags) coll_impl = ThickSodaCollImpl.__new__(ThickSodaCollImpl) coll_impl._db_impl = self with nogil: status = dpiSodaDb_openCollection(self._handle, name_buf.ptr, name_buf.length, flags, &coll_impl._handle) if status < 0: _raise_from_odpi() if coll_impl._handle != NULL: coll_impl._get_name() return coll_impl cdef class ThickSodaCollImpl(BaseSodaCollImpl): cdef: ThickSodaDbImpl _db_impl dpiSodaColl* _handle def __dealloc__(self): if self._handle != NULL: dpiSodaColl_release(self._handle) cdef int _get_name(self) except -1: """ Internal method for getting the name of the collection. """ cdef: uint32_t name_len const char *name if dpiSodaColl_getName(self._handle, &name, &name_len) < 0: _raise_from_odpi() self.name = name[:name_len].decode() cdef int _process_options(self, dpiSodaOperOptions *options, const char *ptr, uint32_t length) except -1: """ Internal method for populating the SODA operations structure with the information provided by the user. """ if dpiContext_initSodaOperOptions(driver_context, options) < 0: _raise_from_odpi() options.hint = ptr options.hintLength = length def create_index(self, str spec): """ Internal method for creating an index on a collection. """ cdef: StringBuffer buf = StringBuffer() uint32_t flags int status buf.set_value(spec) self._db_impl._get_flags(&flags) with nogil: status = dpiSodaColl_createIndex(self._handle, buf.ptr, buf.length, flags) if status < 0: _raise_from_odpi() def drop(self): """ Internal method for dropping a collection. """ cdef: bint is_dropped uint32_t flags self._db_impl._get_flags(&flags) if dpiSodaColl_drop(self._handle, flags, &is_dropped) < 0: _raise_from_odpi() return is_dropped def drop_index(self, str name, bint force): """ Internal method for dropping an index on a collection. """ cdef: StringBuffer buf = StringBuffer() bint is_dropped uint32_t flags int status buf.set_value(name) self._db_impl._get_flags(&flags) if force: flags |= DPI_SODA_FLAGS_INDEX_DROP_FORCE with nogil: status = dpiSodaColl_dropIndex(self._handle, buf.ptr, buf.length, flags, &is_dropped) if status < 0: _raise_from_odpi() return is_dropped def get_count(self, object op): """ Internal method for getting the count of documents matching the criteria. """ cdef: ThickSodaOpImpl options = ThickSodaOpImpl._from_op(op) uint64_t count uint32_t flags int status self._db_impl._get_flags(&flags) with nogil: status = dpiSodaColl_getDocCount(self._handle, &options._options, flags, &count) if status < 0: _raise_from_odpi() return count def get_cursor(self, object op): """ Internal method for getting a cursor which will return the documents matching the criteria. """ cdef: ThickSodaOpImpl options = ThickSodaOpImpl._from_op(op) ThickSodaDocCursorImpl cursor_impl uint32_t flags int status self._db_impl._get_flags(&flags) cursor_impl = ThickSodaDocCursorImpl.__new__(ThickSodaDocCursorImpl) cursor_impl._db_impl = self._db_impl with nogil: status = dpiSodaColl_find(self._handle, &options._options, flags, &cursor_impl._handle) if status < 0: _raise_from_odpi() return cursor_impl def get_data_guide(self): """ Internal method for getting the data guide for a collection. """ cdef: ThickSodaDocImpl doc_impl uint32_t flags int status self._db_impl._get_flags(&flags) doc_impl = ThickSodaDocImpl.__new__(ThickSodaDocImpl) with nogil: status = dpiSodaColl_getDataGuide(self._handle, flags, &doc_impl._handle) if status < 0: _raise_from_odpi() if doc_impl._handle != NULL: return doc_impl def get_metadata(self): """ Internal method for getting the metadata for a collection. """ cdef: uint32_t value_len const char* value if dpiSodaColl_getMetadata(self._handle, &value, &value_len) < 0: _raise_from_odpi() return value[:value_len].decode() def get_one(self, object op): """ Internal method for getting a document matching the criteria. """ cdef: ThickSodaOpImpl options = ThickSodaOpImpl._from_op(op) ThickSodaDocImpl doc_impl uint32_t flags int status self._db_impl._get_flags(&flags) doc_impl = ThickSodaDocImpl.__new__(ThickSodaDocImpl) with nogil: status = dpiSodaColl_findOne(self._handle, &options._options, flags, &doc_impl._handle) if status < 0: _raise_from_odpi() if doc_impl._handle != NULL: return doc_impl def insert_many(self, list doc_impls, str hint, bint return_docs): """ Internal method for inserting many documents into a collection at once. """ cdef: dpiSodaDoc **output_handles = NULL uint32_t i, num_docs, flags ThickSodaDocImpl doc_impl list output_doc_impls dpiSodaDoc **handles ssize_t num_bytes dpiSodaOperOptions options dpiSodaOperOptions *options_ptr = NULL StringBuffer hint_buf = StringBuffer() int status num_docs = len(doc_impls) num_bytes = num_docs * sizeof(dpiSodaDoc *) handles = cpython.PyMem_Malloc(num_bytes) if return_docs: output_handles = _calloc(num_docs, sizeof(dpiSodaDoc*)) if hint is not None: hint_buf.set_value(hint) options_ptr = &options self._process_options(&options, hint_buf.ptr, hint_buf.length) for i, doc_impl in enumerate(doc_impls): handles[i] = doc_impl._handle self._db_impl._get_flags(&flags) with nogil: status = dpiSodaColl_insertManyWithOptions(self._handle, num_docs, handles, options_ptr, flags, output_handles) if status < 0: _raise_from_odpi() if return_docs: output_doc_impls = [] for i in range(num_docs): doc_impl = ThickSodaDocImpl.__new__(ThickSodaDocImpl) doc_impl._handle = output_handles[i] output_doc_impls.append(doc_impl) return output_doc_impls def insert_one(self, ThickSodaDocImpl doc_impl, str hint, bint return_doc): """ Internal method for inserting a single document into a collection. """ cdef: dpiSodaDoc **output_handle = NULL ThickSodaDocImpl output_doc_impl uint32_t flags dpiSodaOperOptions options dpiSodaOperOptions *options_ptr = NULL StringBuffer hint_buf = StringBuffer() int status if return_doc: output_doc_impl = ThickSodaDocImpl.__new__(ThickSodaDocImpl) output_handle = &output_doc_impl._handle if hint is not None: hint_buf.set_value(hint) options_ptr = &options self._process_options(&options, hint_buf.ptr, hint_buf.length) self._db_impl._get_flags(&flags) with nogil: status = dpiSodaColl_insertOneWithOptions(self._handle, doc_impl._handle, options_ptr, flags, output_handle) if status < 0: _raise_from_odpi() if return_doc: return output_doc_impl def remove(self, object op): """ Internal method for removing all of the documents matching the criteria. """ cdef: ThickSodaOpImpl options = ThickSodaOpImpl._from_op(op) uint64_t count uint32_t flags int status self._db_impl._get_flags(&flags) with nogil: status = dpiSodaColl_remove(self._handle, &options._options, flags, &count) if status < 0: _raise_from_odpi() return count def replace_one(self, object op, ThickSodaDocImpl doc_impl, bint return_doc): """ Internal method for replacing the document matching the criteria with the supplied coument. """ cdef: ThickSodaOpImpl options = ThickSodaOpImpl._from_op(op) dpiSodaDoc **output_handle = NULL ThickSodaDocImpl output_doc_impl uint32_t flags bint replaced int status if return_doc: output_doc_impl = ThickSodaDocImpl.__new__(ThickSodaDocImpl) output_handle = &output_doc_impl._handle self._db_impl._get_flags(&flags) with nogil: status = dpiSodaColl_replaceOne(self._handle, &options._options, doc_impl._handle, flags, &replaced, output_handle) if status < 0: _raise_from_odpi() if return_doc: return output_doc_impl def save(self, ThickSodaDocImpl doc_impl, str hint, bint return_doc): """ Internal method for saving a document into the collection. """ cdef: dpiSodaDoc **output_handle = NULL ThickSodaDocImpl output_doc_impl uint32_t flags dpiSodaOperOptions options dpiSodaOperOptions *options_ptr = NULL StringBuffer hint_buf = StringBuffer() int status if return_doc: output_doc_impl = ThickSodaDocImpl.__new__(ThickSodaDocImpl) output_handle = &output_doc_impl._handle if hint is not None: hint_buf.set_value(hint) options_ptr = &options self._process_options(&options, hint_buf.ptr, hint_buf.length) self._db_impl._get_flags(&flags) with nogil: status = dpiSodaColl_saveWithOptions(self._handle, doc_impl._handle, options_ptr, flags, output_handle) if status < 0: _raise_from_odpi() if return_doc: return output_doc_impl def truncate(self): """ Internal method for truncating the collection (removing all documents from it). """ cdef int status with nogil: status = dpiSodaColl_truncate(self._handle) if status < 0: _raise_from_odpi() cdef class ThickSodaDocImpl(BaseSodaDocImpl): cdef: dpiSodaDoc* _handle def __dealloc__(self): if self._handle != NULL: dpiSodaDoc_release(self._handle) def get_content(self): """ Internal method for returning the content of the document. """ cdef: bytes out_content = None str out_encoding = None const char *encoding uint32_t content_len const char *content if dpiSodaDoc_getContent(self._handle, &content, &content_len, &encoding) < 0: _raise_from_odpi() if content != NULL: out_content = content[:content_len] if encoding != NULL: out_encoding = encoding.decode() else: out_encoding = "UTF-8" return (out_content, out_encoding) def get_created_on(self): """ Internal method for getting the date the document was created. """ cdef: uint32_t value_len const char *value if dpiSodaDoc_getCreatedOn(self._handle, &value, &value_len) < 0: _raise_from_odpi() if value_len > 0: return value[:value_len].decode() def get_key(self): """ Internal method for getting the key of the document. """ cdef: uint32_t value_len const char *value if dpiSodaDoc_getKey(self._handle, &value, &value_len) < 0: _raise_from_odpi() if value_len > 0: return value[:value_len].decode() def get_last_modified(self): """ Internal method for getting the date the document was last modified. """ cdef: uint32_t value_len const char *value if dpiSodaDoc_getLastModified(self._handle, &value, &value_len) < 0: _raise_from_odpi() if value_len > 0: return value[:value_len].decode() def get_media_type(self): """ Internal method for getting the media type of the document. """ cdef: uint32_t value_len const char *value if dpiSodaDoc_getMediaType(self._handle, &value, &value_len) < 0: _raise_from_odpi() if value_len > 0: return value[:value_len].decode() def get_version(self): """ Internal method for getting the version of the document. """ cdef: uint32_t value_len const char *value if dpiSodaDoc_getVersion(self._handle, &value, &value_len) < 0: _raise_from_odpi() if value_len > 0: return value[:value_len].decode() cdef class ThickSodaDocCursorImpl(BaseSodaDocCursorImpl): cdef: dpiSodaDocCursor* _handle ThickSodaDbImpl _db_impl def __dealloc__(self): if self._handle != NULL: dpiSodaDocCursor_release(self._handle) def close(self): """ Internal method for closing the cursor. """ cdef int status with nogil: status = dpiSodaDocCursor_close(self._handle) if status < 0: _raise_from_odpi() def get_next_doc(self): """ Internal method for getting the next document from the cursor. """ cdef: ThickSodaDocImpl doc_impl int status doc_impl = ThickSodaDocImpl.__new__(ThickSodaDocImpl) with nogil: status = dpiSodaDocCursor_getNext(self._handle, DPI_SODA_FLAGS_DEFAULT, &doc_impl._handle) if status < 0: _raise_from_odpi() if doc_impl._handle != NULL: return doc_impl cdef class ThickSodaOpImpl: cdef: dpiSodaOperOptions _options const char** _key_values uint32_t* _key_lengths list _buffers def __dealloc__(self): if self._key_values != NULL: cpython.PyMem_Free(self._key_values) if self._key_lengths != NULL: cpython.PyMem_Free(self._key_lengths) cdef int _add_buf(self, object value, const char **ptr, uint32_t *length) except -1: cdef StringBuffer buf = StringBuffer() buf.set_value(value) self._buffers.append(buf) ptr[0] = buf.ptr length[0] = buf.length @staticmethod cdef ThickSodaOpImpl _from_op(object op): """ Internal method for creating a SODA operations implementation object given the object supplied by the user. """ cdef: ThickSodaOpImpl impl = ThickSodaOpImpl.__new__(ThickSodaOpImpl) dpiSodaOperOptions *options ssize_t num_bytes uint32_t i impl._buffers = [] options = &impl._options if dpiContext_initSodaOperOptions(driver_context, options) < 0: _raise_from_odpi() if op._keys: options.numKeys = len(op._keys) num_bytes = options.numKeys * sizeof(char*) impl._key_values = cpython.PyMem_Malloc(num_bytes) num_bytes = options.numKeys * sizeof(uint32_t) impl._key_lengths = cpython.PyMem_Malloc(num_bytes) options.keys = impl._key_values options.keyLengths = impl._key_lengths for i in range(options.numKeys): impl._add_buf(op._keys[i], &impl._key_values[i], &impl._key_lengths[i]) if op._key is not None: impl._add_buf(op._key, &options.key, &options.keyLength) if op._version is not None: impl._add_buf(op._version, &options.version, &options.versionLength) if op._filter is not None: impl._add_buf(op._filter, &options.filter, &options.filterLength) if op._hint is not None: impl._add_buf(op._hint, &options.hint, &options.hintLength) if op._skip is not None: options.skip = op._skip if op._limit is not None: options.limit = op._limit if op._fetch_array_size is not None: options.fetchArraySize = op._fetch_array_size return impl python-oracledb-1.2.1/src/oracledb/impl/thick/subscr.pyx000066400000000000000000000161211434177474600232770ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # subscr.pyx # # Cython file defining the thick Subscription implementation class (embedded in # thick_impl.pyx). #------------------------------------------------------------------------------ cdef void _callback_handler(void* context, dpiSubscrMessage* message) with gil: cdef: object subscr = context ThickSubscrImpl subscr_impl object py_message if message.errorInfo: _raise_from_info(message.errorInfo) else: subscr_impl = subscr._impl py_message = Message(subscr) subscr_impl._populate_message(message, py_message) subscr.callback(py_message) cdef class ThickSubscrImpl(BaseSubscrImpl): cdef: dpiSubscr* _handle def __dealloc__(self): if self._handle != NULL: dpiSubscr_release(self._handle) cdef object _create_message_query(self, dpiSubscrMessageQuery* query): cdef: object py_query = MessageQuery() uint32_t i list temp py_query._id = query.id py_query._operation = query.operation temp = py_query._tables for i in range(query.numTables): temp.append(self._create_message_table(&query.tables[i])) return py_query cdef object _create_message_row(self, dpiSubscrMessageRow* row): cdef object py_row = MessageRow() py_row._operation = row.operation py_row._rowid = row.rowid[:row.rowidLength].decode() return py_row cdef object _create_message_table(self, dpiSubscrMessageTable* table): cdef: object py_table = MessageTable() uint32_t i list temp py_table._operation = table.operation py_table._name = table.name[:table.nameLength].decode() temp = py_table._rows for i in range(table.numRows): temp.append(self._create_message_row(&table.rows[i])) return py_table cdef object _populate_message(self, dpiSubscrMessage* message, object py_message): cdef: const char* txid list temp uint32_t i py_message._type = message.eventType py_message._registered = message.registered py_message._dbname = message.dbName[:message.dbNameLength].decode() if message.txId != NULL: txid = message.txId py_message._txid = txid[:message.txIdLength] if message.queueName != NULL: py_message._queue_name = \ message.queueName[:message.queueNameLength].decode() if message.consumerName != NULL: py_message._consumer_name = \ message.consumerName[:message.consumerNameLength].decode() if message.aqMsgId != NULL: msgid = message.aqMsgId py_message._msgid = msgid[:message.aqMsgIdLength] if message.eventType == DPI_EVENT_OBJCHANGE: temp = py_message._tables for i in range(message.numTables): temp.append(self._create_message_table(&message.tables[i])) elif message.eventType == DPI_EVENT_QUERYCHANGE: temp = py_message._queries for i in range(message.numQueries): temp.append(self._create_message_query(&message.queries[i])) def register_query(self, str sql, object args): """ Internal method for registering a query. """ cdef: StringBuffer sql_buf = StringBuffer() ThickCursorImpl cursor_impl uint64_t query_id object cursor sql_buf.set_value(sql) cursor = self.connection.cursor() cursor_impl = cursor._impl if dpiSubscr_prepareStmt(self._handle, sql_buf.ptr, sql_buf.length, &cursor_impl._handle) < 0: _raise_from_odpi() if args is not None: cursor_impl.bind_one(cursor, args) cursor_impl.execute(cursor) if self.qos & DPI_SUBSCR_QOS_QUERY: if dpiStmt_getSubscrQueryId(cursor_impl._handle, &query_id) < 0: _raise_from_odpi() return query_id def subscribe(self, object subscr, ThickConnImpl conn_impl): """ Internal method for creating the subscription. """ cdef: StringBuffer ip_address_buf = StringBuffer() StringBuffer name_buf = StringBuffer() dpiSubscrCreateParams params int status name_buf.set_value(self.name) ip_address_buf.set_value(self.ip_address) if dpiContext_initSubscrCreateParams(driver_context, ¶ms) < 0: _raise_from_odpi() params.subscrNamespace = self.namespace params.protocol = self.protocol params.qos = self.qos params.operations = self.operations params.portNumber = self.port params.timeout = self.timeout params.name = name_buf.ptr params.nameLength = name_buf.length if self.callback is not None: params.callback = _callback_handler params.callbackContext = subscr params.ipAddress = ip_address_buf.ptr params.ipAddressLength = ip_address_buf.length params.groupingClass = self.grouping_class params.groupingType = self.grouping_type params.groupingValue = self.grouping_value params.clientInitiated = self.client_initiated with nogil: status = dpiConn_subscribe(conn_impl._handle, ¶ms, &self._handle) if status < 0: _raise_from_odpi() self.id = params.outRegId def unsubscribe(self, ThickConnImpl conn_impl): """ Internal method for destroying the subscription. """ cdef int status with nogil: status = dpiConn_unsubscribe(conn_impl._handle, self._handle) if status < 0: _raise_from_odpi() python-oracledb-1.2.1/src/oracledb/impl/thick/utils.pyx000066400000000000000000000524701434177474600231450ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # utils.pyx # # Cython file for utility functions (embedded in thick_impl.pyx). #------------------------------------------------------------------------------ cdef object _convert_from_json_node(dpiJsonNode *node): cdef: dpiTimestamp *as_timestamp dpiIntervalDS *as_interval dpiJsonArray *array dpiBytes *as_bytes dpiJsonObject *obj dict dict_value list list_value int32_t seconds uint32_t i str key if node.nativeTypeNum == DPI_NATIVE_TYPE_NULL: return None elif node.oracleTypeNum == DPI_ORACLE_TYPE_NUMBER: if node.nativeTypeNum == DPI_NATIVE_TYPE_DOUBLE: return node.value.asDouble as_bytes = &node.value.asBytes return PY_TYPE_DECIMAL(as_bytes.ptr[:as_bytes.length].decode()) elif node.oracleTypeNum == DPI_ORACLE_TYPE_VARCHAR: as_bytes = &node.value.asBytes return as_bytes.ptr[:as_bytes.length].decode() elif node.oracleTypeNum == DPI_ORACLE_TYPE_RAW: return node.value.asBytes.ptr[:node.value.asBytes.length] elif node.oracleTypeNum == DPI_ORACLE_TYPE_DATE \ or node.oracleTypeNum == DPI_ORACLE_TYPE_TIMESTAMP: as_timestamp = &node.value.asTimestamp return cydatetime.datetime_new(as_timestamp.year, as_timestamp.month, as_timestamp.day, as_timestamp.hour, as_timestamp.minute, as_timestamp.second, as_timestamp.fsecond // 1000, None) elif node.oracleTypeNum == DPI_ORACLE_TYPE_BOOLEAN: return node.value.asBoolean elif node.oracleTypeNum == DPI_ORACLE_TYPE_INTERVAL_DS: as_interval = &node.value.asIntervalDS seconds = as_interval.hours * 60 * 60 + as_interval.minutes * 60 + \ as_interval.seconds return cydatetime.timedelta_new(as_interval.days, seconds, as_interval.fseconds // 1000) elif node.oracleTypeNum == DPI_ORACLE_TYPE_JSON_OBJECT: obj = &node.value.asJsonObject dict_value = {} for i in range(obj.numFields): key = obj.fieldNames[i][:obj.fieldNameLengths[i]].decode() dict_value[key] = _convert_from_json_node(&obj.fields[i]) return dict_value elif node.oracleTypeNum == DPI_ORACLE_TYPE_JSON_ARRAY: array = &node.value.asJsonArray list_value = [None] * array.numElements for i in range(array.numElements): list_value[i] = _convert_from_json_node(&array.elements[i]) return list_value errors._raise_err(errors.ERR_ORACLE_TYPE_NOT_SUPPORTED, num=node.oracleTypeNum) cdef int _convert_from_python(object value, DbType dbtype, ThickDbObjectTypeImpl obj_type_impl, dpiDataBuffer *dbvalue, StringBuffer buf, ThickVarImpl var_impl=None, uint32_t pos=0) except -1: cdef: uint32_t oracle_type = dbtype.num ThickDbObjectImpl obj_impl dpiTimestamp *timestamp ThickLobImpl lob_impl int seconds, status JsonBuffer json_buf if oracle_type == DPI_ORACLE_TYPE_NUMBER: if isinstance(value, bool): if value: buf.set_value("1") else: buf.set_value("0") elif isinstance(value, (int, float, PY_TYPE_DECIMAL)): buf.set_value(( cpython.PyObject_Str(value)).encode()) else: message = f"expecting number, got {type(value)}" raise TypeError(message) dbvalue.asBytes.ptr = buf.ptr dbvalue.asBytes.length = buf.length elif oracle_type == DPI_ORACLE_TYPE_NATIVE_DOUBLE \ or oracle_type == DPI_ORACLE_TYPE_NATIVE_FLOAT: if oracle_type == DPI_ORACLE_TYPE_NATIVE_DOUBLE: dbvalue.asDouble = value else: dbvalue.asFloat = value elif oracle_type == DPI_ORACLE_TYPE_VARCHAR \ or oracle_type == DPI_ORACLE_TYPE_NVARCHAR \ or oracle_type == DPI_ORACLE_TYPE_CHAR \ or oracle_type == DPI_ORACLE_TYPE_NCHAR \ or oracle_type == DPI_ORACLE_TYPE_LONG_VARCHAR: buf.set_value(value) dbvalue.asBytes.ptr = buf.ptr dbvalue.asBytes.length = buf.length elif oracle_type == DPI_ORACLE_TYPE_RAW \ or oracle_type == DPI_ORACLE_TYPE_LONG_RAW: buf.set_value(value) dbvalue.asBytes.ptr = buf.ptr dbvalue.asBytes.length = buf.length elif oracle_type == DPI_ORACLE_TYPE_DATE \ or oracle_type == DPI_ORACLE_TYPE_TIMESTAMP \ or oracle_type == DPI_ORACLE_TYPE_TIMESTAMP_LTZ \ or oracle_type == DPI_ORACLE_TYPE_TIMESTAMP_TZ: memset(&dbvalue.asTimestamp, 0, sizeof(dbvalue.asTimestamp)) timestamp = &dbvalue.asTimestamp timestamp.year = cydatetime.PyDateTime_GET_YEAR(value) timestamp.month = cydatetime.PyDateTime_GET_MONTH(value) timestamp.day = cydatetime.PyDateTime_GET_DAY(value) if cydatetime.PyDateTime_Check(value): timestamp.hour = cydatetime.PyDateTime_DATE_GET_HOUR(value) timestamp.minute = cydatetime.PyDateTime_DATE_GET_MINUTE(value) timestamp.second = cydatetime.PyDateTime_DATE_GET_SECOND(value) timestamp.fsecond = \ cydatetime.PyDateTime_DATE_GET_MICROSECOND(value) * 1000 elif oracle_type == DPI_ORACLE_TYPE_BOOLEAN: dbvalue.asBoolean = value elif oracle_type == DPI_ORACLE_TYPE_NATIVE_INT: if isinstance(value, bool): dbvalue.asInt64 = 1 if value else 0 else: dbvalue.asInt64 = value elif oracle_type == DPI_ORACLE_TYPE_INTERVAL_DS: seconds = cydatetime.timedelta_seconds(value) dbvalue.asIntervalDS.days = cydatetime.timedelta_days(value) dbvalue.asIntervalDS.hours = seconds // 3600 seconds = seconds % 3600 dbvalue.asIntervalDS.minutes = seconds // 60 dbvalue.asIntervalDS.seconds = seconds % 60 dbvalue.asIntervalDS.fseconds = \ cydatetime.timedelta_microseconds(value) * 1000 elif oracle_type == DPI_ORACLE_TYPE_OBJECT: if not isinstance(value, PY_TYPE_DB_OBJECT): raise TypeError("expecting DbObject") obj_impl = value._impl if var_impl is not None: if dpiVar_setFromObject(var_impl._handle, pos, obj_impl._handle) < 0: _raise_from_odpi() else: dbvalue.asObject = obj_impl._handle elif oracle_type == DPI_ORACLE_TYPE_CLOB \ or oracle_type == DPI_ORACLE_TYPE_BLOB \ or oracle_type == DPI_ORACLE_TYPE_NCLOB: if isinstance(value, PY_TYPE_LOB): lob_impl = value._impl if var_impl is not None: if dpiVar_setFromLob(var_impl._handle, pos, lob_impl._handle) < 0: _raise_from_odpi() else: dbvalue.asLOB = lob_impl._handle else: buf.set_value(value) dbvalue.asBytes.ptr = buf.ptr dbvalue.asBytes.length = buf.length elif oracle_type == DPI_ORACLE_TYPE_JSON: json_buf = JsonBuffer() json_buf.from_object(value) if dpiJson_setValue(dbvalue.asJson, &json_buf._top_node) < 0: _raise_from_odpi() else: errors._raise_err(errors.ERR_DB_TYPE_NOT_SUPPORTED, name=dbtype.name) cdef object _convert_oci_attr_to_python(uint32_t attr_type, dpiDataBuffer *value, uint32_t value_len): """ Convert an OCI attribute value to a Python value. """ if attr_type == PYO_OCI_ATTR_TYPE_STRING: if value.asString == NULL: return None return value.asString[:value_len].decode() elif attr_type == PYO_OCI_ATTR_TYPE_BOOLEAN: return value.asBoolean elif attr_type == PYO_OCI_ATTR_TYPE_UINT8: return value.asUint8 elif attr_type == PYO_OCI_ATTR_TYPE_UINT16: return value.asUint16 elif attr_type == PYO_OCI_ATTR_TYPE_UINT32: return value.asUint32 elif attr_type == PYO_OCI_ATTR_TYPE_UINT64: return value.asUint64 errors._raise_err(errors.ERR_INVALID_OCI_ATTR_TYPE, attr_type=attr_type) cdef int _convert_python_to_oci_attr(object value, uint32_t attr_type, StringBuffer str_buf, dpiDataBuffer *oci_buf, void **oci_value, uint32_t *oci_len) except -1: """ Convert a Python value to the format required by an OCI attribute. """ if attr_type == PYO_OCI_ATTR_TYPE_STRING: str_buf.set_value(value) oci_value[0] = str_buf.ptr oci_len[0] = str_buf.length elif attr_type == PYO_OCI_ATTR_TYPE_BOOLEAN: oci_buf.asBoolean = value oci_value[0] = &oci_buf.asBoolean oci_len[0] = sizeof(oci_buf.asBoolean) elif attr_type == PYO_OCI_ATTR_TYPE_UINT8: oci_buf.asUint8 = value oci_value[0] = &oci_buf.asUint8 oci_len[0] = sizeof(oci_buf.asUint8) elif attr_type == PYO_OCI_ATTR_TYPE_UINT16: oci_buf.asUint16 = value oci_value[0] = &oci_buf.asUint16 oci_len[0] = sizeof(oci_buf.asUint16) elif attr_type == PYO_OCI_ATTR_TYPE_UINT32: oci_buf.asUint32 = value oci_value[0] = &oci_buf.asUint32 oci_len[0] = sizeof(oci_buf.asUint32) elif attr_type == PYO_OCI_ATTR_TYPE_UINT64: oci_buf.asUint64 = value oci_value[0] = &oci_buf.asUint64 oci_len[0] = sizeof(oci_buf.asUint64) else: errors._raise_err(errors.ERR_INVALID_OCI_ATTR_TYPE, attr_type=attr_type) cdef object _convert_to_python(ThickConnImpl conn_impl, DbType dbtype, ThickDbObjectTypeImpl obj_type_impl, dpiDataBuffer *dbvalue, int preferred_num_type=NUM_TYPE_FLOAT, bint bypass_decode=False): cdef: uint32_t oracle_type = dbtype.num ThickDbObjectImpl obj_impl dpiTimestamp *as_timestamp dpiJsonNode *json_node ThickLobImpl lob_impl uint32_t rowid_length dpiBytes *as_bytes const char *rowid int32_t seconds if bypass_decode: oracle_type = DPI_ORACLE_TYPE_RAW if oracle_type == DPI_ORACLE_TYPE_CHAR \ or oracle_type == DPI_ORACLE_TYPE_NCHAR \ or oracle_type == DPI_ORACLE_TYPE_VARCHAR \ or oracle_type == DPI_ORACLE_TYPE_NVARCHAR \ or oracle_type == DPI_ORACLE_TYPE_LONG_VARCHAR \ or oracle_type == DPI_ORACLE_TYPE_LONG_NVARCHAR: as_bytes = &dbvalue.asBytes return as_bytes.ptr[:as_bytes.length].decode() elif oracle_type == DPI_ORACLE_TYPE_NUMBER: as_bytes = &dbvalue.asBytes if preferred_num_type == NUM_TYPE_INT \ and memchr(as_bytes.ptr, b'.', as_bytes.length) == NULL: return int(as_bytes.ptr[:as_bytes.length]) elif preferred_num_type == NUM_TYPE_DECIMAL: return PY_TYPE_DECIMAL(as_bytes.ptr[:as_bytes.length].decode()) return float(as_bytes.ptr[:as_bytes.length]) elif oracle_type == DPI_ORACLE_TYPE_RAW \ or oracle_type == DPI_ORACLE_TYPE_LONG_RAW: as_bytes = &dbvalue.asBytes return as_bytes.ptr[:as_bytes.length] elif oracle_type == DPI_ORACLE_TYPE_DATE \ or oracle_type == DPI_ORACLE_TYPE_TIMESTAMP \ or oracle_type == DPI_ORACLE_TYPE_TIMESTAMP_LTZ \ or oracle_type == DPI_ORACLE_TYPE_TIMESTAMP_TZ: as_timestamp = &dbvalue.asTimestamp return cydatetime.datetime_new(as_timestamp.year, as_timestamp.month, as_timestamp.day, as_timestamp.hour, as_timestamp.minute, as_timestamp.second, as_timestamp.fsecond // 1000, None) elif oracle_type == DPI_ORACLE_TYPE_BOOLEAN: return dbvalue.asBoolean == 1 elif oracle_type == DPI_ORACLE_TYPE_NATIVE_DOUBLE: return dbvalue.asDouble elif oracle_type == DPI_ORACLE_TYPE_NATIVE_FLOAT: return dbvalue.asFloat elif oracle_type == DPI_ORACLE_TYPE_NATIVE_INT: return dbvalue.asInt64 elif oracle_type == DPI_ORACLE_TYPE_ROWID: if dpiRowid_getStringValue(dbvalue.asRowid, &rowid, &rowid_length) < 0: _raise_from_odpi() return rowid[:rowid_length].decode() elif oracle_type == DPI_ORACLE_TYPE_CLOB \ or oracle_type == DPI_ORACLE_TYPE_BLOB \ or oracle_type == DPI_ORACLE_TYPE_NCLOB \ or oracle_type == DPI_ORACLE_TYPE_BFILE: lob_impl = ThickLobImpl._create(conn_impl, dbtype, dbvalue.asLOB) return PY_TYPE_LOB._from_impl(lob_impl) elif oracle_type == DPI_ORACLE_TYPE_OBJECT: obj_impl = ThickDbObjectImpl.__new__(ThickDbObjectImpl) obj_impl.type = obj_type_impl if dpiObject_addRef(dbvalue.asObject) < 0: _raise_from_odpi() obj_impl._handle = dbvalue.asObject return PY_TYPE_DB_OBJECT._from_impl(obj_impl) elif oracle_type == DPI_ORACLE_TYPE_INTERVAL_DS: seconds = dbvalue.asIntervalDS.hours * 60 * 60 + \ dbvalue.asIntervalDS.minutes * 60 + \ dbvalue.asIntervalDS.seconds return cydatetime.timedelta_new(dbvalue.asIntervalDS.days, seconds, dbvalue.asIntervalDS.fseconds // 1000) elif oracle_type == DPI_ORACLE_TYPE_JSON: if dpiJson_getValue(dbvalue.asJson, DPI_JSON_OPT_NUMBER_AS_STRING, &json_node) < 0: _raise_from_odpi() return _convert_from_json_node(json_node) errors._raise_err(errors.ERR_DB_TYPE_NOT_SUPPORTED, name=dbtype.name) cdef uint32_t _get_native_type_num(DbType dbtype): cdef uint32_t oracle_type_num = dbtype.num if oracle_type_num == DPI_ORACLE_TYPE_NATIVE_FLOAT: return DPI_NATIVE_TYPE_FLOAT elif oracle_type_num == DPI_ORACLE_TYPE_NATIVE_DOUBLE: return DPI_NATIVE_TYPE_DOUBLE elif oracle_type_num == DPI_ORACLE_TYPE_BOOLEAN: return DPI_NATIVE_TYPE_BOOLEAN elif oracle_type_num == DPI_ORACLE_TYPE_CLOB \ or oracle_type_num == DPI_ORACLE_TYPE_NCLOB \ or oracle_type_num == DPI_ORACLE_TYPE_BLOB \ or oracle_type_num == DPI_ORACLE_TYPE_BFILE: return DPI_NATIVE_TYPE_LOB elif oracle_type_num == DPI_ORACLE_TYPE_DATE \ or oracle_type_num == DPI_ORACLE_TYPE_TIMESTAMP \ or oracle_type_num == DPI_ORACLE_TYPE_TIMESTAMP_LTZ \ or oracle_type_num == DPI_ORACLE_TYPE_TIMESTAMP_TZ: return DPI_NATIVE_TYPE_TIMESTAMP elif oracle_type_num == DPI_ORACLE_TYPE_STMT: return DPI_NATIVE_TYPE_STMT elif oracle_type_num == DPI_ORACLE_TYPE_NATIVE_INT: return DPI_NATIVE_TYPE_INT64 elif oracle_type_num == DPI_ORACLE_TYPE_OBJECT: return DPI_NATIVE_TYPE_OBJECT elif oracle_type_num == DPI_ORACLE_TYPE_ROWID: return DPI_NATIVE_TYPE_ROWID elif oracle_type_num == DPI_ORACLE_TYPE_INTERVAL_DS: return DPI_NATIVE_TYPE_INTERVAL_DS elif oracle_type_num == DPI_ORACLE_TYPE_JSON: return DPI_NATIVE_TYPE_JSON return DPI_NATIVE_TYPE_BYTES cdef object _create_new_from_info(dpiErrorInfo *error_info): """ Creates a new error object given a dpiErrorInfo structure that is already populated with error information. """ cdef bytes msg_bytes = error_info.message[:error_info.messageLength] context = "%s: %s" % (error_info.fnName, error_info.action) return errors._Error(msg_bytes.decode(), context, code=error_info.code, offset=error_info.offset, isrecoverable=error_info.isRecoverable, iswarning=error_info.isWarning) cdef int _raise_from_info(dpiErrorInfo *error_info) except -1: """ Raises an exception given a dpiErrorInfo structure that is already populated with error information. """ msg_bytes = error_info.message[:error_info.messageLength] context = "%s: %s" % (error_info.fnName, error_info.action) error = errors._Error(msg_bytes.decode(), context, code=error_info.code, offset=error_info.offset, isrecoverable=bool(error_info.isRecoverable), iswarning=error_info.isWarning) exc_type = get_exception_class(error_info.code) raise exc_type(error) cdef int _raise_from_odpi() except -1: """ Raises an exception from ODPI-C, given that an error has been raised by ODPI-C (a return code of -1 has been received). """ cdef dpiErrorInfo error_info dpiContext_getError(driver_context, &error_info) _raise_from_info(&error_info) def clientversion(): """ Returns the version of the Oracle Client library being used as a 5-tuple. The five values are the major version, minor version, update number, patch number and port update number. """ cdef dpiVersionInfo info global client_version if client_version is None: if driver_context == NULL: errors._raise_err(errors.ERR_INIT_ORACLE_CLIENT_NOT_CALLED) if dpiContext_getClientVersion(driver_context, &info) < 0: _raise_from_odpi() client_version = ( info.versionNum, info.releaseNum, info.updateNum, info.portReleaseNum, info.portUpdateNum ) return client_version def init_oracle_client(lib_dir=None, config_dir=None, error_url=None, driver_name=None): """ Initialize the Oracle Client library. This method is available externally in order to be called with parameters that control how the Oracle Client library is initialized. If not called earlier, the first usage of the Oracle Client library will cause this method to be called internally. """ cdef: bytes lib_dir_bytes, config_dir_bytes, driver_name_bytes dpiContextCreateParams params dpiErrorInfo error_info bytes encoding_bytes global driver_context_params params_tuple = (lib_dir, config_dir, error_url, driver_name) if driver_context != NULL: if params_tuple != driver_context_params: errors._raise_err(errors.ERR_LIBRARY_ALREADY_INITIALIZED) return with driver_mode.get_manager(requested_thin_mode=False) as mode_mgr: memset(¶ms, 0, sizeof(dpiContextCreateParams)) encoding_bytes = constants.ENCODING.encode() params.defaultEncoding = encoding_bytes if config_dir is None: config_dir = defaults.config_dir if lib_dir is not None: lib_dir_bytes = lib_dir.encode() params.oracleClientLibDir = lib_dir_bytes if config_dir is not None: config_dir_bytes = config_dir.encode() params.oracleClientConfigDir = config_dir_bytes if driver_name is None: driver_name = f"{constants.DRIVER_NAME} thk : {VERSION}" driver_name_bytes = driver_name.encode() params.defaultDriverName = driver_name_bytes if error_url is not None: error_url_bytes = error_url.encode() else: error_url_bytes = constants.INSTALLATION_URL.encode() params.loadErrorUrl = error_url_bytes if dpiContext_createWithParams(DPI_MAJOR_VERSION, DPI_MINOR_VERSION, ¶ms, &driver_context, &error_info) < 0: _raise_from_info(&error_info) driver_context_params = params_tuple def init_thick_impl(package): """ Initializes globals after the package has been completely initialized. This is to avoid circular imports and eliminate the need for global lookups. """ global PY_TYPE_DB_OBJECT, PY_TYPE_LOB PY_TYPE_DB_OBJECT = package.DbObject PY_TYPE_LOB = package.LOB python-oracledb-1.2.1/src/oracledb/impl/thick/var.pyx000066400000000000000000000263061434177474600225740ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # var.pyx # # Cython file defining the thick Variable implementation class (embedded in # thick_impl.pyx). #------------------------------------------------------------------------------ cdef class ThickVarImpl(BaseVarImpl): cdef: dpiVar *_handle dpiData *_data StringBuffer _buf uint32_t _native_type_num bint _get_returned_data object _conn def __dealloc__(self): if self._handle != NULL: dpiVar_release(self._handle) cdef int _bind(self, object conn, BaseCursorImpl cursor_impl, uint32_t num_execs, object name, uint32_t pos) except -1: cdef: ThickCursorImpl thick_cursor_impl = cursor_impl uint32_t name_length, i dpiDataBuffer *dbvalue const char *name_ptr bytes name_bytes if self._native_type_num == DPI_NATIVE_TYPE_STMT: for i in range(self.num_elements): if self._data[i].isNull: continue dbvalue = &self._data[i].value if dbvalue.asStmt == thick_cursor_impl._handle: errors._raise_err(errors.ERR_SELF_BIND_NOT_SUPPORTED) if name is not None: name_bytes = name.encode() name_ptr = name_bytes name_length = len(name_bytes) if dpiStmt_bindByName(thick_cursor_impl._handle, name_ptr, name_length, self._handle) < 0: _raise_from_odpi() else: if dpiStmt_bindByPos(thick_cursor_impl._handle, pos, self._handle) < 0: _raise_from_odpi() if thick_cursor_impl._stmt_info.isReturning and not self._is_value_set: self._get_returned_data = True self._is_value_set = False cdef int _create_handle(self) except -1: cdef: ThickConnImpl conn_impl = self._conn_impl dpiObjectType *obj_type_handle = NULL ThickDbObjectTypeImpl obj_type_impl if self._handle != NULL: dpiVar_release(self._handle) self._handle = NULL if self.objtype is not None: obj_type_impl = self.objtype obj_type_handle = obj_type_impl._handle self._native_type_num = _get_native_type_num(self.dbtype) if dpiConn_newVar(conn_impl._handle, self.dbtype.num, self._native_type_num, self.num_elements, self.size, 0, self.is_array, obj_type_handle, &self._handle, &self._data) < 0: _raise_from_odpi() cdef int _finalize_init(self) except -1: """ Internal method that finalizes initialization of the variable. """ BaseVarImpl._finalize_init(self) self._create_handle() cdef list _get_array_value(self): """ Internal method to return the value of the array. """ cdef uint32_t i return [self._get_scalar_value(i) \ for i in range(self.num_elements_in_array)] cdef object _get_cursor_value(self, dpiDataBuffer *dbvalue): cdef: ThickCursorImpl cursor_impl object cursor cursor = self._conn.cursor() cursor_impl = cursor._impl if dpiStmt_addRef(dbvalue.asStmt) < 0: _raise_from_odpi() cursor_impl._handle = dbvalue.asStmt cursor_impl._fixup_ref_cursor = True return cursor cdef object _get_scalar_value(self, uint32_t pos): """ Internal method to return the value of the variable at the given position. """ cdef: uint32_t num_returned_rows dpiData *returned_data if self._get_returned_data: if dpiVar_getReturnedData(self._handle, pos, &num_returned_rows, &returned_data) < 0: _raise_from_odpi() return self._transform_array_to_python(num_returned_rows, returned_data) return self._transform_element_to_python(pos, self._data) cdef int _on_reset_bind(self, uint32_t num_rows) except -1: """ Called when the bind variable is being reset, just prior to performing a bind operation. """ cdef: dpiStmtInfo stmt_info uint32_t i BaseVarImpl._on_reset_bind(self, num_rows) if self._native_type_num == DPI_NATIVE_TYPE_STMT: for i in range(self.num_elements): if self._data[i].isNull: continue if dpiStmt_getInfo(self._data[i].value.asStmt, &stmt_info) < 0: self._create_handle() break cdef int _resize(self, uint32_t new_size) except -1: """ Resize the variable to the new size provided. """ cdef: dpiVar *orig_handle = NULL uint32_t num_elements, i dpiData *source_data dpiData *orig_data BaseVarImpl._resize(self, new_size) orig_data = self._data orig_handle = self._handle self._handle = NULL try: self._create_handle() if self.is_array: if dpiVar_getNumElementsInArray(orig_handle, &num_elements) < 0: _raise_from_odpi() if dpiVar_setNumElementsInArray(self._handle, num_elements) < 0: _raise_from_odpi() for i in range(self.num_elements): source_data = &orig_data[i] if source_data.isNull: continue if dpiVar_setFromBytes(self._handle, i, source_data.value.asBytes.ptr, source_data.value.asBytes.length) < 0: _raise_from_odpi() finally: dpiVar_release(orig_handle) cdef int _set_cursor_value(self, object cursor, uint32_t pos) except -1: cdef: ThickCursorImpl cursor_impl = cursor._impl dpiData *data # if the cursor already has a handle, use it directly if cursor_impl._handle != NULL: if dpiVar_setFromStmt(self._handle, pos, cursor_impl._handle) < 0: _raise_from_odpi() # otherwise, make use of the statement handle allocated by the variable else: data = &self._data[pos] if dpiStmt_addRef(data.value.asStmt) < 0: _raise_from_odpi() cursor_impl._handle = data.value.asStmt if dpiStmt_setPrefetchRows(cursor_impl._handle, cursor_impl.prefetchrows) < 0: _raise_from_odpi() cursor_impl._fixup_ref_cursor = True cursor.statement = None cdef int _set_num_elements_in_array(self, uint32_t num_elements) except -1: """ Sets the number of elements in the array. """ BaseVarImpl._set_num_elements_in_array(self, num_elements) if dpiVar_setNumElementsInArray(self._handle, num_elements) < 0: _raise_from_odpi() cdef int _set_scalar_value(self, uint32_t pos, object value) except -1: """ Set the value of the variable at the given position. At this point it is assumed that all checks have been performed! """ cdef: dpiDataBuffer temp_dbvalue dpiDataBuffer *dbvalue dpiBytes *as_bytes bint needs_set dpiData *data data = &self._data[pos] data.isNull = (value is None) if not data.isNull: if self._native_type_num == DPI_NATIVE_TYPE_STMT: self._set_cursor_value(value, pos) else: needs_set = self._native_type_num == DPI_NATIVE_TYPE_BYTES \ or (self._native_type_num == DPI_NATIVE_TYPE_LOB and \ not isinstance(value, PY_TYPE_LOB)) if needs_set: dbvalue = &temp_dbvalue else: dbvalue = &data.value _convert_from_python(value, self.dbtype, self.objtype, dbvalue, self._buf, self, pos) if needs_set: as_bytes = &dbvalue.asBytes if dpiVar_setFromBytes(self._handle, pos, as_bytes.ptr, as_bytes.length) < 0: _raise_from_odpi() cdef object _transform_array_to_python(self, uint32_t num_elements, dpiData *data): """ Transforms an array from ODPI-C to a Python list. """ cdef: object element_value list return_value uint32_t i return_value = cpython.PyList_New(num_elements) for i in range(num_elements): element_value = self._transform_element_to_python(i, data) cpython.Py_INCREF(element_value) cpython.PyList_SET_ITEM(return_value, i, element_value) return return_value cdef object _transform_element_to_python(self, uint32_t pos, dpiData *data): """ Transforms a single element from the value supplied by ODPI-C to its equivalent Python value. """ cdef object value data = &data[pos] if not data.isNull: if self._native_type_num == DPI_NATIVE_TYPE_STMT: return self._get_cursor_value(&data.value) value = _convert_to_python(self._conn_impl, self.dbtype, self.objtype, &data.value, self._preferred_num_type, self.bypass_decode) if self.outconverter is not None: value = self.outconverter(value) return value python-oracledb-1.2.1/src/oracledb/impl/thin/000077500000000000000000000000001434177474600210735ustar00rootroot00000000000000python-oracledb-1.2.1/src/oracledb/impl/thin/buffer.pyx000066400000000000000000001307641434177474600231210ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # buffer.pyx # # Cython file defining the low-level read and write methods for packed data # (packet data or database object pickled data). #------------------------------------------------------------------------------ DEF NUMBER_AS_TEXT_CHARS = 172 DEF NUMBER_MAX_DIGITS = 40 DEF BYTE_ORDER_LSB = 1 DEF BYTE_ORDER_MSB = 2 cdef int MACHINE_BYTE_ORDER = BYTE_ORDER_MSB \ if sys.byteorder == "big" else BYTE_ORDER_LSB cdef inline uint16_t bswap16(uint16_t value): """ Swap the order of bytes for a 16-bit integer. """ return ((value << 8) & 0xff00) | ((value >> 8) & 0x00ff) cdef inline uint32_t bswap32(uint32_t value): """ Swap the order of bytes for a 32-bit integer. """ return ( ((value << 24) & ( 0xff000000)) | ((value << 8) & 0x00ff0000) | ((value >> 8) & 0x0000ff00) | ((value >> 24) & 0x000000ff) ) cdef inline uint64_t bswap64(uint64_t value): """ Swap the order of bytes for a 64-bit integer. """ return ( ((value << 56) & ( 0xff00000000000000ULL)) | ((value << 40) & 0x00ff000000000000ULL) | ((value << 24) & 0x0000ff0000000000ULL) | ((value << 8) & 0x000000ff00000000ULL) | ((value >> 8) & 0x00000000ff000000ULL) | ((value >> 24) & 0x0000000000ff0000ULL) | ((value >> 40) & 0x000000000000ff00ULL) | ((value >> 56) & 0x00000000000000ffULL) ) cdef inline void pack_uint16(char_type *buf, uint16_t x, int order): """ Pack a 16-bit integer into the buffer using the specified type order. """ if order != MACHINE_BYTE_ORDER: x = bswap16(x) memcpy(buf, &x, sizeof(x)) cdef inline void pack_uint32(char_type *buf, uint32_t x, int order): """ Pack a 32-bit integer into the buffer using the specified type order. """ if order != MACHINE_BYTE_ORDER: x = bswap32(x) memcpy(buf, &x, sizeof(x)) cdef inline void pack_uint64(char_type *buf, uint64_t x, int order): """ Pack a 64-bit integer into the buffer using the specified type order. """ if order != MACHINE_BYTE_ORDER: x = bswap64(x) memcpy(buf, &x, sizeof(x)) cdef inline uint16_t unpack_uint16(const char_type *buf, int order): """ Unpacks a 16-bit integer from the buffer using the specified byte order. """ cdef uint16_t raw_value memcpy(&raw_value, buf, sizeof(raw_value)) return raw_value if order == MACHINE_BYTE_ORDER else bswap16(raw_value) cdef inline uint32_t unpack_uint32(const char_type *buf, int order): """ Unpacks a 32-bit integer from the buffer using the specified byte order. """ cdef uint32_t raw_value memcpy(&raw_value, buf, sizeof(raw_value)) return raw_value if order == MACHINE_BYTE_ORDER else bswap32(raw_value) cdef class Buffer: cdef: ssize_t _max_size, _size, _pos char_type[:] _data_view char_type *_data bytearray _data_obj cdef int _get_int_length_and_sign(self, uint8_t *length, bint *is_negative, uint8_t max_length) except -1: """ Returns the length of an integer stored in the buffer. A check is also made to ensure the integer does not exceed the maximum length. If the is_negative pointer is NULL, negative integers will result in an exception being raised. """ cdef const char_type *ptr = self._get_raw(1) if ptr[0] & 0x80: if is_negative == NULL: errors._raise_err(errors.ERR_UNEXPECTED_NEGATIVE_INTEGER) is_negative[0] = True length[0] = ptr[0] & 0x7f else: if is_negative != NULL: is_negative[0] = False length[0] = ptr[0] if length[0] > max_length: errors._raise_err(errors.ERR_INTEGER_TOO_LARGE, length=length[0], max_length=max_length) cdef const char_type* _get_raw(self, ssize_t num_bytes) except NULL: """ Returns a pointer to a buffer containing the requested number of bytes. """ cdef: ssize_t num_bytes_left const char_type *ptr num_bytes_left = self._size - self._pos if num_bytes > num_bytes_left: errors._raise_err(errors.ERR_UNEXPECTED_END_OF_DATA, num_bytes_wanted=num_bytes, num_bytes_available=num_bytes_left) ptr = &self._data[self._pos] self._pos += num_bytes return ptr cdef int _initialize(self, ssize_t max_size) except -1: """ Initialize the buffer with an empty bytearray of the specified size. """ self._max_size = max_size self._data_obj = bytearray(max_size) self._data_view = self._data_obj self._data = self._data_obj cdef int _populate_from_bytes(self, bytes data) except -1: """ Initialize the buffer with the data in the specified byte string. """ self._max_size = self._size = len(data) self._data_obj = bytearray(data) self._data_view = self._data_obj self._data = self._data_obj cdef int _read_raw_bytes_and_length(self, const char_type **ptr, ssize_t *num_bytes) except -1: """ Helper function that processes the length (if needed) and then acquires the specified number of bytes from the buffer. The base function simply uses the length as given. """ ptr[0] = self._get_raw(num_bytes[0]) cdef int _resize(self, ssize_t new_max_size) except -1: """ Resizes the buffer to the new maximum size, copying the data already stored in the buffer first. """ cdef: bytearray data_obj char_type* data data_obj = bytearray(new_max_size) data = data_obj memcpy(data, self._data, self._max_size) self._max_size = new_max_size self._data_obj = data_obj self._data_view = data_obj self._data = data cdef int _skip_int(self, uint8_t max_length, bint *is_negative) except -1: """ Skips reading an integer of the specified maximum length from the buffer. """ cdef uint8_t length self._get_int_length_and_sign(&length, is_negative, max_length) self.skip_raw_bytes(length) cdef uint64_t _unpack_int(self, const char_type *ptr, uint8_t length): """ Unpacks an integer received in the buffer into its native format. """ if length == 1: return ptr[0] elif length == 2: return (ptr[0] << 8) | ptr[1] elif length == 3: return (ptr[0] << 16) | (ptr[1] << 8) | ptr[2] elif length == 4: return (ptr[0] << 24) | (ptr[1] << 16) | (ptr[2] << 8) | ptr[3] elif length == 5: return (( ptr[0]) << 32) | (ptr[1] << 24) | \ (ptr[2] << 16) | (ptr[3] << 8) | ptr[4] elif length == 6: return (( ptr[0]) << 40) | \ (( ptr[1]) << 32) | (ptr[2] << 24) | \ (ptr[3] << 16) | (ptr[4] << 8) | ptr[5] elif length == 7: return (( ptr[0]) << 48) | \ (( ptr[1]) << 40) | \ (( ptr[2]) << 32) | \ (ptr[3] << 24) | (ptr[4] << 16) | (ptr[5] << 8) | ptr[6] elif length == 8: return (( ptr[0]) << 46) | \ (( ptr[1]) << 48) | \ (( ptr[2]) << 40) | \ (( ptr[3]) << 32) | \ (ptr[4] << 24) | (ptr[5] << 16) | (ptr[6] << 8) | ptr[7] cdef int _write_more_data(self, ssize_t num_bytes_available, ssize_t num_bytes_wanted) except -1: """ Called when the amount of buffer available is less than the amount of data requested. By default an error is raised. """ errors._raise_err(errors.ERR_BUFFER_LENGTH_INSUFFICIENT, required_buffer_len=num_bytes_wanted, actual_buffer_len=num_bytes_available) cdef int _write_raw_bytes_and_length(self, const char_type *ptr, ssize_t num_bytes) except -1: """ Helper function that writes the length in the format required before writing the bytes. """ cdef ssize_t chunk_len if num_bytes <= TNS_MAX_SHORT_LENGTH: self.write_uint8( num_bytes) self.write_raw(ptr, num_bytes) else: self.write_uint8(TNS_LONG_LENGTH_INDICATOR) while num_bytes > 0: chunk_len = min(num_bytes, TNS_CHUNK_SIZE) self.write_ub4(chunk_len) num_bytes -= chunk_len self.write_raw(ptr, chunk_len) ptr += chunk_len self.write_ub4(0) cdef inline ssize_t bytes_left(self): """ Return the number of bytes remaining in the buffer. """ return self._size - self._pos cdef object read_binary_double(self): """ Read a binary double value from the buffer and return the corresponding Python object representing that value. """ cdef: uint8_t b0, b1, b2, b3, b4, b5, b6, b7 uint64_t high_bits, low_bits, all_bits const uint8_t *ptr double *double_ptr ssize_t num_bytes self.read_raw_bytes_and_length(&ptr, &num_bytes) if ptr == NULL: return None b0 = ptr[0] b1 = ptr[1] b2 = ptr[2] b3 = ptr[3] b4 = ptr[4] b5 = ptr[5] b6 = ptr[6] b7 = ptr[7] if b0 & 0x80: b0 = b0 & 0x7f else: b0 = ~b0 b1 = ~b1 b2 = ~b2 b3 = ~b3 b4 = ~b4 b5 = ~b5 b6 = ~b6 b7 = ~b7 high_bits = b0 << 24 | b1 << 16 | b2 << 8 | b3 low_bits = b4 << 24 | b5 << 16 | b6 << 8 | b7 all_bits = high_bits << 32 | (low_bits & 0xffffffff) double_ptr = &all_bits return double_ptr[0] cdef object read_binary_float(self): """ Read a binary float value from the buffer and return the corresponding Python object representing that value. """ cdef: uint8_t b0, b1, b2, b3 const uint8_t *ptr uint64_t all_bits ssize_t num_bytes float *float_ptr self.read_raw_bytes_and_length(&ptr, &num_bytes) if ptr == NULL: return None b0 = ptr[0] b1 = ptr[1] b2 = ptr[2] b3 = ptr[3] if b0 & 0x80: b0 = b0 & 0x7f else: b0 = ~b0 b1 = ~b1 b2 = ~b2 b3 = ~b3 all_bits = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3 float_ptr = &all_bits return float_ptr[0] cdef object read_binary_integer(self): """ Read a binary integer from the buffer. """ cdef: const char_type *ptr ssize_t num_bytes self.read_raw_bytes_and_length(&ptr, &num_bytes) if num_bytes > 4: errors._raise_err(errors.ERR_INTEGER_TOO_LARGE, length=num_bytes, max_length=4) if ptr != NULL: return self._unpack_int(ptr, num_bytes) cdef object read_bool(self): """ Read a boolean from the buffer and return True, False or None. """ cdef: const char_type *ptr ssize_t num_bytes self.read_raw_bytes_and_length(&ptr, &num_bytes) if ptr != NULL: return ptr[num_bytes - 1] == 1 cdef object read_bytes(self): """ Read bytes from the buffer and return the corresponding Python object representing that value. """ cdef: const char_type *ptr ssize_t num_bytes self.read_raw_bytes_and_length(&ptr, &num_bytes) if ptr != NULL: return ptr[:num_bytes] cdef object read_date(self): """ Read a date from the buffer and return the corresponding Python object representing that value. """ cdef: int8_t tz_hour = 0, tz_minute = 0 uint32_t fsecond = 0 const uint8_t *ptr ssize_t num_bytes int32_t seconds uint16_t year self.read_raw_bytes_and_length(&ptr, &num_bytes) if ptr == NULL: return None year = (ptr[0] - 100) * 100 + ptr[1] - 100 if num_bytes >= 11: fsecond = unpack_uint32(&ptr[7], BYTE_ORDER_MSB) // 1000 value = cydatetime.datetime_new(year, ptr[2], ptr[3], ptr[4] - 1, ptr[5] - 1, ptr[6] - 1, fsecond, None) if num_bytes > 11 and ptr[11] != 0 and ptr[12] != 0: tz_hour = ptr[11] - TZ_HOUR_OFFSET tz_minute = ptr[12] - TZ_MINUTE_OFFSET if tz_hour != 0 or tz_minute != 0: seconds = tz_hour * 3600 + tz_minute * 60 value += cydatetime.timedelta_new(0, seconds, 0) return value cdef ThinDbObjectImpl read_dbobject(self, BaseDbObjectTypeImpl typ_impl): """ Read a database object from the buffer and return a DbObject object containing it. it. """ cdef: bytes oid = None, toid = None ThinDbObjectImpl obj_impl uint32_t num_bytes self.read_ub4(&num_bytes) if num_bytes > 0: # type OID toid = self.read_bytes() self.read_ub4(&num_bytes) if num_bytes > 0: # OID oid = self.read_bytes() self.read_ub4(&num_bytes) if num_bytes > 0: # snapshot self.read_bytes() self.skip_ub2() # version self.read_ub4(&num_bytes) # length of data self.skip_ub2() # flags if num_bytes > 0: obj_impl = ThinDbObjectImpl.__new__(ThinDbObjectImpl) obj_impl.type = typ_impl obj_impl.toid = toid obj_impl.oid = oid obj_impl.packed_data = self.read_bytes() return obj_impl cdef object read_interval_ds(self): """ Read an interval day to second value from the buffer and return the corresponding Python object representing that value. """ cdef: int32_t days, hours, minutes, seconds, total_seconds, fseconds uint8_t duration_offset = TNS_DURATION_OFFSET uint32_t duration_mid = TNS_DURATION_MID const uint8_t *ptr ssize_t num_bytes self.read_raw_bytes_and_length(&ptr, &num_bytes) if ptr == NULL: return None days = unpack_uint32(ptr, BYTE_ORDER_MSB) - duration_mid fseconds = unpack_uint32(&ptr[7], BYTE_ORDER_MSB) - duration_mid hours = ptr[4] - duration_offset minutes = ptr[5] - duration_offset seconds = ptr[6] - duration_offset total_seconds = hours * 60 * 60 + minutes * 60 + seconds return cydatetime.timedelta_new(days, total_seconds, fseconds // 1000) cdef int read_int32(self, int32_t *value, int byte_order=BYTE_ORDER_MSB) except -1: """ Read a signed 32-bit integer from the buffer in the specified byte order. """ cdef const char_type *ptr = self._get_raw(4) value[0] = unpack_uint32(ptr, byte_order) cdef object read_lob(self, ThinConnImpl conn_impl, DbType dbtype): """ Read a LOB locator from the buffer and return a LOB object containing it. """ cdef: ThinLobImpl lob_impl bytes locator locator = self.read_bytes() lob_impl = ThinLobImpl._create(conn_impl, dbtype, locator) return PY_TYPE_LOB._from_impl(lob_impl) cdef object read_oracle_number(self, int preferred_num_type): """ Read an Oracle number from the buffer and return the corresponding Python object representing that value. The preferred numeric type (int, float, decimal.Decimal and str) is used, if possible. """ cdef: char_type buf[NUMBER_AS_TEXT_CHARS] uint8_t digits[NUMBER_MAX_DIGITS] uint8_t num_digits, byte, digit bint is_positive, is_integer int16_t decimal_point_index const uint8_t *ptr ssize_t num_bytes int8_t exponent str text # read the number of bytes in the number; if the value is 0 or the null # length indicator, return None self.read_raw_bytes_and_length(&ptr, &num_bytes) if ptr == NULL: return None # the first byte is the exponent; positive numbers have the highest # order bit set, whereas negative numbers have the highest order bit # cleared and the bits inverted exponent = ptr[0] is_positive = (exponent & 0x80) if not is_positive: exponent = ~exponent exponent -= 193 decimal_point_index = exponent * 2 + 2 # a mantissa length of 0 implies a value of 0 (if positive) or a value # of -1e126 (if negative) if num_bytes == 1: if is_positive: if preferred_num_type == NUM_TYPE_INT: return 0 elif preferred_num_type == NUM_TYPE_DECIMAL: return PY_TYPE_DECIMAL(0) elif preferred_num_type == NUM_TYPE_STR: return "0" return 0.0 if preferred_num_type == NUM_TYPE_INT: return -10 ** 126 elif preferred_num_type == NUM_TYPE_DECIMAL: return PY_TYPE_DECIMAL("-1e126") elif preferred_num_type == NUM_TYPE_STR: return "-1e126" return -1.0e126 # check for the trailing 102 byte for negative numbers and, if present, # reduce the number of mantissa digits if not is_positive and ptr[num_bytes - 1] == 102: num_bytes -= 1 # process the mantissa bytes which are the remaining bytes; each # mantissa byte is a base-100 digit num_digits = 0 for i in range(1, num_bytes): # positive numbers have 1 added to them; negative numbers are # subtracted from the value 101 byte = ptr[i] if is_positive: byte -= 1 else: byte = 101 - byte # process the first digit; leading zeroes are ignored digit = byte // 10 if digit == 0 and num_digits == 0: decimal_point_index -= 1 elif digit == 10: digits[num_digits] = 1 digits[num_digits + 1] = 0 num_digits += 2 decimal_point_index += 1 elif digit != 0 or i > 0: digits[num_digits] = digit num_digits += 1 # process the second digit; trailing zeroes are ignored digit = byte % 10 if digit != 0 or i < num_bytes - 1: digits[num_digits] = digit num_digits += 1 # create string of digits for transformation to Python value is_integer = 1 num_bytes = 0 # if negative, include the sign if not is_positive: buf[num_bytes] = 45 # minus sign num_bytes += 1 # if the decimal point index is 0 or less, add the decimal point and # any leading zeroes that are needed if decimal_point_index <= 0: buf[num_bytes] = 48 # zero buf[num_bytes + 1] = 46 # decimal point num_bytes += 2 is_integer = 0 for i in range(decimal_point_index, 0): buf[num_bytes] = 48 # zero num_bytes += 1 # add each of the digits for i in range(num_digits): if i > 0 and i == decimal_point_index: buf[num_bytes] = 46 # decimal point is_integer = 0 num_bytes += 1 buf[num_bytes] = 48 + digits[i] num_bytes += 1 # if the decimal point index exceeds the number of digits, add any # trailing zeroes that are needed if decimal_point_index > num_digits: for i in range(num_digits, decimal_point_index): buf[num_bytes] = 48 # zero num_bytes += 1 # convert result to an integer or a decimal number if preferred_num_type == NUM_TYPE_INT and is_integer: return int(buf[:num_bytes]) elif preferred_num_type == NUM_TYPE_DECIMAL: return PY_TYPE_DECIMAL(buf[:num_bytes].decode()) elif preferred_num_type == NUM_TYPE_STR: return buf[:num_bytes].decode() return float(buf[:num_bytes]) cdef inline const char_type* read_raw_bytes(self, ssize_t num_bytes) except NULL: """ Returns a pointer to a contiguous buffer containing the specified number of bytes found in the buffer. """ return self._get_raw(num_bytes) cdef int read_raw_bytes_and_length(self, const char_type **ptr, ssize_t *num_bytes) except -1: """ Reads bytes from the buffer into a contiguous buffer. The first byte read is the number of bytes to read. """ cdef uint8_t length self.read_ub1(&length) if length == 0 or length == TNS_NULL_LENGTH_INDICATOR: ptr[0] = NULL num_bytes[0] = 0 else: num_bytes[0] = length self._read_raw_bytes_and_length(ptr, num_bytes) cdef int read_sb1(self, int8_t *value) except -1: """ Reads a signed 8-bit integer from the buffer. """ cdef const char_type *ptr = self._get_raw(1) value[0] = ptr[0] cdef int read_sb2(self, int16_t *value) except -1: """ Reads a signed 16-bit integer from the buffer. """ cdef: const char_type *ptr bint is_negative uint8_t length self._get_int_length_and_sign(&length, &is_negative, 2) if length == 0: value[0] = 0 else: ptr = self._get_raw(length) value[0] = self._unpack_int(ptr, length) if is_negative: value[0] = -value[0] cdef int read_sb4(self, int32_t *value) except -1: """ Reads a signed 32-bit integer from the buffer. """ cdef: const char_type *ptr bint is_negative uint8_t length self._get_int_length_and_sign(&length, &is_negative, 4) if length == 0: value[0] = 0 else: ptr = self._get_raw(length) value[0] = self._unpack_int(ptr, length) if is_negative: value[0] = -value[0] cdef int read_sb8(self, int64_t *value) except -1: """ Reads a signed 64-bit integer from the buffer. """ cdef: const char_type *ptr bint is_negative uint8_t length self._get_int_length_and_sign(&length, &is_negative, 8) if length == 0: value[0] = 0 else: ptr = self._get_raw(length) value[0] = self._unpack_int(ptr, length) if is_negative: value[0] = -value[0] cdef object read_str(self, int csfrm): """ Reads a string of the specified size (in bytes) from the buffer. """ cdef: const char_type *ptr ssize_t num_bytes self.read_raw_bytes_and_length(&ptr, &num_bytes) if ptr != NULL: if csfrm == TNS_CS_IMPLICIT: return ptr[:num_bytes].decode() return ptr[:num_bytes].decode(TNS_ENCODING_UTF16) cdef int read_ub1(self, uint8_t *value) except -1: """ Reads an unsigned 8-bit integer from the buffer. """ cdef const char_type *ptr = self._get_raw(1) value[0] = ptr[0] cdef int read_ub2(self, uint16_t *value) except -1: """ Reads an unsigned 16-bit integer from the buffer. """ cdef: const char_type *ptr uint8_t length self._get_int_length_and_sign(&length, NULL, 2) if length == 0: value[0] = 0 else: ptr = self._get_raw(length) value[0] = self._unpack_int(ptr, length) cdef int read_ub4(self, uint32_t *value) except -1: """ Reads an unsigned 32-bit integer from the buffer. """ cdef: const char_type *ptr uint8_t length self._get_int_length_and_sign(&length, NULL, 4) if length == 0: value[0] = 0 else: ptr = self._get_raw(length) value[0] = self._unpack_int(ptr, length) cdef int read_ub8(self, uint64_t *value) except -1: """ Reads an unsigned 64-bit integer from the buffer. """ cdef: const char_type *ptr uint8_t length self._get_int_length_and_sign(&length, NULL, 8) if length == 0: value[0] = 0 else: ptr = self._get_raw(length) value[0] = self._unpack_int(ptr, length) cdef int read_uint16(self, uint16_t *value, int byte_order=BYTE_ORDER_MSB) except -1: """ Read a 16-bit integer from the buffer in the specified byte order. """ cdef const char_type *ptr = self._get_raw(2) value[0] = unpack_uint16(ptr, byte_order) cdef int read_uint32(self, uint32_t *value, int byte_order=BYTE_ORDER_MSB) except -1: """ Read a 32-bit integer from the buffer in the specified byte order. """ cdef const char_type *ptr = self._get_raw(4) value[0] = unpack_uint32(ptr, byte_order) cdef object read_xmltype(self, ThinConnImpl conn_impl): """ Reads an XMLType value from the buffer and returns the string value. The XMLType object is a special DbObjectType and is handled separately since the structure is a bit different. """ cdef: uint8_t image_flags, image_version DbObjectPickleBuffer buf ThinLobImpl lob_impl const char_type *ptr uint32_t num_bytes ssize_t bytes_left uint32_t xml_flag bytes packed_data self.read_ub4(&num_bytes) if num_bytes > 0: # type OID self.read_bytes() self.read_ub4(&num_bytes) if num_bytes > 0: # OID self.read_bytes() self.read_ub4(&num_bytes) if num_bytes > 0: # snapshot self.read_bytes() self.skip_ub2() # version self.read_ub4(&num_bytes) # length of data self.skip_ub2() # flags if num_bytes > 0: packed_data = self.read_bytes() buf = DbObjectPickleBuffer.__new__(DbObjectPickleBuffer) buf._populate_from_bytes(packed_data) buf.read_header(&image_flags, &image_version) buf.skip_raw_bytes(1) # XML version buf.read_uint32(&xml_flag) if xml_flag & TNS_XML_TYPE_FLAG_SKIP_NEXT_4: buf.skip_raw_bytes(4) bytes_left = buf.bytes_left() ptr = buf.read_raw_bytes(bytes_left) if xml_flag & TNS_XML_TYPE_STRING: return ptr[:bytes_left].decode() elif xml_flag & TNS_XML_TYPE_LOB: lob_impl = ThinLobImpl._create(conn_impl, DB_TYPE_CLOB, ptr[:bytes_left]) return PY_TYPE_LOB._from_impl(lob_impl) errors._raise_err(errors.ERR_UNEXPECTED_XML_TYPE, flag=xml_flag) cdef int skip_raw_bytes(self, ssize_t num_bytes) except -1: """ Skip the specified number of bytes in the buffer. In order to avoid copying data, the number of bytes left in the packet is determined and only that amount is requested. """ cdef ssize_t num_bytes_this_time while num_bytes > 0: num_bytes_this_time = min(num_bytes, self.bytes_left()) self._get_raw(num_bytes_this_time) num_bytes -= num_bytes_this_time cdef inline int skip_sb4(self) except -1: """ Skips a signed 32-bit integer in the buffer. """ cdef bint is_negative return self._skip_int(4, &is_negative) cdef inline void skip_to(self, ssize_t pos): """ Skips to the specified location in the buffer. """ self._pos = pos cdef inline int skip_ub1(self) except -1: """ Skips an unsigned 8-bit integer in the buffer. """ self._get_raw(1) cdef inline int skip_ub2(self) except -1: """ Skips an unsigned 16-bit integer in the buffer. """ return self._skip_int(2, NULL) cdef inline int skip_ub4(self) except -1: """ Skips an unsigned 32-bit integer in the buffer. """ return self._skip_int(4, NULL) cdef int write_binary_double(self, double value) except -1: cdef: uint8_t b0, b1, b2, b3, b4, b5, b6, b7 uint64_t all_bits char_type buf[8] uint64_t *ptr ptr = &value all_bits = ptr[0] b7 = all_bits & 0xff b6 = (all_bits >> 8) & 0xff b5 = (all_bits >> 16) & 0xff b4 = (all_bits >> 24) & 0xff b3 = (all_bits >> 32) & 0xff b2 = (all_bits >> 40) & 0xff b1 = (all_bits >> 48) & 0xff b0 = (all_bits >> 56) & 0xff if b0 & 0x80 == 0: b0 = b0 | 0x80 else: b0 = ~b0 b1 = ~b1 b2 = ~b2 b3 = ~b3 b4 = ~b4 b5 = ~b5 b6 = ~b6 b7 = ~b7 buf[0] = b0 buf[1] = b1 buf[2] = b2 buf[3] = b3 buf[4] = b4 buf[5] = b5 buf[6] = b6 buf[7] = b7 self.write_uint8(8) self.write_raw(buf, 8) cdef int write_binary_float(self, float value) except -1: cdef: uint8_t b0, b1, b2, b3 uint32_t all_bits char_type buf[4] uint32_t *ptr ptr = &value all_bits = ptr[0] b3 = all_bits & 0xff b2 = (all_bits >> 8) & 0xff b1 = (all_bits >> 16) & 0xff b0 = (all_bits >> 24) & 0xff if b0 & 0x80 == 0: b0 = b0 | 0x80 else: b0 = ~b0 b1 = ~b1 b2 = ~b2 b3 = ~b3 buf[0] = b0 buf[1] = b1 buf[2] = b2 buf[3] = b3 self.write_uint8(4) self.write_raw(buf, 4) cdef int write_bool(self, bint value): """ Writes a boolean value to the buffer. """ if value: self.write_uint8(2) self.write_uint16(0x0101) else: self.write_uint16(0x0100) cdef int write_bytes(self, bytes value) except -1: """ Writes the bytes to the buffer directly. """ cdef: ssize_t value_len char_type *ptr cpython.PyBytes_AsStringAndSize(value, &ptr, &value_len) self.write_raw(ptr, value_len) cdef int write_bytes_with_length(self, bytes value) except -1: """ Writes the bytes to the buffer after first writing the length. """ cdef: ssize_t value_len char_type *ptr cpython.PyBytes_AsStringAndSize(value, &ptr, &value_len) self._write_raw_bytes_and_length(ptr, value_len) cdef object write_dbobject(self, ThinDbObjectImpl obj_impl): """ Writes a database object to the buffer. """ cdef: ThinDbObjectTypeImpl typ_impl = obj_impl.type uint32_t num_bytes bytes packed_data self.write_ub4(len(obj_impl.toid)) self.write_bytes_with_length(obj_impl.toid) if obj_impl.oid is None: self.write_ub4(0) else: self.write_ub4(len(obj_impl.oid)) self.write_bytes_with_length(obj_impl.oid) self.write_ub4(0) # snapshot self.write_ub4(0) # version packed_data = obj_impl._get_packed_data() self.write_ub4(len(packed_data)) self.write_ub4(obj_impl.flags) # flags self.write_bytes_with_length(packed_data) cdef int write_interval_ds(self, object value) except -1: cdef: int32_t days, seconds, fseconds char_type buf[11] days = cydatetime.timedelta_days(value) pack_uint32(buf, days + TNS_DURATION_MID, BYTE_ORDER_MSB) seconds = cydatetime.timedelta_seconds(value) buf[4] = (seconds // 3600) + TNS_DURATION_OFFSET seconds = seconds % 3600 buf[5] = (seconds // 60) + TNS_DURATION_OFFSET buf[6] = (seconds % 60) + TNS_DURATION_OFFSET fseconds = cydatetime.timedelta_microseconds(value) * 1000 pack_uint32(&buf[7], fseconds + TNS_DURATION_MID, BYTE_ORDER_MSB) self.write_uint8(sizeof(buf)) self.write_raw(buf, sizeof(buf)) cdef int write_lob(self, ThinLobImpl lob_impl) except -1: """ Writes a LOB locator to the buffer. """ self.write_bytes_with_length(lob_impl._locator) cdef int write_oracle_date(self, object value, uint8_t length) except -1: cdef: unsigned int year char_type buf[13] uint32_t fsecond year = cydatetime.PyDateTime_GET_YEAR(value) buf[0] = ((year // 100) + 100) buf[1] = ((year % 100) + 100) buf[2] = cydatetime.PyDateTime_GET_MONTH(value) buf[3] = cydatetime.PyDateTime_GET_DAY(value) buf[4] = cydatetime.PyDateTime_DATE_GET_HOUR(value) + 1 buf[5] = cydatetime.PyDateTime_DATE_GET_MINUTE(value) + 1 buf[6] = cydatetime.PyDateTime_DATE_GET_SECOND(value) + 1 if length > 7: fsecond = \ cydatetime.PyDateTime_DATE_GET_MICROSECOND(value) * 1000 if fsecond == 0: length = 7 else: pack_uint32(&buf[7], fsecond, BYTE_ORDER_MSB) if length > 11: buf[11] = TZ_HOUR_OFFSET buf[12] = TZ_MINUTE_OFFSET self.write_uint8(length) self.write_raw(buf, length) cdef int write_oracle_number(self, bytes num_bytes) except -1: cdef: uint8_t num_digits = 0, digit, num_pairs, pair_num, digits_pos bint exponent_is_negative = False, append_sentinel = False ssize_t num_bytes_length, exponent_pos, pos = 0 bint is_negative = False, prepend_zero = False uint8_t digits[NUMBER_AS_TEXT_CHARS] int16_t decimal_point_index int8_t exponent_on_wire const char_type *ptr int16_t exponent # zero length string cannot be converted num_bytes_length = len(num_bytes) if num_bytes_length == 0: errors._raise_err(errors.ERR_NUMBER_STRING_OF_ZERO_LENGTH) elif num_bytes_length > NUMBER_AS_TEXT_CHARS: errors._raise_err(errors.ERR_NUMBER_STRING_TOO_LONG) # check to see if number is negative (first character is '-') ptr = num_bytes if ptr[0] == b'-': is_negative = True pos += 1 # scan for digits until the decimal point or exponent indicator found while pos < num_bytes_length: if ptr[pos] == b'.' or ptr[pos] == b'e' or ptr[pos] == b'E': break if ptr[pos] < b'0' or ptr[pos] > b'9': errors._raise_err(errors.ERR_INVALID_NUMBER) digit = ptr[pos] - b'0' pos += 1 if digit == 0 and num_digits == 0: continue digits[num_digits] = digit num_digits += 1 decimal_point_index = num_digits # scan for digits following the decimal point, if applicable if pos < num_bytes_length and ptr[pos] == b'.': pos += 1 while pos < num_bytes_length: if ptr[pos] == b'e' or ptr[pos] == b'E': break digit = ptr[pos] - b'0' pos += 1 if digit == 0 and num_digits == 0: decimal_point_index -= 1 continue digits[num_digits] = digit num_digits += 1 # handle exponent, if applicable if pos < num_bytes_length and (ptr[pos] == b'e' or ptr[pos] == b'E'): pos += 1 if pos < num_bytes_length: if ptr[pos] == b'-': exponent_is_negative = True pos += 1 elif ptr[pos] == b'+': pos += 1 exponent_pos = pos while pos < num_bytes_length: if ptr[pos] < b'0' or ptr[pos] > b'9': errors._raise_err(errors.ERR_NUMBER_WITH_INVALID_EXPONENT) pos += 1 if exponent_pos == pos: errors._raise_err(errors.ERR_NUMBER_WITH_EMPTY_EXPONENT) exponent = int(ptr[exponent_pos:pos]) if exponent_is_negative: exponent = -exponent decimal_point_index += exponent # if there is anything left in the string, that indicates an invalid # number as well if pos < num_bytes_length: errors._raise_err(errors.ERR_CONTENT_INVALID_AFTER_NUMBER) # skip trailing zeros while num_digits > 0 and digits[num_digits - 1] == 0: num_digits -= 1 # value must be less than 1e126 and greater than 1e-129; the number of # digits also cannot exceed the maximum precision of Oracle numbers if num_digits > NUMBER_MAX_DIGITS or decimal_point_index > 126 \ or decimal_point_index < -129: errors._raise_err(errors.ERR_ORACLE_NUMBER_NO_REPR) # if the exponent is odd, prepend a zero if decimal_point_index % 2 == 1: prepend_zero = True if num_digits > 0: digits[num_digits] = 0 num_digits += 1 decimal_point_index += 1 # determine the number of digit pairs; if the number of digits is odd, # append a zero to make the number of digits even if num_digits % 2 == 1: digits[num_digits] = 0 num_digits += 1 num_pairs = num_digits // 2 # append a sentinel 102 byte for negative numbers if there is room if is_negative and num_digits > 0 and num_digits < NUMBER_MAX_DIGITS: append_sentinel = True # write length of number self.write_uint8(num_pairs + 1 + append_sentinel) # if the number of digits is zero, the value is itself zero since all # leading and trailing zeros are removed from the digits string; this # is a special case if num_digits == 0: self.write_uint8(128) return 0 # write the exponent exponent_on_wire = (decimal_point_index / 2) + 192 if is_negative: exponent_on_wire = ~exponent_on_wire self.write_uint8(exponent_on_wire) # write the mantissa bytes digits_pos = 0 for pair_num in range(num_pairs): if pair_num == 0 and prepend_zero: digit = digits[digits_pos] digits_pos += 1 else: digit = digits[digits_pos] * 10 + digits[digits_pos + 1] digits_pos += 2 if is_negative: digit = 101 - digit else: digit += 1 self.write_uint8(digit) # append 102 byte for negative numbers if the number of digits is less # than the maximum allowable if append_sentinel: self.write_uint8(102) cdef int write_raw(self, const char_type *data, ssize_t length) except -1: cdef ssize_t bytes_to_write while True: bytes_to_write = min(self._max_size - self._pos, length) if bytes_to_write > 0: memcpy(self._data + self._pos, data, bytes_to_write) self._pos += bytes_to_write if bytes_to_write == length: break self._write_more_data(self._max_size - self._pos, length) length -= bytes_to_write data += bytes_to_write cdef int write_str(self, str value) except -1: self.write_bytes(value.encode()) cdef int write_uint8(self, uint8_t value) except -1: if self._pos + 1 > self._max_size: self._write_more_data(self._max_size - self._pos, 1) self._data[self._pos] = value self._pos += 1 cdef int write_uint16(self, uint16_t value, int byte_order=BYTE_ORDER_MSB) except -1: if self._pos + 2 > self._max_size: self._write_more_data(self._max_size - self._pos, 2) pack_uint16(&self._data[self._pos], value, byte_order) self._pos += 2 cdef int write_uint32(self, uint32_t value, int byte_order=BYTE_ORDER_MSB) except -1: if self._pos + 4 > self._max_size: self._write_more_data(self._max_size - self._pos, 4) pack_uint32(&self._data[self._pos], value, byte_order) self._pos += 4 cdef int write_uint64(self, uint64_t value, byte_order=BYTE_ORDER_MSB) except -1: if self._pos + 8 > self._max_size: self._write_more_data(self._max_size - self._pos, 8) pack_uint64(&self._data[self._pos], value, byte_order) self._pos += 8 cdef int write_ub4(self, uint32_t value) except -1: if value == 0: self.write_uint8(0) elif value <= UINT8_MAX: self.write_uint8(1) self.write_uint8( value) elif value <= UINT16_MAX: self.write_uint8(2) self.write_uint16( value) else: self.write_uint8(4) self.write_uint32(value) cdef int write_ub8(self, uint64_t value) except -1: if value == 0: self.write_uint8(0) elif value <= UINT8_MAX: self.write_uint8(1) self.write_uint8( value) elif value <= UINT16_MAX: self.write_uint8(2) self.write_uint16( value) elif value <= UINT32_MAX: self.write_uint8(4) self.write_uint32(value) else: self.write_uint8(8) self.write_uint64(value) python-oracledb-1.2.1/src/oracledb/impl/thin/capabilities.pyx000066400000000000000000000114221434177474600242660ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # capabilities.pyx # # Cython file defining the capabilities (neogiated at connect time) that both # the database server and the client are capable of (embedded in # thin_impl.pyx). #------------------------------------------------------------------------------ cdef class Capabilities: cdef: uint16_t protocol_version uint8_t ttc_field_version uint16_t charset_id uint16_t ncharset_id bytearray compile_caps bytearray runtime_caps bint char_conversion bint supports_oob def __init__(self): self._init_compile_caps() self._init_runtime_caps() cdef void _adjust_for_protocol(self, uint16_t protocol_version, uint16_t protocol_options): self.protocol_version = protocol_version self.supports_oob = protocol_options & TNS_CAN_RECV_ATTENTION @cython.boundscheck(False) cdef void _adjust_for_server_compile_caps(self, bytearray server_caps): if server_caps[TNS_CCAP_FIELD_VERSION] < self.ttc_field_version: self.ttc_field_version = server_caps[TNS_CCAP_FIELD_VERSION] self.compile_caps[TNS_CCAP_FIELD_VERSION] = self.ttc_field_version @cython.boundscheck(False) cdef void _adjust_for_server_runtime_caps(self, bytearray server_caps): pass cdef int _check_ncharset_id(self) except -1: """ Checks that the national character set id is AL16UTF16, which is the only id that is currently supported. """ if self.ncharset_id != TNS_CHARSET_UTF16: errors._raise_err(errors.ERR_NCHAR_CS_NOT_SUPPORTED, charset_id=self.ncharset_id) @cython.boundscheck(False) cdef void _init_compile_caps(self): self.ttc_field_version = TNS_CCAP_FIELD_VERSION_MAX self.compile_caps = bytearray(TNS_CCAP_MAX) self.compile_caps[TNS_CCAP_SQL_VERSION] = TNS_CCAP_SQL_VERSION_MAX self.compile_caps[TNS_CCAP_LOGON_TYPES] = \ TNS_CCAP_O5LOGON | TNS_CCAP_O5LOGON_NP | \ TNS_CCAP_O7LOGON | TNS_CCAP_O8LOGON_LONG_IDENTIFIER self.compile_caps[TNS_CCAP_FIELD_VERSION] = self.ttc_field_version self.compile_caps[TNS_CCAP_SERVER_DEFINE_CONV] = 1 self.compile_caps[TNS_CCAP_TTC1] = \ TNS_CCAP_FAST_BVEC | TNS_CCAP_END_OF_CALL_STATUS | \ TNS_CCAP_IND_RCD self.compile_caps[TNS_CCAP_OCI1] = \ TNS_CCAP_FAST_SESSION_PROPAGATE | TNS_CCAP_APP_CTX_PIGGYBACK self.compile_caps[TNS_CCAP_TDS_VERSION] = TNS_CCAP_TDS_VERSION_MAX self.compile_caps[TNS_CCAP_RPC_VERSION] = TNS_CCAP_RPC_VERSION_MAX self.compile_caps[TNS_CCAP_RPC_SIG] = TNS_CCAP_RPC_SIG_VALUE self.compile_caps[TNS_CCAP_DBF_VERSION] = TNS_CCAP_DBF_VERSION_MAX self.compile_caps[TNS_CCAP_LOB] = TNS_CCAP_LOB_UB8_SIZE | \ TNS_CCAP_LOB_ENCS self.compile_caps[TNS_CCAP_UB2_DTY] = 1 self.compile_caps[TNS_CCAP_TTC3] = TNS_CCAP_IMPLICIT_RESULTS | \ TNS_CCAP_BIG_CHUNK_CLR | TNS_CCAP_KEEP_OUT_ORDER self.compile_caps[TNS_CCAP_TTC2] = TNS_CCAP_ZLNP self.compile_caps[TNS_CCAP_OCI2] = TNS_CCAP_DRCP self.compile_caps[TNS_CCAP_CLIENT_FN] = TNS_CCAP_CLIENT_FN_MAX self.compile_caps[TNS_CCAP_TTC4] = TNS_CCAP_INBAND_NOTIFICATION @cython.boundscheck(False) cdef void _init_runtime_caps(self): self.runtime_caps = bytearray(TNS_RCAP_MAX) self.runtime_caps[TNS_RCAP_COMPAT] = TNS_RCAP_COMPAT_81 self.runtime_caps[TNS_RCAP_TTC] = TNS_RCAP_TTC_ZERO_COPY | \ TNS_RCAP_TTC_32K python-oracledb-1.2.1/src/oracledb/impl/thin/connection.pyx000066400000000000000000000411131434177474600237740ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # connection.pyx # # Cython file defining the thin Connection implementation class (embedded in # thin_impl.pyx). #------------------------------------------------------------------------------ cdef class ThinConnImpl(BaseConnImpl): cdef: object _statement_cache uint32_t _statement_cache_size object _statement_cache_lock Protocol _protocol str _server_version uint32_t _session_id uint32_t _serial_num str _action bint _action_modified str _dbop bint _dbop_modified str _client_info bint _client_info_modified str _client_identifier bint _client_identifier_modified str _module bint _module_modified ThinPoolImpl _pool bytes _ltxid str _current_schema bint _current_schema_modified str _edition str _internal_name str _external_name array.array _cursors_to_close ssize_t _num_cursors_to_close bint _drcp_enabled bint _drcp_establish_session double _time_in_pool list _temp_lobs_to_close uint32_t _temp_lobs_total_size uint32_t _call_timeout str _cclass int _dbobject_type_cache_num def __init__(self, str dsn, ConnectParamsImpl params): if not HAS_CRYPTOGRAPHY: errors._raise_err(errors.ERR_NO_CRYPTOGRAPHY_PACKAGE) BaseConnImpl.__init__(self, dsn, params) self._protocol = Protocol() cdef int _add_cursor_to_close(self, Statement stmt) except -1: if self._num_cursors_to_close == TNS_MAX_CURSORS_TO_CLOSE: raise Exception("too many cursors to close!") self._cursors_to_close[self._num_cursors_to_close] = stmt._cursor_id self._num_cursors_to_close += 1 cdef int _adjust_statement_cache(self) except -1: cdef Statement stmt while len(self._statement_cache) > self._statement_cache_size: stmt = self._statement_cache.popitem(last=False)[1] if stmt._in_use: stmt._return_to_cache = False elif stmt._cursor_id != 0: self._add_cursor_to_close(stmt) cdef int _connect_with_address(self, Address address, Description description, ConnectParamsImpl params, str connect_string, bint raise_exception) except -1: """ Internal method used for connecting with the given description and address. """ try: self._protocol._connect_phase_one(self, params, description, address, connect_string) except exceptions.DatabaseError: if raise_exception: raise return 0 except (socket.gaierror, ConnectionRefusedError) as e: if raise_exception: errors._raise_err(errors.ERR_CONNECTION_FAILED, cause=e, exception=str(e)) return 0 except Exception as e: errors._raise_err(errors.ERR_CONNECTION_FAILED, cause=e, exception=str(e)) self._drcp_enabled = description.server_type == "pooled" if self._cclass is None: self._cclass = description.cclass self._protocol._connect_phase_two(self, description, params) cdef int _connect_with_description(self, Description description, ConnectParamsImpl params, bint final_desc) except -1: """ Internal method used for connecting with the given description. """ cdef: bint load_balance = description.load_balance bint raise_exc = False list address_lists = description.address_lists uint32_t i, j, k, num_addresses, idx1, idx2 uint32_t num_attempts = description.retry_count + 1 uint32_t num_lists = len(address_lists) AddressList address_list str connect_string Address address # Retry connecting to the socket if an attempt fails and retry_count # is specified in the connect string. If an attempt succeeds, return # the socket and the valid address object. connect_string = _get_connect_data(description) for i in range(num_attempts): # iterate through each address_list in the description; if the # description level load_balance is on, keep track of the least # recently used address for subsequent connections; otherwise, # iterate through the list in order; note that if source_route is # enabled that only the first address list is examined as the rest # are used for routing on the server if description.source_route and num_lists > 0: num_lists = 1 for j in range(num_lists): if load_balance: idx1 = (j + description.lru_index) % num_lists else: idx1 = j # iterate through each address in an address_list; if the # address_list level load_balance is on, keep track of the # least recently used address for subsequent connections; # otherwise, iterate through the list in order; note that if # source_route is enabled that only the first address is # examined and the rest are used for routing on the server address_list = address_lists[idx1] num_addresses = len(address_list.addresses) if address_list.source_route and num_addresses > 0: num_addresses = 1 for k in range(num_addresses): if address_list.load_balance: idx2 = (k + address_list.lru_index) % num_addresses else: idx2 = k address = address_list.addresses[idx2] if final_desc: raise_exc = i == num_attempts - 1 \ and j == num_lists - 1 \ and k == num_addresses - 1 self._connect_with_address(address, description, params, connect_string, raise_exc) if self._protocol._in_connect: continue address_list.lru_index = (idx1 + 1) % num_addresses description.lru_index = (idx2 + 1) % num_lists return 0 time.sleep(description.retry_delay) cdef int _connect_with_params(self, ConnectParamsImpl params) except -1: """ Internal method used for connecting with the given parameters. """ cdef: DescriptionList description_list = params.description_list list descriptions = description_list.descriptions ssize_t i, idx, num_descriptions = len(descriptions) Description description bint final_desc = False if description_list.source_route and num_descriptions > 0: num_descriptions = 1 for i in range(num_descriptions): if i == num_descriptions - 1: final_desc = True if description_list.load_balance: idx = (i + description_list.lru_index) % num_descriptions else: idx = i description = descriptions[idx] self._connect_with_description(description, params, final_desc) if not self._protocol._in_connect: description_list.lru_index = (idx + 1) % num_descriptions break cdef Message _create_message(self, type typ): """ Creates a message object that is used to send a request to the database and receive back its response. """ cdef Message message message = typ.__new__(typ) message._initialize(self) return message cdef int _force_close(self) except -1: self._pool = None if self._dbobject_type_cache_num > 0: remove_dbobject_type_cache(self._dbobject_type_cache_num) self._dbobject_type_cache_num = 0 self._protocol._force_close() cdef Statement _get_statement(self, str sql, bint cache_statement): """ Get a statement from the statement cache, or prepare a new statement for use. If a statement is already in use a copy will be made and returned (and will not be returned to the cache). If a statement is being executed for the first time after releasing a DRCP session, a copy will also be made (and will not be returned to the cache) since it is unknown at this point whether the original session or a new session is going to be used. """ cdef Statement statement with self._statement_cache_lock: statement = self._statement_cache.get(sql) if statement is None: statement = Statement() statement._prepare(sql, self._protocol._caps.char_conversion) if len(self._statement_cache) < self._statement_cache_size \ and cache_statement \ and not self._drcp_establish_session: self._statement_cache[sql] = statement statement._return_to_cache = True elif statement._in_use or not cache_statement \ or self._drcp_establish_session: if not cache_statement: del self._statement_cache[sql] statement._return_to_cache = False if statement._in_use or self._drcp_establish_session: statement = statement.copy() else: self._statement_cache.move_to_end(sql) statement._in_use = True return statement cdef int _reset_statement_cache(self) except -1: """ Reset the statement cache. This clears all statements and the list of cursors that need to be cleared. """ with self._statement_cache_lock: self._statement_cache.clear() self._num_cursors_to_close = 0 cdef int _return_statement(self, Statement statement) except -1: """ Return the statement to the statement cache, if applicable. If the statement must not be returned to the statement cache, add the cursor id to the list of cursor ids to close on the next round trip to the database. Clear all bind variables and fetch variables in order to ensure that unnecessary references are not retained. """ cdef: ThinVarImpl var_impl BindInfo bind_info if statement._bind_info_list is not None: for bind_info in statement._bind_info_list: bind_info._bind_var_impl = None if statement._fetch_var_impls is not None: for var_impl in statement._fetch_var_impls: var_impl._values = [None] * var_impl.num_elements with self._statement_cache_lock: if statement._return_to_cache: statement._in_use = False self._statement_cache.move_to_end(statement._sql) self._adjust_statement_cache() elif statement._cursor_id != 0: self._add_cursor_to_close(statement) def cancel(self): self._protocol._break_external() def change_password(self, str old_password, str new_password): cdef AuthMessage message message = self._create_message(AuthMessage) message.password = old_password.encode() message.newpassword = new_password.encode() self._protocol._process_single_message(message) def close(self, bint in_del=False): try: self._protocol._close(self) except (ssl.SSLError, exceptions.DatabaseError): pass def commit(self): cdef Message message message = self._create_message(CommitMessage) self._protocol._process_single_message(message) def connect(self, ConnectParamsImpl params): params._check_credentials() self._connect_with_params(params) self._statement_cache = collections.OrderedDict() self._statement_cache_size = params.stmtcachesize self._statement_cache_lock = threading.Lock() self._dbobject_type_cache_num = create_new_dbobject_type_cache(self) self._cursors_to_close = array.array('I') array.resize(self._cursors_to_close, TNS_MAX_CURSORS_TO_CLOSE) self.invoke_session_callback = True def create_cursor_impl(self): return ThinCursorImpl.__new__(ThinCursorImpl, self) def create_temp_lob_impl(self, DbType dbtype): return ThinLobImpl._create(self, dbtype) def get_call_timeout(self): return self._call_timeout def get_current_schema(self): return self._current_schema def get_edition(self): return self._edition def get_external_name(self): return self._external_name def get_internal_name(self): return self._internal_name def get_is_healthy(self): return self._protocol._socket is not None \ and not self._protocol._read_buf._session_needs_to_be_closed def get_ltxid(self): return self._ltxid or b'' def get_stmt_cache_size(self): return self._statement_cache_size def get_type(self, object conn, str name): cdef ThinDbObjectTypeCache cache = \ get_dbobject_type_cache(self._dbobject_type_cache_num) return cache.get_type(conn, name) def get_version(self): return self._server_version def ping(self): cdef Message message message = self._create_message(PingMessage) self._protocol._process_single_message(message) def rollback(self): cdef Message message message = self._create_message(RollbackMessage) self._protocol._process_single_message(message) def set_action(self, str value): self._action = value self._action_modified = True def set_call_timeout(self, uint32_t value): timeout = None if value == 0 else value / 1000 self._protocol._socket.settimeout(timeout) self._call_timeout = value def set_client_identifier(self, str value): self._client_identifier = value self._client_identifier_modified = True def set_client_info(self, str value): self._client_info = value self._client_info_modified = True def set_current_schema(self, value): self._current_schema = value self._current_schema_modified = True def set_dbop(self, str value): self._dbop = value self._dbop_modified = True def set_external_name(self, value): self._external_name = value def set_internal_name(self, value): self._internal_name = value def set_module(self, str value): self._module = value self._module_modified = True # setting the module by itself results in an error so always force the # action to be set as well (which eliminates this error) self._action_modified = True def set_stmt_cache_size(self, uint32_t value): self._statement_cache_size = value self._adjust_statement_cache() python-oracledb-1.2.1/src/oracledb/impl/thin/constants.pxi000066400000000000000000000511611434177474600236350ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # constants.pxi # # Cython file defining constants used by the thin implementation classes # (embedded in thin_impl.pyx). #------------------------------------------------------------------------------ # packet types DEF TNS_PACKET_TYPE_CONNECT = 1 DEF TNS_PACKET_TYPE_ACCEPT = 2 DEF TNS_PACKET_TYPE_REFUSE = 4 DEF TNS_PACKET_TYPE_DATA = 6 DEF TNS_PACKET_TYPE_RESEND = 11 DEF TNS_PACKET_TYPE_MARKER = 12 DEF TNS_PACKET_TYPE_CONTROL = 14 DEF TNS_PACKET_TYPE_REDIRECT = 5 # packet flags DEF TNS_PACKET_FLAG_TLS_RENEG = 0x08 # data flags DEF TNS_DATA_FLAGS_EOF = 0x0040 # marker types DEF TNS_MARKER_TYPE_BREAK = 1 DEF TNS_MARKER_TYPE_RESET = 2 DEF TNS_MARKER_TYPE_INTERRUPT = 3 # charset forms DEF TNS_CS_IMPLICIT = 1 DEF TNS_CS_NCHAR = 2 # data types DEF TNS_DATA_TYPE_VARCHAR = 1 DEF TNS_DATA_TYPE_NUMBER = 2 DEF TNS_DATA_TYPE_BINARY_INTEGER = 3 DEF TNS_DATA_TYPE_FLOAT = 4 DEF TNS_DATA_TYPE_STR = 5 DEF TNS_DATA_TYPE_VNU = 6 DEF TNS_DATA_TYPE_PDN = 7 DEF TNS_DATA_TYPE_LONG = 8 DEF TNS_DATA_TYPE_VCS = 9 DEF TNS_DATA_TYPE_TIDDEF = 10 DEF TNS_DATA_TYPE_ROWID = 11 DEF TNS_DATA_TYPE_DATE = 12 DEF TNS_DATA_TYPE_VBI = 15 DEF TNS_DATA_TYPE_RAW = 23 DEF TNS_DATA_TYPE_LONG_RAW = 24 DEF TNS_DATA_TYPE_UB2 = 25 DEF TNS_DATA_TYPE_UB4 = 26 DEF TNS_DATA_TYPE_SB1 = 27 DEF TNS_DATA_TYPE_SB2 = 28 DEF TNS_DATA_TYPE_SB4 = 29 DEF TNS_DATA_TYPE_SWORD = 30 DEF TNS_DATA_TYPE_UWORD = 31 DEF TNS_DATA_TYPE_PTRB = 32 DEF TNS_DATA_TYPE_PTRW = 33 DEF TNS_DATA_TYPE_OER8 = 34 + 256 DEF TNS_DATA_TYPE_FUN = 35 + 256 DEF TNS_DATA_TYPE_AUA = 36 + 256 DEF TNS_DATA_TYPE_RXH7 = 37 + 256 DEF TNS_DATA_TYPE_NA6 = 38 + 256 DEF TNS_DATA_TYPE_OAC = 39 DEF TNS_DATA_TYPE_AMS = 40 DEF TNS_DATA_TYPE_BRN = 41 DEF TNS_DATA_TYPE_BRP = 42 + 256 DEF TNS_DATA_TYPE_BRV = 43 + 256 DEF TNS_DATA_TYPE_KVA = 44 + 256 DEF TNS_DATA_TYPE_CLS = 45 + 256 DEF TNS_DATA_TYPE_CUI = 46 + 256 DEF TNS_DATA_TYPE_DFN = 47 + 256 DEF TNS_DATA_TYPE_DQR = 48 + 256 DEF TNS_DATA_TYPE_DSC = 49 + 256 DEF TNS_DATA_TYPE_EXE = 50 + 256 DEF TNS_DATA_TYPE_FCH = 51 + 256 DEF TNS_DATA_TYPE_GBV = 52 + 256 DEF TNS_DATA_TYPE_GEM = 53 + 256 DEF TNS_DATA_TYPE_GIV = 54 + 256 DEF TNS_DATA_TYPE_OKG = 55 + 256 DEF TNS_DATA_TYPE_HMI = 56 + 256 DEF TNS_DATA_TYPE_INO = 57 + 256 DEF TNS_DATA_TYPE_LNF = 59 + 256 DEF TNS_DATA_TYPE_ONT = 60 + 256 DEF TNS_DATA_TYPE_OPE = 61 + 256 DEF TNS_DATA_TYPE_OSQ = 62 + 256 DEF TNS_DATA_TYPE_SFE = 63 + 256 DEF TNS_DATA_TYPE_SPF = 64 + 256 DEF TNS_DATA_TYPE_VSN = 65 + 256 DEF TNS_DATA_TYPE_UD7 = 66 + 256 DEF TNS_DATA_TYPE_DSA = 67 + 256 DEF TNS_DATA_TYPE_UIN = 68 DEF TNS_DATA_TYPE_PIN = 71 + 256 DEF TNS_DATA_TYPE_PFN = 72 + 256 DEF TNS_DATA_TYPE_PPT = 73 + 256 DEF TNS_DATA_TYPE_STO = 75 + 256 DEF TNS_DATA_TYPE_ARC = 77 + 256 DEF TNS_DATA_TYPE_MRS = 78 + 256 DEF TNS_DATA_TYPE_MRT = 79 + 256 DEF TNS_DATA_TYPE_MRG = 80 + 256 DEF TNS_DATA_TYPE_MRR = 81 + 256 DEF TNS_DATA_TYPE_MRC = 82 + 256 DEF TNS_DATA_TYPE_VER = 83 + 256 DEF TNS_DATA_TYPE_LON2 = 84 + 256 DEF TNS_DATA_TYPE_INO2 = 85 + 256 DEF TNS_DATA_TYPE_ALL = 86 + 256 DEF TNS_DATA_TYPE_UDB = 87 + 256 DEF TNS_DATA_TYPE_AQI = 88 + 256 DEF TNS_DATA_TYPE_ULB = 89 + 256 DEF TNS_DATA_TYPE_ULD = 90 + 256 DEF TNS_DATA_TYPE_SLS = 91 DEF TNS_DATA_TYPE_SID = 92 + 256 DEF TNS_DATA_TYPE_NA7 = 93 + 256 DEF TNS_DATA_TYPE_LVC = 94 DEF TNS_DATA_TYPE_LVB = 95 DEF TNS_DATA_TYPE_CHAR = 96 DEF TNS_DATA_TYPE_AVC = 97 DEF TNS_DATA_TYPE_AL7 = 98 + 256 DEF TNS_DATA_TYPE_K2RPC = 99 + 256 DEF TNS_DATA_TYPE_BINARY_FLOAT = 100 DEF TNS_DATA_TYPE_BINARY_DOUBLE = 101 DEF TNS_DATA_TYPE_CURSOR = 102 DEF TNS_DATA_TYPE_RDD = 104 DEF TNS_DATA_TYPE_XDP = 103 + 256 DEF TNS_DATA_TYPE_OSL = 106 DEF TNS_DATA_TYPE_OKO8 = 107 + 256 DEF TNS_DATA_TYPE_EXT_NAMED = 108 DEF TNS_DATA_TYPE_INT_NAMED = 109 DEF TNS_DATA_TYPE_EXT_REF = 110 DEF TNS_DATA_TYPE_INT_REF = 111 DEF TNS_DATA_TYPE_CLOB = 112 DEF TNS_DATA_TYPE_BLOB = 113 DEF TNS_DATA_TYPE_BFILE = 114 DEF TNS_DATA_TYPE_CFILE = 115 DEF TNS_DATA_TYPE_RSET = 116 DEF TNS_DATA_TYPE_CWD = 117 DEF TNS_DATA_TYPE_JSON = 119 DEF TNS_DATA_TYPE_NEW_OAC = 120 DEF TNS_DATA_TYPE_UD12 = 124 + 256 DEF TNS_DATA_TYPE_AL8 = 125 + 256 DEF TNS_DATA_TYPE_LFOP = 126 + 256 DEF TNS_DATA_TYPE_FCRT = 127 + 256 DEF TNS_DATA_TYPE_DNY = 128 + 256 DEF TNS_DATA_TYPE_OPR = 129 + 256 DEF TNS_DATA_TYPE_PLS = 130 + 256 DEF TNS_DATA_TYPE_XID = 131 + 256 DEF TNS_DATA_TYPE_TXN = 132 + 256 DEF TNS_DATA_TYPE_DCB = 133 + 256 DEF TNS_DATA_TYPE_CCA = 134 + 256 DEF TNS_DATA_TYPE_WRN = 135 + 256 DEF TNS_DATA_TYPE_TLH = 137 + 256 DEF TNS_DATA_TYPE_TOH = 138 + 256 DEF TNS_DATA_TYPE_FOI = 139 + 256 DEF TNS_DATA_TYPE_SID2 = 140 + 256 DEF TNS_DATA_TYPE_TCH = 141 + 256 DEF TNS_DATA_TYPE_PII = 142 + 256 DEF TNS_DATA_TYPE_PFI = 143 + 256 DEF TNS_DATA_TYPE_PPU = 144 + 256 DEF TNS_DATA_TYPE_PTE = 145 + 256 DEF TNS_DATA_TYPE_CLV = 146 DEF TNS_DATA_TYPE_RXH8 = 148 + 256 DEF TNS_DATA_TYPE_N12 = 149 + 256 DEF TNS_DATA_TYPE_AUTH = 150 + 256 DEF TNS_DATA_TYPE_KVAL = 151 + 256 DEF TNS_DATA_TYPE_DTR = 152 DEF TNS_DATA_TYPE_DUN = 153 DEF TNS_DATA_TYPE_DOP = 154 DEF TNS_DATA_TYPE_VST = 155 DEF TNS_DATA_TYPE_ODT = 156 DEF TNS_DATA_TYPE_FGI = 157 + 256 DEF TNS_DATA_TYPE_DSY = 158 + 256 DEF TNS_DATA_TYPE_DSYR8 = 159 + 256 DEF TNS_DATA_TYPE_DSYH8 = 160 + 256 DEF TNS_DATA_TYPE_DSYL = 161 + 256 DEF TNS_DATA_TYPE_DSYT8 = 162 + 256 DEF TNS_DATA_TYPE_DSYV8 = 163 + 256 DEF TNS_DATA_TYPE_DSYP = 164 + 256 DEF TNS_DATA_TYPE_DSYF = 165 + 256 DEF TNS_DATA_TYPE_DSYK = 166 + 256 DEF TNS_DATA_TYPE_DSYY = 167 + 256 DEF TNS_DATA_TYPE_DSYQ = 168 + 256 DEF TNS_DATA_TYPE_DSYC = 169 + 256 DEF TNS_DATA_TYPE_DSYA = 170 + 256 DEF TNS_DATA_TYPE_OT8 = 171 + 256 DEF TNS_DATA_TYPE_DOL = 172 DEF TNS_DATA_TYPE_DSYTY = 173 + 256 DEF TNS_DATA_TYPE_AQE = 174 + 256 DEF TNS_DATA_TYPE_KV = 175 + 256 DEF TNS_DATA_TYPE_AQD = 176 + 256 DEF TNS_DATA_TYPE_AQ8 = 177 + 256 DEF TNS_DATA_TYPE_TIME = 178 DEF TNS_DATA_TYPE_TIME_TZ = 179 DEF TNS_DATA_TYPE_TIMESTAMP = 180 DEF TNS_DATA_TYPE_TIMESTAMP_TZ = 181 DEF TNS_DATA_TYPE_INTERVAL_YM = 182 DEF TNS_DATA_TYPE_INTERVAL_DS = 183 DEF TNS_DATA_TYPE_EDATE = 184 DEF TNS_DATA_TYPE_ETIME = 185 DEF TNS_DATA_TYPE_ETTZ = 186 DEF TNS_DATA_TYPE_ESTAMP = 187 DEF TNS_DATA_TYPE_ESTZ = 188 DEF TNS_DATA_TYPE_EIYM = 189 DEF TNS_DATA_TYPE_EIDS = 190 DEF TNS_DATA_TYPE_RFS = 193 + 256 DEF TNS_DATA_TYPE_RXH10 = 194 + 256 DEF TNS_DATA_TYPE_DCLOB = 195 DEF TNS_DATA_TYPE_DBLOB = 196 DEF TNS_DATA_TYPE_DBFILE = 197 DEF TNS_DATA_TYPE_DJSON = 198 DEF TNS_DATA_TYPE_KPN = 198 + 256 DEF TNS_DATA_TYPE_KPDNR = 199 + 256 DEF TNS_DATA_TYPE_DSYD = 200 + 256 DEF TNS_DATA_TYPE_DSYS = 201 + 256 DEF TNS_DATA_TYPE_DSYR = 202 + 256 DEF TNS_DATA_TYPE_DSYH = 203 + 256 DEF TNS_DATA_TYPE_DSYT = 204 + 256 DEF TNS_DATA_TYPE_DSYV = 205 + 256 DEF TNS_DATA_TYPE_AQM = 206 + 256 DEF TNS_DATA_TYPE_OER11 = 207 + 256 DEF TNS_DATA_TYPE_UROWID = 208 DEF TNS_DATA_TYPE_AQL = 210 + 256 DEF TNS_DATA_TYPE_OTC = 211 + 256 DEF TNS_DATA_TYPE_KFNO = 212 + 256 DEF TNS_DATA_TYPE_KFNP = 213 + 256 DEF TNS_DATA_TYPE_KGT8 = 214 + 256 DEF TNS_DATA_TYPE_RASB4 = 215 + 256 DEF TNS_DATA_TYPE_RAUB2 = 216 + 256 DEF TNS_DATA_TYPE_RAUB1 = 217 + 256 DEF TNS_DATA_TYPE_RATXT = 218 + 256 DEF TNS_DATA_TYPE_RSSB4 = 219 + 256 DEF TNS_DATA_TYPE_RSUB2 = 220 + 256 DEF TNS_DATA_TYPE_RSUB1 = 221 + 256 DEF TNS_DATA_TYPE_RSTXT = 222 + 256 DEF TNS_DATA_TYPE_RIDL = 223 + 256 DEF TNS_DATA_TYPE_GLRDD = 224 + 256 DEF TNS_DATA_TYPE_GLRDG = 225 + 256 DEF TNS_DATA_TYPE_GLRDC = 226 + 256 DEF TNS_DATA_TYPE_OKO = 227 + 256 DEF TNS_DATA_TYPE_DPP = 228 + 256 DEF TNS_DATA_TYPE_DPLS = 229 + 256 DEF TNS_DATA_TYPE_DPMOP = 230 + 256 DEF TNS_DATA_TYPE_TIMESTAMP_LTZ = 231 DEF TNS_DATA_TYPE_ESITZ = 232 DEF TNS_DATA_TYPE_UB8 = 233 DEF TNS_DATA_TYPE_STAT = 234 + 256 DEF TNS_DATA_TYPE_RFX = 235 + 256 DEF TNS_DATA_TYPE_FAL = 236 + 256 DEF TNS_DATA_TYPE_CKV = 237 + 256 DEF TNS_DATA_TYPE_DRCX = 238 + 256 DEF TNS_DATA_TYPE_KGH = 239 + 256 DEF TNS_DATA_TYPE_AQO = 240 + 256 DEF TNS_DATA_TYPE_PNTY = 241 DEF TNS_DATA_TYPE_OKGT = 242 + 256 DEF TNS_DATA_TYPE_KPFC = 243 + 256 DEF TNS_DATA_TYPE_FE2 = 244 + 256 DEF TNS_DATA_TYPE_SPFP = 245 + 256 DEF TNS_DATA_TYPE_DPULS = 246 + 256 DEF TNS_DATA_TYPE_BOOLEAN = 252 DEF TNS_DATA_TYPE_AQA = 253 + 256 DEF TNS_DATA_TYPE_KPBF = 254 + 256 DEF TNS_DATA_TYPE_TSM = 513 DEF TNS_DATA_TYPE_MSS = 514 DEF TNS_DATA_TYPE_KPC = 516 DEF TNS_DATA_TYPE_CRS = 517 DEF TNS_DATA_TYPE_KKS = 518 DEF TNS_DATA_TYPE_KSP = 519 DEF TNS_DATA_TYPE_KSPTOP = 520 DEF TNS_DATA_TYPE_KSPVAL = 521 DEF TNS_DATA_TYPE_PSS = 522 DEF TNS_DATA_TYPE_NLS = 523 DEF TNS_DATA_TYPE_ALS = 524 DEF TNS_DATA_TYPE_KSDEVTVAL = 525 DEF TNS_DATA_TYPE_KSDEVTTOP = 526 DEF TNS_DATA_TYPE_KPSPP = 527 DEF TNS_DATA_TYPE_KOL = 528 DEF TNS_DATA_TYPE_LST = 529 DEF TNS_DATA_TYPE_ACX = 530 DEF TNS_DATA_TYPE_SCS = 531 DEF TNS_DATA_TYPE_RXH = 532 DEF TNS_DATA_TYPE_KPDNS = 533 DEF TNS_DATA_TYPE_KPDCN = 534 DEF TNS_DATA_TYPE_KPNNS = 535 DEF TNS_DATA_TYPE_KPNCN = 536 DEF TNS_DATA_TYPE_KPS = 537 DEF TNS_DATA_TYPE_APINF = 538 DEF TNS_DATA_TYPE_TEN = 539 DEF TNS_DATA_TYPE_XSSCS = 540 DEF TNS_DATA_TYPE_XSSSO = 541 DEF TNS_DATA_TYPE_XSSAO = 542 DEF TNS_DATA_TYPE_KSRPC = 543 DEF TNS_DATA_TYPE_KVL = 560 DEF TNS_DATA_TYPE_SESSGET = 563 DEF TNS_DATA_TYPE_SESSREL = 564 DEF TNS_DATA_TYPE_XSSDEF = 565 DEF TNS_DATA_TYPE_PDQCINV = 572 DEF TNS_DATA_TYPE_PDQIDC = 573 DEF TNS_DATA_TYPE_KPDQCSTA = 574 DEF TNS_DATA_TYPE_KPRS = 575 DEF TNS_DATA_TYPE_KPDQIDC = 576 DEF TNS_DATA_TYPE_RTSTRM = 578 DEF TNS_DATA_TYPE_SESSRET = 579 DEF TNS_DATA_TYPE_SCN6 = 580 DEF TNS_DATA_TYPE_KECPA = 581 DEF TNS_DATA_TYPE_KECPP = 582 DEF TNS_DATA_TYPE_SXA = 583 DEF TNS_DATA_TYPE_KVARR = 584 DEF TNS_DATA_TYPE_KPNGN = 585 DEF TNS_DATA_TYPE_XSNSOP = 590 DEF TNS_DATA_TYPE_XSATTR = 591 DEF TNS_DATA_TYPE_XSNS = 592 DEF TNS_DATA_TYPE_TXT = 593 DEF TNS_DATA_TYPE_XSSESSNS = 594 DEF TNS_DATA_TYPE_XSATTOP = 595 DEF TNS_DATA_TYPE_XSCREOP = 596 DEF TNS_DATA_TYPE_XSDETOP = 597 DEF TNS_DATA_TYPE_XSDESOP = 598 DEF TNS_DATA_TYPE_XSSETSP = 599 DEF TNS_DATA_TYPE_XSSIDP = 600 DEF TNS_DATA_TYPE_XSPRIN = 601 DEF TNS_DATA_TYPE_XSKVL = 602 DEF TNS_DATA_TYPE_XSSSDEF2 = 603 DEF TNS_DATA_TYPE_XSNSOP2 = 604 DEF TNS_DATA_TYPE_XSNS2 = 605 DEF TNS_DATA_TYPE_IMPLRES = 611 DEF TNS_DATA_TYPE_OER = 612 DEF TNS_DATA_TYPE_UB1ARRAY = 613 DEF TNS_DATA_TYPE_SESSSTATE = 614 DEF TNS_DATA_TYPE_AC_REPLAY = 615 DEF TNS_DATA_TYPE_AC_CONT = 616 DEF TNS_DATA_TYPE_KPDNREQ = 622 DEF TNS_DATA_TYPE_KPDNRNF = 623 DEF TNS_DATA_TYPE_KPNGNC = 624 DEF TNS_DATA_TYPE_KPNRI = 625 DEF TNS_DATA_TYPE_AQENQ = 626 DEF TNS_DATA_TYPE_AQDEQ = 627 DEF TNS_DATA_TYPE_AQJMS = 628 DEF TNS_DATA_TYPE_KPDNRPAY = 629 DEF TNS_DATA_TYPE_KPDNRACK = 630 DEF TNS_DATA_TYPE_KPDNRMP = 631 DEF TNS_DATA_TYPE_KPDNRDQ = 632 DEF TNS_DATA_TYPE_CHUNKINFO = 636 DEF TNS_DATA_TYPE_SCN = 637 DEF TNS_DATA_TYPE_SCN8 = 638 DEF TNS_DATA_TYPE_UDS = 639 DEF TNS_DATA_TYPE_TNP = 640 # data type representations DEF TNS_TYPE_REP_NATIVE = 0 DEF TNS_TYPE_REP_UNIVERSAL = 1 DEF TNS_TYPE_REP_ORACLE = 10 # errors DEF TNS_ERR_VAR_NOT_IN_SELECT_LIST = 1007 DEF TNS_ERR_INBAND_MESSAGE = 12573 DEF TNS_ERR_INVALID_SERVICE_NAME = 12514 DEF TNS_ERR_INVALID_SID = 12505 DEF TNS_ERR_NO_DATA_FOUND = 1403 DEF TNS_ERR_SESSION_SHUTDOWN = 12572 DEF TNS_ERR_ARRAY_DML_ERRORS = 24381 # message types DEF TNS_MSG_TYPE_PROTOCOL = 1 DEF TNS_MSG_TYPE_DATA_TYPES = 2 DEF TNS_MSG_TYPE_FUNCTION = 3 DEF TNS_MSG_TYPE_ERROR = 4 DEF TNS_MSG_TYPE_ROW_HEADER = 6 DEF TNS_MSG_TYPE_ROW_DATA = 7 DEF TNS_MSG_TYPE_PARAMETER = 8 DEF TNS_MSG_TYPE_STATUS = 9 DEF TNS_MSG_TYPE_IO_VECTOR = 11 DEF TNS_MSG_TYPE_LOB_DATA = 14 DEF TNS_MSG_TYPE_WARNING = 15 DEF TNS_MSG_TYPE_DESCRIBE_INFO = 16 DEF TNS_MSG_TYPE_PIGGYBACK = 17 DEF TNS_MSG_TYPE_FLUSH_OUT_BINDS = 19 DEF TNS_MSG_TYPE_BIT_VECTOR = 21 DEF TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK = 23 DEF TNS_MSG_TYPE_ONEWAY_FN = 26 DEF TNS_MSG_TYPE_IMPLICIT_RESULTSET = 27 # parameter keyword numbers DEF TNS_KEYWORD_NUM_CURRENT_SCHEMA = 168 DEF TNS_KEYWORD_NUM_EDITION = 172 # bind flags DEF TNS_BIND_USE_INDICATORS = 0x0001 DEF TNS_BIND_ARRAY = 0x0040 # bind directions DEF TNS_BIND_DIR_OUTPUT = 16 DEF TNS_BIND_DIR_INPUT = 32 DEF TNS_BIND_DIR_INPUT_OUTPUT = 48 # database object image flags DEF TNS_OBJ_IS_VERSION_81 = 0x80 DEF TNS_OBJ_IS_DEGENERATE = 0x10 DEF TNS_OBJ_IS_COLLECTION = 0x08 DEF TNS_OBJ_NO_PREFIX_SEG = 0x04 DEF TNS_OBJ_IMAGE_VERSION = 1 # database object flags DEF TNS_OBJ_MAX_SHORT_LENGTH = 245 DEF TNS_OBJ_ATOMIC_NULL = 253 DEF TNS_OBJ_NON_NULL_OID = 0x02 DEF TNS_OBJ_HAS_EXTENT_OID = 0x08 DEF TNS_OBJ_TOP_LEVEL = 0x01 DEF TNS_OBJ_HAS_INDEXES = 0x10 # database object collection types DEF TNS_OBJ_PLSQL_INDEX_TABLE = 1 DEF TNS_OBJ_NESTED_TABLE = 2 DEF TNS_OBJ_VARRAY = 3 # database object TDS type codes DEF TNS_OBJ_TDS_TYPE_CHAR = 1 DEF TNS_OBJ_TDS_TYPE_DATE = 2 DEF TNS_OBJ_TDS_TYPE_FLOAT = 5 DEF TNS_OBJ_TDS_TYPE_NUMBER = 6 DEF TNS_OBJ_TDS_TYPE_VARCHAR = 7 DEF TNS_OBJ_TDS_TYPE_BOOLEAN = 8 DEF TNS_OBJ_TDS_TYPE_RAW = 19 DEF TNS_OBJ_TDS_TYPE_TIMESTAMP = 21 DEF TNS_OBJ_TDS_TYPE_TIMESTAMP_TZ = 23 DEF TNS_OBJ_TDS_TYPE_OBJ = 27 DEF TNS_OBJ_TDS_TYPE_COLL = 28 DEF TNS_OBJ_TDS_TYPE_CLOB = 29 DEF TNS_OBJ_TDS_TYPE_BLOB = 30 DEF TNS_OBJ_TDS_TYPE_TIMESTAMP_LTZ = 33 DEF TNS_OBJ_TDS_TYPE_BINARY_FLOAT = 37 DEF TNS_OBJ_TDS_TYPE_BINARY_DOUBLE = 45 # xml type constants DEF TNS_XML_TYPE_LOB = 0x0001 DEF TNS_XML_TYPE_STRING = 0x0004 DEF TNS_XML_TYPE_FLAG_SKIP_NEXT_4 = 0x100000 # execute options DEF TNS_EXEC_OPTION_PARSE = 0x01 DEF TNS_EXEC_OPTION_BIND = 0x08 DEF TNS_EXEC_OPTION_DEFINE = 0x10 DEF TNS_EXEC_OPTION_EXECUTE = 0x20 DEF TNS_EXEC_OPTION_FETCH = 0x40 DEF TNS_EXEC_OPTION_COMMIT = 0x100 DEF TNS_EXEC_OPTION_COMMIT_REEXECUTE = 0x1 DEF TNS_EXEC_OPTION_PLSQL_BIND = 0x400 DEF TNS_EXEC_OPTION_DML_ROWCOUNTS = 0x4000 DEF TNS_EXEC_OPTION_NOT_PLSQL = 0x8000 DEF TNS_EXEC_OPTION_IMPLICIT_RESULTSET = 0x8000 DEF TNS_EXEC_OPTION_DESCRIBE = 0x20000 DEF TNS_EXEC_OPTION_NO_COMPRESSED_FETCH = 0x40000 DEF TNS_EXEC_OPTION_BATCH_ERRORS = 0x80000 # server side piggyback op codes DEF TNS_SERVER_PIGGYBACK_QUERY_CACHE_INVALIDATION = 1 DEF TNS_SERVER_PIGGYBACK_OS_PID_MTS = 2 DEF TNS_SERVER_PIGGYBACK_TRACE_EVENT = 3 DEF TNS_SERVER_PIGGYBACK_SESS_RET = 4 DEF TNS_SERVER_PIGGYBACK_SYNC = 5 DEF TNS_SERVER_PIGGYBACK_LTXID = 7 DEF TNS_SERVER_PIGGYBACK_AC_REPLAY_CONTEXT = 8 DEF TNS_SERVER_PIGGYBACK_EXT_SYNC = 9 # session return constants DEF TNS_SESSGET_SESSION_CHANGED = 4 # LOB operations DEF TNS_LOB_OP_GET_LENGTH = 0x0001 DEF TNS_LOB_OP_READ = 0x0002 DEF TNS_LOB_OP_TRIM = 0x0020 DEF TNS_LOB_OP_WRITE = 0x0040 DEF TNS_LOB_OP_GET_CHUNK_SIZE = 0x4000 DEF TNS_LOB_OP_CREATE_TEMP = 0x0110 DEF TNS_LOB_OP_FREE_TEMP = 0x0111 DEF TNS_LOB_OP_OPEN = 0x8000 DEF TNS_LOB_OP_CLOSE = 0x10000 DEF TNS_LOB_OP_IS_OPEN = 0x11000 DEF TNS_LOB_OP_ARRAY = 0x80000 # LOB locator constants DEF TNS_LOB_LOCATOR_OFFSET_FLAG_3 = 6 DEF TNS_LOB_LOCATOR_VAR_LENGTH_CHARSET = 0x80 # Temporary and Abstract LOB constants DEF TNS_LOB_ABSTRACT_POS = 4 DEF TNS_LOB_TEMP_POS = 7 DEF TNS_LOB_TEMP_VALUE = 0x01 DEF TNS_LOB_ABSTRACT_VALUE = 0x40 # other LOB constants DEF TNS_LOB_OPEN_READ_WRITE = 2 # end-to-end metrics DEF TNS_END_TO_END_ACTION = 0x0010 DEF TNS_END_TO_END_CLIENT_IDENTIFIER = 0x0001 DEF TNS_END_TO_END_CLIENT_INFO = 0x0100 DEF TNS_END_TO_END_DBOP = 0x0200 DEF TNS_END_TO_END_MODULE = 0x0008 # versions DEF TNS_VERSION_DESIRED = 318 DEF TNS_VERSION_MINIMUM = 300 DEF TNS_VERSION_MIN_ACCEPTED = 315 # 12.1 DEF TNS_VERSION_MIN_LARGE_SDU = 315 DEF TNS_VERSION_MIN_OOB_CHECK = 318 # control packet types DEF TNS_CONTROL_TYPE_INBAND_NOTIFICATION = 8 DEF TNS_CONTROL_TYPE_RESET_OOB = 9 # other connection constants DEF TNS_BASE_SERVICE_OPTIONS = 0x801 DEF TNS_PROTOCOL_CHARACTERISTICS = 0x4f98 DEF TNS_CONNECT_FLAGS = 0x8080 DEF TNS_CAN_RECV_ATTENTION = 0x0400 DEF TNS_CHECK_OOB = 0x01 # TTC functions DEF TNS_FUNC_AUTH_PHASE_ONE = 118 DEF TNS_FUNC_AUTH_PHASE_TWO = 115 DEF TNS_FUNC_CLOSE_CURSORS = 105 DEF TNS_FUNC_COMMIT = 14 DEF TNS_FUNC_EXECUTE = 94 DEF TNS_FUNC_FETCH = 5 DEF TNS_FUNC_LOB_OP = 96 DEF TNS_FUNC_LOGOFF = 9 DEF TNS_FUNC_PING = 147 DEF TNS_FUNC_ROLLBACK = 15 DEF TNS_FUNC_SET_END_TO_END_ATTR = 135 DEF TNS_FUNC_REEXECUTE = 4 DEF TNS_FUNC_REEXECUTE_AND_FETCH = 78 DEF TNS_FUNC_SESSION_GET = 162 DEF TNS_FUNC_SESSION_RELEASE = 163 DEF TNS_FUNC_SET_SCHEMA = 152 # TTC authentication modes DEF TNS_AUTH_MODE_LOGON = 0x00000001 DEF TNS_AUTH_MODE_CHANGE_PASSWORD = 0x00000002 DEF TNS_AUTH_MODE_SYSDBA = 0x00000020 DEF TNS_AUTH_MODE_SYSOPER = 0x00000040 DEF TNS_AUTH_MODE_PRELIM = 0x00000080 DEF TNS_AUTH_MODE_WITH_PASSWORD = 0x00000100 DEF TNS_AUTH_MODE_SYSASM = 0x00400000 DEF TNS_AUTH_MODE_SYSBKP = 0x01000000 DEF TNS_AUTH_MODE_SYSDGD = 0x02000000 DEF TNS_AUTH_MODE_SYSKMT = 0x04000000 DEF TNS_AUTH_MODE_SYSRAC = 0x08000000 DEF TNS_AUTH_MODE_IAM_TOKEN = 0x20000000 # character sets and encodings DEF TNS_CHARSET_UTF8 = 873 DEF TNS_CHARSET_UTF16 = 2000 DEF TNS_ENCODING_UTF8 = "UTF-8" DEF TNS_ENCODING_UTF16 = "UTF-16BE" # compile time capability indices DEF TNS_CCAP_SQL_VERSION = 0 DEF TNS_CCAP_LOGON_TYPES = 4 DEF TNS_CCAP_FIELD_VERSION = 7 DEF TNS_CCAP_SERVER_DEFINE_CONV = 8 DEF TNS_CCAP_TTC1 = 15 DEF TNS_CCAP_OCI1 = 16 DEF TNS_CCAP_TDS_VERSION = 17 DEF TNS_CCAP_RPC_VERSION = 18 DEF TNS_CCAP_RPC_SIG = 19 DEF TNS_CCAP_DBF_VERSION = 21 DEF TNS_CCAP_LOB = 23 DEF TNS_CCAP_TTC2 = 26 DEF TNS_CCAP_UB2_DTY = 27 DEF TNS_CCAP_OCI2 = 31 DEF TNS_CCAP_CLIENT_FN = 34 DEF TNS_CCAP_TTC3 = 37 DEF TNS_CCAP_TTC4 = 40 DEF TNS_CCAP_MAX = 45 # compile time capability values DEF TNS_CCAP_SQL_VERSION_MAX = 6 DEF TNS_CCAP_FIELD_VERSION_11_2 = 6 DEF TNS_CCAP_FIELD_VERSION_12_1 = 7 DEF TNS_CCAP_FIELD_VERSION_12_2 = 8 DEF TNS_CCAP_FIELD_VERSION_12_2_EXT1 = 9 DEF TNS_CCAP_FIELD_VERSION_18_1 = 10 DEF TNS_CCAP_FIELD_VERSION_18_1_EXT_1 = 11 DEF TNS_CCAP_FIELD_VERSION_19_1 = 12 DEF TNS_CCAP_FIELD_VERSION_19_1_EXT_1 = 13 DEF TNS_CCAP_FIELD_VERSION_20_1 = 14 DEF TNS_CCAP_FIELD_VERSION_20_1_EXT_1 = 15 DEF TNS_CCAP_FIELD_VERSION_21_1 = 16 DEF TNS_CCAP_FIELD_VERSION_MAX = 16 DEF TNS_CCAP_O5LOGON = 8 DEF TNS_CCAP_O5LOGON_NP = 2 DEF TNS_CCAP_O7LOGON = 32 DEF TNS_CCAP_O8LOGON_LONG_IDENTIFIER = 64 DEF TNS_CCAP_END_OF_CALL_STATUS = 0x01 DEF TNS_CCAP_IND_RCD = 0x08 DEF TNS_CCAP_FAST_BVEC = 0x20 DEF TNS_CCAP_FAST_SESSION_PROPAGATE = 0x10 DEF TNS_CCAP_APP_CTX_PIGGYBACK = 0x80 DEF TNS_CCAP_TDS_VERSION_MAX = 3 DEF TNS_CCAP_RPC_VERSION_MAX = 7 DEF TNS_CCAP_RPC_SIG_VALUE = 3 DEF TNS_CCAP_DBF_VERSION_MAX = 1 DEF TNS_CCAP_IMPLICIT_RESULTS = 0x10 DEF TNS_CCAP_BIG_CHUNK_CLR = 0x20 DEF TNS_CCAP_KEEP_OUT_ORDER = 0x80 DEF TNS_CCAP_LOB_UB8_SIZE = 0x01 DEF TNS_CCAP_LOB_ENCS = 0x02 DEF TNS_CCAP_DRCP = 0x10 DEF TNS_CCAP_ZLNP = 0x04 DEF TNS_CCAP_INBAND_NOTIFICATION = 0x04 DEF TNS_CCAP_CLIENT_FN_MAX = 12 # runtime capability indices DEF TNS_RCAP_COMPAT = 0 DEF TNS_RCAP_TTC = 6 DEF TNS_RCAP_MAX = 7 # runtime capability values DEF TNS_RCAP_COMPAT_81 = 2 DEF TNS_RCAP_TTC_ZERO_COPY = 0x01 DEF TNS_RCAP_TTC_32K = 0x04 # verifier types DEF TNS_VERIFIER_TYPE_11G_1 = 0xb152 DEF TNS_VERIFIER_TYPE_11G_2 = 0x1b25 DEF TNS_VERIFIER_TYPE_12C = 0x4815 # other constants DEF TNS_MAX_SHORT_LENGTH = 252 DEF TNS_ESCAPE_CHAR = 253 DEF TNS_LONG_LENGTH_INDICATOR = 254 DEF TNS_NULL_LENGTH_INDICATOR = 255 DEF TNS_MAX_ROWID_LENGTH = 18 DEF TNS_DURATION_MID = 0x80000000 DEF TNS_DURATION_OFFSET = 60 DEF TNS_DURATION_SESSION = 10 DEF TNS_MIN_LONG_LENGTH = 0x8000 DEF TNS_MAX_LONG_LENGTH = 0x7fffffff DEF TNS_SDU = 8192 DEF TNS_TDU = 65535 DEF TNS_MAX_CURSORS_TO_CLOSE = 500 DEF TNS_TXN_IN_PROGRESS = 0x00000002 DEF TNS_MAX_CONNECT_DATA = 230 DEF TNS_CHUNK_SIZE = 32767 DEF TNS_MAX_UROWID_LENGTH = 5267 # base 64 encoding alphabet DEF TNS_BASE64_ALPHABET = \ b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' cdef bytearray TNS_BASE64_ALPHABET_ARRAY = \ bytearray(TNS_BASE64_ALPHABET) cdef bytes TNS_EXTENT_OID = bytes.fromhex('00000000000000000000000000010001') # purity types DEF PURITY_DEFAULT = 0 DEF PURITY_NEW = 1 DEF PURITY_SELF = 2 # timezone offsets DEF TZ_HOUR_OFFSET = 20 DEF TZ_MINUTE_OFFSET = 60 # drcp release mode DEF DRCP_DEAUTHENTICATE = 0x00000002 python-oracledb-1.2.1/src/oracledb/impl/thin/conversions.pyx000066400000000000000000000170011434177474600242040ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # conversions.pyx # # Cython file defining the conversions between data types that are supported by # the thin client (embedded in thin_impl.pyx). #------------------------------------------------------------------------------ cdef converter_dict = { (TNS_DATA_TYPE_CHAR, TNS_DATA_TYPE_NUMBER): float, (TNS_DATA_TYPE_VARCHAR, TNS_DATA_TYPE_NUMBER): float, (TNS_DATA_TYPE_LONG, TNS_DATA_TYPE_NUMBER): float, (TNS_DATA_TYPE_BINARY_INTEGER, TNS_DATA_TYPE_NUMBER):float, (TNS_DATA_TYPE_CHAR, TNS_DATA_TYPE_BINARY_INTEGER):_to_binary_int, (TNS_DATA_TYPE_VARCHAR, TNS_DATA_TYPE_BINARY_INTEGER): _to_binary_int, (TNS_DATA_TYPE_LONG, TNS_DATA_TYPE_BINARY_INTEGER): _to_binary_int, (TNS_DATA_TYPE_NUMBER, TNS_DATA_TYPE_BINARY_INTEGER): _to_binary_int, (TNS_DATA_TYPE_BINARY_FLOAT, TNS_DATA_TYPE_BINARY_INTEGER): _to_binary_int, (TNS_DATA_TYPE_BINARY_DOUBLE, TNS_DATA_TYPE_BINARY_INTEGER): _to_binary_int, (TNS_DATA_TYPE_DATE, TNS_DATA_TYPE_CHAR): str, (TNS_DATA_TYPE_DATE, TNS_DATA_TYPE_VARCHAR): str, (TNS_DATA_TYPE_DATE, TNS_DATA_TYPE_LONG): str, (TNS_DATA_TYPE_NUMBER, TNS_DATA_TYPE_VARCHAR): NUM_TYPE_STR, (TNS_DATA_TYPE_NUMBER, TNS_DATA_TYPE_CHAR): NUM_TYPE_STR, (TNS_DATA_TYPE_NUMBER, TNS_DATA_TYPE_LONG): NUM_TYPE_STR, (TNS_DATA_TYPE_BINARY_DOUBLE, TNS_DATA_TYPE_VARCHAR): str, (TNS_DATA_TYPE_BINARY_FLOAT, TNS_DATA_TYPE_VARCHAR): str, (TNS_DATA_TYPE_BINARY_DOUBLE, TNS_DATA_TYPE_CHAR): str, (TNS_DATA_TYPE_BINARY_FLOAT, TNS_DATA_TYPE_CHAR): str, (TNS_DATA_TYPE_BINARY_DOUBLE, TNS_DATA_TYPE_LONG): str, (TNS_DATA_TYPE_BINARY_FLOAT, TNS_DATA_TYPE_LONG): str, (TNS_DATA_TYPE_TIMESTAMP, TNS_DATA_TYPE_VARCHAR): str, (TNS_DATA_TYPE_TIMESTAMP, TNS_DATA_TYPE_CHAR): str, (TNS_DATA_TYPE_TIMESTAMP, TNS_DATA_TYPE_LONG): str, (TNS_DATA_TYPE_TIMESTAMP_TZ, TNS_DATA_TYPE_VARCHAR): str, (TNS_DATA_TYPE_TIMESTAMP_TZ, TNS_DATA_TYPE_CHAR): str, (TNS_DATA_TYPE_TIMESTAMP_TZ, TNS_DATA_TYPE_LONG): str, (TNS_DATA_TYPE_TIMESTAMP_LTZ, TNS_DATA_TYPE_VARCHAR): str, (TNS_DATA_TYPE_TIMESTAMP_LTZ, TNS_DATA_TYPE_CHAR): str, (TNS_DATA_TYPE_TIMESTAMP_LTZ, TNS_DATA_TYPE_LONG): str, (TNS_DATA_TYPE_ROWID, TNS_DATA_TYPE_VARCHAR): str, (TNS_DATA_TYPE_ROWID, TNS_DATA_TYPE_CHAR): str, (TNS_DATA_TYPE_ROWID, TNS_DATA_TYPE_LONG): str, (TNS_DATA_TYPE_INTERVAL_DS, TNS_DATA_TYPE_VARCHAR): str, (TNS_DATA_TYPE_INTERVAL_DS, TNS_DATA_TYPE_CHAR): str, (TNS_DATA_TYPE_INTERVAL_DS, TNS_DATA_TYPE_LONG): str, (TNS_DATA_TYPE_BINARY_INTEGER, TNS_DATA_TYPE_VARCHAR): str, (TNS_DATA_TYPE_BINARY_INTEGER, TNS_DATA_TYPE_CHAR): str, (TNS_DATA_TYPE_BINARY_INTEGER, TNS_DATA_TYPE_LONG): str, (TNS_DATA_TYPE_TIMESTAMP, TNS_DATA_TYPE_DATE): _tstamp_to_date, (TNS_DATA_TYPE_TIMESTAMP_TZ, TNS_DATA_TYPE_DATE): _tstamp_to_date, (TNS_DATA_TYPE_TIMESTAMP_LTZ, TNS_DATA_TYPE_DATE): _tstamp_to_date, (TNS_DATA_TYPE_NUMBER, TNS_DATA_TYPE_BINARY_DOUBLE): NUM_TYPE_FLOAT, (TNS_DATA_TYPE_BINARY_FLOAT, TNS_DATA_TYPE_BINARY_DOUBLE): float, (TNS_DATA_TYPE_CHAR, TNS_DATA_TYPE_BINARY_DOUBLE): float, (TNS_DATA_TYPE_VARCHAR, TNS_DATA_TYPE_BINARY_DOUBLE): float, (TNS_DATA_TYPE_LONG, TNS_DATA_TYPE_BINARY_DOUBLE): float, (TNS_DATA_TYPE_NUMBER, TNS_DATA_TYPE_BINARY_FLOAT): NUM_TYPE_FLOAT, (TNS_DATA_TYPE_BINARY_DOUBLE, TNS_DATA_TYPE_BINARY_FLOAT): float, (TNS_DATA_TYPE_CHAR, TNS_DATA_TYPE_BINARY_FLOAT): float, (TNS_DATA_TYPE_VARCHAR, TNS_DATA_TYPE_BINARY_FLOAT): float, (TNS_DATA_TYPE_LONG, TNS_DATA_TYPE_BINARY_FLOAT): float, (TNS_DATA_TYPE_BINARY_FLOAT, TNS_DATA_TYPE_NUMBER): float, (TNS_DATA_TYPE_BINARY_DOUBLE, TNS_DATA_TYPE_NUMBER): float, (TNS_DATA_TYPE_BLOB, TNS_DATA_TYPE_LONG_RAW): TNS_DATA_TYPE_LONG_RAW, (TNS_DATA_TYPE_BLOB, TNS_DATA_TYPE_RAW): TNS_DATA_TYPE_LONG_RAW, (TNS_DATA_TYPE_CLOB, TNS_DATA_TYPE_CHAR): TNS_DATA_TYPE_LONG, (TNS_DATA_TYPE_CLOB, TNS_DATA_TYPE_VARCHAR): TNS_DATA_TYPE_LONG, (TNS_DATA_TYPE_CLOB, TNS_DATA_TYPE_LONG): TNS_DATA_TYPE_LONG, (TNS_DATA_TYPE_JSON, TNS_DATA_TYPE_VARCHAR): TNS_DATA_TYPE_LONG, (TNS_DATA_TYPE_JSON, TNS_DATA_TYPE_CHAR): TNS_DATA_TYPE_LONG, (TNS_DATA_TYPE_JSON, TNS_DATA_TYPE_RAW): TNS_DATA_TYPE_LONG_RAW, (TNS_DATA_TYPE_TIMESTAMP_TZ, TNS_DATA_TYPE_TIMESTAMP_LTZ): None, (TNS_DATA_TYPE_TIMESTAMP_TZ, TNS_DATA_TYPE_TIMESTAMP): None, (TNS_DATA_TYPE_TIMESTAMP_LTZ, TNS_DATA_TYPE_TIMESTAMP_TZ): None, (TNS_DATA_TYPE_TIMESTAMP_LTZ, TNS_DATA_TYPE_TIMESTAMP): None, (TNS_DATA_TYPE_TIMESTAMP, TNS_DATA_TYPE_TIMESTAMP_LTZ): None, (TNS_DATA_TYPE_TIMESTAMP, TNS_DATA_TYPE_TIMESTAMP_TZ): None, (TNS_DATA_TYPE_DATE, TNS_DATA_TYPE_TIMESTAMP_LTZ): None, (TNS_DATA_TYPE_DATE, TNS_DATA_TYPE_TIMESTAMP): None, (TNS_DATA_TYPE_DATE, TNS_DATA_TYPE_TIMESTAMP_TZ): None, (TNS_DATA_TYPE_CHAR, TNS_DATA_TYPE_VARCHAR): None, (TNS_DATA_TYPE_VARCHAR, TNS_DATA_TYPE_CHAR): None, (TNS_DATA_TYPE_LONG, TNS_DATA_TYPE_VARCHAR): None, (TNS_DATA_TYPE_LONG, TNS_DATA_TYPE_CHAR): None, (TNS_DATA_TYPE_VARCHAR, TNS_DATA_TYPE_LONG): None, (TNS_DATA_TYPE_CHAR, TNS_DATA_TYPE_LONG): None, } cdef object _to_binary_int(object fetch_value): return int(PY_TYPE_DECIMAL(fetch_value)) cdef object _tstamp_to_date(object fetch_value): return fetch_value.replace(microsecond=0) cdef int conversion_helper(ThinVarImpl output_var, FetchInfo fetch_info, bint *requires_define) except -1: cdef: uint8_t fetch_ora_type_num, output_ora_type_num, csfrm object key, value fetch_ora_type_num = fetch_info._dbtype._ora_type_num output_ora_type_num = output_var.dbtype._ora_type_num key = (fetch_ora_type_num, output_ora_type_num) try: value = converter_dict[key] if isinstance(value, int): if fetch_ora_type_num == TNS_DATA_TYPE_NUMBER: output_var._preferred_num_type = value else: requires_define[0] = True csfrm = output_var.dbtype._csfrm fetch_info._dbtype = DbType._from_ora_type_and_csfrm(value, csfrm) else: output_var._conv_func = value except: errors._raise_err(errors.ERR_INCONSISTENT_DATATYPES, input_type=fetch_info._dbtype.name, output_type=output_var.dbtype.name) python-oracledb-1.2.1/src/oracledb/impl/thin/crypto.pyx000066400000000000000000000140621434177474600231600ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # crypto.pyx # # Cython file defining the cryptographic methods used by the thin client # (embedded in thin_impl.pyx). #------------------------------------------------------------------------------ try: from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.ciphers import algorithms, modes, Cipher from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.kdf import pbkdf2 except ImportError: HAS_CRYPTOGRAPHY = False DN_REGEX = '(?:^|,\s?)(?:(?P[A-Z]+)=(?P"(?:[^"]|"")+"|[^,]+))+' PEM_WALLET_FILE_NAME = "ewallet.pem" def decrypt_cbc(key, encrypted_text): """ Decrypt the given text using the given key. """ iv = bytes(16) algo = algorithms.AES(key) cipher = Cipher(algo, modes.CBC(iv)) decryptor = cipher.decryptor() return decryptor.update(encrypted_text) def encrypt_cbc(key, plain_text, zeros=False): """ Encrypt the given text using the given key. If the zeros flag is set, use zero padding if required. Otherwise, use number padding. """ block_size = 16 iv = bytes(block_size) algo = algorithms.AES(key) cipher = Cipher(algo, modes.CBC(iv)) encryptor = cipher.encryptor() n = block_size - len(plain_text) % block_size if n: if zeros: plain_text += bytes(n) else: plain_text += (bytes([n]) * n) return encryptor.update(plain_text) + encryptor.finalize() def get_derived_key(key, salt, length, iterations): """ Return a derived key using PBKDF2. """ kdf = pbkdf2.PBKDF2HMAC(algorithm=hashes.SHA512(), salt=salt, length=length, iterations=iterations) return kdf.derive(key) def get_server_dn_matches(sock, expected_dn): """ Return a boolean indicating if the server distinguished name (DN) matches the expected distinguished name (DN). """ cert_data = sock.getpeercert(binary_form=True) cert = x509.load_der_x509_certificate(cert_data) server_dn = cert.subject.rfc4514_string() expected_dn_dict = dict(re.findall(DN_REGEX, expected_dn)) server_dn_dict = dict(re.findall(DN_REGEX, server_dn)) return server_dn_dict == expected_dn_dict def get_signature(private_key_str, text): """ Returns a signed version of the given text (used for IAM token authentication) in base64 encoding. """ private_key = serialization.load_pem_private_key(private_key_str.encode(), password=None) sig = private_key.sign(text.encode(), padding.PKCS1v15(), hashes.SHA256()) return base64.b64encode(sig).decode() def get_ssl_socket(sock, ConnectParamsImpl params, Description description, Address address): """ Returns a wrapped SSL socket given a socket and the parameters supplied by the user. """ ssl_context = ssl.create_default_context() # if the platform is macOS, and one-way TLS or mTLS is being used, check # if the certifi package is installed. If certifi is not installed, load # the certificates from the macOS keychain in PEM format. if sys.platform == "darwin" and certifi is None: global macos_certs if macos_certs is None: certs = subprocess.run(["security", "find-certificate", "-a", "-p"], stdout=subprocess.PIPE).stdout macos_certs = certs.decode("utf-8") ssl_context.load_verify_locations(cadata=macos_certs) if description.wallet_location is not None: pem_file_name = os.path.join(description.wallet_location, PEM_WALLET_FILE_NAME) if not os.path.exists(pem_file_name): errors._raise_err(errors.ERR_WALLET_FILE_MISSING, name=pem_file_name) ssl_context.load_verify_locations(pem_file_name) try: ssl_context.load_cert_chain(pem_file_name, password=params._get_wallet_password()) except ssl.SSLError: pass return perform_tls_negotiation(sock, ssl_context, description, address) def perform_tls_negotiation(sock, ssl_context, Description description, Address address): """ Peforms TLS negotiation. """ if description.ssl_server_dn_match \ and description.ssl_server_cert_dn is None: sock = ssl_context.wrap_socket(sock, server_hostname=address.host) else: ssl_context.check_hostname = False sock = ssl_context.wrap_socket(sock) if description.ssl_server_dn_match \ and description.ssl_server_cert_dn is not None: if not get_server_dn_matches(sock, description.ssl_server_cert_dn): errors._raise_err(errors.ERR_INVALID_SERVER_CERT_DN) return sock python-oracledb-1.2.1/src/oracledb/impl/thin/cursor.pyx000066400000000000000000000205351434177474600231570ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # cursor.pyx # # Cython file defining the thin Cursor implementation class (embedded in # thin_impl.pyx). #------------------------------------------------------------------------------ cdef class ThinCursorImpl(BaseCursorImpl): cdef: ThinConnImpl _conn_impl Statement _statement list _batcherrors list _dmlrowcounts list _implicit_resultsets uint32_t _num_columns uint32_t _last_row_index Rowid _lastrowid def __cinit__(self, conn_impl): self._conn_impl = conn_impl cdef int _close(self, bint in_del) except -1: if self._statement is not None: self._conn_impl._return_statement(self._statement) self._statement = None cdef BaseVarImpl _create_var_impl(self, object conn): cdef ThinVarImpl var_impl var_impl = ThinVarImpl.__new__(ThinVarImpl) var_impl._conn_impl = self._conn_impl return var_impl cdef MessageWithData _create_message(self, type typ, object cursor): """ Creates a message object that is used to send a request to the database and receive back its response. """ cdef MessageWithData message message = typ.__new__(typ, cursor, self) message._initialize(self._conn_impl) message.cursor = cursor message.cursor_impl = self return message @cython.boundscheck(False) @cython.wraparound(False) cdef int _create_fetch_var(self, object conn, object cursor, object type_handler, ssize_t pos, FetchInfo fetch_info) except -1: """ Internal method that creates a fetch variable. A check is made after the variable is created to determine if a conversion is required and therefore a define must be performed. """ cdef: ThinDbObjectTypeImpl typ_impl ThinVarImpl var_impl BaseCursorImpl._create_fetch_var(self, conn, cursor, type_handler, pos, fetch_info) var_impl = self.fetch_var_impls[pos] if var_impl.dbtype._ora_type_num != fetch_info._dbtype._ora_type_num: conversion_helper(var_impl, fetch_info, &self._statement._requires_define) elif var_impl.objtype is not None: typ_impl = var_impl.objtype if typ_impl.is_xml_type: var_impl.outconverter = \ lambda v: v if isinstance(v, str) else v.read() cdef int _fetch_rows(self, object cursor) except -1: """ Internal method used for fetching rows from the database. """ cdef MessageWithData message if self._statement._requires_full_execute: message = self._create_message(ExecuteMessage, cursor) message.num_execs = self._fetch_array_size else: message = self._create_message(FetchMessage, cursor) self._conn_impl._protocol._process_single_message(message) cdef BaseConnImpl _get_conn_impl(self): """ Internal method used to return the connection implementation associated with the cursor implementation. """ return self._conn_impl cdef bint _is_plsql(self): return self._statement._is_plsql cdef int _preprocess_execute(self, object conn) except -1: cdef BindInfo bind_info if self.bind_vars is not None: self._perform_binds(conn, 0) for bind_info in self._statement._bind_info_list: if bind_info._bind_var_impl is None: errors._raise_err(errors.ERR_MISSING_BIND_VALUE, name=bind_info._bind_name) def execute(self, cursor): cdef: object conn = cursor.connection MessageWithData message self._preprocess_execute(conn) message = self._create_message(ExecuteMessage, cursor) message.num_execs = 1 self._conn_impl._protocol._process_single_message(message) self._statement._requires_full_execute = False if self._statement._is_query: self.rowcount = 0 if message.type_cache is not None: message.type_cache.populate_partial_types(conn) def executemany(self, cursor, num_execs, batcherrors, arraydmlrowcounts): cdef: MessageWithData messsage uint32_t i # set up message to send self._preprocess_execute(cursor.connection) message = self._create_message(ExecuteMessage, cursor) message.num_execs = num_execs message.batcherrors = batcherrors message.arraydmlrowcounts = arraydmlrowcounts # if a PL/SQL statement is being executed for the first time, perform # one execute. If the statement contains both in and out binds, # multiple executes will be performed; otherwise, a bulk execute will # be performed. if self._statement._is_plsql and self._statement._cursor_id == 0: message.num_execs = 1 self._conn_impl._protocol._process_single_message(message) self._statement._requires_full_execute = False if self._statement._plsql_multiple_execs: for i in range(num_execs - 1): message.offset = i + 1 self._conn_impl._protocol._process_single_message(message) elif num_execs > 1: message.offset = 1 message.num_execs = num_execs - 1 self._conn_impl._protocol._process_single_message(message) else: self._conn_impl._protocol._process_single_message(message) self._statement._requires_full_execute = False def get_array_dml_row_counts(self): if self._dmlrowcounts is None: errors._raise_err(errors.ERR_ARRAY_DML_ROW_COUNTS_NOT_ENABLED) return self._dmlrowcounts def get_batch_errors(self): return self._batcherrors def get_bind_names(self): return list(self._statement._bind_info_dict.keys()) def get_implicit_results(self, connection): if self._implicit_resultsets is None: errors._raise_err(errors.ERR_NO_STATEMENT_EXECUTED) return self._implicit_resultsets def get_lastrowid(self): if self.rowcount > 0: return _encode_rowid(&self._lastrowid) def is_query(self, connection): return self.fetch_vars is not None def parse(self, cursor): cdef MessageWithData message message = self._create_message(ExecuteMessage, cursor) message.parse_only = True self._conn_impl._protocol._process_single_message(message) def prepare(self, str sql, str tag, bint cache_statement): self.statement = sql if self._statement is not None: self._conn_impl._return_statement(self._statement) self._statement = None self._statement = self._conn_impl._get_statement(sql.strip(), cache_statement) self.fetch_vars = self._statement._fetch_vars self.fetch_var_impls = self._statement._fetch_var_impls self._num_columns = self._statement._num_columns python-oracledb-1.2.1/src/oracledb/impl/thin/data_types.pyx000066400000000000000000000562411434177474600240020ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # data_types.pyx # # Cython file defining the data type array sent to the database server during # connect (embedded in thin_impl.pyx). #------------------------------------------------------------------------------ ctypedef struct DataType: uint16_t data_type uint16_t conv_data_type uint16_t representation cdef DataType[312] DATA_TYPES = [ [TNS_DATA_TYPE_VARCHAR, TNS_DATA_TYPE_VARCHAR, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_NUMBER, TNS_DATA_TYPE_NUMBER, TNS_TYPE_REP_ORACLE], [TNS_DATA_TYPE_LONG, TNS_DATA_TYPE_LONG, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DATE, TNS_DATA_TYPE_DATE, TNS_TYPE_REP_ORACLE], [TNS_DATA_TYPE_RAW, TNS_DATA_TYPE_RAW, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_LONG_RAW, TNS_DATA_TYPE_LONG_RAW, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_UB2, TNS_DATA_TYPE_UB2, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_UB4, TNS_DATA_TYPE_UB4, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_SB1, TNS_DATA_TYPE_SB1, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_SB2, TNS_DATA_TYPE_SB2, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_SB4, TNS_DATA_TYPE_SB4, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_SWORD, TNS_DATA_TYPE_SWORD, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_UWORD, TNS_DATA_TYPE_UWORD, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_PTRB, TNS_DATA_TYPE_PTRB, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_PTRW, TNS_DATA_TYPE_PTRW, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_TIDDEF, TNS_DATA_TYPE_TIDDEF, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_ROWID, TNS_DATA_TYPE_ROWID, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_AMS, TNS_DATA_TYPE_AMS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_BRN, TNS_DATA_TYPE_BRN, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_CWD, TNS_DATA_TYPE_CWD, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_NEW_OAC, TNS_DATA_TYPE_NEW_OAC, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_OER8, TNS_DATA_TYPE_OER8, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_FUN, TNS_DATA_TYPE_FUN, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_AUA, TNS_DATA_TYPE_AUA, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_RXH7, TNS_DATA_TYPE_RXH7, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_NA6, TNS_DATA_TYPE_NA6, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_BRP, TNS_DATA_TYPE_BRP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_BRV, TNS_DATA_TYPE_BRV, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KVA, TNS_DATA_TYPE_KVA, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_CLS, TNS_DATA_TYPE_CLS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_CUI, TNS_DATA_TYPE_CUI, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DFN, TNS_DATA_TYPE_DFN, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DQR, TNS_DATA_TYPE_DQR, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSC, TNS_DATA_TYPE_DSC, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_EXE, TNS_DATA_TYPE_EXE, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_FCH, TNS_DATA_TYPE_FCH, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_GBV, TNS_DATA_TYPE_GBV, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_GEM, TNS_DATA_TYPE_GEM, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_GIV, TNS_DATA_TYPE_GIV, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_OKG, TNS_DATA_TYPE_OKG, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_HMI, TNS_DATA_TYPE_HMI, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_INO, TNS_DATA_TYPE_INO, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_LNF, TNS_DATA_TYPE_LNF, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_ONT, TNS_DATA_TYPE_ONT, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_OPE, TNS_DATA_TYPE_OPE, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_OSQ, TNS_DATA_TYPE_OSQ, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_SFE, TNS_DATA_TYPE_SFE, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_SPF, TNS_DATA_TYPE_SPF, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_VSN, TNS_DATA_TYPE_VSN, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_UD7, TNS_DATA_TYPE_UD7, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSA, TNS_DATA_TYPE_DSA, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_PIN, TNS_DATA_TYPE_PIN, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_PFN, TNS_DATA_TYPE_PFN, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_PPT, TNS_DATA_TYPE_PPT, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_STO, TNS_DATA_TYPE_STO, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_ARC, TNS_DATA_TYPE_ARC, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_MRS, TNS_DATA_TYPE_MRS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_MRT, TNS_DATA_TYPE_MRT, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_MRG, TNS_DATA_TYPE_MRG, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_MRR, TNS_DATA_TYPE_MRR, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_MRC, TNS_DATA_TYPE_MRC, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_VER, TNS_DATA_TYPE_VER, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_LON2, TNS_DATA_TYPE_LON2, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_INO2, TNS_DATA_TYPE_INO2, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_ALL, TNS_DATA_TYPE_ALL, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_UDB, TNS_DATA_TYPE_UDB, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_AQI, TNS_DATA_TYPE_AQI, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_ULB, TNS_DATA_TYPE_ULB, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_ULD, TNS_DATA_TYPE_ULD, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_SID, TNS_DATA_TYPE_SID, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_NA7, TNS_DATA_TYPE_NA7, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_AL7, TNS_DATA_TYPE_AL7, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_K2RPC, TNS_DATA_TYPE_K2RPC, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_XDP, TNS_DATA_TYPE_XDP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_OKO8, TNS_DATA_TYPE_OKO8, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_UD12, TNS_DATA_TYPE_UD12, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_AL8, TNS_DATA_TYPE_AL8, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_LFOP, TNS_DATA_TYPE_LFOP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_FCRT, TNS_DATA_TYPE_FCRT, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DNY, TNS_DATA_TYPE_DNY, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_OPR, TNS_DATA_TYPE_OPR, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_PLS, TNS_DATA_TYPE_PLS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_XID, TNS_DATA_TYPE_XID, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_TXN, TNS_DATA_TYPE_TXN, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DCB, TNS_DATA_TYPE_DCB, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_CCA, TNS_DATA_TYPE_CCA, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_WRN, TNS_DATA_TYPE_WRN, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_TLH, TNS_DATA_TYPE_TLH, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_TOH, TNS_DATA_TYPE_TOH, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_FOI, TNS_DATA_TYPE_FOI, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_SID2, TNS_DATA_TYPE_SID2, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_TCH, TNS_DATA_TYPE_TCH, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_PII, TNS_DATA_TYPE_PII, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_PFI, TNS_DATA_TYPE_PFI, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_PPU, TNS_DATA_TYPE_PPU, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_PTE, TNS_DATA_TYPE_PTE, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_RXH8, TNS_DATA_TYPE_RXH8, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_N12, TNS_DATA_TYPE_N12, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_AUTH, TNS_DATA_TYPE_AUTH, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KVAL, TNS_DATA_TYPE_KVAL, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_FGI, TNS_DATA_TYPE_FGI, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSY, TNS_DATA_TYPE_DSY, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSYR8, TNS_DATA_TYPE_DSYR8, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSYH8, TNS_DATA_TYPE_DSYH8, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSYL, TNS_DATA_TYPE_DSYL, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSYT8, TNS_DATA_TYPE_DSYT8, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSYV8, TNS_DATA_TYPE_DSYV8, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSYP, TNS_DATA_TYPE_DSYP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSYF, TNS_DATA_TYPE_DSYF, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSYK, TNS_DATA_TYPE_DSYK, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSYY, TNS_DATA_TYPE_DSYY, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSYQ, TNS_DATA_TYPE_DSYQ, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSYC, TNS_DATA_TYPE_DSYC, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSYA, TNS_DATA_TYPE_DSYA, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_OT8, TNS_DATA_TYPE_OT8, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSYTY, TNS_DATA_TYPE_DSYTY, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_AQE, TNS_DATA_TYPE_AQE, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KV, TNS_DATA_TYPE_KV, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_AQD, TNS_DATA_TYPE_AQD, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_AQ8, TNS_DATA_TYPE_AQ8, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_RFS, TNS_DATA_TYPE_RFS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_RXH10, TNS_DATA_TYPE_RXH10, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPN, TNS_DATA_TYPE_KPN, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPDNR, TNS_DATA_TYPE_KPDNR, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSYD, TNS_DATA_TYPE_DSYD, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSYS, TNS_DATA_TYPE_DSYS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSYR, TNS_DATA_TYPE_DSYR, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSYH, TNS_DATA_TYPE_DSYH, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSYT, TNS_DATA_TYPE_DSYT, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DSYV, TNS_DATA_TYPE_DSYV, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_AQM, TNS_DATA_TYPE_AQM, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_OER11, TNS_DATA_TYPE_OER11, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_AQL, TNS_DATA_TYPE_AQL, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_OTC, TNS_DATA_TYPE_OTC, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KFNO, TNS_DATA_TYPE_KFNO, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KFNP, TNS_DATA_TYPE_KFNP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KGT8, TNS_DATA_TYPE_KGT8, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_RASB4, TNS_DATA_TYPE_RASB4, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_RAUB2, TNS_DATA_TYPE_RAUB2, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_RAUB1, TNS_DATA_TYPE_RAUB1, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_RATXT, TNS_DATA_TYPE_RATXT, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_RSSB4, TNS_DATA_TYPE_RSSB4, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_RSUB2, TNS_DATA_TYPE_RSUB2, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_RSUB1, TNS_DATA_TYPE_RSUB1, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_RSTXT, TNS_DATA_TYPE_RSTXT, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_RIDL, TNS_DATA_TYPE_RIDL, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_GLRDD, TNS_DATA_TYPE_GLRDD, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_GLRDG, TNS_DATA_TYPE_GLRDG, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_GLRDC, TNS_DATA_TYPE_GLRDC, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_OKO, TNS_DATA_TYPE_OKO, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DPP, TNS_DATA_TYPE_DPP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DPLS, TNS_DATA_TYPE_DPLS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DPMOP, TNS_DATA_TYPE_DPMOP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_STAT, TNS_DATA_TYPE_STAT, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_RFX, TNS_DATA_TYPE_RFX, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_FAL, TNS_DATA_TYPE_FAL, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_CKV, TNS_DATA_TYPE_CKV, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DRCX, TNS_DATA_TYPE_DRCX, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KGH, TNS_DATA_TYPE_KGH, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_AQO, TNS_DATA_TYPE_AQO, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_OKGT, TNS_DATA_TYPE_OKGT, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPFC, TNS_DATA_TYPE_KPFC, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_FE2, TNS_DATA_TYPE_FE2, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_SPFP, TNS_DATA_TYPE_SPFP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DPULS, TNS_DATA_TYPE_DPULS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_AQA, TNS_DATA_TYPE_AQA, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPBF, TNS_DATA_TYPE_KPBF, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_TSM, TNS_DATA_TYPE_TSM, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_MSS, TNS_DATA_TYPE_MSS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPC, TNS_DATA_TYPE_KPC, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_CRS, TNS_DATA_TYPE_CRS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KKS, TNS_DATA_TYPE_KKS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KSP, TNS_DATA_TYPE_KSP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KSPTOP, TNS_DATA_TYPE_KSPTOP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KSPVAL, TNS_DATA_TYPE_KSPVAL, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_PSS, TNS_DATA_TYPE_PSS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_NLS, TNS_DATA_TYPE_NLS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_ALS, TNS_DATA_TYPE_ALS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KSDEVTVAL, TNS_DATA_TYPE_KSDEVTVAL, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KSDEVTTOP, TNS_DATA_TYPE_KSDEVTTOP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPSPP, TNS_DATA_TYPE_KPSPP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KOL, TNS_DATA_TYPE_KOL, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_LST, TNS_DATA_TYPE_LST, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_ACX, TNS_DATA_TYPE_ACX, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_SCS, TNS_DATA_TYPE_SCS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_RXH, TNS_DATA_TYPE_RXH, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPDNS, TNS_DATA_TYPE_KPDNS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPDCN, TNS_DATA_TYPE_KPDCN, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPNNS, TNS_DATA_TYPE_KPNNS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPNCN, TNS_DATA_TYPE_KPNCN, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPS, TNS_DATA_TYPE_KPS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_APINF, TNS_DATA_TYPE_APINF, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_TEN, TNS_DATA_TYPE_TEN, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_XSSCS, TNS_DATA_TYPE_XSSCS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_XSSSO, TNS_DATA_TYPE_XSSSO, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_XSSAO, TNS_DATA_TYPE_XSSAO, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KSRPC, TNS_DATA_TYPE_KSRPC, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KVL, TNS_DATA_TYPE_KVL, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_XSSDEF, TNS_DATA_TYPE_XSSDEF, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_PDQCINV, TNS_DATA_TYPE_PDQCINV, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_PDQIDC, TNS_DATA_TYPE_PDQIDC, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPDQCSTA, TNS_DATA_TYPE_KPDQCSTA, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPRS, TNS_DATA_TYPE_KPRS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPDQIDC, TNS_DATA_TYPE_KPDQIDC, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_RTSTRM, TNS_DATA_TYPE_RTSTRM, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_SESSGET, TNS_DATA_TYPE_SESSGET, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_SESSREL, TNS_DATA_TYPE_SESSREL, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_SESSRET, TNS_DATA_TYPE_SESSRET, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_SCN6, TNS_DATA_TYPE_SCN6, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KECPA, TNS_DATA_TYPE_KECPA, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KECPP, TNS_DATA_TYPE_KECPP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_SXA, TNS_DATA_TYPE_SXA, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KVARR, TNS_DATA_TYPE_KVARR, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPNGN, TNS_DATA_TYPE_KPNGN, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_BINARY_INTEGER, TNS_DATA_TYPE_NUMBER, TNS_TYPE_REP_ORACLE], [TNS_DATA_TYPE_FLOAT, TNS_DATA_TYPE_NUMBER, TNS_TYPE_REP_ORACLE], [TNS_DATA_TYPE_STR, TNS_DATA_TYPE_VARCHAR, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_VNU, TNS_DATA_TYPE_NUMBER, TNS_TYPE_REP_ORACLE], [TNS_DATA_TYPE_PDN, TNS_DATA_TYPE_NUMBER, TNS_TYPE_REP_ORACLE], [TNS_DATA_TYPE_VCS, TNS_DATA_TYPE_VARCHAR, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_VBI, TNS_DATA_TYPE_VARCHAR, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_OAC, TNS_DATA_TYPE_NEW_OAC, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_UIN, TNS_DATA_TYPE_NUMBER, TNS_TYPE_REP_ORACLE], [TNS_DATA_TYPE_SLS, TNS_DATA_TYPE_NUMBER, TNS_TYPE_REP_ORACLE], [TNS_DATA_TYPE_LVC, TNS_DATA_TYPE_VARCHAR, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_LVB, TNS_DATA_TYPE_RAW, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_CHAR, TNS_DATA_TYPE_CHAR, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_AVC, TNS_DATA_TYPE_CHAR, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_BINARY_FLOAT, TNS_DATA_TYPE_BINARY_FLOAT, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_BINARY_DOUBLE, TNS_DATA_TYPE_BINARY_DOUBLE, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_CURSOR, TNS_DATA_TYPE_CURSOR, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_RDD, TNS_DATA_TYPE_ROWID, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_OSL, TNS_DATA_TYPE_OSL, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_EXT_NAMED, TNS_DATA_TYPE_INT_NAMED, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_INT_NAMED, TNS_DATA_TYPE_INT_NAMED, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_EXT_REF, TNS_DATA_TYPE_INT_REF, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_INT_REF, TNS_DATA_TYPE_INT_REF, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_CLOB, TNS_DATA_TYPE_CLOB, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_BLOB, TNS_DATA_TYPE_BLOB, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_BFILE, TNS_DATA_TYPE_BFILE, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_CFILE, TNS_DATA_TYPE_CFILE, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_RSET, TNS_DATA_TYPE_CURSOR, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_JSON, TNS_DATA_TYPE_JSON, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DJSON, TNS_DATA_TYPE_DJSON, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_CLV, TNS_DATA_TYPE_CLV, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DTR, TNS_DATA_TYPE_NUMBER, TNS_TYPE_REP_ORACLE], [TNS_DATA_TYPE_DUN, TNS_DATA_TYPE_NUMBER, TNS_TYPE_REP_ORACLE], [TNS_DATA_TYPE_DOP, TNS_DATA_TYPE_NUMBER, TNS_TYPE_REP_ORACLE], [TNS_DATA_TYPE_VST, TNS_DATA_TYPE_VARCHAR, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_ODT, TNS_DATA_TYPE_DATE, TNS_TYPE_REP_ORACLE], [TNS_DATA_TYPE_DOL, TNS_DATA_TYPE_NUMBER, TNS_TYPE_REP_ORACLE], [TNS_DATA_TYPE_TIME, TNS_DATA_TYPE_TIME, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_TIME_TZ, TNS_DATA_TYPE_TIME_TZ, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_TIMESTAMP, TNS_DATA_TYPE_TIMESTAMP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_TIMESTAMP_TZ, TNS_DATA_TYPE_TIMESTAMP_TZ, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_INTERVAL_YM, TNS_DATA_TYPE_INTERVAL_YM, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_INTERVAL_DS, TNS_DATA_TYPE_INTERVAL_DS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_EDATE, TNS_DATA_TYPE_DATE, TNS_TYPE_REP_ORACLE], [TNS_DATA_TYPE_ETIME, TNS_DATA_TYPE_ETIME, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_ETTZ, TNS_DATA_TYPE_ETTZ, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_ESTAMP, TNS_DATA_TYPE_ESTAMP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_ESTZ, TNS_DATA_TYPE_ESTZ, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_EIYM, TNS_DATA_TYPE_EIYM, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_EIDS, TNS_DATA_TYPE_EIDS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DCLOB, TNS_DATA_TYPE_CLOB, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DBLOB, TNS_DATA_TYPE_BLOB, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_DBFILE, TNS_DATA_TYPE_BFILE, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_UROWID, TNS_DATA_TYPE_UROWID, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_TIMESTAMP_LTZ, TNS_DATA_TYPE_TIMESTAMP_LTZ, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_ESITZ, TNS_DATA_TYPE_TIMESTAMP_LTZ, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_UB8, TNS_DATA_TYPE_UB8, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_PNTY, TNS_DATA_TYPE_INT_NAMED, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_BOOLEAN, TNS_DATA_TYPE_BOOLEAN, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_XSNSOP, TNS_DATA_TYPE_XSNSOP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_XSATTR, TNS_DATA_TYPE_XSATTR, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_XSNS, TNS_DATA_TYPE_XSNS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_UB1ARRAY, TNS_DATA_TYPE_UB1ARRAY, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_SESSSTATE, TNS_DATA_TYPE_SESSSTATE, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_AC_REPLAY, TNS_DATA_TYPE_AC_REPLAY, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_AC_CONT, TNS_DATA_TYPE_AC_CONT, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_IMPLRES, TNS_DATA_TYPE_IMPLRES, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_OER, TNS_DATA_TYPE_OER, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_TXT, TNS_DATA_TYPE_TXT, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_XSSESSNS, TNS_DATA_TYPE_XSSESSNS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_XSATTOP, TNS_DATA_TYPE_XSATTOP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_XSCREOP, TNS_DATA_TYPE_XSCREOP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_XSDETOP, TNS_DATA_TYPE_XSDETOP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_XSDESOP, TNS_DATA_TYPE_XSDESOP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_XSSETSP, TNS_DATA_TYPE_XSSETSP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_XSSIDP, TNS_DATA_TYPE_XSSIDP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_XSPRIN, TNS_DATA_TYPE_XSPRIN, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_XSKVL, TNS_DATA_TYPE_XSKVL, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_XSSSDEF2, TNS_DATA_TYPE_XSSSDEF2, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_XSNSOP2, TNS_DATA_TYPE_XSNSOP2, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_XSNS2, TNS_DATA_TYPE_XSNS2, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPDNREQ, TNS_DATA_TYPE_KPDNREQ, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPDNRNF, TNS_DATA_TYPE_KPDNRNF, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPNGNC, TNS_DATA_TYPE_KPNGNC, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPNRI, TNS_DATA_TYPE_KPNRI, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_AQENQ, TNS_DATA_TYPE_AQENQ, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_AQDEQ, TNS_DATA_TYPE_AQDEQ, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_AQJMS, TNS_DATA_TYPE_AQJMS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPDNRPAY, TNS_DATA_TYPE_KPDNRPAY, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPDNRACK, TNS_DATA_TYPE_KPDNRACK, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPDNRMP, TNS_DATA_TYPE_KPDNRMP, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_KPDNRDQ, TNS_DATA_TYPE_KPDNRDQ, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_SCN, TNS_DATA_TYPE_SCN, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_SCN8, TNS_DATA_TYPE_SCN8, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_CHUNKINFO, TNS_DATA_TYPE_CHUNKINFO, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_UDS, TNS_DATA_TYPE_UDS, TNS_TYPE_REP_UNIVERSAL], [TNS_DATA_TYPE_TNP, TNS_DATA_TYPE_TNP, TNS_TYPE_REP_UNIVERSAL], [0, 0, 0] ] python-oracledb-1.2.1/src/oracledb/impl/thin/dbobject.pyx000066400000000000000000001156411434177474600234210ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # dbobject.pyx # # Cython file defining the thin DbObjectType, DbObjectAttr and DbObject # implementation classes (embedded in thin_impl.pyx). #------------------------------------------------------------------------------ @cython.final cdef class DbObjectPickleBuffer(Buffer): cdef int _read_raw_bytes_and_length(self, const char_type **ptr, ssize_t *num_bytes) except -1: """ Helper function that processes the length (if needed) and then acquires the specified number of bytes from the buffer. """ cdef uint32_t extended_num_bytes if num_bytes[0] == TNS_LONG_LENGTH_INDICATOR: self.read_uint32(&extended_num_bytes) num_bytes[0] = extended_num_bytes ptr[0] = self._get_raw(num_bytes[0]) cdef int _write_raw_bytes_and_length(self, const char_type *ptr, ssize_t num_bytes) except -1: """ Helper function that writes the length in the format required before writing the bytes. """ self.write_length(num_bytes) self.write_raw(ptr, num_bytes) cdef int get_is_atomic_null(self, bint* is_null) except -1: """ Reads the next byte and checks to see if the value is atomically null. If not, the byte is returned to the buffer for further processing. """ cdef uint8_t value self.read_ub1(&value) if value in (TNS_OBJ_ATOMIC_NULL, TNS_NULL_LENGTH_INDICATOR): is_null[0] = True else: is_null[0] = False self._pos -= 1 cdef int read_header(self, uint8_t* flags, uint8_t *version) except -1: """ Reads the header of the pickled data. """ cdef: uint32_t prefix_seg_length uint8_t tmp self.read_ub1(flags) self.read_ub1(version) self.skip_length() if flags[0] & TNS_OBJ_NO_PREFIX_SEG: return 0 self.read_length(&prefix_seg_length) self.skip_raw_bytes(prefix_seg_length) cdef int read_length(self, uint32_t *length) except -1: """ Read the length from the buffer. This will be a single byte, unless the value meets or exceeds TNS_LONG_LENGTH_INDICATOR. In that case, the value is stored as a 4-byte integer. """ cdef uint8_t short_length self.read_ub1(&short_length) if short_length == TNS_LONG_LENGTH_INDICATOR: self.read_uint32(length) else: length[0] = short_length cdef int skip_length(self) except -1: """ Skips the length instead of reading it from the buffer. """ cdef uint8_t short_length self.read_ub1(&short_length) if short_length == TNS_LONG_LENGTH_INDICATOR: self.skip_raw_bytes(4) cdef int write_header(self, ThinDbObjectImpl obj_impl) except -1: """ Writes the header of the pickled data. Since the size is unknown at this point, zero is written initially and the actual size is written later. """ cdef ThinDbObjectTypeImpl typ_impl = obj_impl.type self.write_uint8(obj_impl.image_flags) self.write_uint8(obj_impl.image_version) self.write_uint8(TNS_LONG_LENGTH_INDICATOR) self.write_uint32(0) if typ_impl.is_collection: self.write_uint8(1) # length of prefix segment self.write_uint8(1) # prefix segment contents cdef int write_length(self, ssize_t length) except -1: """ Writes the length to the buffer. """ if length <= TNS_OBJ_MAX_SHORT_LENGTH: self.write_uint8( length) else: self.write_uint8(TNS_LONG_LENGTH_INDICATOR) self.write_uint32( length) @cython.final cdef class TDSBuffer(Buffer): pass cdef class ThinDbObjectImpl(BaseDbObjectImpl): cdef: uint8_t image_flags, image_version bytes toid, oid, packed_data uint32_t num_elements dict unpacked_assoc_array list unpacked_assoc_keys dict unpacked_attrs list unpacked_array uint16_t flags cdef inline int _ensure_assoc_keys(self) except -1: """ Ensure that the keys for the associative array have been calculated. PL/SQL associative arrays keep their keys in sorted order so this must be calculated when indices are required. """ if self.unpacked_assoc_keys is None: self.unpacked_assoc_keys = list(sorted(self.unpacked_assoc_array)) cdef inline int _ensure_unpacked(self) except -1: """ Ensure that the data has been unpacked. """ if self.packed_data is not None: self._unpack_data() cdef bytes _get_packed_data(self): """ Returns the packed data for the object. This will either be the value retrieved from the database or generated packed data (for new objects and those that have had their data unpacked already). """ cdef: ThinDbObjectTypeImpl typ_impl = self.type DbObjectPickleBuffer buf ssize_t size if self.packed_data is not None: return self.packed_data buf = DbObjectPickleBuffer.__new__(DbObjectPickleBuffer) buf._initialize(TNS_CHUNK_SIZE) buf.write_header(self) self._pack_data(buf) size = buf._pos buf.skip_to(3) buf.write_uint32(size) return buf._data[:size] cdef int _pack_data(self, DbObjectPickleBuffer buf) except -1: """ Packs the data from the object into the buffer. """ cdef: ThinDbObjectTypeImpl typ_impl = self.type ThinDbObjectAttrImpl attr int32_t index object value if typ_impl.is_collection: buf.write_uint8(typ_impl.collection_flags) if typ_impl.collection_type == TNS_OBJ_PLSQL_INDEX_TABLE: self._ensure_assoc_keys() buf.write_length(len(self.unpacked_assoc_keys)) for index in self.unpacked_assoc_keys: buf.write_uint32( index) self._pack_value(buf, typ_impl.element_dbtype, typ_impl.element_objtype, self.unpacked_assoc_array[index]) else: buf.write_length(len(self.unpacked_array)) for value in self.unpacked_array: self._pack_value(buf, typ_impl.element_dbtype, typ_impl.element_objtype, value) else: for attr in typ_impl.attrs: self._pack_value(buf, attr.dbtype, attr.objtype, self.unpacked_attrs[attr.name]) cdef int _pack_value(self, DbObjectPickleBuffer buf, DbType dbtype, ThinDbObjectTypeImpl objtype, object value) except -1: """ Packs a value into the buffer. At this point it is assumed that the value matches the correct type. """ cdef: uint8_t ora_type_num = dbtype._ora_type_num ThinDbObjectImpl obj_impl bytes temp_bytes if value is None: if objtype is not None and not objtype.is_collection: buf.write_uint8(TNS_OBJ_ATOMIC_NULL) else: buf.write_uint8(TNS_NULL_LENGTH_INDICATOR) elif ora_type_num in (TNS_DATA_TYPE_CHAR, TNS_DATA_TYPE_VARCHAR): if dbtype._csfrm == TNS_CS_IMPLICIT: temp_bytes = ( value).encode() else: temp_bytes = ( value).encode(TNS_ENCODING_UTF16) buf.write_bytes_with_length(temp_bytes) elif ora_type_num == TNS_DATA_TYPE_NUMBER: temp_bytes = ( cpython.PyObject_Str(value)).encode() buf.write_oracle_number(temp_bytes) elif ora_type_num == TNS_DATA_TYPE_BINARY_INTEGER: buf.write_uint8(4) buf.write_uint32( value) elif ora_type_num == TNS_DATA_TYPE_RAW: buf.write_bytes_with_length(value) elif ora_type_num == TNS_DATA_TYPE_BINARY_DOUBLE: buf.write_binary_double(value) elif ora_type_num == TNS_DATA_TYPE_BINARY_FLOAT: buf.write_binary_float(value) elif ora_type_num == TNS_DATA_TYPE_BOOLEAN: buf.write_uint8(4) buf.write_uint32(value) elif ora_type_num in (TNS_DATA_TYPE_DATE, TNS_DATA_TYPE_TIMESTAMP, TNS_DATA_TYPE_TIMESTAMP_TZ, TNS_DATA_TYPE_TIMESTAMP_LTZ): buf.write_oracle_date(value, dbtype._buffer_size_factor) elif ora_type_num in (TNS_DATA_TYPE_CLOB, TNS_DATA_TYPE_BLOB): buf.write_lob(value._impl) elif ora_type_num == TNS_DATA_TYPE_INT_NAMED: obj_impl = value._impl if self.type.is_collection or obj_impl.type.is_collection: temp_bytes = obj_impl._get_packed_data() buf.write_bytes_with_length(temp_bytes) else: obj_impl._pack_data(buf) else: errors._raise_err(errors.ERR_DB_TYPE_NOT_SUPPORTED, name=dbtype.name) cdef int _unpack_data(self) except -1: """ Unpacks the packed data into a dictionary of Python values. """ cdef DbObjectPickleBuffer buf buf = DbObjectPickleBuffer.__new__(DbObjectPickleBuffer) buf._populate_from_bytes(self.packed_data) buf.read_header(&self.image_flags, &self.image_version) self._unpack_data_from_buf(buf) self.packed_data = None cdef int _unpack_data_from_buf(self, DbObjectPickleBuffer buf) except -1: """ Unpacks the data from the buffer into Python values. """ cdef: dict unpacked_attrs = {}, unpacked_assoc_array = None ThinDbObjectTypeImpl typ_impl = self.type list unpacked_array = None ThinDbObjectAttrImpl attr uint32_t num_elements, i int32_t assoc_index object value if typ_impl.is_collection: if typ_impl.collection_type == TNS_OBJ_PLSQL_INDEX_TABLE: unpacked_assoc_array = {} else: unpacked_array = [] buf.skip_raw_bytes(1) # collection flags buf.read_length(&num_elements) for i in range(num_elements): if typ_impl.collection_type == TNS_OBJ_PLSQL_INDEX_TABLE: buf.read_int32(&assoc_index) value = self._unpack_value(buf, typ_impl.element_dbtype, typ_impl.element_objtype) if typ_impl.collection_type == TNS_OBJ_PLSQL_INDEX_TABLE: unpacked_assoc_array[assoc_index] = value else: unpacked_array.append(value) else: unpacked_attrs = {} for attr in typ_impl.attrs: value = self._unpack_value(buf, attr.dbtype, attr.objtype) unpacked_attrs[attr.name] = value self.unpacked_attrs = unpacked_attrs self.unpacked_array = unpacked_array self.unpacked_assoc_array = unpacked_assoc_array cdef object _unpack_value(self, DbObjectPickleBuffer buf, DbType dbtype, ThinDbObjectTypeImpl objtype): """ Unpacks a single value and returns it. """ cdef: uint8_t ora_type_num = dbtype._ora_type_num uint8_t csfrm = dbtype._csfrm ThinDbObjectImpl obj_impl ThinConnImpl conn_impl bint is_null if ora_type_num == TNS_DATA_TYPE_NUMBER: return buf.read_oracle_number(NUM_TYPE_FLOAT) elif ora_type_num == TNS_DATA_TYPE_BINARY_INTEGER: return buf.read_binary_integer() elif ora_type_num in (TNS_DATA_TYPE_VARCHAR, TNS_DATA_TYPE_CHAR): if csfrm == TNS_CS_NCHAR: conn_impl = self.type._conn_impl conn_impl._protocol._caps._check_ncharset_id() return buf.read_str(csfrm) elif ora_type_num == TNS_DATA_TYPE_RAW: return buf.read_bytes() elif ora_type_num == TNS_DATA_TYPE_BINARY_DOUBLE: return buf.read_binary_double() elif ora_type_num == TNS_DATA_TYPE_BINARY_FLOAT: return buf.read_binary_float() elif ora_type_num in (TNS_DATA_TYPE_DATE, TNS_DATA_TYPE_TIMESTAMP, TNS_DATA_TYPE_TIMESTAMP_LTZ, TNS_DATA_TYPE_TIMESTAMP_TZ): return buf.read_date() elif ora_type_num in (TNS_DATA_TYPE_CLOB, TNS_DATA_TYPE_BLOB): conn_impl = self.type._conn_impl return buf.read_lob(conn_impl, dbtype) elif ora_type_num == TNS_DATA_TYPE_BOOLEAN: return buf.read_bool() elif ora_type_num == TNS_DATA_TYPE_INT_NAMED: buf.get_is_atomic_null(&is_null) if is_null: return None obj_impl = ThinDbObjectImpl.__new__(ThinDbObjectImpl) obj_impl.type = objtype if objtype.is_collection or self.type.is_collection: obj_impl.packed_data = buf.read_bytes() else: obj_impl._unpack_data_from_buf(buf) return PY_TYPE_DB_OBJECT._from_impl(obj_impl) errors._raise_err(errors.ERR_DB_TYPE_NOT_SUPPORTED, name=dbtype.name) def append_checked(self, object value): """ Internal method for appending a value to a collection object. """ cdef int32_t new_index self._ensure_unpacked() if self.unpacked_array is not None: self.unpacked_array.append(value) else: self._ensure_assoc_keys() new_index = self.unpacked_assoc_keys[-1] + 1 \ if self.unpacked_assoc_keys else 0 self.unpacked_assoc_array[new_index] = value self.unpacked_assoc_keys.append(new_index) def copy(self): """ Internal method for creating a copy of an object. """ cdef ThinDbObjectImpl copied_impl copied_impl = ThinDbObjectImpl.__new__(ThinDbObjectImpl) copied_impl.type = self.type copied_impl.flags = self.flags copied_impl.image_flags = self.image_flags copied_impl.image_version = self.image_version copied_impl.toid = self.toid copied_impl.packed_data = self.packed_data copied_impl.num_elements = self.num_elements if self.unpacked_attrs is not None: copied_impl.unpacked_attrs = self.unpacked_attrs.copy() if self.unpacked_array is not None: copied_impl.unpacked_array = list(self.unpacked_array) return copied_impl def delete_by_index(self, int32_t index): """ Internal method for deleting an entry from a collection that is indexed by integers. """ self._ensure_unpacked() if self.unpacked_array is not None: del self.unpacked_array[index] else: self.unpacked_assoc_keys = None del self.unpacked_assoc_array[index] def exists_by_index(self, int32_t index): """ Internal method for determining if an entry exists in a collection that is indexed by integers. """ self._ensure_unpacked() if self.unpacked_array is not None: return index >= 0 and index < len(self.unpacked_array) else: return index in self.unpacked_assoc_array def get_attr_value(self, ThinDbObjectAttrImpl attr): """ Internal method for getting an attribute value. """ self._ensure_unpacked() return self.unpacked_attrs[attr.name] def get_element_by_index(self, int32_t index): """ Internal method for getting an entry from a collection that is indexed by integers. """ self._ensure_unpacked() if self.unpacked_array: return self.unpacked_array[index] elif self.unpacked_assoc_array: return self.unpacked_assoc_array[index] def get_first_index(self): """ Internal method for getting the first index from a collection that is indexed by integers. """ self._ensure_unpacked() if self.unpacked_array: return 0 elif self.unpacked_assoc_array: self._ensure_assoc_keys() return self.unpacked_assoc_keys[0] def get_last_index(self): """ Internal method for getting the last index from a collection that is indexed by integers. """ self._ensure_unpacked() if self.unpacked_array: return len(self.unpacked_array) - 1 elif self.unpacked_assoc_array: self._ensure_assoc_keys() return self.unpacked_assoc_keys[-1] def get_next_index(self, int32_t index): """ Internal method for getting the next index from a collection that is indexed by integers. """ cdef int32_t i self._ensure_unpacked() if self.unpacked_array: if index + 1 < len(self.unpacked_array): return index + 1 elif self.unpacked_assoc_array: self._ensure_assoc_keys() for i in self.unpacked_assoc_keys: if i > index: return i def get_prev_index(self, int32_t index): """ Internal method for getting the next index from a collection that is indexed by integers. """ self._ensure_unpacked() if self.unpacked_array: if index > 0: return index - 1 elif self.unpacked_assoc_array: self._ensure_assoc_keys() for i in reversed(self.unpacked_assoc_keys): if i < index: return i def get_size(self): """ Internal method for getting the size of a collection. """ self._ensure_unpacked() if self.unpacked_array is not None: return len(self.unpacked_array) else: return len(self.unpacked_assoc_array) def set_attr_value_checked(self, ThinDbObjectAttrImpl attr, object value): """ Internal method for setting an attribute value. """ self._ensure_unpacked() self.unpacked_attrs[attr.name] = value def set_element_by_index_checked(self, int32_t index, object value): """ Internal method for setting an entry in a collection that is indexed by integers. """ self._ensure_unpacked() if self.unpacked_array is not None: self.unpacked_array[index] = value else: if index not in self.unpacked_assoc_array: self.unpacked_assoc_keys = None self.unpacked_assoc_array[index] = value def trim(self, int32_t num_to_trim): """ Internal method for trimming a number of entries from a collection. """ self._ensure_unpacked() if num_to_trim > 0: self.unpacked_array = self.unpacked_array[:-num_to_trim] cdef class ThinDbObjectAttrImpl(BaseDbObjectAttrImpl): cdef: bytes oid cdef class ThinDbObjectTypeImpl(BaseDbObjectTypeImpl): cdef: uint8_t collection_type, collection_flags, version uint32_t max_num_elements bint is_xml_type bytes oid def create_new_object(self): """ Internal method for creating a new object. """ cdef ThinDbObjectImpl obj_impl obj_impl = ThinDbObjectImpl.__new__(ThinDbObjectImpl) obj_impl.type = self obj_impl.toid = b'\x00\x22' + \ bytes([TNS_OBJ_NON_NULL_OID, TNS_OBJ_HAS_EXTENT_OID]) + \ self.oid + TNS_EXTENT_OID obj_impl.flags = TNS_OBJ_TOP_LEVEL obj_impl.image_flags = TNS_OBJ_IS_VERSION_81 obj_impl.image_version = TNS_OBJ_IMAGE_VERSION obj_impl.unpacked_attrs = {} if self.is_collection: obj_impl.image_flags |= TNS_OBJ_IS_COLLECTION if self.collection_type == TNS_OBJ_PLSQL_INDEX_TABLE: obj_impl.unpacked_assoc_array = {} else: obj_impl.unpacked_array = [] else: obj_impl.image_flags |= TNS_OBJ_NO_PREFIX_SEG for attr in self.attrs: obj_impl.unpacked_attrs[attr.name] = None return obj_impl cdef class ThinDbObjectTypeSuperCache: cdef: dict caches object lock int cache_num def __init__(self): self.caches = {} self.cache_num = 0 self.lock = threading.Lock() cdef class ThinDbObjectTypeCache: cdef: object return_value_var, full_name_var, oid_var, tds_var object meta_cursor, attrs_ref_cursor_var, version_var object schema_var, package_name_var, name_var ThinConnImpl conn_impl dict types_by_oid dict types_by_name list partial_types cdef int _clear_meta_cursor(self) except -1: """ Clears the cursor used for searching metadata. This is needed when returning a connection to the pool since user-level objects are retained. """ if self.meta_cursor is not None: self.meta_cursor.close() self.meta_cursor = None self.return_value_var = None self.full_name_var = None self.oid_var = None self.tds_var = None self.attrs_ref_cursor_var = None self.version_var = None self.schema_var = None self.package_name_var = None self.name_var = None cdef int _determine_element_type_csfrm(self, ThinDbObjectTypeImpl typ_impl, uint8_t* csfrm) except -1: """ Determine the element type's character set form. This is only needed for CLOB and NCLOB where this information is not stored in the TDS. """ cdef: object cursor str type_name cursor = self.meta_cursor.connection.cursor() if typ_impl.package_name is not None: cursor.execute(""" select elem_type_name from all_plsql_coll_types where owner = :owner and package_name = :package_name and type_name = :name""", owner=typ_impl.schema, package_name=typ_impl.package_name, name=typ_impl.name) else: cursor.execute(""" select elem_type_name from all_coll_types where owner = :owner and type_name = :name""", owner=typ_impl.schema, name=typ_impl.name) type_name, = cursor.fetchone() if type_name == "NCLOB": csfrm[0] = TNS_CS_NCHAR else: csfrm[0] = TNS_CS_IMPLICIT cdef int _determine_element_objtype(self, ThinDbObjectTypeImpl impl) except -1: """ Determine the element type's object type. This is needed when processing collections with object as the element type since this information is not available in the TDS. """ cdef: str schema, name, package_name = None object cursor cursor = self.meta_cursor.connection.cursor() if impl.package_name is not None: cursor.execute(""" select elem_type_owner, elem_type_package, elem_type_name from all_plsql_coll_types where owner = :owner and package_name = :package_name and type_name = :name""", owner=impl.schema, package_name=impl.package_name, name=impl.name) schema, package_name, name = cursor.fetchone() else: cursor.execute(""" select elem_type_owner, elem_type_name from all_coll_types where owner = :owner and type_name = :name""", owner=impl.schema, name=impl.name) schema, name = cursor.fetchone() impl.element_objtype = self.get_type_for_info(None, schema, package_name, name) cdef int _initialize(self, ThinConnImpl conn_impl) except -1: self.types_by_oid = {} self.types_by_name = {} self.partial_types = [] self.conn_impl = conn_impl cdef int _init_meta_cursor(self, object conn) except -1: """ Initializes the cursor that fetches the type metadata. """ cursor = conn.cursor() self.return_value_var = cursor.var(DB_TYPE_BINARY_INTEGER) self.tds_var = cursor.var(bytes) self.full_name_var = cursor.var(str) self.schema_var = cursor.var(str) self.package_name_var = cursor.var(str) self.name_var = cursor.var(str) self.oid_var = cursor.var(bytes) self.version_var = cursor.var(DB_TYPE_BINARY_INTEGER) self.attrs_ref_cursor_var = cursor.var(DB_TYPE_CURSOR) cursor.setinputsizes(ret_val=self.return_value_var, tds=self.tds_var, full_name=self.full_name_var, oid=self.oid_var, schema=self.schema_var, package_name=self.package_name_var, name=self.name_var, version=self.version_var, attrs_rc=self.attrs_ref_cursor_var) cursor.prepare(""" declare t_Instantiable varchar2(3); t_SuperTypeOwner varchar2(128); t_SuperTypeName varchar2(128); t_SubTypeRefCursor sys_refcursor; begin :ret_val := dbms_pickler.get_type_shape(:full_name, :oid, :version, :tds, t_Instantiable, t_SuperTypeOwner, t_SuperTypeName, :attrs_rc, t_SubTypeRefCursor); :package_name := null; begin select owner, type_name into :schema, :name from all_types where type_oid = :oid; exception when no_data_found then select owner, package_name, type_name into :schema, :package_name, :name from all_plsql_types where type_oid = :oid; end; end;""") self.meta_cursor = cursor cdef int _parse_element_type(self, ThinDbObjectTypeImpl typ_impl, TDSBuffer buf) except -1: """ Parses the element type from the TDS buffer. """ cdef uint8_t attr_type, ora_type_num = 0, csfrm = 0 buf.read_ub1(&attr_type) if attr_type in (TNS_OBJ_TDS_TYPE_NUMBER, TNS_OBJ_TDS_TYPE_FLOAT): ora_type_num = TNS_DATA_TYPE_NUMBER elif attr_type in (TNS_OBJ_TDS_TYPE_VARCHAR, TNS_OBJ_TDS_TYPE_CHAR): buf.skip_raw_bytes(2) # maximum length buf.read_ub1(&csfrm) csfrm = csfrm & 0x7f if attr_type == TNS_OBJ_TDS_TYPE_VARCHAR: ora_type_num = TNS_DATA_TYPE_VARCHAR else: ora_type_num = TNS_DATA_TYPE_CHAR elif attr_type == TNS_OBJ_TDS_TYPE_RAW: ora_type_num = TNS_DATA_TYPE_RAW elif attr_type == TNS_OBJ_TDS_TYPE_BINARY_FLOAT: ora_type_num = TNS_DATA_TYPE_BINARY_FLOAT elif attr_type == TNS_OBJ_TDS_TYPE_BINARY_DOUBLE: ora_type_num = TNS_DATA_TYPE_BINARY_DOUBLE elif attr_type == TNS_OBJ_TDS_TYPE_DATE: ora_type_num = TNS_DATA_TYPE_DATE elif attr_type == TNS_OBJ_TDS_TYPE_TIMESTAMP: ora_type_num = TNS_DATA_TYPE_TIMESTAMP elif attr_type == TNS_OBJ_TDS_TYPE_TIMESTAMP_LTZ: ora_type_num = TNS_DATA_TYPE_TIMESTAMP_LTZ elif attr_type == TNS_OBJ_TDS_TYPE_TIMESTAMP_TZ: ora_type_num = TNS_DATA_TYPE_TIMESTAMP_TZ elif attr_type == TNS_OBJ_TDS_TYPE_BOOLEAN: ora_type_num = TNS_DATA_TYPE_BOOLEAN elif attr_type == TNS_OBJ_TDS_TYPE_CLOB: ora_type_num = TNS_DATA_TYPE_CLOB self._determine_element_type_csfrm(typ_impl, &csfrm) elif attr_type == TNS_OBJ_TDS_TYPE_BLOB: ora_type_num = TNS_DATA_TYPE_BLOB elif attr_type == TNS_OBJ_TDS_TYPE_OBJ: ora_type_num = TNS_DATA_TYPE_INT_NAMED self._determine_element_objtype(typ_impl) else: errors._raise_err(errors.ERR_TDS_TYPE_NOT_SUPPORTED, num=attr_type) typ_impl.element_dbtype = DbType._from_ora_type_and_csfrm(ora_type_num, csfrm) cdef int _parse_tds(self, ThinDbObjectTypeImpl typ_impl, bytes tds) except -1: """ Parses the TDS for the type. This is only needed for collection types, so if the TDS is determined to be for an object type, the remaining information is skipped. """ cdef: uint32_t element_pos uint16_t num_attrs uint8_t attr_type TDSBuffer buf # parse initial TDS bytes buf = TDSBuffer.__new__(TDSBuffer) buf._populate_from_bytes(tds) buf.skip_raw_bytes(4) # end offset buf.skip_raw_bytes(2) # version op code and version buf.skip_raw_bytes(2) # unknown # if the number of attributes exceeds 1, the type cannot refer to a # collection, so nothing further needs to be done buf.read_uint16(&num_attrs) if num_attrs > 1: return 0 # continue parsing TDS bytes to discover if type refers to a collection buf.skip_raw_bytes(1) # TDS attributes? buf.skip_raw_bytes(1) # start ADT op code buf.skip_raw_bytes(2) # ADT number (always zero) buf.skip_raw_bytes(4) # offset to index table # if type of first attribute is not a collection, nothing further needs # to be done buf.read_ub1(&attr_type) if attr_type != TNS_OBJ_TDS_TYPE_COLL: return 0 typ_impl.is_collection = True # continue parsing TDS to determine element type buf.read_uint32(&element_pos) buf.read_uint32(&typ_impl.max_num_elements) buf.read_ub1(&typ_impl.collection_type) if typ_impl.collection_type == TNS_OBJ_PLSQL_INDEX_TABLE: typ_impl.collection_flags = TNS_OBJ_HAS_INDEXES buf.skip_to(element_pos) self._parse_element_type(typ_impl, buf) cdef int _populate_type_info(self, str name, ThinDbObjectTypeImpl typ_impl) except -1: """ Populate the type information given the name of the type. """ cdef: ThinDbObjectAttrImpl attr_impl ssize_t pos, name_length list name_components object attrs_rc self.full_name_var.setvalue(0, name) self.meta_cursor.execute(None) if self.return_value_var.getvalue() != 0: errors._raise_err(errors.ERR_INVALID_OBJECT_TYPE_NAME, name=name) typ_impl.version = self.version_var.getvalue() if typ_impl.oid is None: typ_impl.oid = self.oid_var.getvalue() self.types_by_oid[typ_impl.oid] = typ_impl if typ_impl.schema is None: typ_impl.schema = self.schema_var.getvalue() typ_impl.package_name = self.package_name_var.getvalue() typ_impl.name = self.name_var.getvalue() typ_impl.is_xml_type = \ (typ_impl.schema == "SYS" and typ_impl.name == "XMLTYPE") self._parse_tds(typ_impl, self.tds_var.getvalue()) typ_impl.attrs = [] typ_impl.attrs_by_name = {} attrs_rc = self.attrs_ref_cursor_var.getvalue() for cursor_version, attr_name, attr_num, attr_type_name, \ attr_type_owner, attr_type_package, attr_type_oid, \ attr_instantiable, attr_super_type_owner, \ attr_super_type_name in attrs_rc: attr_impl = ThinDbObjectAttrImpl.__new__(ThinDbObjectAttrImpl) attr_impl.name = attr_name if attr_type_owner is not None: attr_impl.dbtype = DB_TYPE_OBJECT attr_impl.objtype = self.get_type_for_info(attr_type_oid, attr_type_owner, attr_type_package, attr_type_name) else: attr_impl.dbtype = DbType._from_ora_name(attr_type_name) typ_impl.attrs.append(attr_impl) typ_impl.attrs_by_name[attr_name] = attr_impl cdef ThinDbObjectTypeImpl get_type(self, object conn, str name): """ Returns the database object type given its name. The cache is first searched and if it is not found, the database is searched and the result stored in the cache. """ cdef ThinDbObjectTypeImpl typ_impl typ_impl = self.types_by_name.get(name) if typ_impl is None: if self.meta_cursor is None: self._init_meta_cursor(conn) typ_impl = ThinDbObjectTypeImpl.__new__(ThinDbObjectTypeImpl) typ_impl._conn_impl = self.conn_impl self._populate_type_info(name, typ_impl) self.types_by_oid[typ_impl.oid] = typ_impl self.types_by_name[name] = typ_impl self.populate_partial_types(conn) return typ_impl cdef ThinDbObjectTypeImpl get_type_for_info(self, bytes oid, str schema, str package_name, str name): """ Returns a type for the specified fetch info, if one has already been cached. If not, a new type object is created and cached. It is also added to the partial_types list which will be fully populated once the current execute has completed. """ cdef: ThinDbObjectTypeImpl typ_impl str full_name if package_name is not None: full_name = f"{schema}.{package_name}.{name}" else: full_name = f"{schema}.{name}" if oid is not None: typ_impl = self.types_by_oid.get(oid) else: typ_impl = self.types_by_name.get(full_name) if typ_impl is None: typ_impl = ThinDbObjectTypeImpl.__new__(ThinDbObjectTypeImpl) typ_impl._conn_impl = self.conn_impl typ_impl.oid = oid typ_impl.schema = schema typ_impl.package_name = package_name typ_impl.name = name typ_impl.is_xml_type = (schema == "SYS" and name == "XMLTYPE") if oid is not None: self.types_by_oid[oid] = typ_impl self.types_by_name[full_name] = typ_impl self.partial_types.append(typ_impl) return typ_impl cdef int populate_partial_types(self, object conn) except -1: """ Populate any partial types that were discovered earlier. Since populating an object type might result in additional object types being discovered, object types are popped from the partial types list until the list is empty. """ cdef: ThinDbObjectTypeImpl typ_impl str full_name while self.partial_types: typ_impl = self.partial_types.pop() if self.meta_cursor is None: self._init_meta_cursor(conn) if typ_impl.package_name is not None: full_name = f'"{typ_impl.schema}".' + \ f'"{typ_impl.package_name}".' + \ f'"{typ_impl.name}"' else: full_name = f'"{typ_impl.schema}"."{typ_impl.name}"' self._populate_type_info(full_name, typ_impl) # global cache of database object types # since the database object types require a reference to the connection (in # order to be able to manage LOBs), storing the cache on the connection would # involve creating a circular reference cdef ThinDbObjectTypeSuperCache DB_OBJECT_TYPE_SUPER_CACHE = \ ThinDbObjectTypeSuperCache() cdef int create_new_dbobject_type_cache(ThinConnImpl conn_impl) except -1: """ Creates a new database object type cache and returns its identifier. """ cdef: ThinDbObjectTypeCache cache int cache_num with DB_OBJECT_TYPE_SUPER_CACHE.lock: DB_OBJECT_TYPE_SUPER_CACHE.cache_num += 1 cache_num = DB_OBJECT_TYPE_SUPER_CACHE.cache_num cache = ThinDbObjectTypeCache.__new__(ThinDbObjectTypeCache) cache._initialize(conn_impl) DB_OBJECT_TYPE_SUPER_CACHE.caches[cache_num] = cache return cache_num cdef ThinDbObjectTypeCache get_dbobject_type_cache(int cache_num): """ Returns the database object type cache given its identifier. """ return DB_OBJECT_TYPE_SUPER_CACHE.caches[cache_num] cdef int remove_dbobject_type_cache(int cache_num) except -1: """ Removes the sub cache given its identifier. """ del DB_OBJECT_TYPE_SUPER_CACHE.caches[cache_num] python-oracledb-1.2.1/src/oracledb/impl/thin/lob.pyx000066400000000000000000000160021434177474600224100ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # lob.pyx # # Cython file defining the thin Lob implementation class (embedded in # thin_impl.pyx). #------------------------------------------------------------------------------ cdef class ThinLobImpl(BaseLobImpl): cdef: ThinConnImpl _conn_impl bytes _locator @staticmethod cdef ThinLobImpl _create(ThinConnImpl conn_impl, DbType dbtype, bytes locator=None): cdef: ThinLobImpl lob_impl = ThinLobImpl.__new__(ThinLobImpl) LobOpMessage message lob_impl._conn_impl = conn_impl lob_impl.dbtype = dbtype if locator is not None: lob_impl._locator = locator else: lob_impl._locator = bytes(40) message = conn_impl._create_message(LobOpMessage) message.operation = TNS_LOB_OP_CREATE_TEMP message.amount = TNS_DURATION_SESSION message.send_amount = True message.source_lob_impl = lob_impl message.source_offset = dbtype._csfrm message.dest_offset = dbtype._ora_type_num conn_impl._protocol._process_single_message(message) return lob_impl cdef str _get_encoding(self): if self.dbtype._csfrm == TNS_CS_NCHAR \ or self._locator[TNS_LOB_LOCATOR_OFFSET_FLAG_3] & \ TNS_LOB_LOCATOR_VAR_LENGTH_CHARSET: return TNS_ENCODING_UTF16 return TNS_ENCODING_UTF8 def close(self): """ Internal method for closing a LOB that was opened earlier. """ cdef LobOpMessage message message = self._conn_impl._create_message(LobOpMessage) message.operation = TNS_LOB_OP_CLOSE message.source_lob_impl = self self._conn_impl._protocol._process_single_message(message) def free_lob(self): """ Internal method for closing a temp LOB during the next piggyback. """ if self._locator[TNS_LOB_ABSTRACT_POS] & TNS_LOB_ABSTRACT_VALUE \ or self._locator[TNS_LOB_TEMP_POS] & TNS_LOB_TEMP_VALUE: if self._conn_impl._temp_lobs_to_close is None: self._conn_impl._temp_lobs_to_close = [] self._conn_impl._temp_lobs_to_close.append(self._locator) self._conn_impl._temp_lobs_total_size += len(self._locator) self._conn_impl = None def get_chunk_size(self): """ Internal method for returning the chunk size of the LOB. """ cdef LobOpMessage message message = self._conn_impl._create_message(LobOpMessage) message.operation = TNS_LOB_OP_GET_CHUNK_SIZE message.source_lob_impl = self message.send_amount = True self._conn_impl._protocol._process_single_message(message) return message.amount def get_is_open(self): """ Internal method for returning whether the LOB is open or not. """ cdef LobOpMessage message message = self._conn_impl._create_message(LobOpMessage) message.operation = TNS_LOB_OP_IS_OPEN message.source_lob_impl = self self._conn_impl._protocol._process_single_message(message) return message.bool_flag def get_max_amount(self): """ Internal method for returning the maximum amount that can be read. """ return 2**32 - 1 def get_size(self): """ Internal method for returning the size of a LOB. """ cdef LobOpMessage message message = self._conn_impl._create_message(LobOpMessage) message.operation = TNS_LOB_OP_GET_LENGTH message.source_lob_impl = self message.send_amount = True self._conn_impl._protocol._process_single_message(message) return message.amount def open(self): """ Internal method for opening a LOB. """ cdef LobOpMessage message message = self._conn_impl._create_message(LobOpMessage) message.operation = TNS_LOB_OP_OPEN message.source_lob_impl = self message.amount = TNS_LOB_OPEN_READ_WRITE message.send_amount = True self._conn_impl._protocol._process_single_message(message) def read(self, uint64_t offset, uint64_t amount): """ Internal method for reading a portion (or all) of the data in the LOB. """ cdef LobOpMessage message message = self._conn_impl._create_message(LobOpMessage) message.operation = TNS_LOB_OP_READ message.source_lob_impl = self message.source_offset = offset message.amount = amount message.send_amount = True self._conn_impl._protocol._process_single_message(message) if message.data is None: if self.dbtype._ora_type_num == TNS_DATA_TYPE_BLOB: return b"" return "" return message.data def trim(self, uint64_t new_size): """ Internal method for trimming the data in the LOB to the new size """ cdef LobOpMessage message message = self._conn_impl._create_message(LobOpMessage) message.operation = TNS_LOB_OP_TRIM message.source_lob_impl = self message.amount = new_size message.send_amount = True self._conn_impl._protocol._process_single_message(message) def write(self, object value, uint64_t offset): """ Internal method for writing data to the LOB object. """ cdef LobOpMessage message message = self._conn_impl._create_message(LobOpMessage) message.operation = TNS_LOB_OP_WRITE message.source_lob_impl = self message.source_offset = offset if self.dbtype._ora_type_num == TNS_DATA_TYPE_BLOB: message.data = value else: message.data = value.encode(self._get_encoding()) self._conn_impl._protocol._process_single_message(message) python-oracledb-1.2.1/src/oracledb/impl/thin/messages.pyx000066400000000000000000002764731434177474600234670ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # messages.pyx # # Cython file defining the various messages that are sent to the database and # the responses that are received by the client (embedded in thin_impl.pyx). #------------------------------------------------------------------------------ @cython.freelist(20) cdef class _OracleErrorInfo: cdef: uint32_t num uint16_t cursor_id uint16_t pos uint64_t rowcount bint is_warning str message Rowid rowid list batcherrors cdef class Message: cdef: ThinConnImpl conn_impl _OracleErrorInfo error_info uint8_t message_type uint8_t function_code uint32_t call_status uint16_t end_to_end_seq_num uint8_t packet_type uint8_t packet_flags bint error_occurred bint flush_out_binds bint processed_error bint resend cdef bint _has_more_data(self, ReadBuffer buf): return buf.bytes_left() > 0 and not self.flush_out_binds cdef int _initialize(self, ThinConnImpl conn_impl) except -1: """ Initializes the message to contain the connection and a place to store error information. For DRCP, the status of the connection may change after the first round-trip to the database so this information needs to be preserved. Child classes may have their own initialization. In order to avoid overhead using the constructor, a special hook method is used instead. """ self.conn_impl = conn_impl self.message_type = TNS_MSG_TYPE_FUNCTION self.error_info = _OracleErrorInfo.__new__(_OracleErrorInfo) self._initialize_hook() cdef int _initialize_hook(self) except -1: """ A hook that is used by subclasses to perform any necessary initialization specific to that class. """ pass cdef int _preprocess(self) except -1: pass cdef int _postprocess(self) except -1: pass cdef int _process_error_info(self, ReadBuffer buf) except -1: cdef: _OracleErrorInfo info = self.error_info uint16_t num_entries, error_code uint32_t num_bytes, i, offset uint8_t first_byte str error_msg buf.read_ub4(&self.call_status) # end of call status buf.skip_ub2() # end to end seq# buf.skip_ub4() # current row number buf.skip_ub2() # error number buf.skip_ub2() # array elem error buf.skip_ub2() # array elem error buf.read_ub2(&info.cursor_id) # cursor id buf.read_ub2(&info.pos) # error position buf.skip_ub1() # sql type buf.skip_ub1() # fatal? buf.skip_ub2() # flags buf.skip_ub2() # user cursor options buf.skip_ub1() # UPI parameter buf.skip_ub1() # warning flag buf.read_rowid(&info.rowid) # rowid buf.skip_ub4() # OS error buf.skip_ub1() # statement number buf.skip_ub1() # call number buf.skip_ub2() # padding buf.skip_ub4() # success iters buf.read_ub4(&num_bytes) # oerrdd (logical rowid) if num_bytes > 0: buf.skip_raw_bytes_chunked() self.processed_error = True # batch error codes buf.read_ub2(&num_entries) # batch error codes array if num_entries > 0: info.batcherrors = [] buf.read_ub1(&first_byte) for i in range(num_entries): if first_byte == TNS_LONG_LENGTH_INDICATOR: buf.skip_ub4() # chunk length ignored buf.read_ub2(&error_code) info.batcherrors.append(errors._Error(code=error_code)) if first_byte == TNS_LONG_LENGTH_INDICATOR: buf.skip_raw_bytes(1) # ignore end marker # batch error offsets buf.read_ub2(&num_entries) # batch error row offset array if num_entries > 0: buf.read_ub1(&first_byte) for i in range(num_entries): if first_byte == TNS_LONG_LENGTH_INDICATOR: buf.skip_ub4() # chunk length ignored buf.read_ub4(&offset) info.batcherrors[i].offset = offset if first_byte == TNS_LONG_LENGTH_INDICATOR: buf.skip_raw_bytes(1) # ignore end marker # batch error messages buf.read_ub2(&num_entries) # batch error messages array if num_entries > 0: buf.skip_raw_bytes(1) # ignore packed size for i in range(num_entries): buf.skip_ub2() # skip chunk length info.batcherrors[i].message = \ buf.read_str(TNS_CS_IMPLICIT).rstrip() info.batcherrors[i]._make_adjustments() buf.skip_raw_bytes(2) # ignore end marker buf.read_ub4(&info.num) # error number (extended) buf.read_ub8(&info.rowcount) # row number (extended) if info.num != 0: self.error_occurred = True info.message = buf.read_str(TNS_CS_IMPLICIT).rstrip() info.is_warning = False cdef int _process_message(self, ReadBuffer buf, uint8_t message_type) except -1: if message_type == TNS_MSG_TYPE_ERROR: self._process_error_info(buf) elif message_type == TNS_MSG_TYPE_WARNING: self._process_warning_info(buf) elif message_type == TNS_MSG_TYPE_STATUS: buf.read_ub4(&self.call_status) buf.read_ub2(&self.end_to_end_seq_num) elif message_type == TNS_MSG_TYPE_PARAMETER: self._process_return_parameters(buf) elif message_type == TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK: self._process_server_side_piggyback(buf) else: errors._raise_err(errors.ERR_MESSAGE_TYPE_UNKNOWN, message_type=message_type) cdef int _process_return_parameters(self, ReadBuffer buf) except -1: raise NotImplementedError() cdef int _process_server_side_piggyback(self, ReadBuffer buf) except -1: cdef: uint16_t num_elements, i, temp16 uint32_t num_bytes, flags uint8_t opcode buf.read_ub1(&opcode) if opcode == TNS_SERVER_PIGGYBACK_LTXID: buf.read_ub4(&num_bytes) if num_bytes > 0: buf.skip_raw_bytes(num_bytes) elif opcode == TNS_SERVER_PIGGYBACK_QUERY_CACHE_INVALIDATION \ or opcode == TNS_SERVER_PIGGYBACK_TRACE_EVENT: pass elif opcode == TNS_SERVER_PIGGYBACK_OS_PID_MTS: buf.read_ub2(&temp16) buf.skip_raw_bytes_chunked() elif opcode == TNS_SERVER_PIGGYBACK_SYNC: buf.skip_ub2() # skip number of DTYs buf.skip_ub1() # skip length of DTYs buf.read_ub2(&num_elements) buf.skip_ub1() # skip length for i in range(num_elements): buf.read_ub2(&temp16) if temp16 > 0: # skip key buf.skip_raw_bytes_chunked() buf.read_ub2(&temp16) if temp16 > 0: # skip value buf.skip_raw_bytes_chunked() buf.skip_ub2() # skip flags buf.skip_ub4() # skip overall flags elif opcode == TNS_SERVER_PIGGYBACK_EXT_SYNC: buf.skip_ub2() # skip number of DTYs buf.skip_ub1() # skip length of DTYs elif opcode == TNS_SERVER_PIGGYBACK_AC_REPLAY_CONTEXT: buf.skip_ub2() # skip number of DTYs buf.skip_ub1() # skip length of DTYs buf.skip_ub4() # skip flags buf.skip_ub4() # skip error code buf.skip_ub1() # skip queue buf.read_ub4(&num_bytes) # skip replay context if num_bytes > 0: buf.skip_raw_bytes(num_bytes) elif opcode == TNS_SERVER_PIGGYBACK_SESS_RET: buf.skip_ub2() buf.skip_ub1() buf.read_ub2(&num_elements) if num_elements > 0: buf.skip_ub1() for i in range(num_elements): buf.read_ub2(&temp16) if temp16 > 0: # skip key buf.skip_raw_bytes_chunked() buf.read_ub2(&temp16) if temp16 > 0: # skip value buf.skip_raw_bytes_chunked() buf.skip_ub2() # skip flags buf.read_ub4(&flags) # session flags if flags & TNS_SESSGET_SESSION_CHANGED: if self.conn_impl._drcp_establish_session: self.conn_impl._reset_statement_cache() self.conn_impl._drcp_establish_session = False buf.skip_ub4() # session id buf.skip_ub2() # serial number else: msg = f"Unhandled server side piggyback opcode: {opcode}" raise Exception(msg) cdef int _process_warning_info(self, ReadBuffer buf) except -1: cdef: _OracleErrorInfo info = self.error_info const char_type *ptr uint16_t num_bytes, temp16 buf.read_ub2(&temp16) # error number info.num = temp16 buf.read_ub2(&num_bytes) # length of error message buf.skip_ub2() # flags if info.num != 0 and num_bytes > 0: ptr = buf.read_raw_bytes(num_bytes) info.message = ptr[:num_bytes].decode().rstrip() info.is_warning = True cdef int _write_function_code(self, WriteBuffer buf) except -1: buf.write_uint8(self.message_type) buf.write_uint8(self.function_code) buf.write_seq_num() cdef int _write_message(self, WriteBuffer buf) except -1: self._write_function_code(buf) cdef int process(self, ReadBuffer buf) except -1: cdef uint8_t message_type self.flush_out_binds = False self.processed_error = False self._preprocess() buf.skip_raw_bytes(2) # skip data flags while self._has_more_data(buf): buf.read_ub1(&message_type) self._process_message(buf, message_type) self._postprocess() cdef int send(self, WriteBuffer buf) except -1: buf.start_request(TNS_PACKET_TYPE_DATA) self._write_message(buf) buf.end_request() cdef class MessageWithData(Message): cdef: ThinDbObjectTypeCache type_cache ThinCursorImpl cursor_impl array.array bit_vector_buf const char_type *bit_vector bint arraydmlrowcounts uint32_t row_index uint32_t num_execs uint16_t num_columns_sent list dmlrowcounts bint batcherrors list out_var_impls bint in_fetch bint parse_only object cursor uint32_t offset cdef int _adjust_fetch_info(self, ThinVarImpl prev_var_impl, FetchInfo fetch_info) except -1: """ When a query is re-executed but the data type of a column has changed the server returns the type information of the new type. However, if the data type returned now is a CLOB or BLOB and the data type previously returned was CHAR/VARCHAR/RAW (or the equivalent long types), then the server returns the data as LONG (RAW), similarly to what happens when a define is done to return CLOB/BLOB as string/bytes. Detect this situation and adjust the fetch type appropriately. """ cdef: FetchInfo prev_fetch_info = prev_var_impl._fetch_info uint8_t csfrm = prev_var_impl.dbtype._csfrm uint8_t type_num if fetch_info._dbtype._ora_type_num == TNS_DATA_TYPE_CLOB \ and prev_fetch_info._dbtype._ora_type_num in \ (TNS_DATA_TYPE_CHAR, TNS_DATA_TYPE_VARCHAR, TNS_DATA_TYPE_LONG): type_num = TNS_DATA_TYPE_LONG fetch_info._dbtype = DbType._from_ora_type_and_csfrm(type_num, csfrm) elif fetch_info._dbtype._ora_type_num == TNS_DATA_TYPE_BLOB \ and prev_fetch_info._dbtype._ora_type_num in \ (TNS_DATA_TYPE_RAW, TNS_DATA_TYPE_LONG_RAW): type_num = TNS_DATA_TYPE_LONG_RAW fetch_info._dbtype = DbType._from_ora_type_and_csfrm(type_num, csfrm) cdef object _create_cursor_from_describe(self, ReadBuffer buf, object cursor=None): cdef ThinCursorImpl cursor_impl if cursor is None: cursor = self.cursor.connection.cursor() cursor_impl = cursor._impl cursor_impl._statement = Statement() cursor_impl._fetch_array_size = cursor.arraysize + cursor.prefetchrows cursor_impl._more_rows_to_fetch = True cursor_impl._statement._is_query = True cursor_impl._statement._requires_full_execute = True self._process_describe_info(buf, cursor, cursor_impl) return cursor cdef int _get_bit_vector(self, ReadBuffer buf, ssize_t num_bytes) except -1: """ Gets the bit vector from the buffer and stores it for later use by the row processing code. Since it is possible that the packet buffer may be overwritten by subsequent packet retrieval, the bit vector must be copied. An array is stored and a pointer to the underlying memory is used for performance reasons. """ cdef const char_type *ptr = buf._get_raw(num_bytes) if self.bit_vector_buf is None: self.bit_vector_buf = array.array('B') array.resize(self.bit_vector_buf, num_bytes) self.bit_vector = self.bit_vector_buf.data.as_chars memcpy( self.bit_vector, ptr, num_bytes) cdef bint _has_more_data(self, ReadBuffer buf): return not self.processed_error and not self.flush_out_binds cdef bint _is_duplicate_data(self, uint32_t column_num): """ Returns a boolean indicating if the given column contains data duplicated from the previous row. When duplicate data exists, the server sends a bit vector. Bits that are set indicate that data is sent with the row data; bits that are not set indicate that data should be duplicated from the previous row. """ cdef int byte_num, bit_num if self.bit_vector == NULL: return False byte_num = column_num // 8 bit_num = column_num % 8 return self.bit_vector[byte_num] & (1 << bit_num) == 0 cdef int _write_bind_params(self, WriteBuffer buf, list params) except -1: cdef: bint returning_only = True, all_values_null = True list bind_var_impls, bind_vals uint32_t i BindInfo bind_info bind_var_impls = [] for bind_info in params: if not bind_info._is_return_bind: returning_only = False bind_vals = bind_info._bind_var_impl._values for i in range(len(bind_vals)): if cpython.PyList_GET_ITEM(bind_vals, i) is not \ None: all_values_null = False break bind_var_impls.append(bind_info._bind_var_impl) self._write_column_metadata(buf, bind_var_impls) # plsql batch executions without bind values if self.cursor_impl._statement._is_plsql and self.num_execs > 1 \ and not all_values_null: buf.write_uint8(TNS_MSG_TYPE_ROW_DATA) buf.write_uint8(TNS_ESCAPE_CHAR) buf.write_uint8(1) # write parameter values unless statement contains only returning binds elif not returning_only: for i in range(self.num_execs): buf.write_uint8(TNS_MSG_TYPE_ROW_DATA) self._write_bind_params_row(buf, params, i) cdef int _preprocess(self) except -1: cdef: Statement statement = self.cursor_impl._statement BindInfo bind_info if statement._is_returning and not self.parse_only: self.out_var_impls = [] for bind_info in statement._bind_info_list: if not bind_info._is_return_bind: continue self.out_var_impls.append(bind_info._bind_var_impl) elif statement._is_query: self._preprocess_query() cdef int _preprocess_query(self) except -1: """ Actions that takes place before query data is processed. """ cdef: ThinCursorImpl cursor_impl = self.cursor_impl Statement statement = cursor_impl._statement object type_handler, conn ThinVarImpl var_impl ssize_t i, num_vals # set values to indicate the start of a new fetch operation self.in_fetch = True cursor_impl._more_rows_to_fetch = True cursor_impl._buffer_rowcount = cursor_impl._buffer_index = 0 # if no fetch variables exist, nothing further to do at this point; the # processing that follows will take the metadata returned by the server # and use it to create new fetch variables if cursor_impl.fetch_var_impls is None: return 0 # if the type handler set on the cursor or connection does not match # the one that was used during the last fetch, rebuild the fetch # variables in order to take the new type handler into account conn = self.cursor.connection type_handler = cursor_impl._get_output_type_handler() if type_handler is not statement._last_output_type_handler: for i, var_impl in enumerate(cursor_impl.fetch_var_impls): cursor_impl._create_fetch_var(conn, self.cursor, type_handler, i, var_impl._fetch_info) statement._last_output_type_handler = type_handler # the list of output variables is equivalent to the fetch variables self.out_var_impls = cursor_impl.fetch_var_impls # resize fetch variables, if necessary, to allow room in each variable # for the fetch array size for var_impl in self.out_var_impls: if var_impl.num_elements >= cursor_impl._fetch_array_size: continue num_vals = (cursor_impl._fetch_array_size - var_impl.num_elements) var_impl.num_elements = cursor_impl._fetch_array_size var_impl._values.extend([None] * num_vals) cdef int _postprocess(self) except -1: """ Run any variable out converter functions on all non-null values that were returned in the current database response. This must be done independently since the out converter function may itself invoke a database round-trip. """ cdef: uint32_t i, j, num_elements object value, element_value ThinVarImpl var_impl if self.out_var_impls is None: return 0 for var_impl in self.out_var_impls: if var_impl is None or var_impl.outconverter is None: continue var_impl._last_raw_value = \ var_impl._values[self.cursor_impl._last_row_index] if var_impl.is_array: num_elements = var_impl.num_elements_in_array else: num_elements = self.row_index for i in range(num_elements): value = var_impl._values[i] if value is None: continue if isinstance(value, list): for j, element_value in enumerate(value): if element_value is None: continue value[j] = var_impl.outconverter(element_value) else: var_impl._values[i] = var_impl.outconverter(value) cdef int _process_bit_vector(self, ReadBuffer buf) except -1: cdef ssize_t num_bytes buf.read_ub2(&self.num_columns_sent) num_bytes = self.cursor_impl._num_columns // 8 if self.cursor_impl._num_columns % 8 > 0: num_bytes += 1 self._get_bit_vector(buf, num_bytes) cdef object _process_column_data(self, ReadBuffer buf, ThinVarImpl var_impl, uint32_t pos): cdef: uint8_t num_bytes, ora_type_num, csfrm ThinDbObjectTypeImpl typ_impl ThinCursorImpl cursor_impl object column_value = None ThinDbObjectImpl obj_impl int32_t actual_num_bytes uint32_t buffer_size FetchInfo fetch_info Rowid rowid fetch_info = var_impl._fetch_info if fetch_info is not None: ora_type_num = fetch_info._dbtype._ora_type_num csfrm = fetch_info._dbtype._csfrm buffer_size = fetch_info._buffer_size else: ora_type_num = var_impl.dbtype._ora_type_num csfrm = var_impl.dbtype._csfrm buffer_size = var_impl.buffer_size if var_impl.bypass_decode: ora_type_num = TNS_DATA_TYPE_RAW if buffer_size == 0 and self.in_fetch \ and ora_type_num not in (TNS_DATA_TYPE_LONG, TNS_DATA_TYPE_LONG_RAW, TNS_DATA_TYPE_UROWID): column_value = None # column is null by describe elif ora_type_num == TNS_DATA_TYPE_VARCHAR \ or ora_type_num == TNS_DATA_TYPE_CHAR \ or ora_type_num == TNS_DATA_TYPE_LONG: if csfrm == TNS_CS_NCHAR: buf._caps._check_ncharset_id() column_value = buf.read_str(csfrm) elif ora_type_num == TNS_DATA_TYPE_RAW \ or ora_type_num == TNS_DATA_TYPE_LONG_RAW: column_value = buf.read_bytes() elif ora_type_num == TNS_DATA_TYPE_NUMBER: column_value = buf.read_oracle_number(var_impl._preferred_num_type) elif ora_type_num == TNS_DATA_TYPE_DATE \ or ora_type_num == TNS_DATA_TYPE_TIMESTAMP \ or ora_type_num == TNS_DATA_TYPE_TIMESTAMP_LTZ \ or ora_type_num == TNS_DATA_TYPE_TIMESTAMP_TZ: column_value = buf.read_date() elif ora_type_num == TNS_DATA_TYPE_ROWID: if not self.in_fetch: column_value = buf.read_str(TNS_CS_IMPLICIT) else: buf.read_ub1(&num_bytes) if num_bytes == 0 or num_bytes == TNS_NULL_LENGTH_INDICATOR: column_value = None else: buf.read_rowid(&rowid) column_value = _encode_rowid(&rowid) elif ora_type_num == TNS_DATA_TYPE_UROWID: if not self.in_fetch: column_value = buf.read_str(TNS_CS_IMPLICIT) else: column_value = buf.read_urowid() elif ora_type_num == TNS_DATA_TYPE_BINARY_DOUBLE: column_value = buf.read_binary_double() elif ora_type_num == TNS_DATA_TYPE_BINARY_FLOAT: column_value = buf.read_binary_float() elif ora_type_num == TNS_DATA_TYPE_BINARY_INTEGER: column_value = buf.read_oracle_number(NUM_TYPE_INT) if column_value is not None: column_value = int(column_value) elif ora_type_num == TNS_DATA_TYPE_CURSOR: buf.skip_ub1() # length (fixed value) if not self.in_fetch: column_value = var_impl._values[pos] column_value = self._create_cursor_from_describe(buf, column_value) cursor_impl = column_value._impl buf.read_ub2(&cursor_impl._statement._cursor_id) elif ora_type_num == TNS_DATA_TYPE_BOOLEAN: column_value = buf.read_bool() elif ora_type_num == TNS_DATA_TYPE_INTERVAL_DS: column_value = buf.read_interval_ds() elif ora_type_num in (TNS_DATA_TYPE_CLOB, TNS_DATA_TYPE_BLOB): column_value = buf.read_lob_with_length(self.conn_impl, var_impl.dbtype) elif ora_type_num == TNS_DATA_TYPE_INT_NAMED: typ_impl = var_impl.objtype if typ_impl.is_xml_type: column_value = buf.read_xmltype(self.conn_impl) else: obj_impl = buf.read_dbobject(typ_impl) if obj_impl is not None: if not self.in_fetch: column_value = var_impl._values[pos] if column_value is not None: column_value._impl = obj_impl else: column_value = PY_TYPE_DB_OBJECT._from_impl(obj_impl) else: errors._raise_err(errors.ERR_DB_TYPE_NOT_SUPPORTED, name=var_impl.dbtype.name) if not self.in_fetch: buf.read_sb4(&actual_num_bytes) if actual_num_bytes != 0 and column_value is not None: unit_type = "bytes" if isinstance(column_value, bytes) \ else "characters" errors._raise_err(errors.ERR_COLUMN_TRUNCATED, col_value_len=len(column_value), unit=unit_type, actual_len=actual_num_bytes) elif ora_type_num == TNS_DATA_TYPE_LONG \ or ora_type_num == TNS_DATA_TYPE_LONG_RAW: buf.skip_sb4() # null indicator buf.skip_ub4() # return code if column_value is not None: if var_impl._conv_func is not None: column_value = var_impl._conv_func(column_value) return column_value cdef FetchInfo _process_column_info(self, ReadBuffer buf, ThinCursorImpl cursor_impl): cdef: ThinDbObjectTypeImpl typ_impl uint8_t data_type, csfrm int8_t precision, scale uint8_t nulls_allowed FetchInfo fetch_info uint32_t num_bytes str schema, name int cache_num bytes oid buf.read_ub1(&data_type) fetch_info = FetchInfo() buf.skip_ub1() # flags buf.read_sb1(&precision) fetch_info._precision = precision if data_type == TNS_DATA_TYPE_NUMBER \ or data_type == TNS_DATA_TYPE_INTERVAL_DS \ or data_type == TNS_DATA_TYPE_TIMESTAMP \ or data_type == TNS_DATA_TYPE_TIMESTAMP_LTZ \ or data_type == TNS_DATA_TYPE_TIMESTAMP_TZ: buf.read_sb2(&fetch_info._scale) else: buf.read_sb1(&scale) fetch_info._scale = scale buf.read_ub4(&fetch_info._buffer_size) buf.skip_ub4() # max number of array elements buf.skip_ub4() # cont flags buf.read_ub4(&num_bytes) # OID if num_bytes > 0: oid = buf.read_bytes() buf.skip_ub2() # version buf.skip_ub2() # character set id buf.read_ub1(&csfrm) # character set form fetch_info._dbtype = DbType._from_ora_type_and_csfrm(data_type, csfrm) buf.read_ub4(&fetch_info._size) if data_type == TNS_DATA_TYPE_RAW: fetch_info._size = fetch_info._buffer_size if buf._caps.ttc_field_version >= TNS_CCAP_FIELD_VERSION_12_2: buf.skip_ub4() # oaccolid buf.read_ub1(&nulls_allowed) fetch_info._nulls_allowed = nulls_allowed buf.skip_ub1() # v7 length of name buf.read_ub4(&num_bytes) if num_bytes > 0: fetch_info._name = buf.read_str(TNS_CS_IMPLICIT) buf.read_ub4(&num_bytes) if num_bytes > 0: schema = buf.read_str(TNS_CS_IMPLICIT) buf.read_ub4(&num_bytes) if num_bytes > 0: name = buf.read_str(TNS_CS_IMPLICIT) buf.skip_ub2() # column position buf.skip_ub4() # uds flag if data_type == TNS_DATA_TYPE_INT_NAMED: if self.type_cache is None: cache_num = self.conn_impl._dbobject_type_cache_num self.type_cache = get_dbobject_type_cache(cache_num) typ_impl = self.type_cache.get_type_for_info(oid, schema, None, name) fetch_info._objtype = typ_impl return fetch_info cdef int _process_describe_info(self, ReadBuffer buf, object cursor, ThinCursorImpl cursor_impl) except -1: cdef: Statement stmt = cursor_impl._statement list prev_fetch_var_impls object type_handler, conn uint32_t num_bytes, i FetchInfo fetch_info str message buf.skip_ub4() # max row size buf.read_ub4(&cursor_impl._num_columns) prev_fetch_var_impls = cursor_impl.fetch_var_impls cursor_impl._init_fetch_vars(cursor_impl._num_columns) if cursor_impl._num_columns > 0: buf.skip_ub1() type_handler = cursor_impl._get_output_type_handler() conn = self.cursor.connection for i in range(cursor_impl._num_columns): fetch_info = self._process_column_info(buf, cursor_impl) if prev_fetch_var_impls is not None: self._adjust_fetch_info(prev_fetch_var_impls[i], fetch_info) cursor_impl._create_fetch_var(conn, self.cursor, type_handler, i, fetch_info) buf.read_ub4(&num_bytes) if num_bytes > 0: buf.skip_raw_bytes_chunked() # current date buf.skip_ub4() # dcbflag buf.skip_ub4() # dcbmdbz buf.skip_ub4() # dcbmnpr buf.skip_ub4() # dcbmxpr buf.read_ub4(&num_bytes) if num_bytes > 0: buf.skip_raw_bytes_chunked() # dcbqcky stmt._fetch_vars = cursor_impl.fetch_vars stmt._fetch_var_impls = cursor_impl.fetch_var_impls stmt._num_columns = cursor_impl._num_columns stmt._last_output_type_handler = type_handler cdef int _process_error_info(self, ReadBuffer buf) except -1: cdef: ThinCursorImpl cursor_impl = self.cursor_impl ThinConnImpl conn_impl = self.conn_impl object exc_type Message._process_error_info(self, buf) cursor_impl._statement._cursor_id = self.error_info.cursor_id if not cursor_impl._statement._is_plsql: cursor_impl.rowcount = self.error_info.rowcount cursor_impl._lastrowid = self.error_info.rowid cursor_impl._batcherrors = self.error_info.batcherrors if self.batcherrors and cursor_impl._batcherrors is None: cursor_impl._batcherrors = [] if self.error_info.num == TNS_ERR_NO_DATA_FOUND: self.error_info.num = 0 cursor_impl._more_rows_to_fetch = False self.error_occurred = False elif self.error_info.num == TNS_ERR_ARRAY_DML_ERRORS: self.error_info.num = 0 self.error_occurred = False elif self.error_info.num == TNS_ERR_VAR_NOT_IN_SELECT_LIST: conn_impl._add_cursor_to_close(cursor_impl._statement) cursor_impl._statement._cursor_id = 0 elif self.error_info.num != 0 and self.error_info.cursor_id != 0: exc_type = get_exception_class(self.error_info.num) if exc_type is not exceptions.IntegrityError: conn_impl._add_cursor_to_close(cursor_impl._statement) cursor_impl._statement._cursor_id = 0 cdef int _process_implicit_result(self, ReadBuffer buf) except -1: cdef: ThinCursorImpl child_cursor_impl uint32_t i, num_results object child_cursor uint8_t num_bytes self.cursor_impl._implicit_resultsets = [] buf.read_ub4(&num_results) for i in range(num_results): buf.read_ub1(&num_bytes) buf.skip_raw_bytes(num_bytes) child_cursor = self._create_cursor_from_describe(buf) child_cursor_impl = child_cursor._impl buf.read_ub2(&child_cursor_impl._statement._cursor_id) self.cursor_impl._implicit_resultsets.append(child_cursor) cdef int _process_io_vector(self, ReadBuffer buf) except -1: """ An I/O vector is sent by the database in response to a PL/SQL execute. It indicates whether binds are IN only, IN/OUT or OUT only. """ cdef: uint16_t i, num_binds, num_bytes, temp16 BindInfo bind_info bint has_in_bind = False buf.skip_ub1() # flag buf.read_ub2(&num_binds) # num requests buf.read_ub4(&self.row_index) # iter num buf.skip_ub4() # num iters this time buf.read_ub2(&temp16) # uac buffer length buf.read_ub2(&num_bytes) # bit vector for fast fetch if num_bytes > 0: buf.skip_raw_bytes(num_bytes) buf.read_ub2(&num_bytes) # rowid if num_bytes > 0: buf.skip_raw_bytes(num_bytes) self.out_var_impls = [] for i in range(num_binds): # bind directions bind_info = self.cursor_impl._statement._bind_info_list[i] buf.read_ub1(&bind_info.bind_dir) if bind_info.bind_dir == TNS_BIND_DIR_INPUT: has_in_bind = True continue self.out_var_impls.append(bind_info._bind_var_impl) if self.cursor_impl._statement._is_plsql and \ self.out_var_impls and has_in_bind: self.cursor_impl._statement._plsql_multiple_execs = True cdef int _process_message(self, ReadBuffer buf, uint8_t message_type) except -1: if message_type == TNS_MSG_TYPE_ROW_HEADER: self._process_row_header(buf) elif message_type == TNS_MSG_TYPE_ROW_DATA: self._process_row_data(buf) elif message_type == TNS_MSG_TYPE_FLUSH_OUT_BINDS: self.flush_out_binds = True elif message_type == TNS_MSG_TYPE_DESCRIBE_INFO: buf.skip_raw_bytes_chunked() self._process_describe_info(buf, self.cursor, self.cursor_impl) self.out_var_impls = self.cursor_impl.fetch_var_impls elif message_type == TNS_MSG_TYPE_ERROR: self._process_error_info(buf) elif message_type == TNS_MSG_TYPE_BIT_VECTOR: self._process_bit_vector(buf) elif message_type == TNS_MSG_TYPE_IO_VECTOR: self._process_io_vector(buf) elif message_type == TNS_MSG_TYPE_IMPLICIT_RESULTSET: self._process_implicit_result(buf) else: Message._process_message(self, buf, message_type) cdef int _process_return_parameters(self, ReadBuffer buf) except -1: cdef: uint16_t keyword_num, num_params, num_bytes uint32_t num_rows, i uint64_t rowcount bytes key_value list rowcounts buf.read_ub2(&num_params) # al8o4l (ignored) for i in range(num_params): buf.skip_ub4() buf.read_ub2(&num_bytes) # al8txl (ignored) if num_bytes > 0: buf.skip_raw_bytes(num_bytes) buf.read_ub2(&num_params) # num key/value pairs for i in range(num_params): buf.read_ub2(&num_bytes) # key if num_bytes > 0: key_value = buf.read_bytes() buf.read_ub2(&num_bytes) # value if num_bytes > 0: buf.skip_raw_bytes_chunked() buf.read_ub2(&keyword_num) # keyword num if keyword_num == TNS_KEYWORD_NUM_CURRENT_SCHEMA: self.conn_impl._current_schema = key_value.decode() elif keyword_num == TNS_KEYWORD_NUM_EDITION: self.conn_impl._edition = key_value.decode() buf.read_ub2(&num_bytes) # registration if num_bytes > 0: buf.skip_raw_bytes(num_bytes) if self.arraydmlrowcounts: buf.read_ub4(&num_rows) rowcounts = self.cursor_impl._dmlrowcounts = [] for i in range(num_rows): buf.read_ub8(&rowcount) rowcounts.append(rowcount) cdef int _process_row_data(self, ReadBuffer buf) except -1: cdef: uint32_t num_rows, pos ThinVarImpl var_impl ssize_t i, j object value list values for i, var_impl in enumerate(self.out_var_impls): if var_impl.is_array: buf.read_ub4(&var_impl.num_elements_in_array) for pos in range(var_impl.num_elements_in_array): value = self._process_column_data(buf, var_impl, pos) var_impl._values[pos] = value elif self.cursor_impl._statement._is_returning: buf.read_ub4(&num_rows) values = [None] * num_rows for j in range(num_rows): values[j] = self._process_column_data(buf, var_impl, j) var_impl._values[self.row_index] = values elif self._is_duplicate_data(i): if self.row_index == 0 and var_impl.outconverter is not None: value = var_impl._last_raw_value else: value = var_impl._values[self.cursor_impl._last_row_index] var_impl._values[self.row_index] = value else: value = self._process_column_data(buf, var_impl, self.row_index) var_impl._values[self.row_index] = value self.row_index += 1 if self.in_fetch: self.cursor_impl._last_row_index = self.row_index - 1 self.cursor_impl._buffer_rowcount = self.row_index self.bit_vector = NULL cdef int _process_row_header(self, ReadBuffer buf) except -1: cdef uint32_t num_bytes buf.skip_ub1() # flags buf.skip_ub2() # num requests buf.skip_ub4() # iteration number buf.skip_ub4() # num iters buf.skip_ub2() # buffer length buf.read_ub4(&num_bytes) if num_bytes > 0: buf.skip_ub1() # skip repeated length self._get_bit_vector(buf, num_bytes) buf.read_ub4(&num_bytes) if num_bytes > 0: buf.skip_raw_bytes_chunked() # rxhrid cdef int _write_column_metadata(self, WriteBuffer buf, list bind_var_impls) except -1: cdef: ThinDbObjectTypeImpl typ_impl uint8_t ora_type_num, flag uint32_t buffer_size ThinVarImpl var_impl for var_impl in bind_var_impls: ora_type_num = var_impl.dbtype._ora_type_num buffer_size = var_impl.buffer_size if ora_type_num in (TNS_DATA_TYPE_ROWID, TNS_DATA_TYPE_UROWID): ora_type_num = TNS_DATA_TYPE_VARCHAR buffer_size = TNS_MAX_UROWID_LENGTH flag = TNS_BIND_USE_INDICATORS if var_impl.is_array: flag |= TNS_BIND_ARRAY buf.write_uint8(ora_type_num) buf.write_uint8(flag) # precision and scale are always written as zero as the server # expects that and complains if any other value is sent! buf.write_uint8(0) buf.write_uint8(0) if buffer_size >= TNS_MIN_LONG_LENGTH: buf.write_ub4(TNS_MAX_LONG_LENGTH) else: buf.write_ub4(buffer_size) if var_impl.is_array: buf.write_ub4(var_impl.num_elements) else: buf.write_ub4(0) # max num elements buf.write_ub4(0) # cont flag if var_impl.objtype is not None: typ_impl = var_impl.objtype buf.write_ub4(len(typ_impl.oid)) buf.write_bytes_with_length(typ_impl.oid) buf.write_ub4(typ_impl.version) else: buf.write_ub4(0) # OID buf.write_ub4(0) # version if var_impl.dbtype._csfrm != 0: buf.write_ub4(TNS_CHARSET_UTF8) else: buf.write_ub4(0) buf.write_uint8(var_impl.dbtype._csfrm) buf.write_ub4(0) # max chars (not used) if buf._caps.ttc_field_version >= TNS_CCAP_FIELD_VERSION_12_2: buf.write_ub4(0) # oaccolid cdef int _write_bind_params_column(self, WriteBuffer buf, ThinVarImpl var_impl, object value) except -1: cdef: uint8_t ora_type_num = var_impl.dbtype._ora_type_num ThinDbObjectTypeImpl typ_impl ThinCursorImpl cursor_impl ThinLobImpl lob_impl uint32_t num_bytes bytes temp_bytes if value is None: if ora_type_num == TNS_DATA_TYPE_BOOLEAN: buf.write_uint8(TNS_ESCAPE_CHAR) buf.write_uint8(1) elif ora_type_num == TNS_DATA_TYPE_INT_NAMED: buf.write_ub4(0) # TOID buf.write_ub4(0) # OID buf.write_ub4(0) # snapshot buf.write_ub4(0) # version buf.write_ub4(0) # packed data length buf.write_ub4(TNS_OBJ_TOP_LEVEL) # flags else: buf.write_uint8(0) elif ora_type_num == TNS_DATA_TYPE_VARCHAR \ or ora_type_num == TNS_DATA_TYPE_CHAR \ or ora_type_num == TNS_DATA_TYPE_LONG: if var_impl.dbtype._csfrm == TNS_CS_IMPLICIT: temp_bytes = ( value).encode() else: buf._caps._check_ncharset_id() temp_bytes = ( value).encode(TNS_ENCODING_UTF16) buf.write_bytes_with_length(temp_bytes) elif ora_type_num == TNS_DATA_TYPE_RAW \ or ora_type_num == TNS_DATA_TYPE_LONG_RAW: buf.write_bytes_with_length(value) elif ora_type_num == TNS_DATA_TYPE_NUMBER \ or ora_type_num == TNS_DATA_TYPE_BINARY_INTEGER: if isinstance(value, bool): temp_bytes = b'1' if value is True else b'0' else: temp_bytes = ( cpython.PyObject_Str(value)).encode() buf.write_oracle_number(temp_bytes) elif ora_type_num == TNS_DATA_TYPE_DATE \ or ora_type_num == TNS_DATA_TYPE_TIMESTAMP \ or ora_type_num == TNS_DATA_TYPE_TIMESTAMP_TZ \ or ora_type_num == TNS_DATA_TYPE_TIMESTAMP_LTZ: buf.write_oracle_date(value, var_impl.dbtype._buffer_size_factor) elif ora_type_num == TNS_DATA_TYPE_BINARY_DOUBLE: buf.write_binary_double(value) elif ora_type_num == TNS_DATA_TYPE_BINARY_FLOAT: buf.write_binary_float(value) elif ora_type_num == TNS_DATA_TYPE_CURSOR: cursor_impl = value._impl if cursor_impl._statement is None: cursor_impl._statement = Statement() if cursor_impl._statement._cursor_id == 0: buf.write_uint8(1) buf.write_uint8(0) else: buf.write_ub4(1) buf.write_ub4(cursor_impl._statement._cursor_id) value.statement = None elif ora_type_num == TNS_DATA_TYPE_BOOLEAN: buf.write_bool(value) elif ora_type_num == TNS_DATA_TYPE_INTERVAL_DS: buf.write_interval_ds(value) elif ora_type_num == TNS_DATA_TYPE_CLOB \ or ora_type_num == TNS_DATA_TYPE_BLOB: buf.write_lob_with_length(value._impl) elif ora_type_num in (TNS_DATA_TYPE_ROWID, TNS_DATA_TYPE_UROWID): temp_bytes = ( value).encode() buf.write_bytes_with_length(temp_bytes) elif ora_type_num == TNS_DATA_TYPE_INT_NAMED: buf.write_dbobject(value._impl) else: errors._raise_err(errors.ERR_DB_TYPE_NOT_SUPPORTED, name=var_impl.dbtype.name) cdef int _write_bind_params_row(self, WriteBuffer buf, list params, uint32_t pos) except -1: """ Write a row of bind parameters. Note that non-LONG values are written first followed by any LONG values. """ cdef: uint32_t i, num_elements, offset = self.offset bint found_long = False ThinVarImpl var_impl BindInfo bind_info for i, bind_info in enumerate(params): if bind_info._is_return_bind: continue var_impl = bind_info._bind_var_impl if var_impl.is_array: num_elements = var_impl.num_elements_in_array buf.write_ub4(num_elements) for value in var_impl._values[:num_elements]: self._write_bind_params_column(buf, var_impl, value) else: if var_impl.buffer_size >= TNS_MIN_LONG_LENGTH: found_long = True continue self._write_bind_params_column(buf, var_impl, var_impl._values[pos + offset]) if found_long: for i, bind_info in enumerate(params): if bind_info._is_return_bind: continue var_impl = bind_info._bind_var_impl if var_impl.buffer_size < TNS_MIN_LONG_LENGTH: continue self._write_bind_params_column(buf, var_impl, var_impl._values[pos + offset]) cdef int _write_close_cursors_piggyback(self, WriteBuffer buf) except -1: cdef: unsigned int *cursor_ids ssize_t i buf.write_uint8(TNS_MSG_TYPE_PIGGYBACK) buf.write_uint8(TNS_FUNC_CLOSE_CURSORS) buf.write_seq_num() buf.write_uint8(1) # pointer buf.write_ub4(self.conn_impl._num_cursors_to_close) cursor_ids = self.conn_impl._cursors_to_close.data.as_uints for i in range(self.conn_impl._num_cursors_to_close): buf.write_ub4(cursor_ids[i]) self.conn_impl._num_cursors_to_close = 0 cdef int _write_current_schema_piggyback(self, WriteBuffer buf) except -1: cdef bytes schema_bytes buf.write_uint8(TNS_MSG_TYPE_PIGGYBACK) buf.write_uint8(TNS_FUNC_SET_SCHEMA) buf.write_seq_num() buf.write_uint8(1) # pointer schema_bytes = self.conn_impl._current_schema.encode() buf.write_ub4(len(schema_bytes)) buf.write_bytes(schema_bytes) cdef int _write_close_temp_lobs_piggyback(self, WriteBuffer buf) except -1: cdef: list lobs_to_close = self.conn_impl._temp_lobs_to_close uint64_t total_size = 0 buf.write_uint8(TNS_MSG_TYPE_PIGGYBACK) buf.write_uint8(TNS_FUNC_LOB_OP) op_code = TNS_LOB_OP_FREE_TEMP | TNS_LOB_OP_ARRAY buf.write_seq_num() # temp lob data buf.write_uint8(1) # pointer buf.write_ub4(self.conn_impl._temp_lobs_total_size) buf.write_uint8(0) # dest lob locator buf.write_ub4(0) buf.write_ub4(0) # source lob locator buf.write_ub4(0) buf.write_uint8(0) # source lob offset buf.write_uint8(0) # dest lob offset buf.write_uint8(0) # charset buf.write_ub4(op_code) buf.write_uint8(0) # scn buf.write_ub4(0) # losbscn buf.write_ub8(0) # lobscnl buf.write_ub8(0) buf.write_uint8(0) # array lob fields buf.write_uint8(0) buf.write_ub4(0) buf.write_uint8(0) buf.write_ub4(0) buf.write_uint8(0) buf.write_ub4(0) for i in range(len(lobs_to_close)): buf.write_bytes(lobs_to_close[i]) # reset values self.conn_impl._temp_lobs_to_close = None self.conn_impl._temp_lobs_total_size = 0 cdef int _write_end_to_end_piggyback(self, WriteBuffer buf) except -1: cdef: bytes action_bytes, client_identifier_bytes, client_info_bytes ThinConnImpl conn = self.conn_impl bytes module_bytes, dbop_bytes uint32_t flags = 0 # determine which flags to send if conn._action_modified: flags |= TNS_END_TO_END_ACTION if conn._client_identifier_modified: flags |= TNS_END_TO_END_CLIENT_IDENTIFIER if conn._client_info_modified: flags |= TNS_END_TO_END_CLIENT_INFO if conn._module_modified: flags |= TNS_END_TO_END_MODULE if conn._dbop_modified: flags |= TNS_END_TO_END_DBOP # write initial packet data buf.write_uint8(TNS_MSG_TYPE_PIGGYBACK) buf.write_uint8(TNS_FUNC_SET_END_TO_END_ATTR) buf.write_seq_num() buf.write_uint8(0) # pointer (cidnam) buf.write_uint8(0) # pointer (cidser) buf.write_ub4(flags) # write client identifier header info if conn._client_identifier_modified: buf.write_uint8(1) # pointer (client identifier) if conn._client_identifier is None: buf.write_ub4(0) else: client_identifier_bytes = conn._client_identifier.encode() buf.write_ub4(len(client_identifier_bytes)) else: buf.write_uint8(0) # pointer (client identifier) buf.write_ub4(0) # length of client identifier # write module header info if conn._module_modified: buf.write_uint8(1) # pointer (module) if conn._module is None: buf.write_ub4(0) else: module_bytes = conn._module.encode() buf.write_ub4(len(module_bytes)) else: buf.write_uint8(0) # pointer (module) buf.write_ub4(0) # length of module # write action header info if conn._action_modified: buf.write_uint8(1) # pointer (action) if conn._action is None: buf.write_ub4(0) else: action_bytes = conn._action.encode() buf.write_ub4(len(action_bytes)) else: buf.write_uint8(0) # pointer (action) buf.write_ub4(0) # length of action # write unsupported bits buf.write_uint8(0) # pointer (cideci) buf.write_ub4(0) # length (cideci) buf.write_uint8(0) # cidcct buf.write_ub4(0) # cidecs # write client info header info if conn._client_info_modified: buf.write_uint8(1) # pointer (client info) if conn._client_info is None: buf.write_ub4(0) else: client_info_bytes = conn._client_info.encode() buf.write_ub4(len(client_info_bytes)) else: buf.write_uint8(0) # pointer (client info) buf.write_ub4(0) # length of client info # write more unsupported bits buf.write_uint8(0) # pointer (cidkstk) buf.write_ub4(0) # length (cidkstk) buf.write_uint8(0) # pointer (cidktgt) buf.write_ub4(0) # length (cidktgt) # write dbop header info if conn._dbop_modified: buf.write_uint8(1) # pointer (dbop) if conn._dbop is None: buf.write_ub4(0) else: dbop_bytes = conn._dbop.encode() buf.write_ub4(len(dbop_bytes)) else: buf.write_uint8(0) # pointer (dbop) buf.write_ub4(0) # length of dbop # write strings if conn._client_identifier_modified \ and conn._client_identifier is not None: buf.write_bytes(client_identifier_bytes) if conn._module_modified and conn._module is not None: buf.write_bytes(module_bytes) if conn._action_modified and conn._action is not None: buf.write_bytes(action_bytes) if conn._client_info_modified and conn._client_info is not None: buf.write_bytes(client_info_bytes) if conn._dbop_modified and conn._dbop is not None: buf.write_bytes(dbop_bytes) # reset flags and values conn._action_modified = False conn._action = None conn._client_identifier_modified = False conn._client_identifier = None conn._client_info_modified = False conn._client_info = None conn._dbop_modified = False conn._dbop = None conn._module_modified = False conn._module = None cdef int _write_piggybacks(self, WriteBuffer buf) except -1: if self.conn_impl._current_schema_modified: self._write_current_schema_piggyback(buf) if self.conn_impl._num_cursors_to_close > 0 \ and not self.conn_impl._drcp_establish_session: self._write_close_cursors_piggyback(buf) if self.conn_impl._action_modified \ or self.conn_impl._client_identifier_modified \ or self.conn_impl._client_info_modified \ or self.conn_impl._dbop_modified \ or self.conn_impl._module_modified: self._write_end_to_end_piggyback(buf) if self.conn_impl._temp_lobs_total_size > 0: self._write_close_temp_lobs_piggyback(buf) @cython.final cdef class AuthMessage(Message): cdef: str encoded_password bytes password bytes newpassword str encoded_newpassword str encoded_jdwp_data str debug_jdwp str session_key str speedy_key str proxy_user str token str private_key str service_name uint8_t purity ssize_t user_bytes_len bytes user_bytes dict session_data uint32_t auth_mode uint32_t verifier_type cdef int _generate_verifier(self, bint verifier_11g) except -1: """ Generate the multi-round verifier. """ cdef bytes jdwp_data # create password hash verifier_data = bytes.fromhex(self.session_data['AUTH_VFR_DATA']) if verifier_11g: keylen = 24 h = hashlib.sha1(self.password) h.update(verifier_data) password_hash = h.digest() + bytes(4) else: keylen = 32 iterations = int(self.session_data['AUTH_PBKDF2_VGEN_COUNT']) salt = verifier_data + b'AUTH_PBKDF2_SPEEDY_KEY' password_key = get_derived_key(self.password, salt, 64, iterations) h = hashlib.new("sha512") h.update(password_key) h.update(verifier_data) password_hash = h.digest()[:32] # decrypt first half of session key encoded_server_key = bytes.fromhex(self.session_data['AUTH_SESSKEY']) session_key_part_a = decrypt_cbc(password_hash, encoded_server_key) # generate second half of session key session_key_part_b = secrets.token_bytes(32) encoded_client_key = encrypt_cbc(password_hash, session_key_part_b) self.session_key = encoded_client_key.hex().upper()[:64] # create session key from combo key mixing_salt = bytes.fromhex(self.session_data['AUTH_PBKDF2_CSK_SALT']) iterations = int(self.session_data['AUTH_PBKDF2_SDER_COUNT']) combo_key = session_key_part_b[:keylen] + session_key_part_a[:keylen] session_key = get_derived_key(combo_key.hex().upper().encode(), mixing_salt, keylen, iterations) # generate speedy key for 12c verifiers if not verifier_11g: salt = secrets.token_bytes(16) speedy_key = encrypt_cbc(session_key, salt + password_key) self.speedy_key = speedy_key[:80].hex().upper() # encrypt password salt = secrets.token_bytes(16) password_with_salt = salt + self.password encrypted_password = encrypt_cbc(session_key, password_with_salt) self.encoded_password = encrypted_password.hex().upper() # encrypt new password if self.newpassword is not None: newpassword_with_salt = salt + self.newpassword encrypted_newpassword = encrypt_cbc(session_key, newpassword_with_salt) self.encoded_newpassword = encrypted_newpassword.hex().upper() # check if debug_jdwp is set. if set, encode the data using the # combo session key with zeros padding if self.debug_jdwp is not None: jdwp_data = self.debug_jdwp.encode() encrypted_jdwp_data = encrypt_cbc(session_key, jdwp_data, zeros=True) # Add a "01" at the end of the hex encrypted data to indicate the # use of AES encryption self.encoded_jdwp_data = encrypted_jdwp_data.hex().upper() + "01" cdef tuple _get_version_tuple(self, ReadBuffer buf): """ Return the 5-tuple for the database version. Note that the format changed with Oracle Database 18. """ cdef uint32_t full_version_num full_version_num = int(self.session_data["AUTH_VERSION_NO"]) if buf._caps.ttc_field_version >= TNS_CCAP_FIELD_VERSION_18_1_EXT_1: return ((full_version_num >> 24) & 0xFF, (full_version_num >> 16) & 0xFF, (full_version_num >> 12) & 0x0F, (full_version_num >> 4) & 0xFF, (full_version_num & 0x0F)) else: return ((full_version_num >> 24) & 0xFF, (full_version_num >> 20) & 0x0F, (full_version_num >> 12) & 0x0F, (full_version_num >> 8) & 0x0F, (full_version_num & 0x0F)) cdef int _initialize_hook(self) except -1: """ Perform initialization. """ self.function_code = TNS_FUNC_AUTH_PHASE_ONE self.session_data = {} if self.conn_impl.username is not None: self.user_bytes = self.conn_impl.username.encode() self.user_bytes_len = len(self.user_bytes) self.resend = True cdef int _process_return_parameters(self, ReadBuffer buf) except -1: cdef: uint16_t num_params, i uint32_t num_bytes str key, value buf.read_ub2(&num_params) for i in range(num_params): buf.skip_ub4() key = buf.read_str(TNS_CS_IMPLICIT) buf.read_ub4(&num_bytes) if num_bytes > 0: value = buf.read_str(TNS_CS_IMPLICIT) else: value = "" if key == "AUTH_VFR_DATA": buf.read_ub4(&self.verifier_type) else: buf.skip_ub4() # skip flags self.session_data[key] = value if self.function_code == TNS_FUNC_AUTH_PHASE_ONE: self.function_code = TNS_FUNC_AUTH_PHASE_TWO else: self.conn_impl._session_id = \ int(self.session_data["AUTH_SESSION_ID"]) self.conn_impl._serial_num = \ int(self.session_data["AUTH_SERIAL_NUM"]) self.conn_impl._server_version = \ "%d.%d.%d.%d.%d" % self._get_version_tuple(buf) cdef int _set_params(self, ConnectParamsImpl params, Description description) except -1: """ Sets the parameters to use for the AuthMessage. The user and auth mode are retained in order to avoid duplicating this effort for both trips to the server. """ self.password = params._get_password() self.newpassword = params._get_new_password() self.service_name = description.service_name self.proxy_user = params.proxy_user self.debug_jdwp = params.debug_jdwp # if drcp is used, use purity = NEW as the default purity for # standalone connections and purity = SELF for connections that belong # to a pool if description.purity == PURITY_DEFAULT \ and self.conn_impl._drcp_enabled: if self.conn_impl._pool is None: self.purity = PURITY_NEW else: self.purity = PURITY_SELF else: self.purity = description.purity # set token parameters; adjust processing so that only phase two is # sent if params._token is not None \ or params.access_token_callback is not None: self.token = params._get_token() if params._private_key is not None: self.private_key = params._get_private_key() self.function_code = TNS_FUNC_AUTH_PHASE_TWO self.resend = False # set authentication mode if params._new_password is None: self.auth_mode = TNS_AUTH_MODE_LOGON if params.mode & constants.AUTH_MODE_SYSDBA: self.auth_mode |= TNS_AUTH_MODE_SYSDBA if params.mode & constants.AUTH_MODE_SYSOPER: self.auth_mode |= TNS_AUTH_MODE_SYSOPER if params.mode & constants.AUTH_MODE_SYSASM: self.auth_mode |= TNS_AUTH_MODE_SYSASM if params.mode & constants.AUTH_MODE_SYSBKP: self.auth_mode |= TNS_AUTH_MODE_SYSBKP if params.mode & constants.AUTH_MODE_SYSDGD: self.auth_mode |= TNS_AUTH_MODE_SYSDGD if params.mode & constants.AUTH_MODE_SYSKMT: self.auth_mode |= TNS_AUTH_MODE_SYSKMT if params.mode & constants.AUTH_MODE_SYSRAC: self.auth_mode |= TNS_AUTH_MODE_SYSRAC if self.private_key is not None: self.auth_mode |= TNS_AUTH_MODE_IAM_TOKEN cdef int _write_key_value(self, WriteBuffer buf, str key, str value, uint32_t flags=0) except -1: cdef: bytes key_bytes = key.encode() bytes value_bytes = value.encode() uint32_t key_len = len(key_bytes) uint32_t value_len = len(value_bytes) buf.write_ub4(key_len) buf.write_bytes_with_length(key_bytes) buf.write_ub4(value_len) if value_len > 0: buf.write_bytes_with_length(value_bytes) buf.write_ub4(flags) cdef int _write_message(self, WriteBuffer buf) except -1: cdef: uint8_t has_user = 1 if self.user_bytes_len > 0 else 0 bint verifier_11g = False uint32_t num_pairs # perform final determination of data to write if self.function_code == TNS_FUNC_AUTH_PHASE_ONE: num_pairs = 5 else: num_pairs = 3 # token authentication if self.token is not None: num_pairs += 1 # normal user/password authentication else: num_pairs += 2 self.auth_mode |= TNS_AUTH_MODE_WITH_PASSWORD if self.verifier_type in (TNS_VERIFIER_TYPE_11G_1, TNS_VERIFIER_TYPE_11G_2): verifier_11g = True elif self.verifier_type != TNS_VERIFIER_TYPE_12C: errors._raise_err(errors.ERR_UNSUPPORTED_VERIFIER_TYPE, verifier_type=self.verifier_type) else: num_pairs += 1 self._generate_verifier(verifier_11g) # determine which other key/value pairs to write if self.newpassword is not None: num_pairs += 1 self.auth_mode |= TNS_AUTH_MODE_CHANGE_PASSWORD if self.proxy_user is not None: num_pairs += 1 if self.conn_impl._cclass is not None: num_pairs += 1 if self.purity != 0: num_pairs += 1 if self.private_key is not None: num_pairs += 2 if self.encoded_jdwp_data is not None: num_pairs += 1 # write basic data to packet self._write_function_code(buf) buf.write_uint8(has_user) # pointer (authusr) buf.write_ub4(self.user_bytes_len) buf.write_ub4(self.auth_mode) # authentication mode buf.write_uint8(1) # pointer (authivl) buf.write_ub4(num_pairs) # number of key/value pairs buf.write_uint8(1) # pointer (authovl) buf.write_uint8(1) # pointer (authovln) if has_user: buf.write_bytes(self.user_bytes) # write key/value pairs if self.function_code == TNS_FUNC_AUTH_PHASE_ONE: self._write_key_value(buf, "AUTH_TERMINAL", _connect_constants.terminal_name) self._write_key_value(buf, "AUTH_PROGRAM_NM", _connect_constants.program_name) self._write_key_value(buf, "AUTH_MACHINE", _connect_constants.machine_name) self._write_key_value(buf, "AUTH_PID", _connect_constants.pid) self._write_key_value(buf, "AUTH_SID", _connect_constants.user_name) else: if self.proxy_user is not None: self._write_key_value(buf, "PROXY_CLIENT_NAME", self.proxy_user) if self.token is not None: self._write_key_value(buf, "AUTH_TOKEN", self.token) else: self._write_key_value(buf, "AUTH_SESSKEY", self.session_key, 1) self._write_key_value(buf, "AUTH_PASSWORD", self.encoded_password) if not verifier_11g: self._write_key_value(buf, "AUTH_PBKDF2_SPEEDY_KEY", self.speedy_key) if self.newpassword is not None: self._write_key_value(buf, "AUTH_NEWPASSWORD", self.encoded_newpassword) self._write_key_value(buf, "SESSION_CLIENT_CHARSET", "873") driver_name = f"{constants.DRIVER_NAME} thn : {VERSION}" self._write_key_value(buf, "SESSION_CLIENT_DRIVER_NAME", driver_name) self._write_key_value(buf, "SESSION_CLIENT_VERSION", str(_connect_constants.full_version_num)) if self.conn_impl._cclass is not None: self._write_key_value(buf, "AUTH_KPPL_CONN_CLASS", self.conn_impl._cclass) if self.purity != 0: self._write_key_value(buf, "AUTH_KPPL_PURITY", str(self.purity), 1) if self.private_key is not None: date_format = "%a, %d %b %Y %H:%M:%S GMT" now = datetime.datetime.utcnow().strftime(date_format) host_info = "%s:%d" % buf._socket.getpeername() header = f"date: {now}\n" + \ f"(request-target): {self.service_name}\n" + \ f"host: {host_info}" signature = get_signature(self.private_key, header) self._write_key_value(buf, "AUTH_HEADER", header) self._write_key_value(buf, "AUTH_SIGNATURE", signature) if self.encoded_jdwp_data is not None: self._write_key_value(buf, "AUTH_ORA_DEBUG_JDWP", self.encoded_jdwp_data) @cython.final cdef class CommitMessage(Message): cdef int _initialize_hook(self) except -1: """ Perform initialization. """ self.function_code = TNS_FUNC_COMMIT @cython.final cdef class ConnectMessage(Message): cdef: uint16_t connect_string_len bytes connect_string_bytes Description description str redirect_data str host int port cdef int process(self, ReadBuffer buf) except -1: cdef: uint16_t redirect_data_length, protocol_version, protocol_options const char_type *redirect_data if self.packet_type == TNS_PACKET_TYPE_REDIRECT: buf.read_uint16(&redirect_data_length) buf.receive_packet(&self.packet_type, &self.packet_flags) buf.skip_raw_bytes(2) # skip data flags redirect_data = buf._get_raw(redirect_data_length) self.redirect_data = \ redirect_data[:redirect_data_length].decode() elif self.packet_type == TNS_PACKET_TYPE_ACCEPT: buf.read_uint16(&protocol_version) buf.read_uint16(&protocol_options) buf._caps._adjust_for_protocol(protocol_version, protocol_options) elif self.packet_type == TNS_PACKET_TYPE_REFUSE: response = self.error_info.message error_code = "unknown" error_code_int = 0 if response is not None: pos = response.find("(ERR=") if pos > 0: end_pos = response.find(")", pos) if end_pos > 0: error_code = response[pos + 5:end_pos] error_code_int = int(error_code) if error_code_int == 0: errors._raise_err(errors.ERR_UNEXPECTED_REFUSE) if error_code_int == TNS_ERR_INVALID_SERVICE_NAME: errors._raise_err(errors.ERR_INVALID_SERVICE_NAME, service_name=self.description.service_name, host=self.host, port=self.port) elif error_code_int == TNS_ERR_INVALID_SID: errors._raise_err(errors.ERR_INVALID_SID, sid=self.description.sid, host=self.host, port=self.port) errors._raise_err(errors.ERR_LISTENER_REFUSED_CONNECTION, error_code=error_code) cdef int send(self, WriteBuffer buf) except -1: cdef: uint16_t service_options = TNS_BASE_SERVICE_OPTIONS uint32_t connect_flags_1 = 0, connect_flags_2 = 0 if buf._caps.supports_oob: service_options |= TNS_CAN_RECV_ATTENTION connect_flags_2 |= TNS_CHECK_OOB buf.start_request(TNS_PACKET_TYPE_CONNECT) buf.write_uint16(TNS_VERSION_DESIRED) buf.write_uint16(TNS_VERSION_MINIMUM) buf.write_uint16(service_options) buf.write_uint16(TNS_SDU) buf.write_uint16(TNS_TDU) buf.write_uint16(TNS_PROTOCOL_CHARACTERISTICS) buf.write_uint16(0) # line turnaround buf.write_uint16(1) # value of 1 buf.write_uint16(self.connect_string_len) buf.write_uint16(74) # offset to connect data buf.write_uint32(0) # max receivable data buf.write_uint16(TNS_CONNECT_FLAGS) buf.write_uint64(0) # obsolete buf.write_uint64(0) # obsolete buf.write_uint64(0) # obsolete buf.write_uint32(TNS_SDU) # SDU (large) buf.write_uint32(TNS_TDU) # TDU (large) buf.write_uint32(connect_flags_1) buf.write_uint32(connect_flags_2) if self.connect_string_len > TNS_MAX_CONNECT_DATA: buf.end_request() buf.start_request(TNS_PACKET_TYPE_DATA) buf.write_bytes(self.connect_string_bytes) buf.end_request() @cython.final cdef class DataTypesMessage(Message): cdef int _write_message(self, WriteBuffer buf) except -1: cdef: DataType* data_type int i # write character set and capabilities buf.write_uint8(TNS_MSG_TYPE_DATA_TYPES) buf.write_uint16(TNS_CHARSET_UTF8, BYTE_ORDER_LSB) buf.write_uint16(TNS_CHARSET_UTF8, BYTE_ORDER_LSB) buf.write_ub4(len(buf._caps.compile_caps)) buf.write_bytes(bytes(buf._caps.compile_caps)) buf.write_uint8(len(buf._caps.runtime_caps)) buf.write_bytes(bytes(buf._caps.runtime_caps)) # write data types i = 0 while True: data_type = &DATA_TYPES[i] if data_type.data_type == 0: break i += 1 buf.write_uint16(data_type.data_type) buf.write_uint16(data_type.conv_data_type) buf.write_uint16(data_type.representation) buf.write_uint16(0) buf.write_uint16(0) cdef int process(self, ReadBuffer buf) except -1: pass @cython.final cdef class ExecuteMessage(MessageWithData): cdef int _postprocess(self) except -1: """ Runs after the database response has been processed. If the statement executed requires define and is not a REF cursor (which would already have performed the define during its execute), then mark the message as needing to be resent. If this is after the second time the message has been sent, mark the statement as no longer needing a define (since this only needs to happen once). """ MessageWithData._postprocess(self) cdef Statement stmt = self.cursor_impl._statement if stmt._requires_define and stmt._sql is not None: if self.resend: stmt._requires_define = False else: stmt._requires_full_execute = True self.resend = True cdef int _write_execute_message(self, WriteBuffer buf) except -1: """ Write the message for a full execute. """ cdef: uint32_t options, dml_options = 0, num_params = 0, num_iters = 1 Statement stmt = self.cursor_impl._statement ThinCursorImpl cursor_impl = self.cursor_impl list params = stmt._bind_info_list # determine the options to use for the execute options = 0 if not stmt._requires_define and not self.parse_only \ and params is not None: num_params = len(params) if stmt._requires_define: options |= TNS_EXEC_OPTION_DEFINE elif not self.parse_only and stmt._sql is not None: dml_options = TNS_EXEC_OPTION_IMPLICIT_RESULTSET options |= TNS_EXEC_OPTION_EXECUTE if stmt._cursor_id == 0 or stmt._is_ddl: options |= TNS_EXEC_OPTION_PARSE if stmt._is_query: if self.parse_only: options |= TNS_EXEC_OPTION_DESCRIBE else: if self.cursor_impl.prefetchrows > 0: options |= TNS_EXEC_OPTION_FETCH if stmt._cursor_id == 0 or stmt._requires_define: num_iters = self.cursor_impl.prefetchrows self.cursor_impl._fetch_array_size = num_iters else: num_iters = self.cursor_impl._fetch_array_size if not stmt._is_plsql: options |= TNS_EXEC_OPTION_NOT_PLSQL elif num_params > 0: options |= TNS_EXEC_OPTION_PLSQL_BIND if num_params > 0: options |= TNS_EXEC_OPTION_BIND if self.batcherrors: options |= TNS_EXEC_OPTION_BATCH_ERRORS if self.arraydmlrowcounts: dml_options = TNS_EXEC_OPTION_DML_ROWCOUNTS if self.conn_impl.autocommit: options |= TNS_EXEC_OPTION_COMMIT # write piggybacks, if needed self._write_piggybacks(buf) # write body of message self._write_function_code(buf) buf.write_ub4(options) # execute options buf.write_ub4(stmt._cursor_id) # cursor id if stmt._cursor_id == 0 or stmt._is_ddl: buf.write_uint8(1) # pointer (cursor id) buf.write_ub4(stmt._sql_length) else: buf.write_uint8(0) # pointer (cursor id) buf.write_ub4(0) buf.write_uint8(1) # pointer (vector) buf.write_ub4(13) # al8i4 array length buf.write_uint8(0) # pointer (al8o4) buf.write_uint8(0) # pointer (al8o4l) buf.write_ub4(0) # prefetch buffer size buf.write_ub4(num_iters) # prefetch number of rows buf.write_ub4(TNS_MAX_LONG_LENGTH) # maximum long size if num_params == 0: buf.write_uint8(0) # pointer (binds) buf.write_ub4(0) # number of binds else: buf.write_uint8(1) # pointer (binds) buf.write_ub4(num_params) # number of binds buf.write_uint8(0) # pointer (al8app) buf.write_uint8(0) # pointer (al8txn) buf.write_uint8(0) # pointer (al8txl) buf.write_uint8(0) # pointer (al8kv) buf.write_uint8(0) # pointer (al8kvl) if stmt._requires_define: buf.write_uint8(1) # pointer (al8doac) buf.write_ub4(len(self.cursor_impl.fetch_vars)) # number of defines else: buf.write_uint8(0) buf.write_ub4(0) buf.write_ub4(0) # registration id buf.write_uint8(0) # pointer (al8objlist) buf.write_uint8(1) # pointer (al8objlen) buf.write_uint8(0) # pointer (al8blv) buf.write_ub4(0) # al8blvl buf.write_uint8(0) # pointer (al8dnam) buf.write_ub4(0) # al8dnaml buf.write_ub4(0) # al8regid_msb if self.arraydmlrowcounts: buf.write_uint8(1) # pointer (al8pidmlrc) buf.write_ub4(self.num_execs) # al8pidmlrcbl buf.write_uint8(1) # pointer (al8pidmlrcl) else: buf.write_uint8(0) # pointer (al8pidmlrc) buf.write_ub4(0) # al8pidmlrcbl buf.write_uint8(0) # pointer (al8pidmlrcl) if buf._caps.ttc_field_version >= TNS_CCAP_FIELD_VERSION_12_2: buf.write_uint8(0) # pointer (al8sqlsig) buf.write_ub4(0) # SQL signature length buf.write_uint8(0) # pointer (SQL ID) buf.write_ub4(0) # allocated size of SQL ID buf.write_uint8(0) # pointer (length of SQL ID) if buf._caps.ttc_field_version >= TNS_CCAP_FIELD_VERSION_12_2_EXT1: buf.write_uint8(0) # pointer (chunk ids) buf.write_ub4(0) # number of chunk ids if stmt._cursor_id == 0 or stmt._is_ddl: if stmt._sql_bytes is None: errors._raise_err(errors.ERR_INVALID_REF_CURSOR) buf.write_bytes(stmt._sql_bytes) buf.write_ub4(1) # al8i4[0] parse else: buf.write_ub4(0) # al8i4[0] parse if stmt._is_query: if stmt._cursor_id == 0: buf.write_ub4(0) # al8i4[1] execution count else: buf.write_ub4(num_iters) else: buf.write_ub4(self.num_execs) # al8i4[1] execution count buf.write_ub4(0) # al8i4[2] buf.write_ub4(0) # al8i4[3] buf.write_ub4(0) # al8i4[4] buf.write_ub4(0) # al8i4[5] SCN (part 1) buf.write_ub4(0) # al8i4[6] SCN (part 2) buf.write_ub4(stmt._is_query) # al8i4[7] is query buf.write_ub4(0) # al8i4[8] buf.write_ub4(dml_options) # al8i4[9] DML row counts/implicit buf.write_ub4(0) # al8i4[10] buf.write_ub4(0) # al8i4[11] buf.write_ub4(0) # al8i4[12] if stmt._requires_define: self._write_column_metadata(buf, self.cursor_impl.fetch_var_impls) elif num_params > 0: self._write_bind_params(buf, params) cdef int _write_reexecute_message(self, WriteBuffer buf) except -1: """ Write the message for a re-execute. """ cdef: uint32_t i, exec_flags_1 = 0, exec_flags_2 = 0, num_iters Statement stmt = self.cursor_impl._statement list params = stmt._bind_info_list BindInfo info if params: if not stmt._is_query: self.out_var_impls = [info._bind_var_impl \ for info in params \ if info.bind_dir != TNS_BIND_DIR_INPUT] params = [info for info in params \ if info.bind_dir != TNS_BIND_DIR_OUTPUT \ and not info._is_return_bind] if self.function_code == TNS_FUNC_REEXECUTE_AND_FETCH: exec_flags_1 |= TNS_EXEC_OPTION_EXECUTE num_iters = self.cursor_impl.prefetchrows self.cursor_impl._fetch_array_size = num_iters else: if self.conn_impl.autocommit: exec_flags_2 |= TNS_EXEC_OPTION_COMMIT_REEXECUTE num_iters = self.num_execs self._write_piggybacks(buf) self._write_function_code(buf) buf.write_ub4(stmt._cursor_id) buf.write_ub4(num_iters) buf.write_ub4(exec_flags_1) buf.write_ub4(exec_flags_2) if params: for i in range(self.num_execs): buf.write_uint8(TNS_MSG_TYPE_ROW_DATA) self._write_bind_params_row(buf, params, i) cdef int _write_message(self, WriteBuffer buf) except -1: """ Write the execute message to the buffer. Two types of execute messages are possible: one for a full execute and the second, simpler message, for when an existing cursor is being re-executed. """ cdef: Statement stmt = self.cursor_impl._statement if stmt._cursor_id != 0 and not stmt._requires_full_execute \ and not self.parse_only and not stmt._is_ddl \ and not self.batcherrors: if stmt._is_query and not stmt._requires_define \ and self.cursor_impl.prefetchrows > 0: self.function_code = TNS_FUNC_REEXECUTE_AND_FETCH else: self.function_code = TNS_FUNC_REEXECUTE self._write_reexecute_message(buf) else: self.function_code = TNS_FUNC_EXECUTE self._write_execute_message(buf) @cython.final cdef class FetchMessage(MessageWithData): cdef int _initialize_hook(self) except -1: """ Perform initialization. """ self.function_code = TNS_FUNC_FETCH cdef int _write_message(self, WriteBuffer buf) except -1: self.cursor_impl._fetch_array_size = self.cursor_impl.arraysize self._write_function_code(buf) buf.write_ub4(self.cursor_impl._statement._cursor_id) buf.write_ub4(self.cursor_impl._fetch_array_size) @cython.final cdef class LobOpMessage(Message): cdef: uint32_t operation ThinLobImpl source_lob_impl ThinLobImpl dest_lob_impl uint64_t source_offset uint64_t dest_offset int64_t amount bint send_amount bint bool_flag object data cdef bint _has_more_data(self, ReadBuffer buf): return not self.processed_error cdef int _initialize_hook(self) except -1: """ Perform initialization. """ self.function_code = TNS_FUNC_LOB_OP cdef int _process_message(self, ReadBuffer buf, uint8_t message_type) except -1: cdef: const char_type *ptr ssize_t num_bytes str encoding if message_type == TNS_MSG_TYPE_LOB_DATA: buf.read_raw_bytes_and_length(&ptr, &num_bytes) if self.source_lob_impl.dbtype._ora_type_num == TNS_DATA_TYPE_BLOB: self.data = ptr[:num_bytes] else: encoding = self.source_lob_impl._get_encoding() self.data = ptr[:num_bytes].decode(encoding) else: Message._process_message(self, buf, message_type) cdef int _process_return_parameters(self, ReadBuffer buf) except -1: cdef: cdef const char_type *ptr ssize_t num_bytes uint16_t temp16 if self.source_lob_impl is not None: num_bytes = len(self.source_lob_impl._locator) ptr = buf.read_raw_bytes(num_bytes) self.source_lob_impl._locator = ptr[:num_bytes] if self.dest_lob_impl is not None: num_bytes = len(self.dest_lob_impl._locator) ptr = buf.read_raw_bytes(num_bytes) self.dest_lob_impl._locator = ptr[:num_bytes] if self.operation == TNS_LOB_OP_CREATE_TEMP: buf.skip_ub2() # skip character set if self.send_amount: buf.read_sb8(&self.amount) if self.operation == TNS_LOB_OP_CREATE_TEMP \ or self.operation == TNS_LOB_OP_IS_OPEN: buf.read_ub2(&temp16) # flag self.bool_flag = temp16 > 0 cdef int _write_message(self, WriteBuffer buf) except -1: cdef int i self._write_function_code(buf) if self.source_lob_impl is None: buf.write_uint8(0) # source pointer buf.write_ub4(0) # source length else: buf.write_uint8(1) # source pointer buf.write_ub4(len(self.source_lob_impl._locator)) if self.dest_lob_impl is None: buf.write_uint8(0) # dest pointer buf.write_ub4(0) # dest length else: buf.write_uint8(1) # dest pointer buf.write_ub4(len(self.dest_lob_impl._locator)) buf.write_ub4(0) # short source offset buf.write_ub4(0) # short dest offset if self.operation == TNS_LOB_OP_CREATE_TEMP: buf.write_uint8(1) # pointer (character set) else: buf.write_uint8(0) # pointer (character set) buf.write_uint8(0) # pointer (short amount) if self.operation == TNS_LOB_OP_CREATE_TEMP \ or self.operation == TNS_LOB_OP_IS_OPEN: buf.write_uint8(1) # pointer (NULL LOB) else: buf.write_uint8(0) # pointer (NULL LOB) buf.write_ub4(self.operation) buf.write_uint8(0) # pointer (SCN array) buf.write_uint8(0) # SCN array length buf.write_ub8(self.source_offset) buf.write_ub8(self.dest_offset) if self.send_amount: buf.write_uint8(1) # pointer (amount) else: buf.write_uint8(0) # pointer (amount) for i in range(3): # array LOB (not used) buf.write_uint16(0) if self.source_lob_impl is not None: buf.write_bytes(self.source_lob_impl._locator) if self.dest_lob_impl is not None: buf.write_bytes(self.dest_lob_impl._locator) if self.operation == TNS_LOB_OP_CREATE_TEMP: if self.source_lob_impl.dbtype._csfrm == TNS_CS_NCHAR: buf._caps._check_ncharset_id() buf.write_ub4(TNS_CHARSET_UTF16) else: buf.write_ub4(TNS_CHARSET_UTF8) if self.data is not None: buf.write_uint8(TNS_MSG_TYPE_LOB_DATA) buf.write_bytes_with_length(self.data) if self.send_amount: buf.write_ub8(self.amount) # LOB amount @cython.final cdef class LogoffMessage(Message): cdef int _initialize_hook(self) except -1: """ Perform initialization. """ self.function_code = TNS_FUNC_LOGOFF @cython.final cdef class NetworkServicesMessage(Message): cdef int _write_message(self, WriteBuffer buf) except -1: cdef: uint16_t packet_length NetworkService service # calculate length of packet packet_length = TNS_NETWORK_HEADER_SIZE for service in TNS_NETWORK_SERVICES: packet_length += service.get_data_size() # write header buf.write_uint32(TNS_NETWORK_MAGIC) buf.write_uint16(packet_length) buf.write_uint32(TNS_NETWORK_VERSION) buf.write_uint16( len(TNS_NETWORK_SERVICES)) buf.write_uint8(0) # flags # write service data for service in TNS_NETWORK_SERVICES: service.write_data(buf) cdef int process(self, ReadBuffer buf) except -1: cdef: uint16_t num_services, num_subpackets, i, j, data_length uint32_t temp32 buf.skip_raw_bytes(2) # data flags buf.read_uint32(&temp32) # network magic num if temp32 != TNS_NETWORK_MAGIC: errors._raise_err(errors.ERR_UNEXPECTED_DATA, data=hex(temp32)) buf.skip_raw_bytes(2) # length of packet buf.skip_raw_bytes(4) # version buf.read_uint16(&num_services) buf.skip_raw_bytes(1) # error flags for i in range(num_services): buf.skip_raw_bytes(2) # service num buf.read_uint16(&num_subpackets) buf.read_uint32(&temp32) # error num if temp32 != 0: errors._raise_err(errors.ERR_LISTENER_REFUSED_CONNECTION, error_code=temp32) for j in range(num_subpackets): buf.read_uint16(&data_length) buf.skip_raw_bytes(2) # data type buf.skip_raw_bytes(data_length) @cython.final cdef class PingMessage(Message): cdef int _initialize_hook(self) except -1: """ Perform initialization. """ self.function_code = TNS_FUNC_PING @cython.final cdef class ProtocolMessage(Message): cdef int _write_message(self, WriteBuffer buf) except -1: buf.write_uint8(TNS_MSG_TYPE_PROTOCOL) buf.write_uint8(6) # protocol version (8.1 and higher) buf.write_uint8(0) # "array" terminator buf.write_str(constants.DRIVER_NAME) buf.write_uint8(0) # NULL terminator cdef int _process_message(self, ReadBuffer buf, uint8_t message_type) except -1: cdef: uint16_t num_elem, fdo_length bytearray server_compile_caps bytearray server_runtime_caps Capabilities caps = buf._caps const char_type *fdo bytes temp_buf ssize_t ix uint8_t c if message_type == TNS_MSG_TYPE_PROTOCOL: buf.skip_raw_bytes(2) # skip protocol array while True: # skip server banner buf.read_ub1(&c) if c == 0: break buf.read_uint16(&caps.charset_id, BYTE_ORDER_LSB) buf._caps.char_conversion = caps.charset_id != TNS_CHARSET_UTF8 buf.skip_ub1() # skip server flags buf.read_uint16(&num_elem, BYTE_ORDER_LSB) if num_elem > 0: # skip elements buf.skip_raw_bytes(num_elem * 5) buf.read_uint16(&fdo_length) fdo = buf.read_raw_bytes(fdo_length) ix = 6 + fdo[5] + fdo[6] caps.ncharset_id = (fdo[ix + 3] << 8) + fdo[ix + 4] temp_buf = buf.read_bytes() if temp_buf is not None: server_compile_caps = bytearray(temp_buf) buf._caps._adjust_for_server_compile_caps(server_compile_caps) temp_buf = buf.read_bytes() if temp_buf is not None: server_runtime_caps = bytearray(temp_buf) buf._caps._adjust_for_server_runtime_caps(server_runtime_caps) else: Message._process_message(self, buf, message_type) @cython.final cdef class RollbackMessage(Message): cdef int _initialize_hook(self) except -1: """ Perform initialization. """ self.function_code = TNS_FUNC_ROLLBACK python-oracledb-1.2.1/src/oracledb/impl/thin/network_services.pyx000066400000000000000000000136051434177474600252360ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # network_services.pyx # # Cython file defining the network services packet sent to the database and the # response received by the client (embedded in thin_impl.pyx). #------------------------------------------------------------------------------ # magic value used to recognize network data DEF TNS_NETWORK_MAGIC = 0xdeadbeef # version used for network packets (11.2.0.2.0) DEF TNS_NETWORK_VERSION = 0xb200200 # network data types DEF TNS_NETWORK_TYPE_RAW = 1 DEF TNS_NETWORK_TYPE_UB2 = 3 DEF TNS_NETWORK_TYPE_VERSION = 5 DEF TNS_NETWORK_TYPE_STATUS = 6 # network service numbers DEF TNS_NETWORK_SERVICE_AUTH = 1 DEF TNS_NETWORK_SERVICE_ENCRYPTION = 2 DEF TNS_NETWORK_SERVICE_DATA_INTEGRITY = 3 DEF TNS_NETWORK_SERVICE_SUPERVISOR = 4 # network header sizes DEF TNS_NETWORK_HEADER_SIZE = 4 + 2 + 4 + 2 + 1 DEF TNS_NETWORK_SERVICE_HEADER_SIZE = 2 + 2 + 4 # network supervisor service constants DEF TNS_NETWORK_SUPERVISOR_CID = 0x0000101c66ec28ea # network authentication service constants DEF TNS_NETWORK_AUTH_TYPE_CLIENT_SERVER = 0xe0e1 DEF TNS_NETWORK_AUTH_STATUS_NOT_REQUIRED = 0xfcff # network data integrity service constants DEF TNS_NETWORK_DATA_INTEGRITY_NONE = 0 # network encryption service constants DEF TNS_NETWORK_ENCRYPTION_NULL = 0 cdef class NetworkService: cdef uint16_t get_data_size(self): return TNS_NETWORK_HEADER_SIZE cdef int write_data(self, WriteBuffer buf) except -1: raise NotImplementedError() cdef int write_header(self, WriteBuffer buf, uint16_t service_num, uint16_t num_sub_packets) except -1: buf.write_uint16(service_num) buf.write_uint16(num_sub_packets) buf.write_uint32(0) cdef int write_version(self, WriteBuffer buf) except -1: buf.write_uint16(4) # length buf.write_uint16(TNS_NETWORK_TYPE_VERSION) buf.write_uint32(TNS_NETWORK_VERSION) cdef class AuthenticationService(NetworkService): cdef uint16_t get_data_size(self): return TNS_NETWORK_SERVICE_HEADER_SIZE + 8 + 6 + 6 cdef int write_data(self, WriteBuffer buf) except -1: self.write_header(buf, TNS_NETWORK_SERVICE_AUTH, num_sub_packets=3) self.write_version(buf) # write auth type buf.write_uint16(2) # length buf.write_uint16(TNS_NETWORK_TYPE_UB2) buf.write_uint16(TNS_NETWORK_AUTH_TYPE_CLIENT_SERVER) # write status buf.write_uint16(2) # length buf.write_uint16(TNS_NETWORK_TYPE_STATUS) buf.write_uint16(TNS_NETWORK_AUTH_STATUS_NOT_REQUIRED) cdef class DataIntegrityService(NetworkService): cdef uint16_t get_data_size(self): return TNS_NETWORK_SERVICE_HEADER_SIZE + 8 + 5 cdef int write_data(self, WriteBuffer buf) except -1: self.write_header(buf, TNS_NETWORK_SERVICE_DATA_INTEGRITY, num_sub_packets=2) self.write_version(buf) # write options buf.write_uint16(1) # length buf.write_uint16(TNS_NETWORK_TYPE_RAW) buf.write_uint8(TNS_NETWORK_DATA_INTEGRITY_NONE) cdef class EncryptionService(NetworkService): cdef uint16_t get_data_size(self): return TNS_NETWORK_SERVICE_HEADER_SIZE + 8 + 5 cdef int write_data(self, WriteBuffer buf) except -1: self.write_header(buf, TNS_NETWORK_SERVICE_ENCRYPTION, num_sub_packets=2) self.write_version(buf) # write options buf.write_uint16(1) # length buf.write_uint16(TNS_NETWORK_TYPE_RAW) buf.write_uint8(TNS_NETWORK_ENCRYPTION_NULL) cdef class SupervisorService(NetworkService): cdef uint16_t get_data_size(self): return TNS_NETWORK_SERVICE_HEADER_SIZE + 8 + 12 + 22 cdef int write_data(self, WriteBuffer buf) except -1: self.write_header(buf, TNS_NETWORK_SERVICE_SUPERVISOR, num_sub_packets=3) self.write_version(buf) # write CID buf.write_uint16(8) # length buf.write_uint16(TNS_NETWORK_TYPE_RAW) buf.write_uint64(TNS_NETWORK_SUPERVISOR_CID) # write supervised services array buf.write_uint16(18) # length buf.write_uint16(TNS_NETWORK_TYPE_RAW) buf.write_uint32(TNS_NETWORK_MAGIC) buf.write_uint16(TNS_NETWORK_TYPE_UB2) buf.write_uint32(4) # length of array buf.write_uint16(TNS_NETWORK_SERVICE_SUPERVISOR) buf.write_uint16(TNS_NETWORK_SERVICE_AUTH) buf.write_uint16(TNS_NETWORK_SERVICE_ENCRYPTION) buf.write_uint16(TNS_NETWORK_SERVICE_DATA_INTEGRITY) cdef list TNS_NETWORK_SERVICES = [ SupervisorService(), AuthenticationService(), EncryptionService(), DataIntegrityService() ] python-oracledb-1.2.1/src/oracledb/impl/thin/packet.pyx000066400000000000000000000631741434177474600231170ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # buffer.pyx # # Cython file defining the low-level network buffer read and write classes and # methods for reading and writing low-level data from those buffers (embedded # in thin_impl.pyx). #------------------------------------------------------------------------------ DEF PACKET_HEADER_SIZE = 8 DEF CHUNKED_BYTES_CHUNK_SIZE = 65536 cdef struct BytesChunk: char_type *ptr uint32_t length uint32_t allocated_length cdef struct Rowid: uint32_t rba uint16_t partition_id uint32_t block_num uint16_t slot_num @cython.final cdef class ChunkedBytesBuffer: cdef: uint32_t _num_chunks uint32_t _allocated_chunks BytesChunk *_chunks def __dealloc__(self): cdef uint32_t i for i in range(self._allocated_chunks): if self._chunks[i].ptr is not NULL: cpython.PyMem_Free(self._chunks[i].ptr) self._chunks[i].ptr = NULL if self._chunks is not NULL: cpython.PyMem_Free(self._chunks) self._chunks = NULL cdef int _allocate_chunks(self) except -1: """ Allocates a new set of chunks and copies data from the original set of chunks if needed. """ cdef: BytesChunk *chunks uint32_t allocated_chunks allocated_chunks = self._allocated_chunks + 8 chunks = \ cpython.PyMem_Malloc(sizeof(BytesChunk) * allocated_chunks) memset(chunks, 0, sizeof(BytesChunk) * allocated_chunks) if self._num_chunks > 0: memcpy(chunks, self._chunks, sizeof(BytesChunk) * self._num_chunks) cpython.PyMem_Free(self._chunks) self._chunks = chunks self._allocated_chunks = allocated_chunks cdef BytesChunk* _get_chunk(self, uint32_t num_bytes) except NULL: """ Return the chunk that can be used to write the number of bytes requested. """ cdef: uint32_t num_allocated_bytes BytesChunk *chunk if self._num_chunks > 0: chunk = &self._chunks[self._num_chunks - 1] if chunk.allocated_length >= chunk.length + num_bytes: return chunk if self._num_chunks >= self._allocated_chunks: self._allocate_chunks() self._num_chunks += 1 chunk = &self._chunks[self._num_chunks - 1] chunk.length = 0 if chunk.allocated_length < num_bytes: num_allocated_bytes = self._get_chunk_size(num_bytes) if chunk.ptr: cpython.PyMem_Free(chunk.ptr) chunk.ptr = cpython.PyMem_Malloc(num_allocated_bytes) chunk.allocated_length = num_allocated_bytes return chunk cdef inline uint32_t _get_chunk_size(self, uint32_t size): """ Returns the size to allocate aligned on a 64K boundary. """ return (size + CHUNKED_BYTES_CHUNK_SIZE - 1) & \ ~(CHUNKED_BYTES_CHUNK_SIZE - 1) cdef char_type* end_chunked_read(self) except NULL: """ Called when a chunked read has ended. Since a chunked read is never started until at least some bytes are being read, it is assumed that at least one chunk is in use. If one chunk is in use, those bytes are returned directly, but if more than one chunk is in use, the first chunk is resized to include all of the bytes in a contiguous section of memory first. """ cdef: uint32_t i, num_allocated_bytes, total_num_bytes = 0, pos = 0 char_type *ptr if self._num_chunks > 1: for i in range(self._num_chunks): total_num_bytes += self._chunks[i].length num_allocated_bytes = self._get_chunk_size(total_num_bytes) ptr = cpython.PyMem_Malloc(num_allocated_bytes) for i in range(self._num_chunks): memcpy(&ptr[pos], self._chunks[i].ptr, self._chunks[i].length) pos += self._chunks[i].length cpython.PyMem_Free(self._chunks[i].ptr) self._chunks[i].ptr = NULL self._chunks[i].allocated_length = 0 self._chunks[i].length = 0 self._num_chunks = 1 self._chunks[0].ptr = ptr self._chunks[0].length = total_num_bytes self._chunks[0].allocated_length = num_allocated_bytes return self._chunks[0].ptr cdef char_type* get_chunk_ptr(self, uint32_t size_required) except NULL: """ Called when memory is required for a chunked read. """ cdef: BytesChunk *chunk char_type *ptr chunk = self._get_chunk(size_required) ptr = &chunk.ptr[chunk.length] chunk.length += size_required return ptr cdef inline void start_chunked_read(self): """ Called when a chunked read is started and simply indicates that no chunks are in use. The memory is retained in order to reduce the overhead in freeing and reallocating memory for each chunked read. """ self._num_chunks = 0 @cython.final cdef class ReadBuffer(Buffer): cdef: ssize_t _max_packet_size, _bytes_to_process ChunkedBytesBuffer _chunked_bytes_buf bint _session_needs_to_be_closed const char_type _split_data[255] ssize_t _packet_start_offset Capabilities _caps object _socket def __cinit__(self, object sock, ssize_t max_packet_size, Capabilities caps): self._socket = sock self._caps = caps self._max_packet_size = max_packet_size self._initialize(max_packet_size * 2) self._chunked_bytes_buf = ChunkedBytesBuffer() cdef inline int _get_data_from_socket(self, object obj, ssize_t bytes_requested, ssize_t *bytes_read) except -1: """ Simple function that performs a socket read while verifying that the server has not reset the connection. If it has, the dead connection error is returned instead. """ try: bytes_read[0] = self._socket.recv_into(obj, bytes_requested) except ConnectionResetError as e: errors._raise_err(errors.ERR_CONNECTION_CLOSED, str(e), cause=e) if bytes_read[0] == 0: try: self._socket.shutdown(socket.SHUT_RDWR) except OSError: pass self._socket.close() self._socket = None errors._raise_err(errors.ERR_CONNECTION_CLOSED) cdef int _get_int_length_and_sign(self, uint8_t *length, bint *is_negative, uint8_t max_length) except -1: """ Returns the length of an integer sent on the wire. A check is also made to ensure the integer does not exceed the maximum length. If the is_negative pointer is NULL, negative integers will result in an exception being raised. """ cdef const char_type *ptr = self._get_raw(1) if ptr[0] & 0x80: if is_negative == NULL: errors._raise_err(errors.ERR_UNEXPECTED_NEGATIVE_INTEGER) is_negative[0] = True length[0] = ptr[0] & 0x7f else: if is_negative != NULL: is_negative[0] = False length[0] = ptr[0] if length[0] > max_length: errors._raise_err(errors.ERR_INTEGER_TOO_LARGE, length=length[0], max_length=max_length) cdef const char_type* _get_raw(self, ssize_t num_bytes, bint in_chunked_read=False) except NULL: """ Returns a pointer to a buffer containing the requested number of bytes. This may be split across multiple packets in which case a chunked bytes buffer is used. """ cdef: ssize_t num_bytes_left, num_bytes_split, max_split_data uint8_t packet_type, packet_flags const char_type *source_ptr char_type *dest_ptr # if no bytes are left in the buffer, a new packet needs to be fetched # before anything else can take place if self._pos == self._size: self.receive_packet(&packet_type, &packet_flags) self.skip_raw_bytes(2) # skip data flags # if there is enough room in the buffer to satisfy the number of bytes # requested, return a pointer to the current location and advance the # offset the required number of bytes source_ptr = &self._data[self._pos] num_bytes_left = self._size - self._pos if num_bytes <= num_bytes_left: if in_chunked_read: dest_ptr = self._chunked_bytes_buf.get_chunk_ptr(num_bytes) memcpy(dest_ptr, source_ptr, num_bytes) self._pos += num_bytes return source_ptr # the requested bytes are split across multiple packets; if a chunked # read is in progress, a chunk is acquired that will accommodate the # remainder of the bytes in the current packet; otherwise, the split # buffer will be used instead (after first checking to see if there is # sufficient room available within it) if in_chunked_read: dest_ptr = self._chunked_bytes_buf.get_chunk_ptr(num_bytes_left) else: max_split_data = sizeof(self._split_data) if max_split_data < num_bytes: errors._raise_err(errors.ERR_BUFFER_LENGTH_INSUFFICIENT, actual_buffer_len=max_split_data, required_buffer_len=num_bytes) dest_ptr = self._split_data memcpy(dest_ptr, source_ptr, num_bytes_left) # acquire packets until the requested number of bytes is satisfied num_bytes -= num_bytes_left while num_bytes > 0: # acquire new packet self.receive_packet(&packet_type, &packet_flags) self.skip_raw_bytes(2) # skip data flags # copy data into the chunked buffer or split buffer, as appropriate source_ptr = &self._data[self._pos] num_bytes_split = min(num_bytes, self._size - self._pos) if in_chunked_read: dest_ptr = \ self._chunked_bytes_buf.get_chunk_ptr(num_bytes_split) else: dest_ptr = &self._split_data[num_bytes_left] memcpy(dest_ptr, source_ptr, num_bytes_split) self._pos += num_bytes_split num_bytes -= num_bytes_split # return the split buffer unconditionally; if performing a chunked read # the return value is ignored anyway return self._split_data cdef int _read_raw_bytes_and_length(self, const char_type **ptr, ssize_t *num_bytes) except -1: """ Helper function that processes the length. If the length is defined as TNS_LONG_LENGTH_INDICATOR, a chunked read is performed. """ cdef uint32_t temp_num_bytes if num_bytes[0] != TNS_LONG_LENGTH_INDICATOR: return Buffer._read_raw_bytes_and_length(self, ptr, num_bytes) self._chunked_bytes_buf.start_chunked_read() num_bytes[0] = 0 while True: self.read_ub4(&temp_num_bytes) if temp_num_bytes == 0: break num_bytes[0] += temp_num_bytes self._get_raw(temp_num_bytes, in_chunked_read=True) ptr[0] = self._chunked_bytes_buf.end_chunked_read() cdef int _process_control_packet(self) except -1: """ Process a control packed received in between data packets. """ cdef uint16_t control_type, error_num self.read_uint16(&control_type) if control_type == TNS_CONTROL_TYPE_RESET_OOB: self._caps.supports_oob = False elif control_type == TNS_CONTROL_TYPE_INBAND_NOTIFICATION: self.skip_raw_bytes(6) # skip flags self.read_uint16(&error_num) self.skip_raw_bytes(4) if error_num == TNS_ERR_SESSION_SHUTDOWN \ or error_num == TNS_ERR_INBAND_MESSAGE: self._session_needs_to_be_closed = True else: errors._raise_err(errors.ERR_UNSUPPORTED_INBAND_NOTIFICATION, err_num=error_num) cdef int _receive_packet_helper(self, uint8_t *packet_type, uint8_t *packet_flags) except -1: """ Receives a packet and updates the pointers appropriately. Note that multiple packets may be received if they are small enough or a portion of a second packet may be received so the buffer needs to be adjusted as needed. This is also why room is available in the buffer for up to two complete packets. """ cdef: ssize_t offset, bytes_to_read, bytes_read uint32_t packet_size uint16_t temp16 # if no bytes are left over from a previous read, perform a read of the # maximum packet size and reset the offset to 0 if self._bytes_to_process == 0: self._packet_start_offset = 0 self._pos = 0 self._get_data_from_socket(self._data_obj, self._max_packet_size, &self._bytes_to_process) # otherwise, set the offset to the end of the previous packet and # ensure that there are at least enough bytes available to cover the # contents of the packet header else: self._packet_start_offset = self._size self._pos = self._size if self._bytes_to_process < PACKET_HEADER_SIZE: offset = self._size + self._bytes_to_process bytes_to_read = PACKET_HEADER_SIZE - self._bytes_to_process self._get_data_from_socket(self._data_view[offset:], bytes_to_read, &bytes_read) self._bytes_to_process += bytes_read # determine the packet length and ensure that all of the bytes for the # packet are available; note that as of version 12.2 the packet size is # 32 bits in size instead of 16 bits, but this doesn't take effect # until it is known that the server is capable of that as well if self._caps.protocol_version >= TNS_VERSION_MIN_LARGE_SDU: self._size += 4 self.read_uint32(&packet_size) else: self._size += 2 self.read_uint16(&temp16) packet_size = temp16 while self._bytes_to_process < packet_size: offset = self._packet_start_offset + self._bytes_to_process bytes_to_read = packet_size - self._bytes_to_process self._get_data_from_socket(self._data_view[offset:], bytes_to_read, &bytes_read) self._bytes_to_process += bytes_read # process remainder of packet header and set size to the new packet self._size = self._packet_start_offset + packet_size self._bytes_to_process -= packet_size if self._caps.protocol_version < TNS_VERSION_MIN_LARGE_SDU: self.skip_raw_bytes(2) # skip packet checksum self.read_ub1(packet_type) self.read_ub1(packet_flags) self.skip_raw_bytes(2) # header checksum # display packet if requested if DEBUG_PACKETS: offset = self._packet_start_offset _print_packet("Receiving packet:", self._socket.fileno(), self._data_view[offset:self._size]) cdef object read_lob_with_length(self, ThinConnImpl conn_impl, DbType dbtype): """ Read a LOB locator from the buffer and return a LOB object containing it. """ cdef uint32_t num_bytes self.read_ub4(&num_bytes) if num_bytes > 0: return self.read_lob(conn_impl, dbtype) cdef int read_rowid(self, Rowid *rowid) except -1: """ Reads a rowid from the buffer and populates the rowid structure. """ self.read_ub4(&rowid.rba) self.read_ub2(&rowid.partition_id) self.skip_ub1() self.read_ub4(&rowid.block_num) self.read_ub2(&rowid.slot_num) cdef object read_urowid(self): """ Read a universal rowid from the buffer and return the Python object representing its value. """ cdef: ssize_t output_len, input_len, remainder, pos int input_offset = 1, output_offset = 0 const char_type *input_ptr bytearray output_value uint32_t num_bytes uint8_t length Rowid rowid # get data (first buffer contains the length, which can be ignored) self.read_raw_bytes_and_length(&input_ptr, &input_len) if input_ptr == NULL: return None self.read_raw_bytes_and_length(&input_ptr, &input_len) # handle physical rowid if input_ptr[0] == 1: rowid.rba = unpack_uint32(&input_ptr[1], BYTE_ORDER_MSB) rowid.partition_id = unpack_uint16(&input_ptr[5], BYTE_ORDER_MSB) rowid.block_num = unpack_uint32(&input_ptr[7], BYTE_ORDER_MSB) rowid.slot_num = unpack_uint16(&input_ptr[11], BYTE_ORDER_MSB) return _encode_rowid(&rowid) # handle logical rowid output_len = (input_len // 3) * 4 remainder = input_len % 3 if remainder == 1: output_len += 1 elif remainder == 2: output_len += 3 output_value = bytearray(output_len) input_len -= 1 output_value[0] = 42 # '*' output_offset += 1 while input_len > 0: # produce first byte of quadruple pos = input_ptr[input_offset] >> 2 output_value[output_offset] = TNS_BASE64_ALPHABET_ARRAY[pos] output_offset += 1 # produce second byte of quadruple, but if only one byte is left, # produce that one byte and exit pos = (input_ptr[input_offset] & 0x3) << 4 if input_len == 1: output_value[output_offset] = TNS_BASE64_ALPHABET_ARRAY[pos] break input_offset += 1 pos |= ((input_ptr[input_offset] & 0xf0) >> 4) output_value[output_offset] = TNS_BASE64_ALPHABET_ARRAY[pos] output_offset += 1 # produce third byte of quadruple, but if only two bytes are left, # produce that one byte and exit pos = (input_ptr[input_offset] & 0xf) << 2 if input_len == 2: output_value[output_offset] = TNS_BASE64_ALPHABET_ARRAY[pos] break input_offset += 1 pos |= ((input_ptr[input_offset] & 0xc0) >> 6) output_value[output_offset] = TNS_BASE64_ALPHABET_ARRAY[pos] output_offset += 1 # produce final byte of quadruple pos = input_ptr[input_offset] & 0x3f output_value[output_offset] = TNS_BASE64_ALPHABET_ARRAY[pos] output_offset += 1 input_offset += 1 input_len -= 3 return bytes(output_value).decode() cdef int check_control_packet(self) except -1: """ Checks for a control packet or final close packet from the server. """ cdef: uint8_t packet_type, packet_flags uint16_t data_flags self._receive_packet_helper(&packet_type, &packet_flags) if packet_type == TNS_PACKET_TYPE_CONTROL: self._process_control_packet() elif packet_type == TNS_PACKET_TYPE_DATA: self.read_uint16(&data_flags) if data_flags == TNS_DATA_FLAGS_EOF: self._session_needs_to_be_closed = True cdef int receive_packet(self, uint8_t *packet_type, uint8_t *packet_flags) except -1: """ Calls _receive_packet_helper() and checks the packet type. If a control packet is received, it is processed and the next packet is received. """ while True: self._receive_packet_helper(packet_type, packet_flags) if packet_type[0] == TNS_PACKET_TYPE_CONTROL: self._process_control_packet() continue break cdef int skip_raw_bytes_chunked(self) except -1: """ Skip a number of bytes that may or may not be chunked in the buffer. The first byte gives the length. If the length is TNS_LONG_LENGTH_INDICATOR, however, chunks are read and discarded. """ cdef: uint32_t temp_num_bytes uint8_t length self.read_ub1(&length) if length != TNS_LONG_LENGTH_INDICATOR: self.skip_raw_bytes(length) else: while True: self.read_ub4(&temp_num_bytes) if temp_num_bytes == 0: break self.skip_raw_bytes(temp_num_bytes) @cython.final cdef class WriteBuffer(Buffer): cdef: uint8_t _packet_type Capabilities _caps object _socket uint8_t _seq_num bint _packet_sent def __cinit__(self, object sock, ssize_t max_size, Capabilities caps): self._socket = sock self._caps = caps self._initialize(max_size) cdef int _send_packet(self, bint final_packet) except -1: """ Write the packet header and then send the packet. Once sent, reset the pointers back to an empty packet. """ cdef ssize_t size = self._pos self._pos = 0 if self._caps.protocol_version >= TNS_VERSION_MIN_LARGE_SDU: self.write_uint32(size) else: self.write_uint16(size) self.write_uint16(0) self.write_uint8(self._packet_type) self.write_uint8(0) self.write_uint16(0) self._pos = size if DEBUG_PACKETS: _print_packet("Sending packet:", self._socket.fileno(), self._data_view[:self._pos]) try: self._socket.send(self._data_view[:self._pos]) except OSError as e: errors._raise_err(errors.ERR_CONNECTION_CLOSED, str(e)) self._packet_sent = True self._pos = PACKET_HEADER_SIZE if not final_packet: self.write_uint16(0) # add data flags for next packet cdef int _write_more_data(self, ssize_t num_bytes_available, ssize_t num_bytes_wanted) except -1: """ Called when the amount of buffer available is less than the amount of data requested. This sends the packet to the server and then resets the buffer for further writing. """ self._send_packet(final_packet=False) cdef int end_request(self) except -1: """ Indicates that the request from the client is completing and will send any packet remaining, if necessary. """ if self._pos > PACKET_HEADER_SIZE: self._send_packet(final_packet=True) cdef inline ssize_t max_payload_bytes(self): """ Return the maximum number of bytes that can be sent in a packet. This is the maximum size of the entire packet, less the bytes in the header and 2 bytes for the data flags. """ return self._max_size - PACKET_HEADER_SIZE - 2 cdef void start_request(self, uint8_t packet_type, uint16_t data_flags=0): """ Indicates that a request from the client is starting. The packet type is retained just in case a request spans multiple packets. The packet header (8 bytes in length) is written when a packet is actually being sent and so is skipped at this point. """ self._packet_sent = False self._packet_type = packet_type self._pos = PACKET_HEADER_SIZE if packet_type == TNS_PACKET_TYPE_DATA: self.write_uint16(data_flags) cdef int write_lob_with_length(self, ThinLobImpl lob_impl) except -1: """ Writes a LOB locator to the buffer. """ self.write_ub4(len(lob_impl._locator)) return self.write_lob(lob_impl) cdef int write_seq_num(self) except -1: self._seq_num += 1 if self._seq_num == 0: self._seq_num = 1 self.write_uint8(self._seq_num) python-oracledb-1.2.1/src/oracledb/impl/thin/pool.pyx000066400000000000000000000465061434177474600226210ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # pool.pyx # # Cython file defining the pool implementation class (embedded in # thin_impl.pyx). #------------------------------------------------------------------------------ cdef class ThinPoolImpl(BasePoolImpl): cdef: list _free_new_conn_impls list _free_used_conn_impls list _busy_conn_impls list _conn_impls_to_drop uint32_t _getmode uint32_t _stmt_cache_size uint32_t _timeout uint32_t _max_lifetime_session uint32_t _num_waiters uint32_t _auth_mode int _ping_interval object _wait_timeout object _bg_exc object _bg_thread object _bg_thread_condition object _condition bint _force_get bint _open def __init__(self, str dsn, PoolParamsImpl params): if not HAS_CRYPTOGRAPHY: errors._raise_err(errors.ERR_NO_CRYPTOGRAPHY_PACKAGE) params._check_credentials() self.connect_params = params self.username = params.user self.dsn = dsn self.min = params.min self.max = params.max self.increment = params.increment self.homogeneous = params.homogeneous self.set_getmode(params.getmode) self.set_wait_timeout(params.wait_timeout) self._stmt_cache_size = params.stmtcachesize self._ping_interval = params.ping_interval self._free_new_conn_impls = [] self._free_used_conn_impls = [] self._busy_conn_impls = [] self._conn_impls_to_drop = [] self._auth_mode = constants.AUTH_MODE_DEFAULT self._open = True self._condition = threading.Condition() self._bg_thread_condition = threading.Condition() self._bg_thread = threading.Thread(target=self._bg_thread_func, daemon=True) self._bg_thread.start() cdef ThinConnImpl _create_conn_impl(self, ConnectParamsImpl params=None): """ Create a single connection using the pool's information. This connection may be placed in the pool or may be returned directly (such as when the pool is full and POOL_GETMODE_FORCEGET is being used). """ cdef ThinConnImpl conn_impl conn_impl = ThinConnImpl(self.dsn, self.connect_params) if params is not None: conn_impl._cclass = params._default_description.cclass conn_impl._pool = self conn_impl.connect(self.connect_params) return conn_impl def _bg_thread_func(self): """ Method which runs in a dedicated thread and is used to create connections and close them when needed. When first started, it creates pool.min connections. After that, it creates pool.increment connections up to the value of pool.max when needed and destroys connections when needed. The thread terminates automatically when the pool is closed. """ cdef: uint32_t num_conns, open_count list conn_impls_to_drop bint wait # create the initial set of connections requested self._create_conn_impls_helper(self.min) # create connections and close connections as needed while True: conn_impls_to_drop = [] num_conns = 0 wait = True # determine if there is any work to do with self._condition: if self._open and self._num_waiters > 0: open_count = self.get_open_count() num_conns = min(self.increment, self.max - open_count) if not self._open or self._bg_exc is None: conn_impls_to_drop = self._conn_impls_to_drop self._conn_impls_to_drop = [] # create connections, if needed if num_conns > 0: wait = False self._create_conn_impls_helper(num_conns) # close connections, if needed if conn_impls_to_drop: wait = False self._drop_conn_impls_helper(conn_impls_to_drop) # if pool has closed, stop thread! if not self._open: break # otherwise, if nothing needed to be done, wait for notification if wait: with self._bg_thread_condition: self._bg_thread_condition.wait() cdef int _create_conn_impls_helper(self, uint32_t num_conns) except -1: """ Helper method which creates the specified number of connections. In order to prevent the thread from dying, exceptions are captured and stored on the pool object. The next attempt to acquire a connection will return this exception. """ cdef: ThinConnImpl conn_impl object exc uint32_t i for i in range(num_conns): conn_impl = None try: conn_impl = self._create_conn_impl() except Exception as e: exc = e with self._condition: if conn_impl is not None: if self._open: self._free_new_conn_impls.append(conn_impl) else: conn_impl._force_close() break else: self._bg_exc = exc self._condition.notify() if conn_impl is None: break cdef int _drop_conn_impl(self, ThinConnImpl conn_impl) except -1: """ Helper method which adds a connection to the list of connections to be closed and notifies the background thread. """ self._conn_impls_to_drop.append(conn_impl) with self._bg_thread_condition: self._bg_thread_condition.notify() cdef int _drop_conn_impls_helper(self, list conn_impls_to_drop) except -1: """ Helper method which drops the requested list of connections. When the pool is closed, exceptions are ignored. """ cdef ThinConnImpl conn_impl for conn_impl in conn_impls_to_drop: try: conn_impl._force_close() except Exception as e: if self._open: self._bg_exc = e cdef object _get_connection(self, bint wants_new, bint cclass_matches, str cclass): """ Returns a connection from the pool if one is available. If one is not available and a new connection needs to be created, the value True is returned. """ cdef: ThinConnImpl conn_impl object exc ssize_t i # if an exception was raised in the background thread, raise it now if self._bg_exc is not None: exc = self._bg_exc self._bg_exc = None raise exc # check for an available new connection (only permitted if the cclass # matches) if cclass_matches and self._free_new_conn_impls: conn_impl = self._free_new_conn_impls.pop() self._busy_conn_impls.append(conn_impl) return conn_impl # check for an available used connection (only permitted if a new # connection is not required); in addition, if the cclass does not # match, a new connection will be forced if one cannot be found if not wants_new and self._free_used_conn_impls: if cclass_matches: conn_impl = self._free_used_conn_impls.pop(0) self._busy_conn_impls.append(conn_impl) return conn_impl for i, conn_impl in enumerate(self._free_used_conn_impls): if conn_impl._cclass == cclass: self._free_used_conn_impls.pop(i) self._busy_conn_impls.append(conn_impl) return conn_impl # no connections are immediately available; if a brand new connection # is desired, the cclass doesn't match, or the pool is full and a # getmode of FORCE has been specified, force the creation of a new # connection if wants_new or not cclass_matches \ or (self._force_get and self.get_open_count() >= self.max): return True # wake up the background thread to create a connection with self._bg_thread_condition: self._bg_thread_condition.notify() cdef ThinConnImpl _acquire_helper(self, ConnectParamsImpl params): cdef: ConnectParamsImpl creation_params = self.connect_params bint wants_new, cclass_matches object result, predicate str pool_cclass, cclass ThinConnImpl conn_impl ssize_t i # initialize values used in determining which connection can be # returned from the pool cclass = params._default_description.cclass pool_cclass = creation_params._default_description.cclass wants_new = (params._default_description.purity == PURITY_NEW) cclass_matches = (cclass is None or cclass == pool_cclass) predicate = lambda: self._get_connection(wants_new, cclass_matches, cclass) # get a connection from the pool; if one is not immediately available, # wait as long as requested for one to be made available. with self._condition: self._num_waiters += 1 try: result = self._condition.wait_for(predicate, self._wait_timeout) finally: self._num_waiters -= 1 if result is None: errors._raise_err(errors.ERR_POOL_NO_CONNECTION_AVAILABLE) if isinstance(result, ThinConnImpl): return result # no connection was returned from the pool so a new connection needs to # be created conn_impl = self._create_conn_impl(params=params) with self._condition: if self.get_open_count() < self.max: self._busy_conn_impls.append(conn_impl) elif self._free_used_conn_impls: self._drop_conn_impl(self._free_used_conn_impls.pop(0)) self._busy_conn_impls.append(conn_impl) else: conn_impl._pool = None return conn_impl cdef int _return_connection(self, ThinConnImpl conn_impl) except -1: """ Returns the connection to the pool. If the connection was closed for some reason it will be dropped; otherwise, it will be returned to the list of connections available for further use. """ cdef: ThinDbObjectTypeCache type_cache int cache_num with self._condition: if conn_impl._dbobject_type_cache_num > 0: cache_num = conn_impl._dbobject_type_cache_num type_cache = get_dbobject_type_cache(cache_num) type_cache._clear_meta_cursor() if conn_impl._protocol._socket is not None: self._free_used_conn_impls.append(conn_impl) conn_impl._time_in_pool = time.monotonic() self._busy_conn_impls.remove(conn_impl) self._condition.notify() def acquire(self, ConnectParamsImpl params): """ Internal method for acquiring a connection from the pool. """ cdef: ThinConnImpl conn_impl double elapsed_time ReadBuffer read_buf bint requires_ping Message message # if pool is closed, raise an exception if not self._open: errors._raise_err(errors.ERR_POOL_NOT_OPEN) # session tagging has not been implemented yet if params.tag is not None: raise NotImplementedError("Tagging has not been implemented yet") # acquire a connection from the pool and perform a ping if the ping # interval is set to 0 or if the connection was previously used and # has been idle for a time greater than the value of ping interval while True: requires_ping = False conn_impl = self._acquire_helper(params) read_buf = conn_impl._protocol._read_buf if not read_buf._session_needs_to_be_closed: socket_list = [conn_impl._protocol._socket] while not read_buf._session_needs_to_be_closed: read_socks, _, _ = select.select(socket_list, [], [], 0) if not read_socks: break read_buf.check_control_packet() if read_buf._session_needs_to_be_closed: with self._condition: self._busy_conn_impls.remove(conn_impl) self._drop_conn_impl(conn_impl) continue if self._ping_interval == 0: requires_ping = True elif self._ping_interval > 0 and conn_impl._time_in_pool > 0: elapsed_time = time.monotonic() - conn_impl._time_in_pool if elapsed_time > self._ping_interval: requires_ping = True if not requires_ping: break try: message = conn_impl._create_message(PingMessage) conn_impl._protocol._process_message(message) break except exceptions.DatabaseError: with self._condition: self._busy_conn_impls.remove(conn_impl) self._drop_conn_impl(conn_impl) return conn_impl def close(self, bint force): """ Internal method for closing the pool. """ cdef: ThinConnImpl conn_impl with self._condition: # if force parameter is not True and busy connections exist in the # pool, raise an exception if not force and self.get_busy_count() > 0: errors._raise_err(errors.ERR_POOL_HAS_BUSY_CONNECTIONS) # close all connections in the pool; this is done by simply adding # to the list of connections that require closing and then waking # up the background thread to perform the work self._open = False for lst in (self._free_used_conn_impls, self._free_new_conn_impls, self._busy_conn_impls): self._conn_impls_to_drop.extend(lst) for conn_impl in lst: conn_impl._pool = None lst.clear() with self._bg_thread_condition: self._bg_thread_condition.notify() # wait for background thread to complete its tasks self._bg_thread.join() def drop(self, ThinConnImpl conn_impl): """ Internal method for dropping a connection from the pool. """ with self._condition: self._busy_conn_impls.remove(conn_impl) conn_impl._pool = None self._drop_conn_impl(conn_impl) self._condition.notify() def get_busy_count(self): """ Internal method for getting the number of busy connections in the pool. """ return len(self._busy_conn_impls) def get_getmode(self): """ Internal method for getting the method by which connections are acquired from the pool. """ return self._getmode def get_max_lifetime_session(self): """ Internal method for getting the maximum lifetime of each session. """ return self._max_lifetime_session def get_open_count(self): """ Internal method for getting the number of connections in the pool. """ return len(self._busy_conn_impls) + \ len(self._free_used_conn_impls) + \ len(self._free_new_conn_impls) def get_ping_interval(self): """ Internal method for getting the value of the pool-ping-interval. """ return self._ping_interval def get_stmt_cache_size(self): """ Internal method for getting the size of the statement cache. """ return self._stmt_cache_size def get_timeout(self): """ Internal method for getting the timeout for idle sessions. """ return self._timeout def get_wait_timeout(self): """ Internal method for getting the wait timeout for acquiring sessions. """ if self._getmode == constants.POOL_GETMODE_TIMEDWAIT: return self._wait_timeout return 0 def set_getmode(self, uint32_t value): """ Internal method for setting the method by which connections are acquired from the pool. """ if self._getmode != value: self._getmode = value self._force_get = \ (self._getmode == constants.POOL_GETMODE_FORCEGET) if self._getmode == constants.POOL_GETMODE_TIMEDWAIT \ or self._getmode == constants.POOL_GETMODE_NOWAIT: self._wait_timeout = 0 else: self._wait_timeout = None def set_max_lifetime_session(self, uint32_t value): """ Internal method for setting the maximum lifetime of each session. """ self._max_lifetime_session = value def set_ping_interval(self, int value): """ Internal method for setting the value of the pool-ping-interval. """ self._ping_interval = value def set_stmt_cache_size(self, uint32_t value): """ Internal method for setting the size of the statement cache. """ self._stmt_cache_size = value def set_timeout(self, uint32_t value): """ Internal method for setting the timeout for idle sessions. """ self._timeout = value def set_wait_timeout(self, uint32_t value): """ Internal method for setting the wait timeout for acquiring sessions. """ if self._getmode == constants.POOL_GETMODE_TIMEDWAIT: self._wait_timeout = value / 1000 elif self._getmode == constants.POOL_GETMODE_NOWAIT: self._wait_timeout = 0 else: self._wait_timeout = None python-oracledb-1.2.1/src/oracledb/impl/thin/protocol.pyx000066400000000000000000000476041434177474600235110ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # protocol.pyx # # Cython file defining the protocol used by the client when communicating with # the database (embedded in thin_impl.pyx). #------------------------------------------------------------------------------ cdef class Protocol: cdef: uint8_t _seq_num object _socket Capabilities _caps ReadBuffer _read_buf WriteBuffer _write_buf object _request_lock bint _in_connect bint _txn_in_progress bint _break_in_progress def __init__(self): self._caps = Capabilities() # mark protocol to indicate that connect is in progress; this prevents # the normal break/reset mechanism from firing, which is unnecessary # since the connection is going to be immediately closed anyway! self._in_connect = True self._request_lock = threading.RLock() cdef int _break_external(self) except -1: """ Method for sending a break to the server from an external request. A separate write buffer is used in order to avoid a potential conflict with any in progress writes. """ cdef WriteBuffer buf if not self._break_in_progress: self._break_in_progress = True if self._caps.supports_oob: if DEBUG_PACKETS: now = datetime.datetime.now() print(now.isoformat(sep=" ", timespec="milliseconds"), f"[socket: {self._socket.fileno()}]", "Sending out of band break") print() self._socket.send(b"!", socket.MSG_OOB) else: buf = WriteBuffer(self._socket, TNS_SDU, self._caps) self._send_marker(buf, TNS_MARKER_TYPE_INTERRUPT) cdef int _close(self, ThinConnImpl conn_impl) except -1: """ Closes the connection. If a transaction is in progress it will be rolled back. DRCP sessions will be released. For standalone connections, the session will be logged off. For pooled connections, the connection will be returned to the pool for subsequent use. """ cdef: Message message uint32_t release_mode = DRCP_DEAUTHENTICATE \ if conn_impl._pool is None else 0 with self._request_lock: # if a read failed on the socket earlier, clear the socket if self._read_buf._socket is None: self._socket = None # if the session was marked as needing to be closed, force it # closed immediately (unless it was already closed) if self._read_buf._session_needs_to_be_closed \ and self._socket is not None: self._force_close() # rollback any open transaction and release the DRCP session, if # applicable if self._socket is not None: if self._txn_in_progress: message = conn_impl._create_message(RollbackMessage) self._process_message(message) if conn_impl._drcp_enabled: self._release_drcp_session(self._write_buf, release_mode) conn_impl._drcp_establish_session = True # if the connection is part of a pool, return it to the pool if conn_impl._pool is not None: return conn_impl._pool._return_connection(conn_impl) # otherwise, destroy the database object type cache, send the # logoff message and final close packet if conn_impl._dbobject_type_cache_num > 0: remove_dbobject_type_cache(conn_impl._dbobject_type_cache_num) conn_impl._dbobject_type_cache_num = 0 if self._socket is not None: if not conn_impl._drcp_enabled: message = conn_impl._create_message(LogoffMessage) self._process_message(message) self._final_close(self._write_buf) cdef int _connect_phase_one(self, ThinConnImpl conn_impl, ConnectParamsImpl params, Description description, Address address, str connect_string) except -1: """ Method for performing the required steps for establishing a connection within the scope of a retry. If the listener refuses the connection, a retry will be performed, if retry_count is set. """ cdef: ConnectMessage connect_message = None object ssl_context, connect_info ConnectParamsImpl temp_params str host, redirect_data Address temp_address uint8_t packet_type int port, pos # store whether OOB processing is possible or not self._caps.supports_oob = not params.disable_oob \ and sys.platform != "win32" # establish initial TCP connection and get initial connect string host = address.host port = address.port self._connect_tcp(params, description, address, host, port) # send connect message and process response; this may request the # message to be resent multiple times; if a redirect packet is # detected, a new TCP connection is established first while True: # create connection message, if needed if connect_message is None: connect_message = conn_impl._create_message(ConnectMessage) connect_message.host = host connect_message.port = port connect_message.description = description connect_message.connect_string_bytes = connect_string.encode() connect_message.connect_string_len = \ len(connect_message.connect_string_bytes) # process connection message self._process_message(connect_message) if connect_message.redirect_data is not None: redirect_data = connect_message.redirect_data pos = redirect_data.find('\x00') if pos < 0: errors._raise_err(errors.ERR_INVALID_REDIRECT_DATA, data=redirect_data) temp_params = ConnectParamsImpl() temp_params._parse_connect_string(redirect_data[:pos]) temp_address = temp_params._get_addresses()[0] host = temp_address.host port = temp_address.port connect_string = redirect_data[pos + 1:] self._connect_tcp(params, description, address, host, port) connect_message = None elif connect_message.packet_type == TNS_PACKET_TYPE_ACCEPT: break # for TCPS connections, OOB processing is not supported; if the # packet flags indicate that TLS renegotiation is required, this is # performed now if address.protocol == "tcps": self._caps.supports_oob = False if connect_message.packet_flags & TNS_PACKET_FLAG_TLS_RENEG: ssl_context = self._socket.context sock = socket.socket(fileno=self._socket.detach()) sock = perform_tls_negotiation(sock, ssl_context, description, address) self._set_socket(sock) cdef int _connect_phase_two(self, ThinConnImpl conn_impl, Description description, ConnectParamsImpl params) except -1: """" Method for perfoming the required steps for establishing a connection oustide the scope of a retry. If any of the steps in this method fail, an exception will be raised. """ cdef: AuthMessage auth_message # check if the protocol version supported by the database is high # enough; if not, reject the connection immediately if self._caps.protocol_version < TNS_VERSION_MIN_ACCEPTED: errors._raise_err(errors.ERR_SERVER_VERSION_NOT_SUPPORTED) # if we can use OOB, send an urgent message now followed by a reset # marker to see if the server understands it if self._caps.supports_oob \ and self._caps.protocol_version >= TNS_VERSION_MIN_OOB_CHECK: self._socket.send(b"!", socket.MSG_OOB) self._send_marker(self._write_buf, TNS_MARKER_TYPE_RESET) # send services, protocol and data types messages and process responses self._process_message(conn_impl._create_message(NetworkServicesMessage)) self._process_message(conn_impl._create_message(ProtocolMessage)) self._process_message(conn_impl._create_message(DataTypesMessage)) # send authorization packet twice, the first time to get the session # key and the second time to return the response to the challenge auth_message = conn_impl._create_message(AuthMessage) auth_message._set_params(params, description) self._process_message(auth_message) self._process_message(auth_message) # mark protocol to indicate that connect is no longer in progress; this # allows the normal break/reset mechanism to fire self._in_connect = False cdef int _connect_tcp(self, ConnectParamsImpl params, Description description, Address address, str host, int port) except -1: """ Creates a socket on which to communicate using the provided parameters. If a proxy is configured, a connection to the proxy is established and the target host and port is forwarded to the proxy. """ cdef: bint use_proxy = (address.https_proxy is not None) double timeout = description.tcp_connect_timeout bint use_tcps = (address.protocol == "tcps") object connect_info, sock, data, reply, m # establish connection to appropriate host/port if use_proxy: if not use_tcps: errors._raise_err(errors.ERR_HTTPS_PROXY_REQUIRES_TCPS) connect_info = (address.https_proxy, address.https_proxy_port) else: connect_info = (host, port) if not use_tcps and (params._token is not None or params.access_token_callback is not None): errors._raise_err(errors.ERR_ACCESS_TOKEN_REQUIRES_TCPS) sock = socket.create_connection(connect_info, timeout) # complete connection through proxy, if applicable if use_proxy: data = f"CONNECT {host}:{port} HTTP/1.0\r\n\r\n" sock.send(data.encode()) reply = sock.recv(1024) m = re.search('HTTP/1.[01]\\s+(\\d+)\\s+', reply.decode()) if m is None or m.groups()[0] != '200': errors._raise_err(errors.ERR_PROXY_FAILURE, response=reply.decode()) # set socket options if description.expire_time > 0: sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) if hasattr(socket, "TCP_KEEPIDLE") \ and hasattr(socket, "TCP_KEEPINTVL") \ and hasattr(socket, "TCP_KEEPCNT"): sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, description.expire_time * 60) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 6) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 10) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) sock.settimeout(None) # establish TLS connection, if applicable if use_tcps: sock = get_ssl_socket(sock, params, description, address) # save final socket object self._set_socket(sock) cdef int _final_close(self, WriteBuffer buf) except -1: """ Send the final close packet to the server and close the socket. """ buf.start_request(TNS_PACKET_TYPE_DATA, TNS_DATA_FLAGS_EOF) buf.end_request() self._socket.shutdown(socket.SHUT_RDWR) self._socket.close() self._socket = None cdef int _force_close(self) except -1: """ Forces the connection closed. This is used when an unrecoverable error has taken place. """ sock = self._socket if DEBUG_PACKETS: now = datetime.datetime.now() print(now.isoformat(sep=" ", timespec="milliseconds"), f"[socket: {sock.fileno()}]", "force closing connection") print() self._socket = None self._read_buf._socket = None self._write_buf._socket = None sock.shutdown(socket.SHUT_RDWR) sock.close() cdef int _process_message(self, Message message) except -1: try: message.send(self._write_buf) self._receive_packet(message) message.process(self._read_buf) except socket.timeout: try: self._break_external() self._receive_packet(message) self._break_in_progress = False timeout = int(self._socket.gettimeout() * 1000) errors._raise_err(errors.ERR_CALL_TIMEOUT_EXCEEDED, timeout=timeout) except socket.timeout: self._force_close() errors._raise_err(errors.ERR_CONNECTION_CLOSED, "socket timed out while recovering from " \ "previous socket timeout") raise except: if not self._in_connect \ and self._write_buf._packet_sent \ and self._read_buf._socket is not None: self._send_marker(self._write_buf, TNS_MARKER_TYPE_BREAK) self._reset(message) raise if message.flush_out_binds: self._write_buf.start_request(TNS_PACKET_TYPE_DATA) self._write_buf.write_uint8(TNS_MSG_TYPE_FLUSH_OUT_BINDS) self._write_buf.end_request() self._reset(message) message.process(self._read_buf) if self._break_in_progress: try: if self._caps.supports_oob: self._send_marker(self._write_buf, TNS_MARKER_TYPE_INTERRUPT) self._receive_packet(message) except socket.timeout: errors._raise_err(errors.ERR_CONNECTION_CLOSED, "socket timed out while awaiting break " \ "response from server") message.process(self._read_buf) self._break_in_progress = False self._txn_in_progress = message.call_status & TNS_TXN_IN_PROGRESS if message.error_occurred: if message.error_info.num == TNS_ERR_VAR_NOT_IN_SELECT_LIST: message.error_occurred = False return self._process_message(message) error = errors._Error(message.error_info.message, code=message.error_info.num, offset=message.error_info.pos) # if a connection has received dead connection error then it is no # longer usable if error.is_session_dead: self._force_close() exc_type = get_exception_class(message.error_info.num) raise exc_type(error) cdef int _process_single_message(self, Message message) except -1: """ Process a single message within a request. """ with self._request_lock: self._process_message(message) if message.resend: self._process_message(message) cdef int _receive_packet(self, Message message) except -1: cdef ReadBuffer buf = self._read_buf buf.receive_packet(&message.packet_type, &message.packet_flags) if message.packet_type == TNS_PACKET_TYPE_MARKER: self._reset(message) elif message.packet_type == TNS_PACKET_TYPE_REFUSE: self._write_buf._packet_sent = False buf.skip_raw_bytes(3) message.error_info.message = buf.read_str(TNS_CS_IMPLICIT) cdef int _release_drcp_session(self, WriteBuffer buf, uint32_t release_mode) except -1: """ Release the session back to DRCP. Standalone sessions are marked for deauthentication. """ buf.start_request(TNS_PACKET_TYPE_DATA) buf.write_uint8(TNS_MSG_TYPE_ONEWAY_FN) buf.write_uint8(TNS_FUNC_SESSION_RELEASE) buf.write_uint8(0) # seq number buf.write_uint8(0) # pointer (tag name) buf.write_uint8(0) # tag name length buf.write_ub4(release_mode) # mode buf.end_request() cdef int _reset(self, Message message) except -1: cdef uint8_t marker_type # send reset marker self._send_marker(self._write_buf, TNS_MARKER_TYPE_RESET) # read and discard all packets until a marker packet is received while True: if message.packet_type == TNS_PACKET_TYPE_MARKER: self._read_buf.skip_raw_bytes(2) self._read_buf.read_ub1(&marker_type) if marker_type == TNS_MARKER_TYPE_RESET: break self._read_buf.receive_packet(&message.packet_type, &message.packet_flags) # read error packet; first skip as many marker packets as may be sent # by the server; if the server doesn't handle out-of-band breaks # properly, some quit immediately and others send multiple reset # markers (this addresses both situations without resulting in strange # errors) while message.packet_type == TNS_PACKET_TYPE_MARKER: self._read_buf.receive_packet(&message.packet_type, &message.packet_flags) self._break_in_progress = False cdef int _send_marker(self, WriteBuffer buf, uint8_t marker_type): """ Sends a marker of the specified type to the server. Internal method for sending a break to the server. """ buf.start_request(TNS_PACKET_TYPE_MARKER) buf.write_uint8(1) buf.write_uint8(0) buf.write_uint8(marker_type) buf.end_request() cdef int _set_socket(self, sock): self._socket = sock self._read_buf = ReadBuffer(sock, TNS_SDU, self._caps) self._write_buf = WriteBuffer(sock, TNS_SDU, self._caps) python-oracledb-1.2.1/src/oracledb/impl/thin/statement.pyx000066400000000000000000000236211434177474600236450ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # statement.pyx # # Cython file defining the Statement and BindInfo classes used to hold # information about statements that are executed and any bind parameters that # may be bound to those statements (embedded in thin_impl.pyx). #------------------------------------------------------------------------------ # Rules for named binds: # 1. Quoted and non-quoted bind names are allowed. # 2. Quoted binds can contain any characters. # 3. Non-quoted binds must begin with an alphabet character. # 4. Non-quoted binds can only contain alphanumeric characters, the underscore, # the dollar sign and the pound sign. # 5. Non-quoted binds cannot be Oracle Database Reserved Names (Server handles # this case and returns an appropriate error) BIND_PATTERN = r'(? len(self._sql) else: self._sql_length = len(self._sql_bytes) # create empty list (bind by position) and dict (bind by name) self._bind_info_dict = collections.OrderedDict() self._bind_info_list = [] # Strip single/multiline comments and strings from the sql statement to # ease searching for bind variables. sql = re.sub(r"/\*[\S\n ]+?\*/", "", sql) sql = re.sub(r"\--.*(\n|$)", "", sql) sql = re.sub(r"""'[^']*'(?=(?:[^']*[^']*')*[^']*$)*""", "", sql, flags=re.MULTILINE) sql = re.sub(r'(:\s*)?("([^"]*)")', lambda m: m.group(0) if sql[m.start(0)] == ":" else "", sql) # determine statement type self._determine_statement_type(sql) # bind variables can only be found in queries, DML and PL/SQL if self._is_query or self._is_dml or self._is_plsql: input_sql = sql if not self._is_plsql: match = re.search(DML_RETURNING_PATTERN, sql) if match is not None: pos = match.start(2) input_sql = sql[:pos] returning_sql = sql[pos:] self._add_binds(input_sql, is_return_bind=False) if returning_sql is not None: self._is_returning = True self._add_binds(returning_sql, is_return_bind=True) cdef int _set_var(self, BindInfo bind_info, ThinVarImpl var_impl, ThinCursorImpl cursor_impl) except -1: """ Set the variable on the bind information and copy across metadata that will be used for binding. If the bind metadata has changed, mark the statement as requiring a full execute. In addition, binding a REF cursor also requires a full execute. """ cdef object value if var_impl.dbtype._ora_type_num == TNS_DATA_TYPE_CURSOR: for value in var_impl._values: if value is not None and value._impl is cursor_impl: errors._raise_err(errors.ERR_SELF_BIND_NOT_SUPPORTED) self._requires_full_execute = True if var_impl.dbtype._ora_type_num != bind_info.ora_type_num \ or var_impl.size != bind_info.size \ or var_impl.buffer_size != bind_info.buffer_size \ or var_impl.precision != bind_info.precision \ or var_impl.scale != bind_info.scale \ or var_impl.is_array != bind_info.is_array \ or var_impl.num_elements != bind_info.num_elements \ or var_impl.dbtype._csfrm != bind_info.csfrm: bind_info.ora_type_num = var_impl.dbtype._ora_type_num bind_info.csfrm = var_impl.dbtype._csfrm bind_info.is_array = var_impl.is_array bind_info.num_elements = var_impl.num_elements bind_info.size = var_impl.size bind_info.buffer_size = var_impl.buffer_size bind_info.precision = var_impl.precision bind_info.scale = var_impl.scale self._requires_full_execute = True bind_info._bind_var_impl = var_impl python-oracledb-1.2.1/src/oracledb/impl/thin/utils.pyx000066400000000000000000000132541434177474600230020ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # utils.pyx # # Cython file defining utility classes and methods (embedded in thin_impl.pyx). #------------------------------------------------------------------------------ cdef bint DEBUG_PACKETS = ("PYO_DEBUG_PACKETS" in os.environ) class ConnectConstants: def __init__(self): self.program_name = sys.executable self.machine_name = socket.gethostname() self.pid = str(os.getpid()) self.user_name = getpass.getuser() self.terminal_name = "unknown" self.sanitized_program_name = self._sanitize(self.program_name) self.sanitized_machine_name = self._sanitize(self.machine_name) self.sanitized_user_name = self._sanitize(self.user_name) pattern = r"(?P\d+)\.(?P\d+)\.(?P\d+)" match_dict = re.match(pattern, VERSION) major_num = int(match_dict["major_num"]) minor_num = int(match_dict["minor_num"]) patch_num = int(match_dict["patch_num"]) self.full_version_num = \ major_num << 24 | minor_num << 20 | patch_num << 12 def _sanitize(self, value): """ Sanitize the value by replacing all the "(", ")" and "=" characters with "?". These are invalid characters within connect data. """ return value.replace("(", "?").replace(")", "?").replace("=", "?") _connect_constants = ConnectConstants() cdef int _convert_base64(char_type *buf, long value, int size, int offset): """ Converts every 6 bits into a character, from left to right. This is similar to ordinary base64 encoding with a few differences and written here for performance. """ cdef int i for i in range(size): buf[offset + size - i - 1] = TNS_BASE64_ALPHABET_ARRAY[value & 0x3f] value = value >> 6 return offset + size cdef object _encode_rowid(Rowid *rowid): """ Converts the rowid structure into an encoded string, if the rowid structure contains valid data; otherwise, it returns None. """ cdef: char_type buf[TNS_MAX_ROWID_LENGTH] int offset = 0 if rowid.rba != 0 or rowid.partition_id != 0 or rowid.block_num != 0 \ or rowid.slot_num != 0: offset = _convert_base64(buf, rowid.rba, 6, offset) offset = _convert_base64(buf, rowid.partition_id, 3, offset) offset = _convert_base64(buf, rowid.block_num, 6, offset) offset = _convert_base64(buf, rowid.slot_num, 3, offset) return buf[:TNS_MAX_ROWID_LENGTH].decode() cdef str _get_connect_data(Description description): """ Return the connect data required by the listener in order to connect. """ constants = _connect_constants cid = f"(PROGRAM={constants.sanitized_program_name})" + \ f"(HOST={constants.sanitized_machine_name})" + \ f"(USER={constants.sanitized_user_name})" return description.build_connect_string(cid) def _print_packet(operation, socket_fileno, data): """ Print the packet content in a format suitable for debugging. """ offset = 0 hex_data = memoryview(data).hex().upper() current_date = datetime.datetime.now().isoformat(sep=" ", timespec="milliseconds") output_lines = [f"{current_date} [socket: {socket_fileno}] {operation}"] while hex_data: line_hex_data = hex_data[:16] hex_data = hex_data[16:] hex_dump_values = [] printable_values = [] for i in range(0, len(line_hex_data), 2): hex_byte = line_hex_data[i:i + 2] hex_dump_values.append(hex_byte) byte_val = ord(bytes.fromhex(hex_byte)) char_byte = chr(byte_val) if char_byte.isprintable() and byte_val < 128 \ and char_byte != " ": printable_values.append(char_byte) else: printable_values.append(".") while len(hex_dump_values) < 8: hex_dump_values.append(" ") printable_values.append(" ") output_lines.append(f'{offset:04} : {" ".join(hex_dump_values)} ' + \ f'|{"".join(printable_values)}|') offset += 8 output_lines.append("") print("\n".join(output_lines)) def init_thin_impl(package): """ Initializes globals after the package has been completely initialized. This is to avoid circular imports and eliminate the need for global lookups. """ global PY_TYPE_DB_OBJECT, PY_TYPE_LOB PY_TYPE_DB_OBJECT = package.DbObject PY_TYPE_LOB = package.LOB python-oracledb-1.2.1/src/oracledb/impl/thin/var.pyx000066400000000000000000000125051434177474600224300ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # var.pyx # # Cython file defining the variable implementation class (embedded in # thin_impl.pyx). #------------------------------------------------------------------------------ cdef class ThinVarImpl(BaseVarImpl): cdef: list _values object _conv_func object _last_raw_value cdef int _bind(self, object conn, BaseCursorImpl cursor_impl, uint32_t num_execs, object name, uint32_t pos) except -1: cdef: ThinCursorImpl thin_cursor_impl = cursor_impl Statement stmt = thin_cursor_impl._statement object bind_info_dict = stmt._bind_info_dict list bind_info_list = stmt._bind_info_list ssize_t idx, num_binds, num_vars BindInfo bind_info str normalized_name object value, lob # for PL/SQL blocks, if the size of a string or bytes object exceeds # 32,767 bytes it must be converted to a BLOB/CLOB; and out converter # needs to be established as well to return the string in the way that # the user expects to get it if stmt._is_plsql and self.size > 32767: if self.dbtype._ora_type_num == TNS_DATA_TYPE_RAW \ or self.dbtype._ora_type_num == TNS_DATA_TYPE_LONG_RAW: self.dbtype = DB_TYPE_BLOB elif self.dbtype._csfrm == TNS_CS_NCHAR: self.dbtype = DB_TYPE_NCLOB else: self.dbtype = DB_TYPE_CLOB orig_converter = self.outconverter def converter(v): v = v.read() if orig_converter is not None: v = orig_converter(v) return v self.outconverter = converter # for variables containing LOBs, create temporary LOBs, if needed if self.dbtype._ora_type_num == TNS_DATA_TYPE_CLOB \ or self.dbtype._ora_type_num == TNS_DATA_TYPE_BLOB: for idx, value in enumerate(self._values): if value is not None and not isinstance(value, PY_TYPE_LOB): lob = conn.createlob(self.dbtype) if value: lob.write(value) self._values[idx] = lob # bind by name if name is not None: if name.startswith('"') and name.endswith('"'): normalized_name = name[1:-1] else: normalized_name = name.upper() if normalized_name.startswith(":"): normalized_name = normalized_name[1:] if normalized_name not in bind_info_dict: errors._raise_err(errors.ERR_INVALID_BIND_NAME, name=name) for bind_info in bind_info_dict[normalized_name]: stmt._set_var(bind_info, self, thin_cursor_impl) # bind by position else: num_binds = len(bind_info_list) num_vars = len(cursor_impl.bind_vars) if num_binds != num_vars: errors._raise_err(errors.ERR_WRONG_NUMBER_OF_POSITIONAL_BINDS, expected_num=num_binds, actual_num=num_vars) bind_info = bind_info_list[pos - 1] stmt._set_var(bind_info, self, thin_cursor_impl) cdef int _finalize_init(self) except -1: """ Internal method that finalizes initialization of the variable. """ BaseVarImpl._finalize_init(self) self._values = [None] * self.num_elements cdef list _get_array_value(self): """ Internal method to return the value of the array. """ return self._values[:self.num_elements_in_array] cdef object _get_scalar_value(self, uint32_t pos): """ Internal method to return the value of the variable at the given position. """ return self._values[pos] @cython.boundscheck(False) @cython.wraparound(False) cdef int _set_scalar_value(self, uint32_t pos, object value) except -1: """ Set the value of the variable at the given position. At this point it is assumed that all checks have been performed! """ self._values[pos] = value python-oracledb-1.2.1/src/oracledb/lob.py000066400000000000000000000136741434177474600203310ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # lob.py # # Contains the LOB class for managing BLOB, CLOB, NCLOB and BFILE data. #------------------------------------------------------------------------------ from typing import Union from . import __name__ as MODULE_NAME from . import errors class LOB: __module__ = MODULE_NAME def __del__(self): self._impl.free_lob() def __reduce__(self): value = self.read() return (type(value), (value,)) def __str__(self): return self.read() @classmethod def _from_impl(cls, impl): lob = cls.__new__(cls) lob._impl = impl return lob def close(self) -> None: """ Close the LOB. Call this when writing is completed so that the indexes associated with the LOB can be updated -– but only if open() was called first. """ self._impl.close() def fileexists(self) -> bool: """ Return a boolean indicating if the file referenced by a BFILE type LOB exists. """ return self._impl.file_exists() def getchunksize(self) -> int: """ Return the chunk size for the LOB. Reading and writing to the LOB in chunks of multiples of this size will improve performance. """ return self._impl.get_chunk_size() def getfilename(self) -> tuple: """ Return a two-tuple consisting of the directory alias and file name for a BFILE type LOB. """ return self._impl.get_file_name() def isopen(self) -> bool: """ Return a boolean indicating if the LOB has been opened using the method open(). """ return self._impl.get_is_open() def open(self) -> None: """ Open the LOB for writing. This will improve performance when writing to the LOB in chunks and there are functional or extensible indexes associated with the LOB. If this method is not called, each write will perform an open internally followed by a close after the write has been completed. """ self._impl.open() def read(self, offset: int=1, amount: int=None) -> Union[str, bytes]: """ Return a portion (or all) of the data in the LOB. Note that the amount and offset are in bytes for BLOB and BFILE type LOBs and in UCS-2 code points for CLOB and NCLOB type LOBs. UCS-2 code points are equivalent to characters for all but supplemental characters. If supplemental characters are in the LOB, the offset and amount will have to be chosen carefully to avoid splitting a character. """ if amount is None: amount = self._impl.get_max_amount() if amount >= offset: amount = amount - offset + 1 else: amount = 1 if offset <= 0: errors._raise_err(errors.ERR_INVALID_LOB_OFFSET) return self._impl.read(offset, amount) def setfilename(self, dir_alias: str, name: str) -> None: """ Set the directory alias and name of a BFILE type LOB. """ self._impl.set_file_name(dir_alias, name) def size(self) -> int: """ Returns the size of the data in the LOB. For BLOB and BFILE type LOBs this is the number of bytes. For CLOB and NCLOB type LOBs this is the number of UCS-2 code points. UCS-2 code points are equivalent to characters for all but supplemental characters. """ return self._impl.get_size() def trim(self, new_size: int=0, *, newSize: int=None) -> None: """ Trim the LOB to the new size (the second parameter is deprecated and should not be used). """ if newSize is not None: if new_size != 0: errors._raise_err(errors.ERR_DUPLICATED_PARAMETER, deprecated_name="newSize", new_name="new_size") new_size = newSize self._impl.trim(new_size) @property def type(self) -> object: """ Returns the type of the LOB as one of the database type constants. """ return self._impl.dbtype def write(self, data: Union[str, bytes], offset: int=1) -> None: """ Write the data to the LOB at the given offset. The offset is in bytes for BLOB type LOBs and in UCS-2 code points for CLOB and NCLOB type LOBs. UCS-2 code points are equivalent to characters for all but supplemental characters. If supplemental characters are in the LOB, the offset will have to be chosen carefully to avoid splitting a character. Note that if you want to make the LOB value smaller, you must use the trim() function. """ self._impl.write(data, offset) python-oracledb-1.2.1/src/oracledb/pool.py000066400000000000000000001046351434177474600205240ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # pool.py # # Contains the ConnectionPool class and the factory method create_pool() used # for creating connection pools. # # *** NOTICE *** This file is generated from a template and should not be # modified directly. See build_from_template.py in the utils subdirectory for # more information. #------------------------------------------------------------------------------ import functools from typing import Callable, Type, Union import oracledb from . import errors, exceptions, utils from . import base_impl, thick_impl, thin_impl from . import connection as connection_module from . import driver_mode from .connect_params import ConnectParams from .pool_params import PoolParams from .defaults import defaults class ConnectionPool: __module__ = oracledb.__name__ def __init__(self, dsn: str=None, *, params: PoolParams=None, **kwargs) -> None: """ Constructor for creating a connection pool. Connection pooling creates a pool of available connections to the database, allowing applications to acquire a connection very quickly. It is of primary use in a server where connections are requested in rapid succession and used for a short period of time, for example in a web server. The dsn parameter (data source name) can be a string in the format user/password@connect_string or can simply be the connect string (in which case authentication credentials such as the username and password need to be specified separately). See the documentation on connection strings for more information. The params parameter is expected to be of type PoolParams and contains parameters that are used to create the pool. See the documentation on PoolParams for more information. If this parameter is not specified, the additional keyword parameters will be used to create an instance of PoolParams. If both the params parameter and additional keyword parameters are specified, the values in the keyword parameters have precedence. Note that if a dsn is also supplied, then in the python-oracledb Thin mode, the values of the parameters specified (if any) within the dsn will override the values passed as additional keyword parameters, which themselves override the values set in the params parameter object. """ self._impl = None if params is None: params_impl = base_impl.PoolParamsImpl() elif not isinstance(params, PoolParams): errors._raise_err(errors.ERR_INVALID_POOL_PARAMS) else: params_impl = params._impl.copy() if kwargs: params_impl.set(kwargs) self._connection_type = \ params_impl.connectiontype or connection_module.Connection with driver_mode.get_manager() as mode_mgr: thin = mode_mgr.thin if dsn is not None: dsn = params_impl.parse_dsn(dsn, thin) if dsn is None: dsn = params_impl.get_connect_string() if thin: impl = thin_impl.ThinPoolImpl(dsn, params_impl) else: impl = thick_impl.ThickPoolImpl(dsn, params_impl) self._impl = impl self.session_callback = params_impl.session_callback def __del__(self): if self._impl is not None: self._impl.close(True) self._impl = None def _verify_open(self) -> None: """ Verifies that the pool is open and able to perform its work. """ if self._impl is None: errors._raise_err(errors.ERR_POOL_NOT_OPEN) def acquire(self, user: str=None, password: str=None, cclass: str=None, purity: int=oracledb.PURITY_DEFAULT, tag: str=None, matchanytag: bool=False, shardingkey: list=None, supershardingkey: list=None) -> "connection_module.Connection": """ Acquire a connection from the pool and return it. If the pool is homogeneous, the user and password parameters cannot be specified. If they are, an exception will be raised. The cclass parameter, if specified, should be a string corresponding to the connection class for database resident connection pooling (DRCP). The purity parameter is expected to be one of PURITY_DEFAULT, PURITY_NEW, or PURITY_SELF. The tag parameter, if specified, is expected to be a string with name=value pairs like “k1=v1;k2=v2” and will limit the connections that can be returned from a pool unless the matchanytag parameter is set to True. In that case connections with the specified tag will be preferred over others, but if no such connections are available a connection with a different tag may be returned instead. In any case, untagged connections will always be returned if no connections with the specified tag are available. Connections are tagged when they are released back to the pool. The shardingkey and supershardingkey parameters, if specified, are expected to be a sequence of values which will be used to identify the database shard to connect to. The key values can be strings, numbers, bytes or dates. """ self._verify_open() return self._connection_type(user=user, password=password, cclass=cclass, purity=purity, tag=tag, matchanytag=matchanytag, shardingkey=shardingkey, supershardingkey=supershardingkey, pool=self) @property def busy(self) -> int: """ Returns the number of connections that have been acquired from the pool and have not yet been returned to the pool. """ self._verify_open() return self._impl.get_busy_count() def close(self, force: bool=False) -> None: """ Close the pool now, rather than when the last reference to it is released, which makes it unusable for further work. If any connections have been acquired and not released back to the pool, this method will fail unless the force parameter is set to True. """ self._verify_open() self._impl.close(force) self._impl = None def drop(self, connection: "connection_module.Connection") -> None: """ Drop the connection from the pool, which is useful if the connection is no longer usable (such as when the database session is killed). """ self._verify_open() if not isinstance(connection, connection_module.Connection): message = "connection must be an instance of " \ "oracledb.Connection" raise TypeError(message) connection._verify_connected() self._impl.drop(connection._impl) connection._impl = None @property def dsn(self) -> str: """ Returns the connection string (TNS entry) of the database to which connections in the pool have been established. """ self._verify_open() return self._impl.dsn @property def getmode(self) -> int: self._verify_open() return self._impl.get_getmode() @getmode.setter def getmode(self, value: int) -> None: self._verify_open() self._impl.set_getmode(value) @property def homogeneous(self) -> bool: """ Returns a boolean indicating if the pool is homogeneous or not. If the pool is not homogeneous, different authentication can be used for each connection acquired from the pool. """ self._verify_open() return self._impl.homogeneous @property def increment(self) -> int: """ Returns the number of connections that will be created when additional connections need to be created to satisfy requests. """ self._verify_open() return self._impl.increment @property def max(self) -> int: """ Returns the maximum number of connections that the pool can control. """ self._verify_open() return self._impl.max @property def max_lifetime_session(self) -> int: """ Returns the maximum length of time (in seconds) that a pooled connection may exist. Connections that are in use will not be closed. They become candidates for termination only when they are released back to the pool and have existed for longer than max_lifetime_session seconds. Note that termination only occurs when the pool is accessed. A value of 0 means that there is no maximum length of time that a pooled connection may exist. This attribute is only available in Oracle Database 12.1. """ self._verify_open() return self._impl.get_max_lifetime_session() @max_lifetime_session.setter def max_lifetime_session(self, value: int) -> None: self._verify_open() self._impl.set_max_lifetime_session(value) @property def max_sessions_per_shard(self) -> int: """ Returns the number of sessions that can be created per shard in the pool. Setting this attribute greater than zero specifies the maximum number of sessions in the pool that can be used for any given shard in a sharded database. This lets connections in the pool be balanced across the shards. A value of zero will not set any maximum number of sessions for each shard. This attribute is only available in Oracle Client 18.3 and higher. """ self._verify_open() return self._impl.get_max_sessions_per_shard() @max_sessions_per_shard.setter def max_sessions_per_shard(self, value: int) -> None: self._verify_open() self._impl.set_max_sessions_per_shard(value) @property def min(self) -> int: """ Returns the minimum number of connections that the pool will control. These are created when the pool is first created. """ self._verify_open() return self._impl.min @property def name(self) -> str: """ Returns the name assigned to the pool by Oracle. This attribute is only relevant in python-oracledb thick mode. """ self._verify_open() return self._impl.name @property def opened(self) -> int: """ Returns the number of connections currently opened by the pool. """ self._verify_open() return self._impl.get_open_count() @property def ping_interval(self) -> int: """ Returns the pool ping interval in seconds. When a connection is acquired from the pool, a check is first made to see how long it has been since the connection was put into the pool. If this idle time exceeds ping_interval, then a round-trip ping to the database is performed. If the connection is unusable, it is discarded and a different connection is selected to be returned by SessionPool.acquire(). Setting ping_interval to a negative value disables pinging. Setting it to 0 forces a ping for every aquire() and is not recommended. """ self._verify_open() return self._impl.get_ping_interval() @ping_interval.setter def ping_interval(self, value: int) -> None: self._impl.set_ping_interval(value) def release(self, connection: "connection_module.Connection", tag: str=None) -> None: """ Release the connection back to the pool now, rather than whenever __del__ is called. The connection will be unusable from this point forward; an Error exception will be raised if any operation is attempted with the connection. Any cursors or LOBs created by the connection will also be marked unusable and an Error exception will be raised if any operation is attempted with them. Internally, references to the connection are held by cursor objects, LOB objects, etc. Once all of these references are released, the connection itself will be released back to the pool automatically. Either control references to these related objects carefully or explicitly release connections back to the pool in order to ensure sufficient resources are available. If the tag is not None, it is expected to be a string with name=value pairs like “k1=v1;k2=v2” and will override the value in the property Connection.tag. If either Connection.tag or the tag parameter are not None, the connection will be retagged when it is released back to the pool. """ self._verify_open() if not isinstance(connection, connection_module.Connection): message = "connection must be an instance of " \ "oracledb.Connection" raise TypeError(message) if tag is not None: connection.tag = tag connection.close() def reconfigure(self, min: int=None, max: int=None, increment: int=None, getmode: int=None, timeout: int=None, wait_timeout: int=None, max_lifetime_session: int=None, max_sessions_per_shard: int=None, soda_metadata_cache: bool=None, stmtcachesize: int=None, ping_interval: int=None) -> None: """ Reconfigures various parameters of a connection pool. The pool size can be altered with reconfigure() by passing values for min, max or increment. The getmode, timeout, wait_timeout, max_lifetime_session, max_sessions_per_shard, soda_metadata_cache, stmtcachesize and ping_interval can be set directly or by using reconfigure(). All parameters are optional. Unspecified parameters will leave those pool attributes unchanged. The parameters are processed in two stages. After any size change has been processed, reconfiguration on the other parameters is done sequentially. If an error such as an invalid value occurs when changing one attribute, then an exception will be generated but any already changed attributes will retain their new values. During reconfiguration of a pool's size, the behavior of acquire() depends on the getmode in effect when acquire() is called: * With mode POOL_GETMODE_FORCEGET, an acquire() call will wait until the pool has been reconfigured. * With mode POOL_GETMODE__TIMEDWAIT, an acquire() call will try to acquire a connection in the time specified by pool.wait_timeout and return an error if the time taken exceeds that value. * With mode POOL_GETMODE_WAIT, an acquire() call will wait until after the pool has been reconfigured and a connection is available. * With mode POOL_GETMODE_NOWAIT, if the number of busy connections is less than the pool size, acquire() will return a new connection after pool reconfiguration is complete. Closing connections with pool.release() or connection.close() will wait until any pool size reconfiguration is complete. Closing the connection pool with pool.close() will wait until reconfiguration is complete. """ if min is None: min = self.min if max is None: max = self.max if increment is None: increment = self.increment if self.min != min or self.max != max or self.increment != increment: self._impl.reconfigure(min, max, increment) if getmode is not None: self.getmode = getmode if timeout is not None: self.timeout = timeout if wait_timeout is not None: self.wait_timeout = wait_timeout if max_lifetime_session is not None: self.max_lifetime_session = max_lifetime_session if max_sessions_per_shard is not None: self.max_sessions_per_shard = max_sessions_per_shard if soda_metadata_cache is not None: self.soda_metadata_cache = soda_metadata_cache if stmtcachesize is not None: self.stmtcachesize = stmtcachesize if ping_interval is not None: self.ping_interval = ping_interval @property def soda_metadata_cache(self) -> bool: """ Specifies whether the SODA metadata cache is enabled or not. Enabling the cache significantly improves the performance of methods SodaDatabase.createCollection() (when not specifying a value for the metadata parameter) and SodaDatabase.openCollection(). Note that the cache can become out of date if changes to the metadata of cached collections are made externally. """ self._verify_open() return self._impl.get_soda_metadata_cache() @soda_metadata_cache.setter def soda_metadata_cache(self, value: bool) -> None: if not isinstance(value, bool): message = "soda_metadata_cache must be a boolean value." raise TypeError(message) self._verify_open() self._impl.set_soda_metadata_cache(value) @property def stmtcachesize(self) -> int: """ Specifies the size of the statement cache that will be used as the starting point for any connections that are created by the pool. Once a connection is created, that connection’s statement cache size can only be changed by setting the stmtcachesize attribute on the connection itself. """ self._verify_open() return self._impl.get_stmt_cache_size() @stmtcachesize.setter def stmtcachesize(self, value: int) -> None: self._verify_open() self._impl.set_stmt_cache_size(value) @property def thin(self) -> bool: """ Returns a boolean indicating if the pool was created in python-oracledb's thin mode (True) or thick mode (False). """ self._verify_open() return isinstance(self._impl, thin_impl.ThinPoolImpl) @property def timeout(self) -> int: """ Specifies the time (in seconds) after which idle connections will be terminated in order to maintain an optimum number of open connections. A value of 0 means that no idle connections are terminated. Note that in thick mode with older Oracle Client libraries termination only occurs when the pool is accessed. """ self._verify_open() return self._impl.get_timeout() @timeout.setter def timeout(self, value: int) -> None: self._verify_open() self._impl.set_timeout(value) @property def tnsentry(self) -> str: """ Deprecated. Use dsn instead. """ return self.dsn @property def username(self) -> str: """ Returns the name of the user which was used to create the pool. """ self._verify_open() return self._impl.username @property def wait_timeout(self) -> int: """ Specifies the time (in milliseconds) that the caller should wait for a connection to become available in the pool before returning with an error. This value is only used if the getmode parameter used to create the pool was POOL_GETMODE_TIMEDWAIT. """ self._verify_open() return self._impl.get_wait_timeout() @wait_timeout.setter def wait_timeout(self, value: int) -> None: self._verify_open() self._impl.set_wait_timeout(value) def _pool_factory(f): """ Decorator which checks the validity of the supplied keyword parameters by calling the original function (which does nothing), then creates and returns an instance of the requested ConnectionPool class. The base ConnectionPool class constructor does not check the validity of the supplied keyword parameters. """ @functools.wraps(f) def create_pool(dsn: str=None, *, pool_class: Type[ConnectionPool]=ConnectionPool, params: PoolParams=None, **kwargs) -> ConnectionPool: f(dsn=dsn, pool_class=pool_class, params=params, **kwargs) if not issubclass(pool_class, ConnectionPool): errors._raise_err(errors.ERR_INVALID_POOL_CLASS) return pool_class(dsn, params=params, **kwargs) return create_pool @_pool_factory def create_pool(dsn: str=None, *, pool_class: Type[ConnectionPool]=ConnectionPool, params: PoolParams=None, min: int=1, max: int=2, increment: int=1, connectiontype: Type["oracledb.Connection"]=None, getmode: int=oracledb.POOL_GETMODE_WAIT, homogeneous: bool=True, timeout: int=0, wait_timeout: int=0, max_lifetime_session: int=0, session_callback: Callable=None, max_sessions_per_shard: int=0, soda_metadata_cache: bool=False, ping_interval: int=60, user: str=None, proxy_user: str=None, password: str=None, newpassword: str=None, wallet_password: str=None, access_token: Union[str, tuple, Callable]=None, host: str=None, port: int=1521, protocol: str="tcp", https_proxy: str=None, https_proxy_port: int=0, service_name: str=None, sid: str=None, server_type: str=None, cclass: str=None, purity: int=oracledb.PURITY_DEFAULT, expire_time: int=0, retry_count: int=0, retry_delay: int=0, tcp_connect_timeout: float=60.0, ssl_server_dn_match: bool=True, ssl_server_cert_dn: str=None, wallet_location: str=None, events: bool=False, externalauth: bool=False, mode: int=oracledb.AUTH_MODE_DEFAULT, disable_oob: bool=False, stmtcachesize: int=oracledb.defaults.stmtcachesize, edition: str=None, tag: str=None, matchanytag: bool=False, config_dir: str=oracledb.defaults.config_dir, appcontext: list=None, shardingkey: list=None, supershardingkey: list=None, debug_jdwp: str=None, handle: int=0, threaded: bool=True, encoding: str=None, nencoding: str=None, waitTimeout: int=None, maxLifetimeSession: int=None, maxSessionsPerShard: int=None, sessionCallback: Callable=None ) -> ConnectionPool: """ Creates a connection pool with the supplied parameters and returns it. The dsn parameter (data source name) can be a string in the format user/password@connect_string or can simply be the connect string (in which case authentication credentials such as the username and password need to be specified separately). See the documentation on connection strings for more information. The pool_class parameter is expected to be ConnectionPool or a subclass of ConnectionPool. The params parameter is expected to be of type PoolParams and contains parameters that are used to create the pool. See the documentation on PoolParams for more information. If this parameter is not specified, the additional keyword parameters will be used to create an instance of PoolParams. If both the params parameter and additional keyword parameters are specified, the values in the keyword parameters have precedence. Note that if a dsn is also supplied, then in the python-oracledb Thin mode, the values of the parameters specified (if any) within the dsn will override the values passed as additional keyword parameters, which themselves override the values set in the params parameter object. The following parameters are all optional. A brief description of each parameter follows: - min: the minimum number of connections the pool should contain (default: 1) - max: the maximum number of connections the pool should contain (default: 2) - increment: the number of connections that should be added to the pool whenever a new connection needs to be created (default: 1) - connectiontype: the class of the connection that should be returned during calls to pool.acquire(). It must be Connection or a subclass of Connection (default: None) - getmode: how pool.acquire() will behave. One of the constants oracledb.POOL_GETMODE_WAIT, oracledb.POOL_GETMODE_NOWAIT, oracledb.POOL_GETMODE_FORCEGET, or oracledb.POOL_GETMODE_TIMEDWAIT (default: oracledb.POOL_GETMODE_WAIT) - homogeneous: a boolean indicating whether the connections are homogeneous (same user) or heterogeneous (multiple users) (default: True) - timeout: length of time (in seconds) that a connection may remain idle in the pool before it is terminated. If it is 0 then connections are never terminated (default: 0) - wait_timeout: length of time (in milliseconds) that a caller should wait when acquiring a connection from the pool with getmode set to oracledb.POOL_GETMODE_TIMEDWAIT (default: 0) - max_lifetime_session: length of time (in seconds) that connections can remain in the pool. If it is 0 then connections may remain in the pool indefinitely (default: 0) - session_callback: a callable that is invoked when a connection is returned from the pool for the first time, or when the connection tag differs from the one requested (default: None) - max_sessions_per_shard: the maximum number of connections that may be associated with a particular shard (default: 0) - soda_metadata_cache: boolean indicating whether or not the SODA metadata cache should be enabled (default: False) - ping_interval: length of time (in seconds) after which an unused connection in the pool will be a candidate for pinging when pool.acquire() is called. If the ping to the database indicates the connection is not alive a replacement connection will be returned by pool.acquire(). If ping_interval is a negative value the ping functionality will be disabled (default: 60) - user: the name of the user to connect to (default: None) - proxy_user: the name of the proxy user to connect to. If this value is not specified, it will be parsed out of user if user is in the form "user[proxy_user]" (default: None) - password: the password for the user (default: None) - newpassword: the new password for the user. The new password will take effect immediately upon a successful connection to the database (default: None) - wallet_password: the password to use to decrypt the wallet, if it is encrypted. This value is only used in thin mode (default: None) - access_token: expected to be a string or a 2-tuple or a callable. If it is a string, it specifies an Azure AD OAuth2 token used for Open Authorization (OAuth 2.0) token based authentication. If it is a 2-tuple, it specifies the token and private key strings used for Oracle Cloud Infrastructure (OCI) Identity and Access Management (IAM) token based authentication. If it is a callable, it returns either a string or a 2-tuple used for OAuth 2.0 or OCI IAM token based authentication and is useful when the pool needs to expand and create new connections but the current authentication token has expired (default: None) - host: the name or IP address of the machine hosting the database or the database listener (default: None) - port: the port number on which the database listener is listening (default: 1521) - protocol: one of the strings "tcp" or "tcps" indicating whether to use unencrypted network traffic or encrypted network traffic (TLS) (default: "tcp") - https_proxy: the name or IP address of a proxy host to use for tunneling secure connections (default: None) - https_proxy_port: the port on which to communicate with the proxy host (default: 0) - service_name: the service name of the database (default: None) - sid: the system identifier (SID) of the database. Note using a service_name instead is recommended (default: None) - server_type: the type of server connection that should be established. If specified, it should be one of "dedicated", "shared" or "pooled" (default: None) - cclass: connection class to use for Database Resident Connection Pooling (DRCP) (default: None) - purity: purity to use for Database Resident Connection Pooling (DRCP) (default: oracledb.PURITY_DEFAULT) - expire_time: an integer indicating the number of minutes between the sending of keepalive probes. If this parameter is set to a value greater than zero it enables keepalive (default: 0) - retry_count: the number of times that a connection attempt should be retried before the attempt is terminated (default: 0) - retry_delay: the number of seconds to wait before making a new connection attempt (default: 0) - tcp_connect_timeout: a float indicating the maximum number of seconds to wait for establishing a connection to the database host (default: 60.0) - ssl_server_dn_match: boolean indicating whether the server certificate distinguished name (DN) should be matched in addition to the regular certificate verification that is performed. Note that if the ssl_server_cert_dn parameter is not privided, host name matching is performed instead (default: True) - ssl_server_cert_dn: the distinguished name (DN) which should be matched with the server. This value is ignored if the ssl_server_dn_match parameter is not set to the value True. If specified this value is used for any verfication. Otherwise the hostname will be used. (default: None) - wallet_location: the directory where the wallet can be found. In thin mode this must be the directory containing the PEM-encoded wallet file ewallet.pem. In thick mode this must be the directory containing the file cwallet.sso (default: None) - events: boolean specifying whether events mode should be enabled. This value is only used in thick mode and is needed for continuous query notification and high availability event notifications (default: False) - externalauth: a boolean indicating whether to use external authentication (default: False) - mode: authorization mode to use. For example oracledb.AUTH_MODE_SYSDBA (default: oracledb.AUTH_MODE_DEFAULT) - disable_oob: boolean indicating whether out-of-band breaks should be disabled. This value is only used in thin mode. It has no effect on Windows which does not support this functionality (default: False) - stmtcachesize: identifies the initial size of the statement cache (default: oracledb.defaults.stmtcachesize) - edition: edition to use for the connection. This parameter cannot be used simultaneously with the cclass parameter (default: None) - tag: identifies the type of connection that should be returned from a pool. This value is only used in thick mode (default: None) - matchanytag: boolean specifying whether any tag can be used when acquiring a connection from the pool. This value is only used in thick mode. (default: False) - config_dir: directory in which the optional tnsnames.ora configuration file is located. This value is only used in thin mode. For thick mode use the config_dir parameter of init_oracle_client() (default: oracledb.defaults.config_dir) - appcontext: application context used by the connection. It should be a list of 3-tuples (namespace, name, value) and each entry in the tuple should be a string. This value is only used in thick mode (default: None) - shardingkey: a list of strings, numbers, bytes or dates that identify the database shard to connect to. This value is only used in thick mode (default: None) - supershardingkey: a list of strings, numbers, bytes or dates that identify the database shard to connect to. This value is only used in thick mode (default: None) - debug_jdwp: a string with the format "host=;port=" that specifies the host and port of the PL/SQL debugger. This value is only used in thin mode. For thick mode set the ORA_DEBUG_JDWP environment variable (default: None) - handle: an integer representing a pointer to a valid service context handle. This value is only used in thick mode. It should be used with extreme caution (default: 0) """ pass python-oracledb-1.2.1/src/oracledb/pool_params.py000066400000000000000000000750721434177474600220710ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # pool_params.py # # Contains the PoolParams class used for managing the parameters required to # create a connection pool. # # *** NOTICE *** This file is generated from a template and should not be # modified directly. See build_from_template.py in the utils subdirectory for # more information. #------------------------------------------------------------------------------ from typing import Callable, Type, Union import oracledb from . import base_impl, constants, errors, utils from . import connection as connection_module from .connect_params import ConnectParams class PoolParams(ConnectParams): """ Contains all parameters used for creating a connection pool. """ __module__ = oracledb.__name__ __slots__ = ["_impl"] _impl_class = base_impl.PoolParamsImpl @utils.params_initer def __init__(self, *, min: int=1, max: int=2, increment: int=1, connectiontype: Type["oracledb.Connection"]=None, getmode: int=oracledb.POOL_GETMODE_WAIT, homogeneous: bool=True, timeout: int=0, wait_timeout: int=0, max_lifetime_session: int=0, session_callback: Callable=None, max_sessions_per_shard: int=0, soda_metadata_cache: bool=False, ping_interval: int=60, user: str=None, proxy_user: str=None, password: str=None, newpassword: str=None, wallet_password: str=None, access_token: Union[str, tuple, Callable]=None, host: str=None, port: int=1521, protocol: str="tcp", https_proxy: str=None, https_proxy_port: int=0, service_name: str=None, sid: str=None, server_type: str=None, cclass: str=None, purity: int=oracledb.PURITY_DEFAULT, expire_time: int=0, retry_count: int=0, retry_delay: int=0, tcp_connect_timeout: float=60.0, ssl_server_dn_match: bool=True, ssl_server_cert_dn: str=None, wallet_location: str=None, events: bool=False, externalauth: bool=False, mode: int=oracledb.AUTH_MODE_DEFAULT, disable_oob: bool=False, stmtcachesize: int=oracledb.defaults.stmtcachesize, edition: str=None, tag: str=None, matchanytag: bool=False, config_dir: str=oracledb.defaults.config_dir, appcontext: list=None, shardingkey: list=None, supershardingkey: list=None, debug_jdwp: str=None, handle: int=0, threaded: bool=True, encoding: str=None, nencoding: str=None, waitTimeout: int=None, maxLifetimeSession: int=None, maxSessionsPerShard: int=None, sessionCallback: Callable=None ): """ All parameters are optional. A brief description of each parameter follows: - min: the minimum number of connections the pool should contain (default: 1) - max: the maximum number of connections the pool should contain (default: 2) - increment: the number of connections that should be added to the pool whenever a new connection needs to be created (default: 1) - connectiontype: the class of the connection that should be returned during calls to pool.acquire(). It must be Connection or a subclass of Connection (default: None) - getmode: how pool.acquire() will behave. One of the constants oracledb.POOL_GETMODE_WAIT, oracledb.POOL_GETMODE_NOWAIT, oracledb.POOL_GETMODE_FORCEGET, or oracledb.POOL_GETMODE_TIMEDWAIT (default: oracledb.POOL_GETMODE_WAIT) - homogeneous: a boolean indicating whether the connections are homogeneous (same user) or heterogeneous (multiple users) (default: True) - timeout: length of time (in seconds) that a connection may remain idle in the pool before it is terminated. If it is 0 then connections are never terminated (default: 0) - wait_timeout: length of time (in milliseconds) that a caller should wait when acquiring a connection from the pool with getmode set to oracledb.POOL_GETMODE_TIMEDWAIT (default: 0) - max_lifetime_session: length of time (in seconds) that connections can remain in the pool. If it is 0 then connections may remain in the pool indefinitely (default: 0) - session_callback: a callable that is invoked when a connection is returned from the pool for the first time, or when the connection tag differs from the one requested (default: None) - max_sessions_per_shard: the maximum number of connections that may be associated with a particular shard (default: 0) - soda_metadata_cache: boolean indicating whether or not the SODA metadata cache should be enabled (default: False) - ping_interval: length of time (in seconds) after which an unused connection in the pool will be a candidate for pinging when pool.acquire() is called. If the ping to the database indicates the connection is not alive a replacement connection will be returned by pool.acquire(). If ping_interval is a negative value the ping functionality will be disabled (default: 60) - user: the name of the user to connect to (default: None) - proxy_user: the name of the proxy user to connect to. If this value is not specified, it will be parsed out of user if user is in the form "user[proxy_user]" (default: None) - password: the password for the user (default: None) - newpassword: the new password for the user. The new password will take effect immediately upon a successful connection to the database (default: None) - wallet_password: the password to use to decrypt the wallet, if it is encrypted. This value is only used in thin mode (default: None) - access_token: expected to be a string or a 2-tuple or a callable. If it is a string, it specifies an Azure AD OAuth2 token used for Open Authorization (OAuth 2.0) token based authentication. If it is a 2-tuple, it specifies the token and private key strings used for Oracle Cloud Infrastructure (OCI) Identity and Access Management (IAM) token based authentication. If it is a callable, it returns either a string or a 2-tuple used for OAuth 2.0 or OCI IAM token based authentication and is useful when the pool needs to expand and create new connections but the current authentication token has expired (default: None) - host: the name or IP address of the machine hosting the database or the database listener (default: None) - port: the port number on which the database listener is listening (default: 1521) - protocol: one of the strings "tcp" or "tcps" indicating whether to use unencrypted network traffic or encrypted network traffic (TLS) (default: "tcp") - https_proxy: the name or IP address of a proxy host to use for tunneling secure connections (default: None) - https_proxy_port: the port on which to communicate with the proxy host (default: 0) - service_name: the service name of the database (default: None) - sid: the system identifier (SID) of the database. Note using a service_name instead is recommended (default: None) - server_type: the type of server connection that should be established. If specified, it should be one of "dedicated", "shared" or "pooled" (default: None) - cclass: connection class to use for Database Resident Connection Pooling (DRCP) (default: None) - purity: purity to use for Database Resident Connection Pooling (DRCP) (default: oracledb.PURITY_DEFAULT) - expire_time: an integer indicating the number of minutes between the sending of keepalive probes. If this parameter is set to a value greater than zero it enables keepalive (default: 0) - retry_count: the number of times that a connection attempt should be retried before the attempt is terminated (default: 0) - retry_delay: the number of seconds to wait before making a new connection attempt (default: 0) - tcp_connect_timeout: a float indicating the maximum number of seconds to wait for establishing a connection to the database host (default: 60.0) - ssl_server_dn_match: boolean indicating whether the server certificate distinguished name (DN) should be matched in addition to the regular certificate verification that is performed. Note that if the ssl_server_cert_dn parameter is not privided, host name matching is performed instead (default: True) - ssl_server_cert_dn: the distinguished name (DN) which should be matched with the server. This value is ignored if the ssl_server_dn_match parameter is not set to the value True. If specified this value is used for any verfication. Otherwise the hostname will be used. (default: None) - wallet_location: the directory where the wallet can be found. In thin mode this must be the directory containing the PEM-encoded wallet file ewallet.pem. In thick mode this must be the directory containing the file cwallet.sso (default: None) - events: boolean specifying whether events mode should be enabled. This value is only used in thick mode and is needed for continuous query notification and high availability event notifications (default: False) - externalauth: a boolean indicating whether to use external authentication (default: False) - mode: authorization mode to use. For example oracledb.AUTH_MODE_SYSDBA (default: oracledb.AUTH_MODE_DEFAULT) - disable_oob: boolean indicating whether out-of-band breaks should be disabled. This value is only used in thin mode. It has no effect on Windows which does not support this functionality (default: False) - stmtcachesize: identifies the initial size of the statement cache (default: oracledb.defaults.stmtcachesize) - edition: edition to use for the connection. This parameter cannot be used simultaneously with the cclass parameter (default: None) - tag: identifies the type of connection that should be returned from a pool. This value is only used in thick mode (default: None) - matchanytag: boolean specifying whether any tag can be used when acquiring a connection from the pool. This value is only used in thick mode. (default: False) - config_dir: directory in which the optional tnsnames.ora configuration file is located. This value is only used in thin mode. For thick mode use the config_dir parameter of init_oracle_client() (default: oracledb.defaults.config_dir) - appcontext: application context used by the connection. It should be a list of 3-tuples (namespace, name, value) and each entry in the tuple should be a string. This value is only used in thick mode (default: None) - shardingkey: a list of strings, numbers, bytes or dates that identify the database shard to connect to. This value is only used in thick mode (default: None) - supershardingkey: a list of strings, numbers, bytes or dates that identify the database shard to connect to. This value is only used in thick mode (default: None) - debug_jdwp: a string with the format "host=;port=" that specifies the host and port of the PL/SQL debugger. This value is only used in thin mode. For thick mode set the ORA_DEBUG_JDWP environment variable (default: None) - handle: an integer representing a pointer to a valid service context handle. This value is only used in thick mode. It should be used with extreme caution (default: 0) """ pass def __repr__(self): return self.__class__.__qualname__ + "(" + \ f"min={self.min!r}, " + \ f"max={self.max!r}, " + \ f"increment={self.increment!r}, " + \ f"connectiontype={self.connectiontype!r}, " + \ f"getmode={self.getmode!r}, " + \ f"homogeneous={self.homogeneous!r}, " + \ f"timeout={self.timeout!r}, " + \ f"wait_timeout={self.wait_timeout!r}, " + \ f"max_lifetime_session={self.max_lifetime_session!r}, " + \ f"session_callback={self.session_callback!r}, " + \ f"max_sessions_per_shard={self.max_sessions_per_shard!r}, " + \ f"soda_metadata_cache={self.soda_metadata_cache!r}, " + \ f"ping_interval={self.ping_interval!r}, " + \ f"user={self.user!r}, " + \ f"proxy_user={self.proxy_user!r}, " + \ f"host={self.host!r}, " + \ f"port={self.port!r}, " + \ f"protocol={self.protocol!r}, " + \ f"https_proxy={self.https_proxy!r}, " + \ f"https_proxy_port={self.https_proxy_port!r}, " + \ f"service_name={self.service_name!r}, " + \ f"sid={self.sid!r}, " + \ f"server_type={self.server_type!r}, " + \ f"cclass={self.cclass!r}, " + \ f"purity={self.purity!r}, " + \ f"expire_time={self.expire_time!r}, " + \ f"retry_count={self.retry_count!r}, " + \ f"retry_delay={self.retry_delay!r}, " + \ f"tcp_connect_timeout={self.tcp_connect_timeout!r}, " + \ f"ssl_server_dn_match={self.ssl_server_dn_match!r}, " + \ f"ssl_server_cert_dn={self.ssl_server_cert_dn!r}, " + \ f"wallet_location={self.wallet_location!r}, " + \ f"events={self.events!r}, " + \ f"externalauth={self.externalauth!r}, " + \ f"mode={self.mode!r}, " + \ f"disable_oob={self.disable_oob!r}, " + \ f"stmtcachesize={self.stmtcachesize!r}, " + \ f"edition={self.edition!r}, " + \ f"tag={self.tag!r}, " + \ f"matchanytag={self.matchanytag!r}, " + \ f"config_dir={self.config_dir!r}, " + \ f"appcontext={self.appcontext!r}, " + \ f"shardingkey={self.shardingkey!r}, " + \ f"supershardingkey={self.supershardingkey!r}, " + \ f"debug_jdwp={self.debug_jdwp!r}" + \ ")" @property def connectiontype(self) -> Type["oracledb.Connection"]: """ The class of the connection that should be returned during calls to pool.acquire(). It must be Connection or a subclass of Connection. """ return self._impl.connectiontype @property def getmode(self) -> int: """ How pool.acquire() will behave. One of the constants oracledb.POOL_GETMODE_WAIT, oracledb.POOL_GETMODE_NOWAIT, oracledb.POOL_GETMODE_FORCEGET, or oracledb.POOL_GETMODE_TIMEDWAIT. """ return self._impl.getmode @property def homogeneous(self) -> bool: """ A boolean indicating whether the connections are homogeneous (same user) or heterogeneous (multiple users). """ return self._impl.homogeneous @property def increment(self) -> int: """ The number of connections that should be added to the pool whenever a new connection needs to be created. """ return self._impl.increment @property def max(self) -> int: """ The maximum number of connections the pool should contain. """ return self._impl.max @property def max_lifetime_session(self) -> int: """ Length of time (in seconds) that connections can remain in the pool. If it is 0 then connections may remain in the pool indefinitely. """ return self._impl.max_lifetime_session @property def max_sessions_per_shard(self) -> int: """ The maximum number of connections that may be associated with a particular shard. """ return self._impl.max_sessions_per_shard @property def min(self) -> int: """ The minimum number of connections the pool should contain. """ return self._impl.min @property def ping_interval(self) -> int: """ Length of time (in seconds) after which an unused connection in the pool will be a candidate for pinging when pool.acquire() is called. If the ping to the database indicates the connection is not alive a replacement connection will be returned by pool.acquire(). If ping_interval is a negative value the ping functionality will be disabled. """ return self._impl.ping_interval @property def session_callback(self) -> Callable: """ A callable that is invoked when a connection is returned from the pool for the first time, or when the connection tag differs from the one requested. """ return self._impl.session_callback @property def soda_metadata_cache(self) -> bool: """ Boolean indicating whether or not the SODA metadata cache should be enabled. """ return self._impl.soda_metadata_cache @property def timeout(self) -> int: """ Length of time (in seconds) that a connection may remain idle in the pool before it is terminated. If it is 0 then connections are never terminated. """ return self._impl.timeout @property def wait_timeout(self) -> int: """ Length of time (in milliseconds) that a caller should wait when acquiring a connection from the pool with getmode set to oracledb.POOL_GETMODE_TIMEDWAIT. """ return self._impl.wait_timeout def copy(self) -> "PoolParams": """ Creates a copy of the parameters and returns it. """ params = PoolParams.__new__(PoolParams) params._impl = self._impl.copy() return params @utils.params_setter def set(self, *, min: int=None, max: int=None, increment: int=None, connectiontype: Type["oracledb.Connection"]=None, getmode: int=None, homogeneous: bool=None, timeout: int=None, wait_timeout: int=None, max_lifetime_session: int=None, session_callback: Callable=None, max_sessions_per_shard: int=None, soda_metadata_cache: bool=None, ping_interval: int=None, user: str=None, proxy_user: str=None, password: str=None, newpassword: str=None, wallet_password: str=None, access_token: Union[str, tuple, Callable]=None, host: str=None, port: int=None, protocol: str=None, https_proxy: str=None, https_proxy_port: int=None, service_name: str=None, sid: str=None, server_type: str=None, cclass: str=None, purity: int=None, expire_time: int=None, retry_count: int=None, retry_delay: int=None, tcp_connect_timeout: float=None, ssl_server_dn_match: bool=None, ssl_server_cert_dn: str=None, wallet_location: str=None, events: bool=None, externalauth: bool=None, mode: int=None, disable_oob: bool=None, stmtcachesize: int=None, edition: str=None, tag: str=None, matchanytag: bool=None, config_dir: str=None, appcontext: list=None, shardingkey: list=None, supershardingkey: list=None, debug_jdwp: str=None, handle: int=None, threaded: bool=None, encoding: str=None, nencoding: str=None, waitTimeout: int=None, maxLifetimeSession: int=None, maxSessionsPerShard: int=None, sessionCallback: Callable=None ): """ All parameters are optional. A brief description of each parameter follows: - min: the minimum number of connections the pool should contain - max: the maximum number of connections the pool should contain - increment: the number of connections that should be added to the pool whenever a new connection needs to be created - connectiontype: the class of the connection that should be returned during calls to pool.acquire(). It must be Connection or a subclass of Connection - getmode: how pool.acquire() will behave. One of the constants oracledb.POOL_GETMODE_WAIT, oracledb.POOL_GETMODE_NOWAIT, oracledb.POOL_GETMODE_FORCEGET, or oracledb.POOL_GETMODE_TIMEDWAIT - homogeneous: a boolean indicating whether the connections are homogeneous (same user) or heterogeneous (multiple users) - timeout: length of time (in seconds) that a connection may remain idle in the pool before it is terminated. If it is 0 then connections are never terminated - wait_timeout: length of time (in milliseconds) that a caller should wait when acquiring a connection from the pool with getmode set to oracledb.POOL_GETMODE_TIMEDWAIT - max_lifetime_session: length of time (in seconds) that connections can remain in the pool. If it is 0 then connections may remain in the pool indefinitely - session_callback: a callable that is invoked when a connection is returned from the pool for the first time, or when the connection tag differs from the one requested - max_sessions_per_shard: the maximum number of connections that may be associated with a particular shard - soda_metadata_cache: boolean indicating whether or not the SODA metadata cache should be enabled - ping_interval: length of time (in seconds) after which an unused connection in the pool will be a candidate for pinging when pool.acquire() is called. If the ping to the database indicates the connection is not alive a replacement connection will be returned by pool.acquire(). If ping_interval is a negative value the ping functionality will be disabled - user: the name of the user to connect to - proxy_user: the name of the proxy user to connect to. If this value is not specified, it will be parsed out of user if user is in the form "user[proxy_user]" - password: the password for the user - newpassword: the new password for the user. The new password will take effect immediately upon a successful connection to the database - wallet_password: the password to use to decrypt the wallet, if it is encrypted. This value is only used in thin mode - access_token: expected to be a string or a 2-tuple or a callable. If it is a string, it specifies an Azure AD OAuth2 token used for Open Authorization (OAuth 2.0) token based authentication. If it is a 2-tuple, it specifies the token and private key strings used for Oracle Cloud Infrastructure (OCI) Identity and Access Management (IAM) token based authentication. If it is a callable, it returns either a string or a 2-tuple used for OAuth 2.0 or OCI IAM token based authentication and is useful when the pool needs to expand and create new connections but the current authentication token has expired - host: the name or IP address of the machine hosting the database or the database listener - port: the port number on which the database listener is listening - protocol: one of the strings "tcp" or "tcps" indicating whether to use unencrypted network traffic or encrypted network traffic (TLS) - https_proxy: the name or IP address of a proxy host to use for tunneling secure connections - https_proxy_port: the port on which to communicate with the proxy host - service_name: the service name of the database - sid: the system identifier (SID) of the database. Note using a service_name instead is recommended - server_type: the type of server connection that should be established. If specified, it should be one of "dedicated", "shared" or "pooled" - cclass: connection class to use for Database Resident Connection Pooling (DRCP) - purity: purity to use for Database Resident Connection Pooling (DRCP) - expire_time: an integer indicating the number of minutes between the sending of keepalive probes. If this parameter is set to a value greater than zero it enables keepalive - retry_count: the number of times that a connection attempt should be retried before the attempt is terminated - retry_delay: the number of seconds to wait before making a new connection attempt - tcp_connect_timeout: a float indicating the maximum number of seconds to wait for establishing a connection to the database host - ssl_server_dn_match: boolean indicating whether the server certificate distinguished name (DN) should be matched in addition to the regular certificate verification that is performed. Note that if the ssl_server_cert_dn parameter is not privided, host name matching is performed instead - ssl_server_cert_dn: the distinguished name (DN) which should be matched with the server. This value is ignored if the ssl_server_dn_match parameter is not set to the value True. If specified this value is used for any verfication. Otherwise the hostname will be used. - wallet_location: the directory where the wallet can be found. In thin mode this must be the directory containing the PEM-encoded wallet file ewallet.pem. In thick mode this must be the directory containing the file cwallet.sso - events: boolean specifying whether events mode should be enabled. This value is only used in thick mode and is needed for continuous query notification and high availability event notifications - externalauth: a boolean indicating whether to use external authentication - mode: authorization mode to use. For example oracledb.AUTH_MODE_SYSDBA - disable_oob: boolean indicating whether out-of-band breaks should be disabled. This value is only used in thin mode. It has no effect on Windows which does not support this functionality - stmtcachesize: identifies the initial size of the statement cache - edition: edition to use for the connection. This parameter cannot be used simultaneously with the cclass parameter - tag: identifies the type of connection that should be returned from a pool. This value is only used in thick mode - matchanytag: boolean specifying whether any tag can be used when acquiring a connection from the pool. This value is only used in thick mode. - config_dir: directory in which the optional tnsnames.ora configuration file is located. This value is only used in thin mode. For thick mode use the config_dir parameter of init_oracle_client() - appcontext: application context used by the connection. It should be a list of 3-tuples (namespace, name, value) and each entry in the tuple should be a string. This value is only used in thick mode - shardingkey: a list of strings, numbers, bytes or dates that identify the database shard to connect to. This value is only used in thick mode - supershardingkey: a list of strings, numbers, bytes or dates that identify the database shard to connect to. This value is only used in thick mode - debug_jdwp: a string with the format "host=;port=" that specifies the host and port of the PL/SQL debugger. This value is only used in thin mode. For thick mode set the ORA_DEBUG_JDWP environment variable - handle: an integer representing a pointer to a valid service context handle. This value is only used in thick mode. It should be used with extreme caution """ pass python-oracledb-1.2.1/src/oracledb/py.typed000066400000000000000000000000001434177474600206550ustar00rootroot00000000000000python-oracledb-1.2.1/src/oracledb/soda.py000066400000000000000000000655511434177474600205040ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # soda.py # # Contains the classes for managing Simple Oracle Document Access (SODA): # SodaDatabase, SodaCollection, SodaDocument, SodaDocCursor and SodaOperation. #------------------------------------------------------------------------------ from typing import Union, List import json from . import connection, errors class SodaDatabase: def __repr__(self): return f" bytes: """ Internal method used for returning the bytes to store in document content. """ if isinstance(content, str): return content.encode() elif isinstance(content, bytes): return content return json.dumps(content).encode() def createCollection(self, name: str, metadata: Union[str, dict]=None, mapMode: bool=False) -> "SodaCollection": """ Creates a SODA collection with the given name and returns a new SODA collection object. If you try to create a collection, and a collection with the same name and metadata already exists, then that existing collection is opened without error. If metadata is specified, it is expected to be a string containing valid JSON or a dictionary that will be transformed into a JSON string. This JSON permits you to specify the configuration of the collection including storage options; specifying the presence or absence of columns for creation timestamp, last modified timestamp and version; whether the collection can store only JSON documents; and methods of key and version generation. The default metadata creates a collection that only supports JSON documents and uses system generated keys. If the mapMode parameter is set to True, the new collection is mapped to an existing table instead of creating a table. If a collection is created in this way, dropping the collection will not drop the existing table either. """ if metadata is not None and not isinstance(metadata, str): metadata = json.dumps(metadata) collection_impl = self._impl.create_collection(name, metadata, mapMode) return SodaCollection._from_impl(self, collection_impl) def createDocument(self, content: object, key: str=None, mediaType: str="application/json") -> "SodaDocument": """ Creates a SODA document usable for SODA write operations. You only need to use this method if your collection requires client-assigned keys or has non-JSON content; otherwise, you can pass your content directly to SODA write operations. SodaDocument attributes "createdOn", "lastModified" and "version" will be None. The content parameter can be a dictionary or list which will be transformed into a JSON string and then UTF-8 encoded. It can also be a string which will be UTF-8 encoded or it can be a bytes object which will be stored unchanged. If a bytes object is provided and the content is expected to be JSON, note that SODA only supports UTF-8, UTF-16LE and UTF-16BE encodings. The key parameter should only be supplied if the collection in which the document is to be placed requires client-assigned keys. The mediaType parameter should only be supplied if the collection in which the document is to be placed supports non-JSON documents and the content for this document is non-JSON. Using a standard MIME type for this value is recommended but any string will be accepted. """ content_bytes = self._get_content_bytes(content) doc_impl = self._impl.create_document(content_bytes, key, mediaType) return SodaDocument._from_impl(doc_impl) def getCollectionNames(self, startName: str=None, limit: int=0) -> List[str]: """ Returns a list of the names of collections in the database that match the criteria, in alphabetical order. If the startName parameter is specified, the list of names returned will start with this value and also contain any names that fall after this value in alphabetical order. If the limit parameter is specified and is non-zero, the number of collection names returned will be limited to this value. """ return self._impl.get_collection_names(startName, limit) def openCollection(self, name: str) -> "SodaCollection": """ Opens an existing collection with the given name and returns a new SODA collection object. If a collection with that name does not exist, None is returned. """ collection_impl = self._impl.open_collection(name) if collection_impl is not None: return SodaCollection._from_impl(self, collection_impl) class SodaCollection: @classmethod def _from_impl(cls, db, impl): coll = cls.__new__(cls) coll._db = db coll._impl = impl return coll def _process_doc_arg(self, arg): if isinstance(arg, SodaDocument): return arg._impl content_bytes = self._db._get_content_bytes(arg) return self._db._impl.create_document(content_bytes, None, None) def createIndex(self, spec: Union[dict, str]) -> None: """ Creates an index on a SODA collection. The spec is expected to be a dictionary or a JSON-encoded string. Note that a commit should be performed before attempting to create an index. """ if isinstance(spec, dict): spec = json.dumps(spec) elif not isinstance(spec, str): raise TypeError("expecting a dictionary or string") self._impl.create_index(spec) def drop(self) -> bool: """ Drops the collection from the database, if it exists. Note that if the collection was created with mapMode set to True the underlying table will not be dropped. A boolean value is returned indicating if the collection was actually dropped. """ return self._impl.drop() def dropIndex(self, name: str, force: bool=False) -> bool: """ Drops the index with the specified name, if it exists. The force parameter, if set to True, can be used to force the dropping of an index that the underlying Oracle Database domain index doesn’t normally permit. This is only applicable to spatial and JSON search indexes. See here for more information. A boolean value is returned indicating if the index was actually dropped. """ return self._impl.drop_index(name, force) def find(self) -> "SodaOperation": """ This method is used to begin an operation that will act upon documents in the collection. It creates and returns a SodaOperation object which is used to specify the criteria and the operation that will be performed on the documents that match that criteria. """ return SodaOperation(self) def getDataGuide(self) -> "SodaDocument": """ Returns a SODA document object containing property names, data types and lengths inferred from the JSON documents in the collection. It can be useful for exploring the schema of a collection. Note that this method is only supported for JSON-only collections where a JSON search index has been created with the ‘dataguide’ option enabled. If there are no documents in the collection, None is returned. """ doc_impl = self._impl.get_data_guide() if doc_impl is not None: return SodaDocument._from_impl(doc_impl) def insertMany(self, docs: list) -> None: """ Inserts a list of documents into the collection at one time. Each of the input documents can be a dictionary or list or an existing SODA document object. """ doc_impls = [self._process_doc_arg(d) for d in docs] self._impl.insert_many(doc_impls, hint=None, return_docs=False) def insertManyAndGet(self, docs: list, hint: str=None) -> list: """ Similarly to insertMany() this method inserts a list of documents into the collection at one time. The only difference is that it returns a list of SODA Document objects. Note that for performance reasons the returned documents do not contain the content. The hint parameter, if specified, supplies a hint to the database when processing the SODA operation. This is expected to be a string in the same format as SQL hints but without any comment characters, for example hint="MONITOR". While you could use this to pass any SQL hint, the hints MONITOR (turn on monitoring) and NO_MONITOR (turn off monitoring) are the most useful. Use of the hint parameter requires Oracle Client 21.3 or higher (or Oracle Client 19 from 19.11). """ doc_impls = [self._process_doc_arg(d) for d in docs] if hint is not None and not isinstance(hint, str): raise TypeError("expecting a string") return_doc_impls = self._impl.insert_many(doc_impls, hint, return_docs=True) return [SodaDocument._from_impl(i) for i in return_doc_impls] def insertOne(self, doc: object) -> None: """ Inserts a given document into the collection. The input document can be a dictionary or list or an existing SODA document object. """ doc_impl = self._process_doc_arg(doc) self._impl.insert_one(doc_impl, hint=None, return_doc=False) def insertOneAndGet(self, doc: object, hint: str=None) -> "SodaDocument": """ Similarly to insertOne() this method inserts a given document into the collection. The only difference is that it returns a SODA Document object. Note that for performance reasons the returned document does not contain the content. The hint parameter, if specified, supplies a hint to the database when processing the SODA operation. This is expected to be a string in the same format as SQL hints but without any comment characters, for example hint="MONITOR". While you could use this to pass any SQL hint, the hints MONITOR (turn on monitoring) and NO_MONITOR (turn off monitoring) are the most useful. Use of the hint parameter requires Oracle Client 21.3 or higher (or Oracle Client 19 from 19.11). """ doc_impl = self._process_doc_arg(doc) if hint is not None and not isinstance(hint, str): raise TypeError("expecting a string") return_doc_impl = self._impl.insert_one(doc_impl, hint, return_doc=True) return SodaDocument._from_impl(return_doc_impl) @property def metadata(self) -> dict: """ This read-only attribute returns a dictionary containing the metadata that was used to create the collection. """ return json.loads(self._impl.get_metadata()) @property def name(self) -> str: """ This read-only attribute returns the name of the collection. """ return self._impl.name def save(self, doc: object) -> None: """ Saves a document into the collection. This method is equivalent to insertOne() except that if client-assigned keys are used, and the document with the specified key already exists in the collection, it will be replaced with the input document. """ doc_impl = self._process_doc_arg(doc) self._impl.save(doc_impl, hint=None, return_doc=False) def saveAndGet(self, doc: object, hint: str=None) -> "SodaDocument": """ Saves a document into the collection. This method is equivalent to insertOneAndGet() except that if client-assigned keys are used, and the document with the specified key already exists in the collection, it will be replaced with the input document. The hint parameter, if specified, supplies a hint to the database when processing the SODA operation. This is expected to be a string in the same format as SQL hints but without any comment characters, for example hint="MONITOR". While you could use this to pass any SQL hint, the hints MONITOR (turn on monitoring) and NO_MONITOR (turn off monitoring) are the most useful. Use of the hint parameter requires Oracle Client 21.3 or higher (or Oracle Client 19 from 19.11). """ doc_impl = self._process_doc_arg(doc) if hint is not None and not isinstance(hint, str): raise TypeError("expecting a string") return_doc_impl = self._impl.save(doc_impl, hint, return_doc=True) return SodaDocument._from_impl(return_doc_impl) def truncate(self) -> None: """ Removes all of the documents in the collection, similarly to what is done for rows in a table by the TRUNCATE TABLE statement. """ self._impl.truncate() class SodaDocument: @classmethod def _from_impl(cls, impl): doc = cls.__new__(cls) doc._impl = impl return doc @property def createdOn(self) -> str: """ This read-only attribute returns the creation time of the document in ISO 8601 format. Documents created by SodaDatabase.createDocument() or fetched from collections where this attribute is not stored will return None. """ return self._impl.get_created_on() def getContent(self) -> Union[dict, list]: """ Returns the content of the document as a dictionary or list. This method assumes that the content is application/json and will raise an exception if this is not the case. If there is no content, however, None will be returned. """ return json.loads(self.getContentAsString()) def getContentAsBytes(self) -> bytes: """ Returns the content of the document as a bytes object. If there is no content, however, None will be returned. """ content_bytes, encoding = self._impl.get_content() return content_bytes def getContentAsString(self) -> str: """ Returns the content of the document as a string. If the document encoding is not known, UTF-8 will be used. If there is no content, however, None will be returned. """ content_bytes, encoding = self._impl.get_content() return content_bytes.decode(encoding) @property def key(self) -> str: """ This read-only attribute returns the unique key assigned to this document. Documents created by SodaDatabase.createDocument() may not have a value assigned to them and return None. """ return self._impl.get_key() @property def lastModified(self) -> str: """ This read-only attribute returns the last modified time of the document in ISO 8601 format. Documents created by SodaDatabase.createDocument() or fetched from collections where this attribute is not stored will return None. """ return self._impl.get_last_modified() @property def mediaType(self) -> str: """ This read-only attribute returns the media type assigned to the document. By convention this is expected to be a MIME type but no checks are performed on this value. If a value is not specified when calling SodaDatabase.createDocument() or the document is fetched from a collection where this component is not stored, the string “application/json” is returned. """ return self._impl.get_media_type() @property def version(self) -> str: """ This read-only attribute returns the version assigned to this document. Documents created by SodaDatabase.createDocument() or fetched from collections where this attribute is not stored will return None. """ return self._impl.get_version() class SodaDocCursor: def __iter__(self): return self def __next__(self): if self._impl is None: errors._raise_err(errors.ERR_CURSOR_NOT_OPEN) doc_impl = self._impl.get_next_doc() if doc_impl is not None: return SodaDocument._from_impl(doc_impl) raise StopIteration @classmethod def _from_impl(cls, impl): cursor = cls.__new__(cls) cursor._impl = impl return cursor def close(self) -> None: """ Close the cursor now, rather than whenever __del__ is called. The cursor will be unusable from this point forward; an Error exception will be raised if any operation is attempted with the cursor. """ if self._impl is None: errors._raise_err(errors.ERR_CURSOR_NOT_OPEN) self._impl.close() self._impl = None class SodaOperation: def __init__(self, collection: SodaCollection) -> None: self._collection = collection self._key = None self._keys = None self._version = None self._filter = None self._hint = None self._skip = None self._limit = None self._fetch_array_size = None def count(self) -> int: """ Returns a count of the number of documents in the collection that match the criteria. If skip() or limit() were called on this object, an exception is raised. """ return self._collection._impl.get_count(self) def fetchArraySize(self, value: int) -> "SodaOperation": """ This is a tuning method to specify the number of documents that are internally fetched in batches by calls to getCursor() and getDocuments(). It does not affect how many documents are returned to the application. A value of 0 will use the default value (100). This method is only available in Oracle Client 19.5 and higher. As a convenience, the SodaOperation object is returned so that further criteria can be specified by chaining methods together. """ if not isinstance(value, int) or value < 0: raise TypeError("expecting integer >= 0") if value == 0: self._fetch_array_size = None else: self._fetch_array_size = value return self def filter(self, value: Union[dict, str]) -> "SodaOperation": """ Sets a filter specification for complex document queries and ordering of JSON documents. Filter specifications must be provided as a dictionary or JSON-encoded string and can include comparisons, regular expressions, logical and spatial operators, among others. See the overview of SODA filter specifications for more information. As a convenience, the SodaOperation object is returned so that further criteria can be specified by chaining methods together. """ if isinstance(value, dict): self._filter = json.dumps(value) elif isinstance(value, str): self._filter = value else: raise TypeError("expecting string or dictionary") return self def getCursor(self) -> "SodaDocCursor": """ Returns a SodaDocCursor object that can be used to iterate over the documents that match the criteria. """ impl = self._collection._impl.get_cursor(self) return SodaDocCursor._from_impl(impl) def getDocuments(self) -> list: """ Returns a list of SodaDocument objects that match the criteria. """ return [d for d in self.getCursor()] def getOne(self) -> Union["SodaDocument", None]: """ Returns a single SodaDocument object that matches the criteria. Note that if multiple documents match the criteria only the first one is returned. """ doc_impl = self._collection._impl.get_one(self) if doc_impl is not None: return SodaDocument._from_impl(doc_impl) def hint(self, value: str) -> "SodaOperation": """ Specifies a hint that will be provided to the SODA operation when it is performed. This is expected to be a string in the same format as SQL hints but without any comment characters. While you could use this to pass any SQL hint, the hints MONITOR (turn on monitoring) and NO_MONITOR (turn off monitoring) are the most useful. Use of this method requires Oracle Client 21.3 or higher (or Oracle Client 19 from 19.11). As a convenience, the SodaOperation object is returned so that further criteria can be specified by chaining methods together. """ if not isinstance(value, str): raise TypeError("expecting a string") self._hint = value return self def key(self, value: str) -> "SodaOperation": """ Specifies that the document with the specified key should be returned. This causes any previous calls made to this method and keys() to be ignored. As a convenience, the SodaOperation object is returned so that further criteria can be specified by chaining methods together. """ if not isinstance(value, str): raise TypeError("expecting string") self._key = value self._keys = None return self def keys(self, value: list) -> "SodaOperation": """ Specifies that documents that match the keys found in the supplied sequence should be returned. This causes any previous calls made to this method and key() to be ignored. As a convenience, the SodaOperation object is returned so that further criteria can be specified by chaining methods together. """ value_as_list = list(value) for element in value_as_list: if not isinstance(element, str): raise TypeError("expecting sequence of strings") self._keys = value_as_list self._key = None return self def limit(self, value: int) -> "SodaOperation": """ Specifies that only the specified number of documents should be returned. This method is only usable for read operations such as getCursor() and getDocuments(). For write operations, any value set using this method is ignored. As a convenience, the SodaOperation object is returned so that further criteria can be specified by chaining methods together. """ if not isinstance(value, int) or value <= 0: raise TypeError("expecting positive integer") self._limit = value return self def remove(self) -> int: """ Removes all of the documents in the collection that match the criteria. The number of documents that have been removed is returned. """ return self._collection._impl.remove(self) def replaceOne(self, doc: object) -> bool: """ Replaces a single document in the collection with the specified document. The input document can be a dictionary or list or an existing SODA document object. A boolean indicating if a document was replaced or not is returned. Currently the method key() must be called before this method can be called. """ doc_impl = self._collection._process_doc_arg(doc) return self._collection._impl.replace_one(self, doc_impl, return_doc=False) def replaceOneAndGet(self, doc: object) -> "SodaDocument": """ Similarly to replaceOne(), this method replaces a single document in the collection with the specified document. The only difference is that it returns a SodaDocument object. Note that for performance reasons the returned document does not contain the content. """ doc_impl = self._collection._process_doc_arg(doc) return_doc_impl = self._collection._impl.replace_one(self, doc_impl, return_doc=True) return SodaDocument._from_impl(return_doc_impl) def skip(self, value: int) -> "SodaOperation": """ Specifies the number of documents that match the other criteria that will be skipped. This method is only usable for read operations such as getCursor() and getDocuments(). For write operations, any value set using this method is ignored. As a convenience, the SodaOperation object is returned so that further criteria can be specified by chaining methods together. """ if not isinstance(value, int) or value < 0: raise TypeError("expecting integer >= 0") self._skip = value return self def version(self, value: str) -> "SodaOperation": """ Specifies that documents with the specified version should be returned. Typically this is used with key() to implement optimistic locking, so that the write operation called later does not affect a document that someone else has modified. As a convenience, the SodaOperation object is returned so that further criteria can be specified by chaining methods together. """ if not isinstance(value, str): raise TypeError("expecting string") self._version = value return self python-oracledb-1.2.1/src/oracledb/subscr.py000066400000000000000000000255061434177474600210530ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # subscr.py # # Contains the Subscription class and Message classes used for managing # subscriptions to database events and the messages that are sent when those # events are detected. #------------------------------------------------------------------------------ from typing import Callable, Union, List from . import connection class Subscription: def __repr__(self): return f"" @classmethod def _from_impl(cls, impl): subscr = cls.__new__(cls) subscr._impl = impl return subscr @property def callback(self) -> Callable: """ Returns the callback that was registered when the subscription was created. """ return self._impl.callback @property def connection(self) -> "connection.Connection": """ Returns the connection that was used to register the subscription when it was created. """ return self._impl.connection @property def id(self) -> int: """ Returns the value of REGID found in the database view USER_CHANGE_NOTIFICATION_REGS or the value of REG_ID found in the database view USER_SUBSCR_REGISTRATIONS. For AQ subscriptions, this value is 0. """ return self._impl.id @property def ip_address(self) -> str: """ Returns the IP address used for callback notifications from the database server. If not set during construction, this value is None. """ return self._impl.ip_address @property def ipAddress(self) -> str: """ Deprecated. Use property ip_address instead. """ return self.ip_address @property def name(self) -> str: """ Returns the name used to register the subscription when it was created. """ return self._impl.name @property def namespace(self) -> int: """ Returns the namespace used to register the subscription when it was created. """ return self._impl.namespace @property def operations(self) -> int: """ Returns the operations that will send notifications for each table or query that is registered using this subscription. """ return self._impl.operations @property def port(self) -> int: """ Returns the port used for callback notifications from the database server. If not set during construction, this value is zero. """ return self._impl.port @property def protocol(self) -> int: """ Returns the protocol used to register the subscription when it was created. """ return self._impl.protocol @property def qos(self) -> int: """ Returns the quality of service flags used to register the subscription when it was created. """ return self._impl.qos def registerquery(self, statement: str, args: Union[list, dict]=None) -> int: """ Register the query for subsequent notification when tables referenced by the query are changed. This behaves similarly to cursor.execute() but only queries are permitted and the args parameter, if specified, must be a sequence or dictionary. If the qos parameter included the flag SUBSCR_QOS_QUERY when the subscription was created, then the ID for the registered query is returned; otherwise, None is returned. """ if args is not None and not isinstance(args, (list, dict)): raise TypeError("expecting args to be a dictionary or list") return self._impl.register_query(statement, args) @property def timeout(self) -> int: """ Returns the timeout (in seconds) that was specified when the subscription was created. A value of 0 indicates that there is no timeout. """ return self._impl.timeout class Message: def __init__(self, subscription: Subscription) -> None: self._subscription = subscription self._consumer_name = None self._dbname = None self._queries = [] self._queue_name = None self._registered = False self._tables = [] self._txid = None self._type = 0 self._msgid = None @property def consumer_name(self) -> Union[str, None]: """ Returns the name of the consumer which generated the notification. It will be populated if the subscription was created with the namespace SUBSCR_NAMESPACE_AQ and the queue is a multiple consumer queue. """ return self._consumer_name @property def consumerName(self) -> Union[str, None]: """ Deprecated. Use property consumer_name instead. """ return self.consumer_name @property def dbname(self) -> Union[str, None]: """ Returns the name of the database that generated the notification. """ return self._dbname @property def msgid(self) -> Union[bytes, None]: """ Returns the message id of the AQ message that generated the notification. """ return self._msgid @property def queries(self) -> List["MessageQuery"]: """ Returns a list of message query objects that give information about query result sets changed for this notification. This attribute will be an empty list if the qos parameter did not include the flag SUBSCR_QOS_QUERY when the subscription was created. """ return self._queries @property def queue_name(self) -> Union[str, None]: """ Returns the name of the queue which generated the notification. It will only be populated if the subscription was created with the namespace SUBSCR_NAMESPACE_AQ. """ return self._queue_name @property def queueName(self) -> Union[str, None]: """ Deprecated. Use property queue_name instead. """ return self.queue_name @property def registered(self) -> bool: """ Returns whether the subscription which generated this notification is still registered with the database. The subscription is automatically deregistered with the database when the subscription timeout value is reached or when the first notification is sent (when the quality of service flag SUBSCR_QOS_DEREG_NFY is used). """ return self._registered @property def subscription(self) -> Subscription: """ Returns the subscription object for which this notification was generated. """ return self._subscription @property def tables(self) -> List["MessageTable"]: """ Returns a list of message table objects that give information about the tables changed for this notification. This attribute will be an empty list if the qos parameter included the flag SUBSCR_QOS_QUERY when the subscription was created. """ return self._tables @property def txid(self) -> Union[bytes, None]: """ Returns the id of the transaction that generated the notification. """ return self._txid @property def type(self) -> int: """ Returns the type of message that has been sent. """ return self._type class MessageQuery: def __init__(self) -> None: self._id = 0 self._operation = 0 self._tables = [] @property def id(self) -> int: """ Returns the query id of the query for which the result set changed. The value will match the value returned by Subscription.registerquery() when the related query was registered. """ return self._id @property def operation(self) -> int: """ Returns the operation that took place on the query result set that was changed. Valid values for this attribute are EVENT_DEREG and EVENT_QUERYCHANGE. """ return self._operation @property def tables(self) -> List["MessageTable"]: """ Returns a list of message table objects that give information about the table changes that caused the query result set to change for this notification. """ return self._tables class MessageRow: def __init__(self) -> None: self._operation = 0 self._rowid = None @property def operation(self) -> int: """ Returns the operation that took place on the row that was changed. """ return self._operation @property def rowid(self) -> Union[str, None]: """ Returns the rowid of the row that was changed. """ return self._rowid class MessageTable: def __init__(self) -> None: self._name = None self._operation = 0 self._rows = [] @property def name(self) -> Union[str, None]: """ Returns the name of the table that was changed. """ return self._name @property def operation(self) -> int: """ Returns the operation that took place on the table that was changed. """ return self._operation @property def rows(self) -> List["MessageRow"]: """ Returns a list of message row objects that give information about the rows changed on the table. This value is only filled in if the qos parameter to the Connection.subscribe() method included the flag SUBSCR_QOS_ROWIDS. """ return self._rows python-oracledb-1.2.1/src/oracledb/thick_impl.pyx000066400000000000000000000060721434177474600220620ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # thick_impl.pyx # # Cython file for interfacing with ODPI-C. #------------------------------------------------------------------------------ # cython: language_level=3 cimport cython cimport cpython cimport cpython.datetime as cydatetime import datetime import decimal cydatetime.import_datetime() from . import constants, driver_mode, errors, exceptions from .subscr import Message, MessageQuery, MessageRow, MessageTable from .defaults import defaults from . import __version__ as VERSION from .base_impl cimport get_exception_class from .base_impl cimport BaseConnImpl, BaseCursorImpl, BaseVarImpl, DbType from .base_impl cimport BaseDbObjectTypeImpl, BaseDbObjectAttrImpl from .base_impl cimport BaseDbObjectImpl, BaseLobImpl, BasePoolImpl from .base_impl cimport BaseSodaDbImpl, BaseSodaCollImpl, BaseSodaDocImpl from .base_impl cimport BaseSodaDocCursorImpl, BaseQueueImpl from .base_impl cimport BaseDeqOptionsImpl, BaseEnqOptionsImpl from .base_impl cimport BaseMsgPropsImpl, BaseSubscrImpl, BindVar, FetchInfo from .base_impl cimport ConnectParamsImpl, PoolParamsImpl from .base_impl cimport NUM_TYPE_FLOAT, NUM_TYPE_INT, NUM_TYPE_DECIMAL from libc.string cimport memchr, memset include "impl/thick/odpi.pxd" cdef type PY_TYPE_DATE = datetime.date cdef type PY_TYPE_DATETIME = datetime.datetime cdef type PY_TYPE_DECIMAL = decimal.Decimal cdef type PY_TYPE_DB_OBJECT cdef type PY_TYPE_LOB cdef type PY_TYPE_TIMEDELTA = datetime.timedelta cdef dpiContext *driver_context = NULL driver_context_params = None client_version = None include "impl/thick/buffer.pyx" include "impl/thick/connection.pyx" include "impl/thick/pool.pyx" include "impl/thick/cursor.pyx" include "impl/thick/lob.pyx" include "impl/thick/json.pyx" include "impl/thick/var.pyx" include "impl/thick/dbobject.pyx" include "impl/thick/soda.pyx" include "impl/thick/queue.pyx" include "impl/thick/subscr.pyx" include "impl/thick/utils.pyx" python-oracledb-1.2.1/src/oracledb/thin_impl.pyx000066400000000000000000000070631434177474600217230ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # thin_impl.pyx # # Cython file for communicating with the server directly without the use of # any Oracle Client library. #------------------------------------------------------------------------------ # cython: language_level=3 cimport cython cimport cpython cimport cpython.datetime as cydatetime cimport cpython.ref from libc.stdint cimport int8_t, int16_t, int32_t, int64_t from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t from libc.stdint cimport UINT8_MAX, UINT16_MAX, UINT32_MAX, UINT64_MAX from libc.string cimport memcpy, memset from cpython cimport array import array import base64 import collections import datetime import decimal import getpass import hashlib import os import socket import re import secrets import select import ssl import subprocess import sys import threading import time try: import certifi except ImportError: certifi = None macos_certs = None cydatetime.import_datetime() from . import __version__ as VERSION from . import constants, errors, exceptions from .defaults import defaults from .base_impl cimport get_exception_class, NUM_TYPE_FLOAT from .base_impl cimport NUM_TYPE_INT, NUM_TYPE_DECIMAL, NUM_TYPE_STR from .base_impl cimport BaseConnImpl, BaseCursorImpl, BaseVarImpl, DbType from .base_impl cimport BaseLobImpl, BasePoolImpl, FetchInfo from .base_impl cimport Address, AddressList, Description, DescriptionList from .base_impl cimport ConnectParamsImpl, PoolParamsImpl, BaseDbObjectAttrImpl from .base_impl cimport BaseDbObjectImpl, BaseDbObjectTypeImpl from .base_impl import DB_TYPE_BLOB, DB_TYPE_CLOB, DB_TYPE_NCLOB from .base_impl import DB_TYPE_BINARY_INTEGER, DB_TYPE_CURSOR, DB_TYPE_OBJECT ctypedef unsigned char char_type cdef type PY_TYPE_DECIMAL = decimal.Decimal cdef type PY_TYPE_DB_OBJECT cdef type PY_TYPE_LOB cdef bint HAS_CRYPTOGRAPHY = True include "impl/thin/constants.pxi" include "impl/thin/utils.pyx" include "impl/thin/crypto.pyx" include "impl/thin/capabilities.pyx" include "impl/thin/buffer.pyx" include "impl/thin/packet.pyx" include "impl/thin/network_services.pyx" include "impl/thin/data_types.pyx" include "impl/thin/messages.pyx" include "impl/thin/protocol.pyx" include "impl/thin/connection.pyx" include "impl/thin/statement.pyx" include "impl/thin/cursor.pyx" include "impl/thin/var.pyx" include "impl/thin/dbobject.pyx" include "impl/thin/lob.pyx" include "impl/thin/pool.pyx" include "impl/thin/conversions.pyx" python-oracledb-1.2.1/src/oracledb/utils.py000066400000000000000000000064661434177474600207160ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # utils.py # # Contains utility classes and methods. #------------------------------------------------------------------------------ from . import errors class CheckImpls: """ Decorator class which is used on the base implementation method and checks to see which implementation is currently being used (and therefore has no support for the method). An exception is raised letting the user know which implementation does support the method. Currently there are only two implementations (thick and thin) so the assumption is made that the implementation not currently running does support the method. """ def __init__(self, feature): self.feature = feature def __call__(self, f): feature = self.feature def wrapped_f(self, *args, **kwargs): class_name = type(self).__name__ driver_type = "thin" if class_name.startswith("Thick") else "thick" errors._raise_err(errors.ERR_FEATURE_NOT_SUPPORTED, feature=feature, driver_type=driver_type) return wrapped_f def params_initer(f): """ Decorator function which is used on the ConnectParams and PoolParams classes. It creates the implementation object using the implementation class stored on the parameter class. It first, however, calls the original method to ensure that the keyword parameters supplied are valid (the original method itself does nothing). """ def wrapped_f(self, *args, **kwargs): f(self, *args, **kwargs) self._impl = self._impl_class() if kwargs: self._impl.set(kwargs) return wrapped_f def params_setter(f): """ Decorator function which is used on the ConnectParams and PoolParams classes. It calls the set() method on the parameter implementation object with the supplied keyword arguments. It first, however, calls the original method to ensure that the keyword parameters supplied are valid (the original method itself does nothing). """ def wrapped_f(self, *args, **kwargs): f(self, *args, **kwargs) self._impl.set(kwargs) return wrapped_f python-oracledb-1.2.1/src/oracledb/var.py000066400000000000000000000143721434177474600203410ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # var.py # # Contains the Var class used for managing variables used during bind and # fetch. These hold the metadata as well as any necessary buffers. #------------------------------------------------------------------------------ from typing import Callable, Union, Type from . import errors from .dbobject import DbObjectType from .base_impl import DbType class Var: def __repr__(self): value = self._impl.get_all_values() if not self._impl.is_array and len(value) == 1: value = value[0] typ = self._type return f"" @classmethod def _from_impl(cls, impl, typ=None): var = cls.__new__(cls) var._impl = impl if typ is not None: var._type = typ elif impl.objtype is not None: var._type = DbObjectType._from_impl(impl.objtype) else: var._type = impl.dbtype return var @property def actual_elements(self) -> int: """ This read-only attribute returns the actual number of elements in the variable. This corresponds to the number of elements in a PL/SQL index-by table for variables that are created using the method Cursor.arrayvar(). For all other variables this value will be identical to the attribute num_elements. """ if self._impl.is_array: return self._impl.num_elements_in_array return self._impl.num_elements @property def actualElements(self) -> int: """ Deprecated. Use property actual_elements instead. """ return self.actual_elements @property def buffer_size(self) -> int: """ This read-only attribute returns the size of the buffer allocated for each element in bytes. """ return self._impl.buffer_size @property def bufferSize(self) -> int: """ Deprecated. Use property buffer_size intead(). """ return self.buffer_size def getvalue(self, pos: int=0) -> object: """ Return the value at the given position in the variable. For variables created using the method Cursor.arrayvar() the value returned will be a list of each of the values in the PL/SQL index-by table. For variables bound to DML returning statements, the value returned will also be a list corresponding to the returned data for the given execution of the statement (as identified by the pos parameter). """ return self._impl.get_value(pos) @property def inconverter(self) -> Callable: """ This read-only attribute specifies the method used to convert data from Python to the Oracle database. The method signature is converter(value) and the expected return value is the value to bind to the database. If this attribute is None, the value is bound directly without any conversion. """ return self._impl.inconverter @property def num_elements(self) -> int: """ This read-only attribute returns the number of elements allocated in an array, or the number of scalar items that can be fetched into the variable or bound to the variable. """ return self._impl.num_elements @property def numElements(self) -> int: """ Deprecated. Use property num_elements instead. """ return self.num_elements @property def outconverter(self) -> Callable: """ This read-only attribute specifies the method used to convert data from the Oracle database to Python. The method signature is converter(value) and the expected return value is the value to return to Python. If this attribute is None, the value is returned directly without any conversion. """ return self._impl.outconverter def setvalue(self, pos: int, value: object) -> None: """ Set the value at the given position in the variable. """ self._impl.set_value(pos, value) @property def size(self) -> int: """ This read-only attribute returns the size of the variable. For strings this value is the size in characters. For all others, this is same value as the attribute buffer_size. """ return self._impl.size @property def type(self) -> Union[DbType, DbObjectType]: """ This read-only attribute returns the type of the variable. This will be an Oracle Object Type if the variable binds Oracle objects; otherwise, it will be one of the database type constants. """ return self._type @property def values(self) -> list: """ This read-only attribute returns a copy of the value of all actual positions in the variable as a list. This is the equivalent of calling getvalue() for each valid position and the length will correspond to the value of the actual_elements attribute. """ return self._impl.get_all_values() python-oracledb-1.2.1/src/oracledb/version.py000066400000000000000000000027751434177474600212420ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # version.py # # Defines the version of the package. This is the only place where this is # found. The setup.cfg configuration file and the documentation configuration # file doc/src/conf.py both reference this file directly. #------------------------------------------------------------------------------ __version__ = "1.2.1" python-oracledb-1.2.1/tests/000077500000000000000000000000001434177474600157705ustar00rootroot00000000000000python-oracledb-1.2.1/tests/README.md000066400000000000000000000055651434177474600172620ustar00rootroot00000000000000This directory contains the test suite for python-oracledb. 1. The schemas and SQL objects that are referenced in the test suite can be created by running the Python script [create_schema.py][1]. The script requires administrative privileges and will prompt for these credentials as well as the names of the schemas that will be created, unless a number of environment variables are set as documented in the Python script [test_env.py][2]. Run the script using the following command: python create_schema.py 2. Run the test suite by issuing the following command in the top-level directory of your oracledb installation: tox This will build the module in an independent environment and run the test suite using the module that was just built in that environment. Alternatively, you can use the currently installed build of oracledb and run the following command instead: python -m unittest discover -v -s tests You may also run each of the test scripts independently, as in: python test_1000_module.py 3. After running the test suite, the schemas can be dropped by running the Python script [drop_schema.py][3]. The script requires administrative privileges and will prompt for these credentials as well as the names of the schemas that will be dropped, unless a number of environment variables are set as documented in the Python script [test_env.py][2]. Run the script using the following command: python drop_schema.py 4. Enable tests that require extra configuration The following test(s) are automatically skipped if their required environment variable(s) and setup is not available. 4.1 test_5000_externalauth.py This test aims to test the usage of external authentication. - Set the PYO_TEST_EXTERNAL_USER environment variable to the externally identified user that will be connected using external authentication. - Set up external authentication. See [Connecting Using External Authentication][4] for creating an Oracle Wallet or enabling OS authentication. - Run the following SQL commands as a user with administrative privileges (such as SYSTEM or ADMIN) to allow the external user to connect to the database and behave as proxy for testing external authentication with proxy: grant create session to ; alter user grant connect through ; [1]: https://github.com/oracle/python-oracledb/blob/main/tests/create_schema.py [2]: https://github.com/oracle/python-oracledb/blob/main/tests/test_env.py [3]: https://github.com/oracle/python-oracledb/blob/main/tests/drop_schema.py [4]: https://python-oracledb.readthedocs.io/en/latest/user_guide/connection_handling.html#connecting-using-external-authentication python-oracledb-1.2.1/tests/create_schema.py000066400000000000000000000042351434177474600211310ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # create_schema.py # # Creates users and populates their schemas with the tables and packages # necessary for running the python-oracledb test suite. #------------------------------------------------------------------------------ import oracledb import drop_schema import test_env # connect as administrative user (usually SYSTEM or ADMIN) conn = test_env.get_admin_connection() # drop existing users and editions, if applicable drop_schema.drop_schema(conn) # create test schemas print("Creating test schemas...") test_env.run_sql_script(conn, "create_schema", main_user=test_env.get_main_user(), main_password=test_env.get_main_password(), proxy_user=test_env.get_proxy_user(), proxy_password=test_env.get_proxy_password()) if test_env.is_on_oracle_cloud(conn): test_env.run_sql_script(conn, "create_schema_cloud", main_user=test_env.get_main_user()) print("Done.") python-oracledb-1.2.1/tests/drop_schema.py000066400000000000000000000036101434177474600206260ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # drop_schema.py # # Drops the database objects used by the python-oracledb test suite. # # This script is also executed by the Python script create_schema.py for # dropping the existing users and editions, if applicable, before creating the # test schemas. #------------------------------------------------------------------------------ import oracledb import test_env def drop_schema(conn): print("Dropping test schemas...") test_env.run_sql_script(conn, "drop_schema", main_user=test_env.get_main_user(), proxy_user=test_env.get_proxy_user()) if __name__ == "__main__": conn = test_env.get_admin_connection() drop_schema(conn) print("Done.") python-oracledb-1.2.1/tests/sql/000077500000000000000000000000001434177474600165675ustar00rootroot00000000000000python-oracledb-1.2.1/tests/sql/create_schema.sql000066400000000000000000001210161434177474600220740ustar00rootroot00000000000000/*----------------------------------------------------------------------------- * Copyright (c) 2020, 2022, Oracle and/or its affiliates. * * This software is dual-licensed to you under the Universal Permissive License * (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License * 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose * either license.* * * If you elect to accept the software under the Apache License, Version 2.0, * the following applies: * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *---------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------- * create_schema.sql * * Performs the actual work of creating and populating the schemas with the * database objects used by the python-oracledb test suite. It is executed by * the Python script create_schema.py. *---------------------------------------------------------------------------*/ alter session set nls_date_format = 'YYYY-MM-DD HH24:MI:SS' / alter session set nls_numeric_characters='.,' / create user &main_user identified by &main_password / create user &proxy_user identified by &proxy_password / alter user &proxy_user grant connect through &main_user / grant create session to &proxy_user / grant create session, create table, create procedure, create type, create view, select any dictionary, change notification, unlimited tablespace to &main_user / grant aq_administrator_role to &main_user / begin for r in ( select role from dba_roles where role in ('SODA_APP') ) loop execute immediate 'grant ' || r.role || ' to &main_user'; end loop; end; / -- create types create type &main_user..udt_SubObject as object ( SubNumberValue number, SubStringValue varchar2(60) ); / create type &main_user..udt_ObjectArray as varray(10) of &main_user..udt_SubObject; / create type &main_user..udt_Object as object ( NumberValue number, StringValue varchar2(60), FixedCharValue char(10), NStringValue nvarchar2(60), NFixedCharValue nchar(10), RawValue raw(16), IntValue integer, SmallIntValue smallint, RealValue real, DoublePrecisionValue double precision, FloatValue float, BinaryFloatValue binary_float, BinaryDoubleValue binary_double, DateValue date, TimestampValue timestamp, TimestampTZValue timestamp with time zone, TimestampLTZValue timestamp with local time zone, CLOBValue clob, NCLOBValue nclob, BLOBValue blob, SubObjectValue &main_user..udt_SubObject, SubObjectArray &main_user..udt_ObjectArray ); / create type &main_user..udt_Array as varray(10) of number; / create or replace type &main_user..udt_Building as object ( BuildingId number(9), NumFloors number(3), Description varchar2(60), DateBuilt date ); / create or replace type &main_user..udt_Book as object ( Title varchar2(100), Authors varchar2(100), Price number(5,2) ); / -- create tables create table &main_user..TestNumbers ( IntCol number(9) not null, LongIntCol number(16) not null, NumberCol number(9, 2) not null, FloatCol float not null, UnconstrainedCol number not null, NullableCol number(38) ) / create table &main_user..TestStrings ( IntCol number(9) not null, StringCol varchar2(20) not null, RawCol raw(30) not null, FixedCharCol char(40) not null, NullableCol varchar2(50) ) / create table &main_user..TestUnicodes ( IntCol number(9) not null, UnicodeCol nvarchar2(20) not null, FixedUnicodeCol nchar(40) not null, NullableCol nvarchar2(50) ) / create table &main_user..TestDates ( IntCol number(9) not null, DateCol date not null, NullableCol date ) / create table &main_user..TestCLOBs ( IntCol number(9) not null, CLOBCol clob not null, ExtraNumCol1 number(9), ExtraCLOBCol1 clob, ExtraNumCol2 number(9), ExtraCLOBCol2 clob ) / create table &main_user..TestNCLOBs ( IntCol number(9) not null, NCLOBCol nclob not null, ExtraNumCol1 number(9), ExtraNCLOBCol1 nclob, ExtraNumCol2 number(9), ExtraNCLOBCol2 nclob ) / create table &main_user..TestBLOBs ( IntCol number(9) not null, BLOBCol blob not null, ExtraNumCol1 number(9), ExtraBLOBCol1 blob, ExtraNumCol2 number(9), ExtraBLOBCol2 blob ) / create table &main_user..TestXML ( IntCol number(9) not null, XMLCol xmltype not null ) / create table &main_user..TestTempXML ( IntCol number(9) not null, XMLCol xmltype not null ) / create table &main_user..TestLongs ( IntCol number(9) not null, LongCol long ) nocompress / create table &main_user..TestLongRaws ( IntCol number(9) not null, LongRawCol long raw ) nocompress / create table &main_user..TestTempTable ( IntCol number(9) not null, StringCol1 varchar2(400), StringCol2 varchar2(400), NumberCol number(25,2), constraint TestTempTable_pk primary key (IntCol) ) / create table &main_user..TestArrayDML ( IntCol number(9) not null, StringCol varchar2(100), IntCol2 number(3), constraint TestArrayDML_pk primary key (IntCol) ) / create table &main_user..TestObjects ( IntCol number(9) not null, ObjectCol &main_user..udt_Object, ArrayCol &main_user..udt_Array ) / create table &main_user..TestTimestamps ( IntCol number(9) not null, TimestampCol timestamp not null, NullableCol timestamp ) / create table &main_user..TestTimestampLTZs ( IntCol number(9) not null, TimestampLTZCol timestamp with local time zone not null, NullableCol timestamp with local time zone ) / create table &main_user..TestTimestampTZs ( IntCol number(9) not null, TimestampTZCol timestamp with time zone not null, NullableCol timestamp with time zone ) / create table &main_user..TestIntervals ( IntCol number(9) not null, IntervalCol interval day to second not null, NullableCol interval day to second ) / create table &main_user..TestUniversalRowids ( IntCol number(9) not null, StringCol varchar2(250) not null, DateCol date not null, constraint TestUniversalRowids_pk primary key (IntCol, StringCol, DateCol) ) organization index / create table &main_user..TestBuildings ( BuildingId number(9) not null, BuildingObj &main_user..udt_Building not null ) / create table &main_user..TestRowids ( IntCol number(9) not null, RowidCol rowid, URowidCol urowid ) / create table &main_user..PlsqlSessionCallbacks ( RequestedTag varchar2(250), ActualTag varchar2(250), FixupTimestamp timestamp ) / declare t_Version number; begin select to_number(substr(version, 1, instr(version, '.') - 1)) into t_Version from product_component_version where product like 'Oracle Database%'; if t_Version >= 21 then execute immediate 'create table &main_user..TestJson (' || ' IntCol number(9) not null,' || ' JsonCol json not null' || ')'; end if; end; / -- create queue table and queues for testing advanced queuing declare t_Version number; begin select to_number(substr(version, 1, instr(version, '.') - 1)) into t_Version from product_component_version where product like 'Oracle Database%'; dbms_aqadm.create_queue_table('&main_user..BOOK_QUEUE_TAB', '&main_user..UDT_BOOK'); dbms_aqadm.create_queue('&main_user..TEST_BOOK_QUEUE', '&main_user..BOOK_QUEUE_TAB'); dbms_aqadm.start_queue('&main_user..TEST_BOOK_QUEUE'); dbms_aqadm.create_queue_table('&main_user..RAW_QUEUE_TAB', 'RAW'); dbms_aqadm.create_queue('&main_user..TEST_RAW_QUEUE', '&main_user..RAW_QUEUE_TAB'); dbms_aqadm.start_queue('&main_user..TEST_RAW_QUEUE'); dbms_aqadm.create_queue_table('&main_user..BOOK_QUEUE_MULTI_TAB', '&main_user..UDT_BOOK', multiple_consumers => TRUE); dbms_aqadm.create_queue('&main_user..BOOK_QUEUE_MULTI', '&main_user..BOOK_QUEUE_MULTI_TAB'); dbms_aqadm.start_queue('&main_user..BOOK_QUEUE_MULTI'); dbms_aqadm.add_subscriber('&main_user..BOOK_QUEUE_MULTI', sys.aq$_agent('Sub1', null, null)); if t_Version >= 21 then dbms_aqadm.create_queue_table('&main_user..JSON_QUEUE_TAB', 'JSON'); dbms_aqadm.create_queue('&main_user..TEST_JSON_QUEUE', '&main_user..JSON_QUEUE_TAB'); dbms_aqadm.start_queue('&main_user..TEST_JSON_QUEUE'); end if; end; / -- create transformations begin dbms_transform.create_transformation('&main_user', 'transform1', '&main_user', 'UDT_BOOK', '&main_user', 'UDT_BOOK', '&main_user..UDT_BOOK(source.user_data.TITLE, ' || 'source.user_data.AUTHORS, source.user_data.PRICE + 5)'); dbms_transform.create_transformation('&main_user', 'transform2', '&main_user', 'UDT_BOOK', '&main_user', 'UDT_BOOK', '&main_user..UDT_BOOK(source.user_data.TITLE, ' || 'source.user_data.AUTHORS, source.user_data.PRICE + 10)'); end; / -- populate tables begin for i in 1..10 loop insert into &main_user..TestNumbers values (i, power(38, i), i + i * 0.25, i + i * .75, i * i * i + i *.5, decode(mod(i, 2), 0, null, power(143, i))); end loop; end; / declare t_RawValue raw(30); function ConvertHexDigit(a_Value number) return varchar2 is begin if a_Value between 0 and 9 then return to_char(a_Value); end if; return chr(ascii('A') + a_Value - 10); end; function ConvertToHex(a_Value varchar2) return varchar2 is t_HexValue varchar2(60); t_Digit number; begin for i in 1..length(a_Value) loop t_Digit := ascii(substr(a_Value, i, 1)); t_HexValue := t_HexValue || ConvertHexDigit(trunc(t_Digit / 16)) || ConvertHexDigit(mod(t_Digit, 16)); end loop; return t_HexValue; end; begin for i in 1..10 loop t_RawValue := hextoraw(ConvertToHex('Raw ' || to_char(i))); insert into &main_user..TestStrings values (i, 'String ' || to_char(i), t_RawValue, 'Fixed Char ' || to_char(i), decode(mod(i, 2), 0, null, 'Nullable ' || to_char(i))); end loop; end; / begin for i in 1..10 loop insert into &main_user..TestUnicodes values (i, 'Unicode ' || unistr('\3042') || ' ' || to_char(i), 'Fixed Unicode ' || to_char(i), decode(mod(i, 2), 0, null, unistr('Nullable ') || to_char(i))); end loop; end; / begin for i in 1..10 loop insert into &main_user..TestDates values (i, to_date(20021209, 'YYYYMMDD') + i + i * .1, decode(mod(i, 2), 0, null, to_date(20021209, 'YYYYMMDD') + i + i + i * .15)); end loop; end; / begin for i in 1..100 loop insert into &main_user..TestXML values (i, '' || dbms_random.string('x', 1024) || ''); end loop; end; / begin for i in 1..10 loop insert into &main_user..TestTimestamps values (i, to_timestamp('20021209', 'YYYYMMDD') + to_dsinterval(to_char(i) || ' 00:00:' || to_char(i * 2) || '.' || to_char(i * 50)), decode(mod(i, 2), 0, to_timestamp(null, 'YYYYMMDD'), to_timestamp('20021209', 'YYYYMMDD') + to_dsinterval(to_char(i + 1) || ' 00:00:' || to_char(i * 3) || '.' || to_char(i * 125)))); end loop; end; / begin for i in 1..10 loop insert into &main_user..TestTimestampLTZs values (i, to_timestamp_tz('20220602 ' || decode(mod(i, 2), 0, '-', '+') || ltrim(to_char(i, '00')) || ':' || decode(mod(i, 4), 0, '00', '30'), 'YYYYMMDD TZH:TZM') + to_dsinterval(to_char(i) || ' 00:00:' || to_char(i * 2) || '.' || to_char(i * 50)), decode(mod(i, 2), 0, to_timestamp(null, 'YYYYMMDD'), to_timestamp_tz('20220602 00:00', 'YYYYMMDD TZH:TZM') + to_dsinterval(to_char(i + 1) || ' 00:00:' || to_char(i * 3) || '.' || to_char(i * 125)))); end loop; end; / begin for i in 1..10 loop insert into &main_user..TestTimestampTZs values (i, to_timestamp_tz('20220603 ' || decode(mod(i, 2), 0, '-', '+') || ltrim(to_char(i, '00')) || ':' || decode(mod(i, 4), 0, '00', '30'), 'YYYYMMDD TZH:TZM') + to_dsinterval(to_char(i) || ' 00:00:' || to_char(i * 2) || '.' || to_char(i * 50)), decode(mod(i, 2), 0, to_timestamp(null, 'YYYYMMDD'), to_timestamp_tz('20220603 00:00', 'YYYYMMDD TZH:TZM') + to_dsinterval(to_char(i + 1) || ' 00:00:' || to_char(i * 3) || '.' || to_char(i * 125)))); end loop; end; / begin for i in 1..10 loop insert into &main_user..TestIntervals values (i, to_dsinterval(to_char(i) || ' ' || to_char(i) || ':' || to_char(i * 2) || ':' || to_char(i * 3)), decode(mod(i, 2), 0, to_dsinterval(null), to_dsinterval(to_char(i + 5) || ' ' || to_char(i + 2) || ':' || to_char(i * 2 + 5) || ':' || to_char(i * 3 + 5)))); end loop; end; / insert into &main_user..TestObjects values (1, &main_user..udt_Object(1, 'First row', 'First', 'N First Row', 'N First', '52617720446174612031', 2, 5, 12.125, 0.5, 12.5, 25.25, 50.125, to_date(20070306, 'YYYYMMDD'), to_timestamp('20080912 16:40:00', 'YYYYMMDD HH24:MI:SS'), to_timestamp_tz('20091013 17:50:00 00:00', 'YYYYMMDD HH24:MI:SS TZH:TZM'), to_timestamp_tz('20101114 18:55:00 00:00', 'YYYYMMDD HH24:MI:SS TZH:TZM'), 'Short CLOB value', 'Short NCLOB Value', utl_raw.cast_to_raw('Short BLOB value'), &main_user..udt_SubObject(11, 'Sub object 1'), &main_user..udt_ObjectArray( &main_user..udt_SubObject(5, 'first element'), &main_user..udt_SubObject(6, 'second element'))), &main_user..udt_Array(5, 10, null, 20)) / insert into &main_user..TestObjects values (2, null, &main_user..udt_Array(3, null, 9, 12, 15)) / insert into &main_user..TestObjects values (3, &main_user..udt_Object(3, 'Third row', 'Third', 'N Third Row', 'N Third', '52617720446174612033', 4, 10, 6.5, 0.75, 43.25, 86.5, 192.125, to_date(20070621, 'YYYYMMDD'), to_timestamp('20071213 07:30:45', 'YYYYMMDD HH24:MI:SS'), to_timestamp_tz('20170621 23:18:45 00:00', 'YYYYMMDD HH24:MI:SS TZH:TZM'), to_timestamp_tz('20170721 08:27:13 00:00', 'YYYYMMDD HH24:MI:SS TZH:TZM'), 'Another short CLOB value', 'Another short NCLOB Value', utl_raw.cast_to_raw('Yet another short BLOB value'), &main_user..udt_SubObject(13, 'Sub object 3'), &main_user..udt_ObjectArray( &main_user..udt_SubObject(10, 'element #1'), &main_user..udt_SubObject(20, 'element #2'), &main_user..udt_SubObject(30, 'element #3'), &main_user..udt_SubObject(40, 'element #4'))), null) / commit / -- create procedures for testing callproc() create procedure &main_user..proc_Test ( a_InValue varchar2, a_InOutValue in out number, a_OutValue out number ) as begin a_InOutValue := a_InOutValue * length(a_InValue); a_OutValue := length(a_InValue); end; / create procedure &main_user..proc_TestNoArgs as begin null; end; / -- create procedure for testing refcursor create procedure &main_user..myrefcursorproc ( a_RefCursor out sys_refcursor ) as begin open a_RefCursor for select * from TestTempTable; end; / -- create functions for testing callfunc() create function &main_user..func_Test ( a_String varchar2, a_ExtraAmount number ) return number as begin return length(a_String) + a_ExtraAmount; end; / create function &main_user..func_TestNoArgs return number as begin return 712; end; / -- create packages create or replace package &main_user..pkg_TestStringArrays as type udt_StringList is table of varchar2(100) index by binary_integer; function TestInArrays ( a_StartingLength number, a_Array udt_StringList ) return number; procedure TestInOutArrays ( a_NumElems number, a_Array in out nocopy udt_StringList ); procedure TestOutArrays ( a_NumElems number, a_Array out nocopy udt_StringList ); procedure TestIndexBy ( a_Array out nocopy udt_StringList ); end; / create or replace package body &main_user..pkg_TestStringArrays as function TestInArrays ( a_StartingLength number, a_Array udt_StringList ) return number is t_Length number; begin t_Length := a_StartingLength; for i in 1..a_Array.count loop t_Length := t_Length + length(a_Array(i)); end loop; return t_Length; end; procedure TestInOutArrays ( a_NumElems number, a_Array in out udt_StringList ) is begin for i in 1..a_NumElems loop a_Array(i) := 'Converted element # ' || to_char(i) || ' originally had length ' || to_char(length(a_Array(i))); end loop; end; procedure TestOutArrays ( a_NumElems number, a_Array out udt_StringList ) is begin for i in 1..a_NumElems loop a_Array(i) := 'Test out element # ' || to_char(i); end loop; end; procedure TestIndexBy ( a_Array out nocopy udt_StringList ) is begin a_Array(-1048576) := 'First element'; a_Array(-576) := 'Second element'; a_Array(284) := 'Third element'; a_Array(8388608) := 'Fourth element'; end; end; / create or replace package &main_user..pkg_TestUnicodeArrays as type udt_UnicodeList is table of nvarchar2(100) index by binary_integer; function TestInArrays ( a_StartingLength number, a_Array udt_UnicodeList ) return number; procedure TestInOutArrays ( a_NumElems number, a_Array in out nocopy udt_UnicodeList ); procedure TestOutArrays ( a_NumElems number, a_Array out nocopy udt_UnicodeList ); end; / create or replace package body &main_user..pkg_TestUnicodeArrays as function TestInArrays ( a_StartingLength number, a_Array udt_UnicodeList ) return number is t_Length number; begin t_Length := a_StartingLength; for i in 1..a_Array.count loop t_Length := t_Length + length(a_Array(i)); end loop; return t_Length; end; procedure TestInOutArrays ( a_NumElems number, a_Array in out udt_UnicodeList ) is begin for i in 1..a_NumElems loop a_Array(i) := unistr('Converted element ' || unistr('\3042') || ' # ') || to_char(i) || ' originally had length ' || to_char(length(a_Array(i))); end loop; end; procedure TestOutArrays ( a_NumElems number, a_Array out udt_UnicodeList ) is begin for i in 1..a_NumElems loop a_Array(i) := unistr('Test out element ') || unistr('\3042') || ' # ' || to_char(i); end loop; end; end; / create or replace package &main_user..pkg_TestNumberArrays as type udt_NumberList is table of number index by binary_integer; function TestInArrays ( a_StartingValue number, a_Array udt_NumberList ) return number; procedure TestInOutArrays ( a_NumElems number, a_Array in out nocopy udt_NumberList ); procedure TestOutArrays ( a_NumElems number, a_Array out nocopy udt_NumberList ); end; / create or replace package body &main_user..pkg_TestNumberArrays as function TestInArrays ( a_StartingValue number, a_Array udt_NumberList ) return number is t_Value number; begin t_Value := a_StartingValue; for i in 1..a_Array.count loop t_Value := t_Value + a_Array(i); end loop; return t_Value; end; procedure TestInOutArrays ( a_NumElems number, a_Array in out udt_NumberList ) is begin for i in 1..a_NumElems loop a_Array(i) := a_Array(i) * 10; end loop; end; procedure TestOutArrays ( a_NumElems number, a_Array out udt_NumberList ) is begin for i in 1..a_NumElems loop a_Array(i) := i * 100; end loop; end; end; / create or replace package &main_user..pkg_TestDateArrays as type udt_DateList is table of date index by binary_integer; function TestInArrays ( a_StartingValue number, a_BaseDate date, a_Array udt_DateList ) return number; procedure TestInOutArrays ( a_NumElems number, a_Array in out nocopy udt_DateList ); procedure TestOutArrays ( a_NumElems number, a_Array out nocopy udt_DateList ); end; / create or replace package body &main_user..pkg_TestDateArrays as function TestInArrays ( a_StartingValue number, a_BaseDate date, a_Array udt_DateList ) return number is t_Value number; begin t_Value := a_StartingValue; for i in 1..a_Array.count loop t_Value := t_Value + a_Array(i) - a_BaseDate; end loop; return t_Value; end; procedure TestInOutArrays ( a_NumElems number, a_Array in out udt_DateList ) is begin for i in 1..a_NumElems loop a_Array(i) := a_Array(i) + 7; end loop; end; procedure TestOutArrays ( a_NumElems number, a_Array out udt_DateList ) is begin for i in 1..a_NumElems loop a_Array(i) := to_date(20021212, 'YYYYMMDD') + i * 1.2; end loop; end; end; / create or replace package &main_user..pkg_TestRefCursors as procedure TestOutCursor ( a_MaxIntValue number, a_Cursor out sys_refcursor ); function TestInCursor ( a_Cursor sys_refcursor ) return varchar2; function TestReturnCursor ( a_MaxIntValue number ) return sys_refcursor; procedure TestLobCursor ( a_Value varchar2, a_Cursor out sys_refcursor ); procedure TestCloseCursor ( a_Cursor sys_refcursor ); end; / create or replace package body &main_user..pkg_TestRefCursors as procedure TestOutCursor ( a_MaxIntValue number, a_Cursor out sys_refcursor ) is begin open a_Cursor for select IntCol, StringCol from TestStrings where IntCol <= a_MaxIntValue order by IntCol; end; function TestInCursor ( a_Cursor sys_refcursor ) return varchar2 is t_String varchar2(100); begin fetch a_Cursor into t_String; return t_String || ' (Modified)'; end; function TestReturnCursor ( a_MaxIntValue number ) return sys_refcursor is t_Cursor sys_refcursor; begin open t_Cursor for select IntCol, StringCol from TestSTrings where IntCol <= a_MaxIntValue order by IntCol; return t_Cursor; end; procedure TestLobCursor ( a_Value varchar2, a_Cursor out sys_refcursor ) is begin open a_Cursor for select to_clob(a_Value) from dual; end; procedure TestCloseCursor ( a_Cursor sys_refcursor ) is t_Id number; t_StrVal varchar2(400); begin delete from TestTempTable; fetch a_Cursor into t_Id, t_StrVal; if not a_Cursor%notfound then insert into TestTempTable (IntCol, StringCol1) values (t_Id, t_StrVal); end if; close a_Cursor; commit; end; end; / create or replace package &main_user..pkg_TestBooleans as type udt_BooleanList is table of boolean index by binary_integer; function GetStringRep ( a_Value boolean ) return varchar2; function IsLessThan10 ( a_Value number ) return boolean; function TestInArrays ( a_Value udt_BooleanList ) return number; procedure TestOutArrays ( a_NumElements number, a_Value out nocopy udt_BooleanList ); end; / create or replace package body &main_user..pkg_TestBooleans as function GetStringRep ( a_Value boolean ) return varchar2 is begin if a_Value is null then return 'NULL'; elsif a_Value then return 'TRUE'; end if; return 'FALSE'; end; function IsLessThan10 ( a_Value number ) return boolean is begin return a_Value < 10; end; function TestInArrays ( a_Value udt_BooleanList ) return number is t_Result pls_integer; begin t_Result := 0; for i in 1..a_Value.count loop if a_Value(i) then t_Result := t_Result + 1; end if; end loop; return t_Result; end; procedure TestOutArrays ( a_NumElements number, a_Value out nocopy udt_BooleanList ) is begin for i in 1..a_NumElements loop a_Value(i) := (mod(i, 2) = 1); end loop; end; end; / create or replace package &main_user..pkg_TestBindObject as function GetStringRep ( a_Object udt_Object ) return varchar2; procedure BindObjectOut ( a_NumberValue number, a_StringValue varchar2, a_Object out nocopy udt_Object ); end; / create or replace package body &main_user..pkg_TestBindObject as function GetStringRep ( a_Object udt_SubObject ) return varchar2 is begin if a_Object is null then return 'null'; end if; return 'udt_SubObject(' || nvl(to_char(a_Object.SubNumberValue), 'null') || ', ' || case when a_Object.SubStringValue is null then 'null' else '''' || a_Object.SubStringValue || '''' end || ')'; end; function GetStringRep ( a_Array udt_ObjectArray ) return varchar2 is t_StringRep varchar2(4000); begin if a_Array is null then return 'null'; end if; t_StringRep := 'udt_ObjectArray('; for i in 1..a_Array.count loop if i > 1 then t_StringRep := t_StringRep || ', '; end if; t_StringRep := t_StringRep || GetStringRep(a_Array(i)); end loop; return t_StringRep || ')'; end; function GetStringRep ( a_Object udt_Object ) return varchar2 is begin if a_Object is null then return 'null'; end if; return 'udt_Object(' || nvl(to_char(a_Object.NumberValue), 'null') || ', ' || case when a_Object.StringValue is null then 'null' else '''' || a_Object.StringValue || '''' end || ', ' || case when a_Object.FixedCharValue is null then 'null' else '''' || a_Object.FixedCharValue || '''' end || ', ' || case when a_Object.DateValue is null then 'null' else 'to_date(''' || to_char(a_Object.DateValue, 'YYYY-MM-DD') || ''', ''YYYY-MM-DD'')' end || ', ' || case when a_Object.TimestampValue is null then 'null' else 'to_timestamp(''' || to_char(a_Object.TimestampValue, 'YYYY-MM-DD HH24:MI:SS') || ''', ''YYYY-MM-DD HH24:MI:SS'')' end || ', ' || GetStringRep(a_Object.SubObjectValue) || ', ' || GetStringRep(a_Object.SubObjectArray) || ')'; end; procedure BindObjectOut ( a_NumberValue number, a_StringValue varchar2, a_Object out nocopy udt_Object ) is begin a_Object := udt_Object(a_NumberValue, a_StringValue, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); end; end; / create or replace package &main_user..pkg_TestRecords as type udt_Record is record ( NumberValue number, StringValue varchar2(30), DateValue date, TimestampValue timestamp, BooleanValue boolean, PlsIntegerValue pls_integer, BinaryIntegerValue binary_integer ); type udt_RecordArray is table of udt_Record index by binary_integer; function GetStringRep ( a_Value udt_Record ) return varchar2; procedure TestOut ( a_Value out nocopy udt_Record ); function TestInArrays ( a_Value udt_RecordArray ) return varchar2; end; / create or replace package body &main_user..pkg_TestRecords as function GetStringRep ( a_Value udt_Record ) return varchar2 is begin return 'udt_Record(' || nvl(to_char(a_Value.NumberValue), 'null') || ', ' || case when a_Value.StringValue is null then 'null' else '''' || a_Value.StringValue || '''' end || ', ' || case when a_Value.DateValue is null then 'null' else 'to_date(''' || to_char(a_Value.DateValue, 'YYYY-MM-DD') || ''', ''YYYY-MM-DD'')' end || ', ' || case when a_Value.TimestampValue is null then 'null' else 'to_timestamp(''' || to_char(a_Value.TimestampValue, 'YYYY-MM-DD HH24:MI:SS') || ''', ''YYYY-MM-DD HH24:MI:SS'')' end || ', ' || case when a_Value.BooleanValue is null then 'null' when a_Value.BooleanValue then 'true' else 'false' end || ', ' || nvl(to_char(a_Value.PlsIntegerValue), 'null') || ', ' || nvl(to_char(a_Value.BinaryIntegerValue), 'null') || ')'; end; procedure TestOut ( a_Value out nocopy udt_Record ) is begin a_Value.NumberValue := 25; a_Value.StringValue := 'String in record'; a_Value.DateValue := to_date(20160216, 'YYYYMMDD'); a_Value.TimestampValue := to_timestamp('20160216 18:23:55', 'YYYYMMDD HH24:MI:SS'); a_Value.BooleanValue := true; a_Value.PlsIntegerValue := 45; a_Value.BinaryIntegerValue := 10; end; function TestInArrays ( a_Value udt_RecordArray ) return varchar2 is t_Result varchar2(4000); begin for i in 0..a_Value.count - 1 loop if t_Result is not null then t_Result := t_Result || '; '; end if; t_Result := t_Result || GetStringRep(a_Value(i)); end loop; return t_Result; end; end; / create or replace package &main_user..pkg_SessionCallback as procedure TheCallback ( a_RequestedTag varchar2, a_ActualTag varchar2 ); end; / create or replace package body &main_user..pkg_SessionCallback as type udt_Properties is table of varchar2(64) index by varchar2(64); procedure LogCall ( a_RequestedTag varchar2, a_ActualTag varchar2 ) is pragma autonomous_transaction; begin insert into PlsqlSessionCallbacks values (a_RequestedTag, a_ActualTag, systimestamp); commit; end; procedure ParseProperty ( a_Property varchar2, a_Name out nocopy varchar2, a_Value out nocopy varchar2 ) is t_Pos number; begin t_Pos := instr(a_Property, '='); if t_Pos = 0 then raise_application_error(-20000, 'Tag must contain key=value pairs'); end if; a_Name := substr(a_Property, 1, t_Pos - 1); a_Value := substr(a_Property, t_Pos + 1); end; procedure SetProperty ( a_Name varchar2, a_Value varchar2 ) is t_ValidValues udt_Properties; begin if a_Name = 'TIME_ZONE' then t_ValidValues('UTC') := 'UTC'; t_ValidValues('MST') := '-07:00'; elsif a_Name = 'NLS_DATE_FORMAT' then t_ValidValues('SIMPLE') := 'YYYY-MM-DD HH24:MI'; t_ValidValues('FULL') := 'YYYY-MM-DD HH24:MI:SS'; else raise_application_error(-20000, 'Unsupported session setting'); end if; if not t_ValidValues.exists(a_Value) then raise_application_error(-20000, 'Unsupported session setting'); end if; execute immediate 'ALTER SESSION SET ' || a_Name || '=''' || t_ValidValues(a_Value) || ''''; end; procedure ParseTag ( a_Tag varchar2, a_Properties out nocopy udt_Properties ) is t_PropertyName varchar2(64); t_PropertyValue varchar2(64); t_StartPos number; t_EndPos number; begin t_StartPos := 1; while t_StartPos < length(a_Tag) loop t_EndPos := instr(a_Tag, ';', t_StartPos); if t_EndPos = 0 then t_EndPos := length(a_Tag) + 1; end if; ParseProperty(substr(a_Tag, t_StartPos, t_EndPos - t_StartPos), t_PropertyName, t_PropertyValue); a_Properties(t_PropertyName) := t_PropertyValue; t_StartPos := t_EndPos + 1; end loop; end; procedure TheCallback ( a_RequestedTag varchar2, a_ActualTag varchar2 ) is t_RequestedProps udt_Properties; t_ActualProps udt_Properties; t_PropertyName varchar2(64); begin LogCall(a_RequestedTag, a_ActualTag); ParseTag(a_RequestedTag, t_RequestedProps); ParseTag(a_ActualTag, t_ActualProps); t_PropertyName := t_RequestedProps.first; while t_PropertyName is not null loop if not t_ActualProps.exists(t_PropertyName) or t_ActualProps(t_PropertyName) != t_RequestedProps(t_PropertyName) then SetProperty(t_PropertyName, t_RequestedProps(t_PropertyName)); end if; t_PropertyName := t_RequestedProps.next(t_PropertyName); end loop; end; end; / python-oracledb-1.2.1/tests/sql/create_schema_cloud.sql000066400000000000000000000032741434177474600232670ustar00rootroot00000000000000/*----------------------------------------------------------------------------- * Copyright (c) 2022, Oracle and/or its affiliates. * * This software is dual-licensed to you under the Universal Permissive License * (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License * 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose * either license.* * * If you elect to accept the software under the Apache License, Version 2.0, * the following applies: * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *---------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------- * create_schema_cloud.sql * * Performs the actual work of creating and populating the schemas with the * database objects used by the python-oracledb test suite. This script is * specific to Oracle Cloud and is only executed when being run against the * Oracle Cloud, after the main script (create_schema.sql) is run. It is * executed by the Python script create_schema.py. *---------------------------------------------------------------------------*/ grant select on v$session to &main_user / python-oracledb-1.2.1/tests/sql/drop_schema.sql000066400000000000000000000033301434177474600215730ustar00rootroot00000000000000/*----------------------------------------------------------------------------- * Copyright (c) 2020, 2022, Oracle and/or its affiliates. * * This software is dual-licensed to you under the Universal Permissive License * (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License * 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose * either license.* * * If you elect to accept the software under the Apache License, Version 2.0, * the following applies: * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *---------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------- * drop_schema.sql * * Performs the actual work of dropping the database schemas used by the * python-oracledb test suite. It is executed by the Python script * drop_schema.py. *---------------------------------------------------------------------------*/ begin for r in ( select username from dba_users where username in (upper('&main_user'), upper('&proxy_user')) ) loop execute immediate 'drop user ' || r.username || ' cascade'; end loop; end; / python-oracledb-1.2.1/tests/test_1000_module.py000066400000000000000000000156651434177474600213430ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 1000 - Module for testing top-level module methods """ import datetime import time import oracledb import test_env class TestCase(test_env.BaseTestCase): requires_connection = False def test_1000_date_from_ticks(self): "1000 - test DateFromTicks()" today = datetime.datetime.today() timestamp = time.mktime(today.timetuple()) date = oracledb.DateFromTicks(timestamp) self.assertEqual(date, today.date()) def test_1001_future_obj(self): "1001 - test management of __future__ object" self.assertEqual(oracledb.__future__.dummy, None) oracledb.__future__.dummy = "Unimportant" self.assertEqual(oracledb.__future__.dummy, None) def test_1002_timestamp_from_ticks(self): "1002 - test TimestampFromTicks()" timestamp = time.mktime(datetime.datetime.today().timetuple()) today = datetime.datetime.fromtimestamp(timestamp) date = oracledb.TimestampFromTicks(timestamp) self.assertEqual(date, today) def test_1003_unsupported_functions(self): "1003 - test unsupported time functions" self.assertRaisesRegex(oracledb.NotSupportedError, "^DPY-3000:", oracledb.Time, 12, 0, 0) self.assertRaisesRegex(oracledb.NotSupportedError, "^DPY-3000:", oracledb.TimeFromTicks, 100) def test_1004_makedsn(self): "1004 - test makedsn() with valid arguments" format_string = "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)" + \ "(HOST=%s)(PORT=%d))(CONNECT_DATA=(SID=%s)))" args = ("hostname", 1521, "TEST") result = oracledb.makedsn(*args) self.assertEqual(result, format_string % args) def test_1005_makedsn_invalid_args(self): "1005 - test makedsn() with invalid arguments" self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2020:", oracledb.makedsn, host="(invalid)", port=1521) self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2020:", oracledb.makedsn, host="host", port=1521, sid="(invalid)") self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2020:", oracledb.makedsn, host="host", port=1521, service_name="(invalid)") self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2020:", oracledb.makedsn, host="host", port=1521, region="(invalid)") self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2020:", oracledb.makedsn, host="host", port=1521, sharding_key="(invalid)") self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2020:", oracledb.makedsn, host="host", port=1521, super_sharding_key="(invalid)") def test_1006_aliases(self): "1006 - test aliases match" # database type aliases self.assertIs(oracledb.BFILE, oracledb.DB_TYPE_BFILE) self.assertIs(oracledb.BLOB, oracledb.DB_TYPE_BLOB) self.assertIs(oracledb.BOOLEAN, oracledb.DB_TYPE_BOOLEAN) self.assertIs(oracledb.CLOB, oracledb.DB_TYPE_CLOB) self.assertIs(oracledb.CURSOR, oracledb.DB_TYPE_CURSOR) self.assertIs(oracledb.FIXED_CHAR, oracledb.DB_TYPE_CHAR) self.assertIs(oracledb.FIXED_NCHAR, oracledb.DB_TYPE_NCHAR) self.assertIs(oracledb.INTERVAL, oracledb.DB_TYPE_INTERVAL_DS) self.assertIs(oracledb.LONG_BINARY, oracledb.DB_TYPE_LONG_RAW) self.assertIs(oracledb.LONG_STRING, oracledb.DB_TYPE_LONG) self.assertIs(oracledb.NATIVE_INT, oracledb.DB_TYPE_BINARY_INTEGER) self.assertIs(oracledb.NATIVE_FLOAT, oracledb.DB_TYPE_BINARY_DOUBLE) self.assertIs(oracledb.NCHAR, oracledb.DB_TYPE_NVARCHAR) self.assertIs(oracledb.NCLOB, oracledb.DB_TYPE_NCLOB) self.assertIs(oracledb.OBJECT, oracledb.DB_TYPE_OBJECT) self.assertIs(oracledb.TIMESTAMP, oracledb.DB_TYPE_TIMESTAMP) # type aliases self.assertIs(oracledb.ObjectType, oracledb.DbObjectType) self.assertIs(oracledb.Object, oracledb.DbObject) self.assertIs(oracledb.SessionPool, oracledb.ConnectionPool) # authentication mode aliases self.assertIs(oracledb.DEFAULT_AUTH, oracledb.AUTH_MODE_DEFAULT) self.assertIs(oracledb.SYSASM, oracledb.AUTH_MODE_SYSASM) self.assertIs(oracledb.SYSBKP, oracledb.AUTH_MODE_SYSBKP) self.assertIs(oracledb.SYSDBA, oracledb.AUTH_MODE_SYSDBA) self.assertIs(oracledb.SYSDGD, oracledb.AUTH_MODE_SYSDGD) self.assertIs(oracledb.SYSKMT, oracledb.AUTH_MODE_SYSKMT) self.assertIs(oracledb.SYSOPER, oracledb.AUTH_MODE_SYSOPER) self.assertIs(oracledb.SYSRAC, oracledb.AUTH_MODE_SYSRAC) self.assertIs(oracledb.PRELIM_AUTH, oracledb.AUTH_MODE_PRELIM) # pool "get" mode aliases self.assertIs(oracledb.SPOOL_ATTRVAL_WAIT, oracledb.POOL_GETMODE_WAIT) self.assertIs(oracledb.SPOOL_ATTRVAL_NOWAIT, oracledb.POOL_GETMODE_NOWAIT) self.assertIs(oracledb.SPOOL_ATTRVAL_FORCEGET, oracledb.POOL_GETMODE_FORCEGET) self.assertIs(oracledb.SPOOL_ATTRVAL_TIMEDWAIT, oracledb.POOL_GETMODE_TIMEDWAIT) # purity aliases self.assertIs(oracledb.ATTR_PURITY_DEFAULT, oracledb.PURITY_DEFAULT) self.assertIs(oracledb.ATTR_PURITY_NEW, oracledb.PURITY_NEW) self.assertIs(oracledb.ATTR_PURITY_SELF, oracledb.PURITY_SELF) # other aliases self.assertIs(oracledb.SUBSCR_PROTO_OCI, oracledb.SUBSCR_PROTO_CALLBACK) self.assertIs(oracledb.version, oracledb.__version__) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_1100_connection.py000066400000000000000000000616421434177474600222120ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 1100 - Module for testing connections """ import random import string import threading import time import unittest import oracledb import test_env class TestCase(test_env.BaseTestCase): requires_connection = False def __connect_and_drop(self): """Connect to the database, perform a query and drop the connection.""" with test_env.get_connection() as connection: cursor = connection.cursor() cursor.execute("select count(*) from TestNumbers") count, = cursor.fetchone() self.assertEqual(count, 10) def __verify_fetched_data(self, connection): expected_data = [f"String {i + 1}" for i in range(10)] sql = "select StringCol from TestStrings order by IntCol" for i in range(5): with connection.cursor() as cursor: fetched_data = [s for s, in cursor.execute(sql)] self.assertEqual(fetched_data, expected_data) def __verify_args(self, connection): self.assertEqual(connection.username, test_env.get_main_user(), "user name differs") self.assertEqual(connection.dsn, test_env.get_connect_string(), "dsn differs") def __verify_attributes(self, connection, attrName, value, sql): setattr(connection, attrName, value) cursor = connection.cursor() cursor.execute(sql) result, = cursor.fetchone() self.assertEqual(result, value, "%s value mismatch" % attrName) def test_1100_simple_connection(self): "1100 - simple connection to database" connection = test_env.get_connection() self.__verify_args(connection) @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support application context yet") def test_1101_app_context(self): "1101 - test use of application context" namespace = "CLIENTCONTEXT" app_context_entries = [ ( namespace, "ATTR1", "VALUE1" ), ( namespace, "ATTR2", "VALUE2" ), ( namespace, "ATTR3", "VALUE3" ) ] connection = test_env.get_connection(appcontext=app_context_entries) cursor = connection.cursor() for namespace, name, value in app_context_entries: cursor.execute("select sys_context(:1, :2) from dual", (namespace, name)) actual_value, = cursor.fetchone() self.assertEqual(actual_value, value) @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support application context yet") def test_1102_app_context_negative(self): "1102 - test invalid use of application context" self.assertRaises(TypeError, test_env.get_connection, appcontext=[('userenv', 'action')]) def test_1103_attributes(self): "1103 - test connection end-to-end tracing attributes" connection = test_env.get_connection() if test_env.get_client_version() >= (12, 1) \ and not self.is_on_oracle_cloud(connection): sql = "select dbop_name from v$sql_monitor " \ "where sid = sys_context('userenv', 'sid')" \ "and status = 'EXECUTING'" self.__verify_attributes(connection, "dbop", "oracledb_dbop", sql) sql = "select sys_context('userenv', 'action') from dual" self.__verify_attributes(connection, "action", "oracledb_Action", sql) sql = "select sys_context('userenv', 'module') from dual" self.__verify_attributes(connection, "module", "oracledb_Module", sql) sql = "select sys_context('userenv', 'client_info') from dual" self.__verify_attributes(connection, "clientinfo", "oracledb_cinfo", sql) sql = "select sys_context('userenv', 'client_identifier') from dual" self.__verify_attributes(connection, "client_identifier", "oracledb_cid", sql) if not test_env.get_is_thin(): sql = "select ecid from v$session " \ "where sid = sys_context('userenv', 'sid')" self.__verify_attributes(connection, "econtext_id", "oracledb_ecid", sql) def test_1104_autocommit(self): "1104 - test use of autocommit" connection = test_env.get_connection() cursor = connection.cursor() other_connection = test_env.get_connection() other_cursor = other_connection.cursor() cursor.execute("truncate table TestTempTable") cursor.execute("insert into TestTempTable (IntCol) values (1)") other_cursor.execute("select IntCol from TestTempTable") rows = other_cursor.fetchall() self.assertEqual(rows, []) connection.autocommit = True cursor.execute("insert into TestTempTable (IntCol) values (2)") other_cursor.execute("select IntCol from TestTempTable order by IntCol") rows = other_cursor.fetchall() self.assertEqual(rows, [(1,), (2,)]) def test_1105_bad_connect_string(self): "1105 - connection to database with bad connect string" errors = "^DPY-4000:|^DPY-4001:|^DPY-4026:|^DPY-4027:|^ORA-12154:" self.assertRaisesRegex(oracledb.DatabaseError, errors, oracledb.connect, test_env.get_main_user()) self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4000:|^DPY-4001:", oracledb.connect, test_env.get_main_user() + \ "@" + test_env.get_connect_string()) errors = "^DPY-4000:|^DPY-4001:|^DPY-4017:|^ORA-12154:|^ORA-12521:" self.assertRaisesRegex(oracledb.DatabaseError, errors, oracledb.connect, test_env.get_main_user() + \ "@" + test_env.get_connect_string() + "/" + \ test_env.get_main_password()) def test_1106_bad_password(self): "1106 - connection to database with bad password" self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-01017:", test_env.get_connection, password=test_env.get_main_password() + "X") def test_1107_change_password(self): "1107 - test changing password" connection = test_env.get_connection() if self.is_on_oracle_cloud(connection): self.skipTest("passwords on Oracle Cloud are strictly controlled") sys_random = random.SystemRandom() new_password = "".join(sys_random.choice(string.ascii_letters) \ for i in range(20)) connection.changepassword(test_env.get_main_password(), new_password) connection = test_env.get_connection(password=new_password) connection.changepassword(new_password, test_env.get_main_password()) def test_1108_change_password_negative(self): "1108 - test changing password to an invalid value" connection = test_env.get_connection() if self.is_on_oracle_cloud(connection): self.skipTest("passwords on Oracle Cloud are strictly controlled") new_password = "1" * 1500 self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-01017:|^ORA-00988:", connection.changepassword, test_env.get_main_password(), new_password) def test_1109_parse_password(self): "1109 - test connecting with password containing / and @ symbols" connection = test_env.get_connection() if self.is_on_oracle_cloud(connection): self.skipTest("passwords on Oracle Cloud are strictly controlled") sys_random = random.SystemRandom() chars = list(sys_random.choice(string.ascii_letters) for i in range(20)) chars[4] = "/" chars[8] = "@" new_password = "".join(chars) connection.changepassword(test_env.get_main_password(), new_password) try: test_env.get_connection(password=new_password) finally: connection.changepassword(new_password, test_env.get_main_password()) def test_1112_exception_on_close(self): "1112 - confirm an exception is raised after closing a connection" connection = test_env.get_connection() connection.close() self.assertRaisesRegex(oracledb.InterfaceError, "^DPY-1001:", connection.rollback) @unittest.skipIf(test_env.get_is_thin(), "not relevant for thin mode") def test_1113_connect_with_handle(self): "1113 - test creating a connection using a handle" connection = test_env.get_connection() cursor = connection.cursor() cursor.execute("truncate table TestTempTable") int_value = random.randint(1, 32768) cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (:val, null)""", val=int_value) connection2 = oracledb.connect(handle=connection.handle) cursor = connection2.cursor() cursor.execute("select IntCol from TestTempTable") fetched_int_value, = cursor.fetchone() self.assertEqual(fetched_int_value, int_value) cursor.close() self.assertRaisesRegex(oracledb.DatabaseError, "^DPI-1034:", connection2.close) connection.close() def test_1115_single_arg(self): "1115 - connection to database with user, password, DSN together" arg = "%s/%s@%s" % (test_env.get_main_user(), test_env.get_main_password(), test_env.get_connect_string()) connection = test_env.get_connection(arg) self.__verify_args(connection) def test_1116_version(self): "1116 - connection version is a string" connection = test_env.get_connection() self.assertTrue(isinstance(connection.version, str)) def test_1117_rollback_on_close(self): "1117 - connection rolls back before close" connection = test_env.get_connection() cursor = connection.cursor() cursor.execute("truncate table TestTempTable") other_connection = test_env.get_connection() other_cursor = other_connection.cursor() other_cursor.execute("insert into TestTempTable (IntCol) values (1)") other_cursor.close() other_connection.close() cursor.execute("select count(*) from TestTempTable") count, = cursor.fetchone() self.assertEqual(count, 0) def test_1118_rollback_on_del(self): "1118 - connection rolls back before destruction" connection = test_env.get_connection() cursor = connection.cursor() cursor.execute("truncate table TestTempTable") other_connection = test_env.get_connection() other_cursor = other_connection.cursor() other_cursor.execute("insert into TestTempTable (IntCol) values (1)") del other_cursor del other_connection cursor.execute("select count(*) from TestTempTable") count, = cursor.fetchone() self.assertEqual(count, 0) def test_1119_threading(self): "1119 - multiple connections to database with multiple threads" threads = [] for i in range(20): thread = threading.Thread(None, self.__connect_and_drop) threads.append(thread) thread.start() time.sleep(0.1) for thread in threads: thread.join() def test_1120_string_format(self): "1120 - test string format of connection" connection = test_env.get_connection() expected_value = "" % \ (test_env.get_main_user(), test_env.get_connect_string()) self.assertEqual(str(connection), expected_value) def test_1121_ctx_mgr_close(self): "1121 - test context manager - close" connection = test_env.get_connection() with connection: cursor = connection.cursor() cursor.execute("truncate table TestTempTable") cursor.execute("insert into TestTempTable (IntCol) values (1)") connection.commit() cursor.execute("insert into TestTempTable (IntCol) values (2)") self.assertRaisesRegex(oracledb.InterfaceError, "^DPY-1001:", connection.ping) connection = test_env.get_connection() cursor = connection.cursor() cursor.execute("select count(*) from TestTempTable") count, = cursor.fetchone() self.assertEqual(count, 1) def test_1122_connection_attributes(self): "1122 - test connection attribute values" connection = test_env.get_connection() if test_env.get_client_version() >= (12, 1): self.assertEqual(connection.ltxid, b'') self.assertEqual(connection.current_schema, None) connection.current_schema = "test_schema" self.assertEqual(connection.current_schema, "test_schema") self.assertEqual(connection.edition, None) connection.external_name = "test_external" self.assertEqual(connection.external_name, "test_external") connection.internal_name = "test_internal" self.assertEqual(connection.internal_name, "test_internal") connection.stmtcachesize = 30 self.assertEqual(connection.stmtcachesize, 30) self.assertRaises(TypeError, connection.stmtcachesize, 20.5) self.assertRaises(TypeError, connection.stmtcachesize, "value") def test_1123_closed_connection_attributes(self): "1123 - test closed connection attribute values" connection = test_env.get_connection() connection.close() attr_names = ["current_schema", "edition", "external_name", "internal_name", "stmtcachesize"] if test_env.get_client_version() >= (12, 1): attr_names.append("ltxid") for name in attr_names: self.assertRaisesRegex(oracledb.InterfaceError, "^DPY-1001:", getattr, connection, name) def test_1124_ping(self): "1124 - test connection ping" connection = test_env.get_connection() connection.ping() @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support two-phase commit yet") def test_1125_transaction_begin(self): "1125 - test begin, prepare, cancel transaction" connection = test_env.get_connection() cursor = connection.cursor() cursor.execute("truncate table TestTempTable") connection.begin(10, 'trxnId', 'branchId') self.assertEqual(connection.prepare(), False) connection.begin(10, 'trxnId', 'branchId') cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (1, 'tesName')""") self.assertEqual(connection.prepare(), True) connection.cancel() connection.rollback() cursor.execute("select count(*) from TestTempTable") count, = cursor.fetchone() self.assertEqual(count, 0) @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support two-phase commit yet") def test_1126_multiple_transactions(self): "1126 - test multiple transactions on the same connection" connection = test_env.get_connection() with connection.cursor() as cursor: cursor.execute("truncate table TestTempTable") id_ = random.randint(0, 2 ** 128) xid = (0x1234, "%032x" % id_, "%032x" % 9) connection.begin(*xid) with connection.cursor() as cursor: cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (1, 'tesName')""") self.assertEqual(connection.prepare(), True) connection.commit() for begin_trans in (True, False): val = 3 if begin_trans: connection.begin() val = 2 with connection.cursor() as cursor: cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (:int_val, 'tesName')""", int_val=val) connection.commit() expected_rows = [(1, "tesName"), (2, "tesName"), (3, "tesName")] with connection.cursor() as cursor: cursor.execute("select IntCol, StringCol1 from TestTempTable") self.assertEqual(cursor.fetchall(), expected_rows) @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support two-phase commit yet") def test_1127_multiple_global_transactions(self): "1127 - test multiple global transactions on the same connection" connection = test_env.get_connection() with connection.cursor() as cursor: cursor.execute("truncate table TestTempTable") id_ = random.randint(0, 2 ** 128) xid = (0x1234, "%032x" % id_, "%032x" % 9) connection.begin(*xid) with connection.cursor() as cursor: cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (1, 'tesName')""") self.assertEqual(connection.prepare(), True) connection.commit() for begin_trans in (True, False): val = 3 if begin_trans: connection.begin() val = 2 with connection.cursor() as cursor: cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (:int_val, 'tesName')""", int_val=val) connection.commit() id2_ = random.randint(0, 2 ** 128) xid2 = (0x1234, "%032x" % id2_, "%032x" % 9) connection.begin(*xid2) with connection.cursor() as cursor: cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (4, 'tesName')""") self.assertEqual(connection.prepare(), True) connection.commit() expected_rows = [(1, "tesName"), (2, "tesName"), (3, "tesName"), (4, "tesName")] with connection.cursor() as cursor: cursor.execute("select IntCol, StringCol1 from TestTempTable") self.assertEqual(cursor.fetchall(), expected_rows) @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support two-phase commit yet") def test_1128_exception_creating_global_txn_after_local_txn(self): "1128 - test creating global txn after a local txn" connection = test_env.get_connection() with connection.cursor() as cursor: cursor.execute("truncate table TestTempTable") val = 2 with connection.cursor() as cursor: cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (:int_val, 'tesName')""", int_val=val) id_ = random.randint(0, 2 ** 128) xid = (0x1234, "%032x" % id_, "%032x" % 9) self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-24776:", connection.begin, *xid) def test_1129_threading_single_connection(self): "1129 - single connection to database with multiple threads" with test_env.get_connection() as connection: threads = [threading.Thread(target=self.__verify_fetched_data, args=(connection,)) for i in range(3)] for t in threads: t.start() for t in threads: t.join() def test_1130_cancel(self): "1130 - test connection cancel" conn = test_env.get_connection() sleep_proc_name = test_env.get_sleep_proc_name() def perform_cancel(): time.sleep(0.1) conn.cancel() thread = threading.Thread(target=perform_cancel) thread.start() try: with conn.cursor() as cursor: self.assertRaises(oracledb.OperationalError, cursor.callproc, sleep_proc_name, [2]) finally: thread.join() with conn.cursor() as cursor: cursor.execute("select user from dual") user, = cursor.fetchone() self.assertEqual(user, test_env.get_main_user().upper()) def test_1131_change_password_during_connect(self): "1131 - test changing password during connect" connection = test_env.get_connection() if self.is_on_oracle_cloud(connection): self.skipTest("passwords on Oracle Cloud are strictly controlled") sys_random = random.SystemRandom() new_password = "".join(sys_random.choice(string.ascii_letters) \ for i in range(20)) connection = test_env.get_connection(newpassword=new_password) connection = test_env.get_connection(password=new_password) connection.changepassword(new_password, test_env.get_main_password()) def test_1132_autocommit_during_reexecute(self): "1132 - test use of autocommit during reexecute" sql = "insert into TestTempTable (IntCol, StringCol1) values (:1, :2)" data_to_insert = [ (1, "Test String #1"), (2, "Test String #2") ] connection = test_env.get_connection() cursor = connection.cursor() other_connection = test_env.get_connection() other_cursor = other_connection.cursor() cursor.execute("truncate table TestTempTable") cursor.execute(sql, data_to_insert[0]) other_cursor.execute("select IntCol, StringCol1 from TestTempTable") rows = other_cursor.fetchall() self.assertEqual(rows, []) connection.autocommit = True cursor.execute(sql, data_to_insert[1]) other_cursor.execute("select IntCol, StringCol1 from TestTempTable") rows = other_cursor.fetchall() self.assertEqual(rows, data_to_insert) def test_1133_current_schema(self): "1133 - test current_schema is set properly" conn = test_env.get_connection() self.assertEqual(conn.current_schema, None) user = test_env.get_main_user().upper() proxy_user = test_env.get_proxy_user().upper() cursor = conn.cursor() cursor.execute(f'alter session set current_schema={proxy_user}') self.assertEqual(conn.current_schema, proxy_user) conn.current_schema = user self.assertEqual(conn.current_schema, user) cursor.execute(""" select sys_context('userenv', 'current_schema') from dual""") result, = cursor.fetchone() self.assertEqual(result, user) def test_1134_dbms_output(self): "1134 - test dbms_output package" conn = test_env.get_connection() cursor = conn.cursor() test_string = "Testing DBMS_OUTPUT package" cursor.callproc("dbms_output.enable") cursor.execute(""" begin dbms_output.put_line(:val); end; """, val=test_string) string_var = cursor.var(str) number_var = cursor.var(int) cursor.callproc("dbms_output.get_line", (string_var, number_var)) self.assertEqual(string_var.getvalue(), test_string) @unittest.skipIf(not test_env.get_is_thin() and \ test_env.get_client_version() < (18, 1), "unsupported client") def test_1135_calltimeout(self): "1135 - test connection call_timeout" conn = test_env.get_connection() conn.call_timeout = 500 # milliseconds self.assertEqual(conn.call_timeout, 500) self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4011:|^DPY-4024:", conn.cursor().callproc, test_env.get_sleep_proc_name(), [2]) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_1300_cursor_var.py000066400000000000000000000320211434177474600222270ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 1300 - Module for testing cursor variables """ import oracledb import test_env class TestCase(test_env.BaseTestCase): def test_1300_bind_cursor(self): "1300 - test binding in a cursor" cursor = self.connection.cursor() self.assertEqual(cursor.description, None) self.cursor.execute(""" begin open :cursor for select 'X' StringValue from dual; end;""", cursor=cursor) varchar_ratio, nvarchar_ratio = test_env.get_charset_ratios() expected_value = [ ('STRINGVALUE', oracledb.DB_TYPE_CHAR, 1, varchar_ratio, None, None, True) ] self.assertEqual(cursor.description, expected_value) self.assertEqual(cursor.fetchall(), [('X',)]) def test_1301_bind_cursor_in_package(self): "1301 - test binding in a cursor from a package" cursor = self.connection.cursor() self.assertEqual(cursor.description, None) self.cursor.callproc("pkg_TestRefCursors.TestOutCursor", (2, cursor)) varchar_ratio, nvarchar_ratio = test_env.get_charset_ratios() expected_value = [ ('INTCOL', oracledb.DB_TYPE_NUMBER, 10, None, 9, 0, False), ('STRINGCOL', oracledb.DB_TYPE_VARCHAR, 20, 20 * varchar_ratio, None, None, False) ] self.assertEqual(cursor.description, expected_value) self.assertEqual(cursor.fetchall(), [(1, 'String 1'), (2, 'String 2')]) def test_1302_bind_self(self): "1302 - test that binding the cursor itself is not supported" cursor = self.connection.cursor() sql = """ begin open :pcursor for select 1 from dual; end;""" self.assertRaisesRegex(oracledb.NotSupportedError, "^DPY-3009:", cursor.execute, sql, pcursor=cursor) def test_1303_execute_after_close(self): "1303 - test returning a ref cursor after closing it" out_cursor = self.connection.cursor() sql = """ begin open :pcursor for select IntCol from TestNumbers order by IntCol; end;""" self.cursor.execute(sql, pcursor=out_cursor) rows = out_cursor.fetchall() out_cursor.close() out_cursor = self.connection.cursor() self.cursor.execute(sql, pcursor=out_cursor) rows2 = out_cursor.fetchall() self.assertEqual(rows, rows2) def test_1304_fetch_cursor(self): "1304 - test fetching a cursor" self.cursor.execute(""" select IntCol, cursor(select IntCol + 1 from dual) CursorValue from TestNumbers order by IntCol""") expected_value = [ ('INTCOL', oracledb.DB_TYPE_NUMBER, 10, None, 9, 0, False), ('CURSORVALUE', oracledb.DB_TYPE_CURSOR, None, None, None, None, True) ] self.assertEqual(self.cursor.description, expected_value) for i in range(1, 11): number, cursor = self.cursor.fetchone() self.assertEqual(number, i) self.assertEqual(cursor.fetchall(), [(i + 1,)]) def test_1305_ref_cursor_binds(self): "1305 - test that ref cursor binds cannot use optimised path" ref_cursor = self.connection.cursor() sql = """ begin open :rcursor for select IntCol, StringCol from TestStrings where IntCol between :start_value and :end_value; end;""" self.cursor.execute(sql, rcursor=ref_cursor, start_value=2, end_value=4) expected_value = [ (2, 'String 2'), (3, 'String 3'), (4, 'String 4') ] rows = ref_cursor.fetchall() ref_cursor.close() self.assertEqual(rows, expected_value) ref_cursor = self.connection.cursor() self.cursor.execute(sql, rcursor=ref_cursor, start_value=5, end_value=6) expected_value = [ (5, 'String 5'), (6, 'String 6') ] rows = ref_cursor.fetchall() self.assertEqual(rows, expected_value) def test_1306_refcursor_prefetchrows(self): "1306 - test prefetch rows and arraysize using a refcursor" self.setup_round_trip_checker() # simple DDL only requires a single round trip with self.connection.cursor() as cursor: cursor.execute("truncate table TestTempTable") self.assertRoundTrips(1) # array execution only requires a single round trip num_rows = 590 with self.connection.cursor() as cursor: sql = "insert into TestTempTable (IntCol) values (:1)" data = [(n + 1,) for n in range(num_rows)] cursor.executemany(sql, data) self.assertRoundTrips(1) # create refcursor and execute stored procedure with self.connection.cursor() as cursor: refcursor = self.connection.cursor() refcursor.prefetchrows = 150 refcursor.arraysize = 50 cursor.callproc("myrefcursorproc", [refcursor]) refcursor.fetchall() self.assertRoundTrips(4) def test_1307_refcursor_execute_different_sql(self): "1307 - test executing different SQL after getting a REF cursor" with self.connection.cursor() as cursor: refcursor = self.connection.cursor() cursor.callproc("myrefcursorproc", [refcursor]) var = cursor.var(int) refcursor.execute("begin :1 := 15; end;", [var]) self.assertEqual(var.getvalue(), 15) def test_1308_function_with_ref_cursor_return(self): "1308 - test calling a function that returns a REF cursor" with self.connection.cursor() as cursor: ref_cursor = cursor.callfunc("pkg_TestRefCursors.TestReturnCursor", oracledb.DB_TYPE_CURSOR, [2]) rows = ref_cursor.fetchall() self.assertEqual(rows, [(1, 'String 1'), (2, 'String 2')]) def test_1309_output_type_handler_with_ref_cursor(self): "1309 - test using an output type handler with a REF cursor" def type_handler(cursor, name, default_type, size, precision, scale): return cursor.var(str, arraysize=cursor.arraysize) self.connection.outputtypehandler = type_handler var = self.cursor.var(oracledb.DB_TYPE_CURSOR) string_val = "Test String - 1309" with self.connection.cursor() as cursor: cursor.callproc("pkg_TestRefCursors.TestLobCursor", [string_val, var]) ref_cursor = var.getvalue() self.assertEqual(ref_cursor.fetchall(), [(string_val,)]) def test_1310_unassigned_ref_cursor(self): "1310 - bind a REF cursor but never open it" ref_cursor_var = self.cursor.var(oracledb.DB_TYPE_CURSOR) self.cursor.execute(""" begin if false then open :cursor for select user from dual; end if; end;""", cursor=ref_cursor_var) ref_cursor = ref_cursor_var.getvalue() self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4025:", ref_cursor.fetchall) def test_1311_fetch_cursor_uses_custom_class(self): "1311 - test fetching a cursor with a custom class" class Counter: num_cursors_created = 0 @classmethod def cursor_created(cls): cls.num_cursors_created += 1 class MyConnection(oracledb.Connection): def cursor(self): Counter.cursor_created() return super().cursor() conn = test_env.get_connection(conn_class=MyConnection) cursor = conn.cursor() cursor.execute(""" select cursor(select 1 from dual), cursor(select 2 from dual) from dual""") cursor.fetchall() self.assertEqual(Counter.num_cursors_created, 3) def test_1312_fetch_nested_cursors_for_complex_sql(self): "1312 - test that nested cursors are fetched correctly" sql = """ select 'Level 1 String', cursor( select 'Level 2 String', cursor( select 'Level 3 String', cursor( select 1, 'Level 4 String A' from dual union all select 2, 'Level 4 String B' from dual union all select 3, 'Level 4 String C' from dual ) as nc3 from dual ) as nc2 from dual ) as nc1 from dual""" self.cursor.execute(sql) transform_fn = lambda v: \ [transform_row(r) for r in v] \ if isinstance(v, oracledb.Cursor) \ else v transform_row = lambda r: tuple(transform_fn(v) for v in r) rows = [transform_row(r) for r in self.cursor] expected_value = [ ('Level 1 String', [ ('Level 2 String', [ ('Level 3 String', [ (1, 'Level 4 String A'), (2, 'Level 4 String B'), (3, 'Level 4 String C') ]), ]), ]) ] self.assertEqual(rows, expected_value) def test_1313_fetch_nested_cursors_with_more_cols_than_parent(self): "1313 - test fetching nested cursors with more columns than parent" sql = """ select 'Top Level String', cursor( select 'Nested String 1', 'Nested String 2', 'Nested String 3' from dual ) from dual""" self.cursor.execute(sql) transform_fn = lambda v: \ [transform_row(r) for r in v] \ if isinstance(v, oracledb.Cursor) \ else v transform_row = lambda r: tuple(transform_fn(v) for v in r) rows = [transform_row(r) for r in self.cursor] expected_value = [ ('Top Level String', [ ('Nested String 1', 'Nested String 2', 'Nested String 3') ]) ] self.assertEqual(rows, expected_value) def test_1314_reuse_closed_ref_cursor_with_different_sql(self): "1314 - test reusing a closed ref cursor for executing different sql" sql = "select 13141, 'String 13141' from dual" ref_cursor = self.connection.cursor() ref_cursor.prefetchrows = 0 ref_cursor.execute(sql) plsql = "begin pkg_TestRefCursors.TestCloseCursor(:rcursor); end;" self.cursor.execute(plsql, rcursor=ref_cursor) sql = "select 13142, 'String 13142' from dual" ref_cursor.execute(sql) self.assertEqual(ref_cursor.fetchall(), [(13142, 'String 13142'),]) def test_1315_reuse_closed_ref_cursor_with_same_sql(self): "1315 - test reusing a closed ref cursor for executing same sql" sql = "select 1315, 'String 1315' from dual" ref_cursor = self.connection.cursor() ref_cursor.prefetchrows = 0 ref_cursor.execute(sql) plsql = "begin pkg_TestRefCursors.TestCloseCursor(:rcursor); end;" self.cursor.execute(plsql, rcursor=ref_cursor) ref_cursor.execute(sql) self.assertEqual(ref_cursor.fetchall(), [(1315, 'String 1315'),]) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_1400_datetime_var.py000066400000000000000000000264501434177474600225200ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 1400 - Module for testing date/time variables """ import datetime import time import oracledb import test_env class TestCase(test_env.BaseTestCase): def setUp(self): super().setUp() self.raw_data = [] self.data_by_key = {} for i in range(1, 11): time_tuple = (2002, 12, 9, 0, 0, 0, 0, 0, -1) time_in_ticks = time.mktime(time_tuple) + i * 86400 + i * 8640 date_col = oracledb.TimestampFromTicks(int(time_in_ticks)) if i % 2: time_in_ticks = time.mktime(time_tuple) + i * 86400 * 2 + \ i * 12960 nullable_col = oracledb.TimestampFromTicks(int(time_in_ticks)) else: nullable_col = None tuple = (i, date_col, nullable_col) self.raw_data.append(tuple) self.data_by_key[i] = tuple def test_1400_bind_date(self): "1400 - test binding in a date" self.cursor.execute(""" select * from TestDates where DateCol = :value""", value = oracledb.Timestamp(2002, 12, 13, 9, 36, 0)) self.assertEqual(self.cursor.fetchall(), [self.data_by_key[4]]) def test_1401_bind_datetime(self): "1401 - test binding in a datetime.datetime value" self.cursor.execute(""" select * from TestDates where DateCol = :value""", value=datetime.datetime(2002, 12, 13, 9, 36, 0)) self.assertEqual(self.cursor.fetchall(), [self.data_by_key[4]]) def test_1402_bind_date_in_datetime_var(self): "1402 - test binding date in a datetime variable" var = self.cursor.var(oracledb.DATETIME) date_val = datetime.date.today() var.setvalue(0, date_val) self.cursor.execute("select :1 from dual", [var]) result, = self.cursor.fetchone() self.assertEqual(result.date(), date_val) def test_1403_bind_date_after_string(self): "1403 - test binding in a date after setting input sizes to a string" self.cursor.setinputsizes(value=15) self.cursor.execute(""" select * from TestDates where DateCol = :value""", value = oracledb.Timestamp(2002, 12, 14, 12, 0, 0)) self.assertEqual(self.cursor.fetchall(), [self.data_by_key[5]]) def test_1404_bind_null(self): "1404 - test binding in a null" self.cursor.setinputsizes(value=oracledb.DATETIME) self.cursor.execute(""" select * from TestDates where DateCol = :value""", value = None) self.assertEqual(self.cursor.fetchall(), []) def test_1405_bind_date_array_direct(self): "1405 - test binding in a date array" return_value = self.cursor.var(oracledb.NUMBER) array = [r[1] for r in self.raw_data] statement = """ begin :return_value := pkg_TestDateArrays.TestInArrays( :start_value, :base_date, :array); end;""" self.cursor.execute(statement, return_value=return_value, start_value=5, base_date=oracledb.Date(2002, 12, 12), array=array) self.assertEqual(return_value.getvalue(), 35.5) array = array + array[:5] self.cursor.execute(statement, start_value=7, base_date=oracledb.Date(2002, 12, 13), array=array) self.assertEqual(return_value.getvalue(), 24.0) def test_1406_bind_date_array_by_sizes(self): "1406 - test binding in a date array (with setinputsizes)" return_value = self.cursor.var(oracledb.NUMBER) self.cursor.setinputsizes(array=[oracledb.DATETIME, 10]) array = [r[1] for r in self.raw_data] self.cursor.execute(""" begin :return_value := pkg_TestDateArrays.TestInArrays( :start_value, :base_date, :array); end;""", return_value=return_value, start_value=6, base_date=oracledb.Date(2002, 12, 13), array=array) self.assertEqual(return_value.getvalue(), 26.5) def test_1407_bind_date_array_by_var(self): "1407 - test binding in a date array (with arrayvar)" return_value = self.cursor.var(oracledb.NUMBER) array = self.cursor.arrayvar(oracledb.DATETIME, 10, 20) array.setvalue(0, [r[1] for r in self.raw_data]) self.cursor.execute(""" begin :return_value := pkg_TestDateArrays.TestInArrays( :start_value, :base_date, :array); end;""", return_value=return_value, start_value=7, base_date=oracledb.Date(2002, 12, 14), array=array) self.assertEqual(return_value.getvalue(), 17.5) def test_1408_bind_in_out_date_array_by_var(self): "1408 - test binding in/out a date array (with arrayvar)" array = self.cursor.arrayvar(oracledb.DATETIME, 10, 100) original_data = [r[1] for r in self.raw_data] array.setvalue(0, original_data) self.cursor.execute(""" begin pkg_TestDateArrays.TestInOutArrays(:num_elems, :array); end;""", num_elems=5, array=array) self.assertEqual(array.getvalue(), [ oracledb.Timestamp(2002, 12, 17, 2, 24, 0), oracledb.Timestamp(2002, 12, 18, 4, 48, 0), oracledb.Timestamp(2002, 12, 19, 7, 12, 0), oracledb.Timestamp(2002, 12, 20, 9, 36, 0), oracledb.Timestamp(2002, 12, 21, 12, 0, 0) ] + \ original_data[5:]) def test_1409_bind_out_date_array_by_var(self): "1409 - test binding out a date array (with arrayvar)" array = self.cursor.arrayvar(oracledb.DATETIME, 6, 100) self.cursor.execute(""" begin pkg_TestDateArrays.TestOutArrays(:num_elems, :array); end;""", num_elems=6, array=array) self.assertEqual(array.getvalue(), [ oracledb.Timestamp(2002, 12, 13, 4, 48, 0), oracledb.Timestamp(2002, 12, 14, 9, 36, 0), oracledb.Timestamp(2002, 12, 15, 14, 24, 0), oracledb.Timestamp(2002, 12, 16, 19, 12, 0), oracledb.Timestamp(2002, 12, 18, 0, 0, 0), oracledb.Timestamp(2002, 12, 19, 4, 48, 0) ]) def test_1410_bind_out_set_input_sizes(self): "1410 - test binding out with set input sizes defined" bind_vars = self.cursor.setinputsizes(value=oracledb.DATETIME) self.cursor.execute(""" begin :value := to_date(20021209, 'YYYYMMDD'); end;""") self.assertEqual(bind_vars["value"].getvalue(), oracledb.Timestamp(2002, 12, 9)) def test_1411_bind_in_out_set_input_sizes(self): "1411 - test binding in/out with set input sizes defined" bind_vars = self.cursor.setinputsizes(value=oracledb.DATETIME) self.cursor.execute(""" begin :value := :value + 5.25; end;""", value=oracledb.Timestamp(2002, 12, 12, 10, 0, 0)) self.assertEqual(bind_vars["value"].getvalue(), oracledb.Timestamp(2002, 12, 17, 16, 0, 0)) def test_1412_bind_out_var(self): "1412 - test binding out with cursor.var() method" var = self.cursor.var(oracledb.DATETIME) self.cursor.execute(""" begin :value := to_date('20021231 12:31:00', 'YYYYMMDD HH24:MI:SS'); end;""", value=var) self.assertEqual(var.getvalue(), oracledb.Timestamp(2002, 12, 31, 12, 31, 0)) def test_1413_bind_in_out_var_direct_set(self): "1413 - test binding in/out with cursor.var() method" var = self.cursor.var(oracledb.DATETIME) var.setvalue(0, oracledb.Timestamp(2002, 12, 9, 6, 0, 0)) self.cursor.execute(""" begin :value := :value + 5.25; end;""", value=var) self.assertEqual(var.getvalue(), oracledb.Timestamp(2002, 12, 14, 12, 0, 0)) def test_1414_cursor_description(self): "1414 - test cursor description is accurate" self.cursor.execute("select * from TestDates") expected_value = [ ('INTCOL', oracledb.DB_TYPE_NUMBER, 10, None, 9, 0, False), ('DATECOL', oracledb.DB_TYPE_DATE, 23, None, None, None, False), ('NULLABLECOL', oracledb.DB_TYPE_DATE, 23, None, None, None, True) ] self.assertEqual(self.cursor.description, expected_value) def test_1415_fetchall(self): "1415 - test that fetching all of the data returns the correct results" self.cursor.execute("select * From TestDates order by IntCol") self.assertEqual(self.cursor.fetchall(), self.raw_data) self.assertEqual(self.cursor.fetchall(), []) def test_1416_fetchmany(self): "1416 - test that fetching data in chunks returns the correct results" self.cursor.execute("select * From TestDates order by IntCol") self.assertEqual(self.cursor.fetchmany(3), self.raw_data[0:3]) self.assertEqual(self.cursor.fetchmany(2), self.raw_data[3:5]) self.assertEqual(self.cursor.fetchmany(4), self.raw_data[5:9]) self.assertEqual(self.cursor.fetchmany(3), self.raw_data[9:]) self.assertEqual(self.cursor.fetchmany(3), []) def test_1417_fetchone(self): "1417 - test that fetching a single row returns the correct results" self.cursor.execute(""" select * from TestDates where IntCol in (3, 4) order by IntCol""") self.assertEqual(self.cursor.fetchone(), self.data_by_key[3]) self.assertEqual(self.cursor.fetchone(), self.data_by_key[4]) self.assertEqual(self.cursor.fetchone(), None) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_1500_types.py000066400000000000000000000215461434177474600212220ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 1500 - Module for testing comparisons with database types and API types, including the synonyms retained for backwards compatibility. This module also tests for pickling/unpickling of database types and API types. """ import pickle import oracledb import test_env class TestCase(test_env.BaseTestCase): requires_connection = False def __test_compare(self, db_type, api_type): self.assertEqual(db_type, db_type) self.assertEqual(db_type, api_type) self.assertEqual(api_type, db_type) self.assertNotEqual(db_type, 5) self.assertNotEqual(db_type, oracledb.DB_TYPE_OBJECT) def __test_pickle(self, typ): self.assertIs(typ, pickle.loads(pickle.dumps(typ))) def test_1500_DB_TYPE_BFILE(self): "1500 - test oracledb.DB_TYPE_BFILE comparisons and pickling" self.assertEqual(oracledb.DB_TYPE_BFILE, oracledb.BFILE) self.__test_pickle(oracledb.DB_TYPE_BFILE) def test_1501_DB_TYPE_BINARY_DOUBLE(self): "1501 - test oracledb.DB_TYPE_BINARY_DOUBLE comparisons and pickling" self.__test_compare(oracledb.DB_TYPE_BINARY_DOUBLE, oracledb.NUMBER) self.assertEqual(oracledb.DB_TYPE_BINARY_DOUBLE, oracledb.NATIVE_FLOAT) self.__test_pickle(oracledb.DB_TYPE_BINARY_DOUBLE) def test_1502_DB_TYPE_BINARY_FLOAT(self): "1502 - test oracledb.DB_TYPE_BINARY_FLOAT comparisons and pickling" self.__test_compare(oracledb.DB_TYPE_BINARY_FLOAT, oracledb.NUMBER) self.__test_pickle(oracledb.DB_TYPE_BINARY_FLOAT) def test_1503_DB_TYPE_BINARY_INTEGER(self): "1503 - test oracledb.DB_TYPE_BINARY_INTEGER comparisons and pickling" self.__test_compare(oracledb.DB_TYPE_BINARY_INTEGER, oracledb.NUMBER) self.assertEqual(oracledb.DB_TYPE_BINARY_INTEGER, oracledb.NATIVE_INT) self.__test_pickle(oracledb.DB_TYPE_BINARY_INTEGER) def test_1504_DB_TYPE_BLOB(self): "1504 - test oracledb.DB_TYPE_BLOB comparisons and pickling" self.assertEqual(oracledb.DB_TYPE_BLOB, oracledb.BLOB) self.__test_pickle(oracledb.DB_TYPE_BLOB) def test_1505_DB_TYPE_BOOLEAN(self): "1505 - test oracledb.DB_TYPE_BOOLEAN comparisons and pickling" self.assertEqual(oracledb.DB_TYPE_BOOLEAN, oracledb.BOOLEAN) self.__test_pickle(oracledb.DB_TYPE_BOOLEAN) def test_1506_DB_TYPE_CHAR(self): "1506 - test oracledb.DB_TYPE_CHAR comparisons and pickling" self.__test_compare(oracledb.DB_TYPE_CHAR, oracledb.STRING) self.assertEqual(oracledb.DB_TYPE_CHAR, oracledb.FIXED_CHAR) self.__test_pickle(oracledb.DB_TYPE_CHAR) def test_1507_DB_TYPE_CLOB(self): "1507 - test oracledb.DB_TYPE_CLOB comparisons and pickling" self.assertEqual(oracledb.DB_TYPE_CLOB, oracledb.CLOB) self.__test_pickle(oracledb.DB_TYPE_CLOB) def test_1508_DB_TYPE_CURSOR(self): "1508 - test oracledb.DB_TYPE_CURSOR comparisons and pickling" self.assertEqual(oracledb.DB_TYPE_CURSOR, oracledb.CURSOR) self.__test_pickle(oracledb.DB_TYPE_CURSOR) def test_1509_DB_TYPE_DATE(self): "1509 - test oracledb.DB_TYPE_DATE comparisons and pickling" self.__test_compare(oracledb.DB_TYPE_DATE, oracledb.DATETIME) self.__test_pickle(oracledb.DB_TYPE_DATE) def test_1510_DB_TYPE_INTERVAL_DS(self): "1510 - test oracledb.DB_TYPE_INTERVAL_DS comparisons and pickling" self.assertEqual(oracledb.DB_TYPE_INTERVAL_DS, oracledb.INTERVAL) self.__test_pickle(oracledb.DB_TYPE_INTERVAL_DS) def test_1511_DB_TYPE_LONG(self): "1511 - test oracledb.DB_TYPE_LONG comparisons and pickling" self.__test_compare(oracledb.DB_TYPE_LONG, oracledb.STRING) self.assertEqual(oracledb.DB_TYPE_LONG, oracledb.LONG_STRING) self.__test_pickle(oracledb.DB_TYPE_LONG) def test_1512_DB_TYPE_LONG_RAW(self): "1512 - test oracledb.DB_TYPE_LONG_RAW comparisons and pickling" self.__test_compare(oracledb.DB_TYPE_LONG_RAW, oracledb.BINARY) self.assertEqual(oracledb.DB_TYPE_LONG_RAW, oracledb.LONG_BINARY) self.__test_pickle(oracledb.DB_TYPE_LONG_RAW) def test_1513_DB_TYPE_NCHAR(self): "1513 - test oracledb.DB_TYPE_NCHAR comparisons and pickling" self.__test_compare(oracledb.DB_TYPE_NCHAR, oracledb.STRING) self.assertEqual(oracledb.DB_TYPE_NCHAR, oracledb.FIXED_NCHAR) self.__test_pickle(oracledb.DB_TYPE_NCHAR) def test_1514_DB_TYPE_NCLOB(self): "1514 - test oracledb.DB_TYPE_NCLOB comparisons and pickling" self.assertEqual(oracledb.DB_TYPE_NCLOB, oracledb.NCLOB) self.__test_pickle(oracledb.DB_TYPE_NCLOB) def test_1515_DB_TYPE_NUMBER(self): "1515 - test oracledb.DB_TYPE_NUMBER comparisons and pickling" self.__test_compare(oracledb.DB_TYPE_NUMBER, oracledb.NUMBER) self.__test_pickle(oracledb.DB_TYPE_NUMBER) def test_1516_DB_TYPE_NVARCHAR(self): "1516 - test oracledb.DB_TYPE_NVARCHAR comparisons and pickling" self.__test_compare(oracledb.DB_TYPE_NVARCHAR, oracledb.STRING) self.assertEqual(oracledb.DB_TYPE_NVARCHAR, oracledb.NCHAR) self.__test_pickle(oracledb.DB_TYPE_NVARCHAR) def test_1517_DB_TYPE_OBJECT(self): "1517 - test oracledb.DB_TYPE_OBJECT comparisons and pickling" self.assertEqual(oracledb.DB_TYPE_OBJECT, oracledb.OBJECT) self.__test_pickle(oracledb.DB_TYPE_OBJECT) def test_1518_DB_TYPE_RAW(self): "1518 - test oracledb.DB_TYPE_RAW comparisons and pickling" self.__test_compare(oracledb.DB_TYPE_RAW, oracledb.BINARY) self.__test_pickle(oracledb.DB_TYPE_RAW) def test_1519_DB_TYPE_ROWID(self): "1519 - test oracledb.DB_TYPE_ROWID comparisons and pickling" self.__test_compare(oracledb.DB_TYPE_ROWID, oracledb.ROWID) self.__test_pickle(oracledb.DB_TYPE_ROWID) def test_1520_DB_TYPE_TIMESTAMP(self): "1520 - test oracledb.DB_TYPE_TIMESTAMP comparisons and pickling" self.__test_compare(oracledb.DB_TYPE_TIMESTAMP, oracledb.DATETIME) self.assertEqual(oracledb.DB_TYPE_TIMESTAMP, oracledb.TIMESTAMP) self.__test_pickle(oracledb.DB_TYPE_TIMESTAMP) def test_1521_DB_TYPE_TIMESTAMP_LTZ(self): "1521 - test oracledb.DB_TYPE_TIMESTAMP_LTZ comparisons and pickling" self.__test_compare(oracledb.DB_TYPE_TIMESTAMP_LTZ, oracledb.DATETIME) self.__test_pickle(oracledb.DB_TYPE_TIMESTAMP_LTZ) def test_1522_DB_TYPE_TIMESTAMP_TZ(self): "1522 - test oracledb.DB_TYPE_TIMESTAMP_TZ comparisons and pickling" self.__test_compare(oracledb.DB_TYPE_TIMESTAMP_TZ, oracledb.DATETIME) self.__test_pickle(oracledb.DB_TYPE_TIMESTAMP_TZ) def test_1523_DB_TYPE_VARCHAR(self): "1523 - test oracledb.DB_TYPE_VARCHAR comparisons and pickling" self.__test_compare(oracledb.DB_TYPE_VARCHAR, oracledb.STRING) self.__test_pickle(oracledb.DB_TYPE_VARCHAR) def test_1524_NUMBER(self): "1524 - test oracledb.NUMBER pickling" self.__test_pickle(oracledb.NUMBER) def test_1525_STRING(self): "1525 - test oracledb.STRING pickling" self.__test_pickle(oracledb.STRING) def test_1526_DATETIME(self): "1526 - test oracledb.DATETIME pickling" self.__test_pickle(oracledb.DATETIME) def test_1527_BINARY(self): "1527 - test oracledb.BINARY pickling" self.__test_pickle(oracledb.BINARY) def test_1528_ROWID(self): "1528 - test oracledb.ROWID pickling" self.__test_pickle(oracledb.ROWID) def test_1529_DB_TYPE_UROWID(self): "1529 - test oracledb.DB_TYPE_UROWID comparisons and pickling" self.__test_compare(oracledb.DB_TYPE_UROWID, oracledb.ROWID) self.__test_pickle(oracledb.DB_TYPE_UROWID) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_1600_dml_returning.py000066400000000000000000000446101434177474600227250ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 1600 - Module for testing DML returning clauses """ import datetime import unittest import oracledb import test_env class TestCase(test_env.BaseTestCase): def test_1600_insert(self): "1600 - test insert (single row) with DML returning" self.cursor.execute("truncate table TestTempTable") int_val = 5 str_val = "A test string" int_var = self.cursor.var(oracledb.NUMBER) str_var = self.cursor.var(str) self.cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (:int_val, :str_val) returning IntCol, StringCol1 into :int_var, :str_var""", int_val=int_val, str_val=str_val, int_var=int_var, str_var=str_var) self.assertEqual(int_var.values, [[int_val]]) self.assertEqual(str_var.values, [[str_val]]) def test_1601_insert_many(self): "1601 - test insert (multiple rows) with DML returning" self.cursor.execute("truncate table TestTempTable") int_values = [5, 8, 17, 24, 6] str_values = ["Test 5", "Test 8", "Test 17", "Test 24", "Test 6"] int_var = self.cursor.var(oracledb.NUMBER, arraysize=len(int_values)) str_var = self.cursor.var(str, arraysize=len(int_values)) self.cursor.setinputsizes(None, None, int_var, str_var) data = list(zip(int_values, str_values)) self.cursor.executemany(""" insert into TestTempTable (IntCol, StringCol1) values (:int_val, :str_val) returning IntCol, StringCol1 into :int_var, :str_var""", data) self.assertEqual(int_var.values, [[v] for v in int_values]) self.assertEqual(str_var.values, [[v] for v in str_values]) def test_1602_insert_with_small_size(self): "1602 - test insert with DML returning into too small a variable" self.cursor.execute("truncate table TestTempTable") int_val = 6 str_val = "A different test string" int_var = self.cursor.var(oracledb.NUMBER) str_var = self.cursor.var(str, 2) parameters = dict(int_val=int_val, str_val=str_val, int_var=int_var, str_var=str_var) self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4002:|^DPI-1037:", self.cursor.execute, """ insert into TestTempTable (IntCol, StringCol1) values (:int_val, :str_val) returning IntCol, StringCol1 into :int_var, :str_var""", parameters) def test_1603_update_single_row(self): "1603 - test update single row with DML returning" int_val = 7 str_val = "The updated value of the string" self.cursor.execute("truncate table TestTempTable") self.cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (:1, :2)""", (int_val, "The initial value of the string")) int_var = self.cursor.var(oracledb.NUMBER) str_var = self.cursor.var(str) self.cursor.execute(""" update TestTempTable set StringCol1 = :str_val where IntCol = :int_val returning IntCol, StringCol1 into :int_var, :str_var""", int_val=int_val, str_val=str_val, int_var=int_var, str_var=str_var) self.assertEqual(int_var.values, [[int_val]]) self.assertEqual(str_var.values, [[str_val]]) def test_1604_update_no_rows(self): "1604 - test update no rows with DML returning" int_val = 8 str_val = "The updated value of the string" self.cursor.execute("truncate table TestTempTable") self.cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (:1, :2)""", (int_val, "The initial value of the string")) int_var = self.cursor.var(oracledb.NUMBER) str_var = self.cursor.var(str) self.cursor.execute(""" update TestTempTable set StringCol1 = :str_val where IntCol = :int_val returning IntCol, StringCol1 into :int_var, :str_var""", int_val=int_val + 1, str_val=str_val, int_var=int_var, str_var=str_var) self.assertEqual(int_var.values, [[]]) self.assertEqual(str_var.values, [[]]) self.assertEqual(int_var.getvalue(), []) self.assertEqual(str_var.getvalue(), []) def test_1605_update_multiple_rows(self): "1605 - test update multiple rows with DML returning" self.cursor.execute("truncate table TestTempTable") for i in (8, 9, 10): self.cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (:1, :2)""", (i, "The initial value of string %d" % i)) int_var = self.cursor.var(oracledb.NUMBER) str_var = self.cursor.var(str) self.cursor.execute(""" update TestTempTable set IntCol = IntCol + 15, StringCol1 = 'The final value of string ' || to_char(IntCol) returning IntCol, StringCol1 into :int_var, :str_var""", int_var=int_var, str_var=str_var) self.assertEqual(self.cursor.rowcount, 3) self.assertEqual(int_var.values, [[23, 24, 25]]) expected_values = [[ "The final value of string 8", "The final value of string 9", "The final value of string 10" ]] self.assertEqual(str_var.values, expected_values) def test_1606_update_multiple_rows_executemany(self): "1606 - test update multiple rows with DML returning (executemany)" data = [(i, "The initial value of string %d" % i) \ for i in range(1, 11)] self.cursor.execute("truncate table TestTempTable") self.cursor.executemany(""" insert into TestTempTable (IntCol, StringCol1) values (:1, :2)""", data) int_var = self.cursor.var(oracledb.NUMBER, arraysize=3) str_var = self.cursor.var(str, arraysize=3) self.cursor.setinputsizes(None, int_var, str_var) self.cursor.executemany(""" update TestTempTable set IntCol = IntCol + 25, StringCol1 = 'Updated value of string ' || to_char(IntCol) where IntCol < :inVal returning IntCol, StringCol1 into :int_var, :str_var""", [[3], [8], [11]]) expected_values = [ [26, 27], [28, 29, 30, 31, 32], [33, 34, 35] ] self.assertEqual(int_var.values, expected_values) expected_values = [ [ "Updated value of string 1", "Updated value of string 2" ], [ "Updated value of string 3", "Updated value of string 4", "Updated value of string 5", "Updated value of string 6", "Updated value of string 7" ], [ "Updated value of string 8", "Updated value of string 9", "Updated value of string 10" ] ] self.assertEqual(str_var.values, expected_values) def test_1607_insert_and_return_object(self): "1607 - test inserting an object with DML returning" type_obj = self.connection.gettype("UDT_OBJECT") string_value = "The string that will be verified" obj = type_obj.newobject() obj.STRINGVALUE = string_value out_var = self.cursor.var(oracledb.DB_TYPE_OBJECT, typename="UDT_OBJECT") self.cursor.execute(""" insert into TestObjects (IntCol, ObjectCol) values (4, :obj)returning ObjectCol into :outObj""", obj=obj, outObj=out_var) result, = out_var.getvalue() self.assertEqual(result.STRINGVALUE, string_value) self.connection.rollback() def test_1608_insert_and_return_rowid(self): "1608 - test inserting a row and returning a rowid" self.cursor.execute("truncate table TestTempTable") var = self.cursor.var(oracledb.ROWID) self.cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (278, 'String 278') returning rowid into :1""", (var,)) rowid, = var.getvalue() self.cursor.execute(""" select IntCol, StringCol1 from TestTempTable where rowid = :1""", (rowid,)) self.assertEqual(self.cursor.fetchall(), [(278, 'String 278')]) def test_1609_insert_with_ref_cursor(self): "1609 - test inserting with a REF cursor and returning a rowid" self.cursor.execute("truncate table TestTempTable") var = self.cursor.var(oracledb.ROWID) in_cursor = self.connection.cursor() in_cursor.execute(""" select StringCol from TestStrings where IntCol >= 5 order by IntCol""") self.cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (187, pkg_TestRefCursors.TestInCursor(:1)) returning rowid into :2""", (in_cursor, var)) rowid, = var.getvalue() self.cursor.execute(""" select IntCol, StringCol1 from TestTempTable where rowid = :1""", (rowid,)) self.assertEqual(self.cursor.fetchall(), [(187, 'String 7 (Modified)')]) def test_1610_delete_returning_decreasing_rows_returned(self): "1610 - test delete returning decreasing number of rows" data = [(i, "Test String %d" % i) for i in range(1, 11)] self.cursor.execute("truncate table TestTempTable") self.cursor.executemany(""" insert into TestTempTable (IntCol, StringCol1) values (:1, :2)""", data) results = [] int_var = self.cursor.var(int) self.cursor.setinputsizes(None, int_var) for int_val in (5, 8, 10): self.cursor.execute(""" delete from TestTempTable where IntCol < :1 returning IntCol into :2""", [int_val]) results.append(int_var.getvalue()) self.assertEqual(results, [[1, 2, 3, 4], [5, 6, 7], [8, 9]]) def test_1611_delete_returning_no_rows_after_many_rows(self): "1611 - test delete returning no rows after returning many rows" data = [(i, "Test String %d" % i) for i in range(1, 11)] self.cursor.execute("truncate table TestTempTable") self.cursor.executemany(""" insert into TestTempTable (IntCol, StringCol1) values (:1, :2)""", data) int_var = self.cursor.var(int) self.cursor.execute(""" delete from TestTempTable where IntCol < :1 returning IntCol into :2""", [5, int_var]) self.assertEqual(int_var.getvalue(), [1, 2, 3, 4]) self.cursor.execute(None, [4, int_var]) self.assertEqual(int_var.getvalue(), []) def test_1612_insert_with_dml_returning_and_error(self): "1612 - test DML returning when an error occurs" self.cursor.execute("truncate table TestTempTable") int_val = 7 str_val = "A" * 401 int_var = self.cursor.var(oracledb.NUMBER) str_var = self.cursor.var(str) sql = """ insert into TestTempTable (IntCol, StringCol1) values (:int_val, :str_val) returning IntCol, StringCol1 into :int_var, :str_var""" parameters = dict(int_val=int_val, str_val=str_val, int_var=int_var, str_var=str_var) self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-12899:", self.cursor.execute, sql, parameters) def test_1613_insert_with_dml_returning_no_input_vars(self): "1613 - test DML returning with no input variables, multiple iters" self.cursor.execute("truncate table TestTempTable") sql = """ insert into TestTempTable (IntCol) values ((select count(*) + 1 from TestTempTable)) returning IntCol into :1""" var = self.cursor.var(int) self.cursor.execute(sql, [var]) self.assertEqual(var.getvalue(), [1]) self.cursor.execute(sql, [var]) self.assertEqual(var.getvalue(), [2]) def test_1614_parse_quoted_returning_bind(self): "1614 - test DML returning with a quoted bind name" sql = ''' insert into TestTempTable (IntCol, StringCol1) values (:int_val, :str_val) returning IntCol, StringCol1 into :"_val1" , :"VaL_2"''' self.cursor.parse(sql) expected_bind_names = ['INT_VAL', 'STR_VAL', '_val1', 'VaL_2'] self.assertEqual(self.cursor.bindnames(), expected_bind_names) def test_1615_parse_invalid_returning_bind(self): "1615 - test DML returning with an invalid bind name" sql = ''' insert into TestTempTable (IntCol) values (:int_val) returning IntCol, StringCol1 into :ROWID''' self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-01745:", self.cursor.parse, sql) def test_1616_parse_non_ascii_returning_bind(self): "1616 - test DML returning with a non-ascii bind name" sql = ''' insert into TestTempTable (IntCol) values (:int_val) returning IntCol, StringCol1 into :méil''' self.cursor.prepare(sql) self.assertEqual(self.cursor.bindnames(), ["INT_VAL", "MÉIL"]) def test_1617_dml_returning_with_input_bind_vars(self): "1617 - test DML returning with input bind variable data" self.cursor.execute("truncate table TestTempTable") out_var = self.cursor.var(int) self.cursor.execute(""" insert into TestTempTable (IntCol) values (:int_val) returning IntCol + :add_val into :out_val""", int_val=5, add_val=18, out_val=out_var) self.connection.commit() self.assertEqual(out_var.getvalue(), [23]) def test_1618_dml_returning_with_lob_and_outconverter(self): "1618 - test DML returning with LOBs and an output converter" self.cursor.execute("truncate table TestCLOBs") out_var = self.cursor.var(oracledb.DB_TYPE_CLOB, outconverter=lambda value: value.read()) lob_value = "A short CLOB - 1618" self.cursor.execute(""" insert into TestCLOBs (IntCol, ClobCol) values (1, :in_val) returning CLOBCol into :out_val""", in_val=lob_value, out_val=out_var) self.connection.commit() self.assertEqual(out_var.getvalue(), [lob_value]) def test_1619_dml_returning_with_clob_converted_to_long(self): "1619 - test DML returning with CLOB converted to LONG" self.cursor.execute("truncate table TestCLOBs") out_var = self.cursor.var(oracledb.DB_TYPE_LONG) lob_value = "A short CLOB - 1619" self.cursor.execute(""" insert into TestCLOBs (IntCol, ClobCol) values (1, :in_val) returning CLOBCol into :out_val""", in_val=lob_value, out_val=out_var) self.connection.commit() self.assertEqual(out_var.getvalue(), [lob_value]) def test_1620_dml_returning_with_index_organized_table(self): "1620 - test dml returning with an index organized table" self.cursor.execute("truncate table TestUniversalRowids") rowid_var = self.cursor.var(oracledb.ROWID) data = (1, "ABC", datetime.datetime(2017, 4, 11), rowid_var) sql = "insert into TestUniversalRowids values (:1, :2, :3)\n" + \ "returning rowid into :4" self.cursor.execute(sql, data) rowid_value, = rowid_var.getvalue() self.cursor.execute(""" select * from TestUniversalRowids where rowid = :1""", [rowid_value]) row, = self.cursor.fetchall() self.assertEqual(data[:3], row) def test_1621_plsql_returning_rowids_with_index_organized_table(self): "1621 - test plsql returning rowids with index organized table" self.cursor.execute("truncate table TestUniversalRowids") rowid_var = self.cursor.var(oracledb.ROWID) data = (1, "ABC", datetime.datetime(2017, 4, 11), rowid_var) self.cursor.execute(""" begin insert into TestUniversalRowids values (:1, :2, :3) returning rowid into :4; end;""", data) rowid_value = rowid_var.values[0] self.cursor.execute(""" select * from TestUniversalRowids where rowid = :1""", [rowid_value]) row, = self.cursor.fetchall() self.assertEqual(data[:3], row) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_1700_error.py000066400000000000000000000072701434177474600212070ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 1700 - Module for testing error objects """ import pickle import oracledb import test_env class TestCase(test_env.BaseTestCase): def test_1700_parse_error(self): "1700 - test parse error returns offset correctly" with self.assertRaises(oracledb.Error) as cm: self.cursor.execute("begin t_Missing := 5; end;") error_obj, = cm.exception.args self.assertEqual(error_obj.full_code, "ORA-06550") self.assertEqual(error_obj.offset, 6) def test_1701_pickle_error(self): "1701 - test picking/unpickling an error object" with self.assertRaises(oracledb.Error) as cm: self.cursor.execute(""" begin raise_application_error(-20101, 'Test!'); end;""") error_obj, = cm.exception.args self.assertEqual(type(error_obj), oracledb._Error) self.assertTrue("Test!" in error_obj.message) self.assertEqual(error_obj.code, 20101) self.assertEqual(error_obj.offset, 0) self.assertTrue(isinstance(error_obj.isrecoverable, bool)) new_error_obj = pickle.loads(pickle.dumps(error_obj)) self.assertEqual(type(new_error_obj), oracledb._Error) self.assertTrue(new_error_obj.message == error_obj.message) self.assertTrue(new_error_obj.code == error_obj.code) self.assertTrue(new_error_obj.offset == error_obj.offset) self.assertTrue(new_error_obj.context == error_obj.context) self.assertTrue(new_error_obj.isrecoverable == error_obj.isrecoverable) def test_1702_error_full_code(self): "1702 - test generation of full_code for ORA, DPI and DPY errors" with self.assertRaises(oracledb.Error) as cm: self.cursor.execute(None) error_obj, = cm.exception.args self.assertEqual(error_obj.full_code, "DPY-2001") if not self.connection.thin: with self.assertRaises(oracledb.Error) as cm: self.cursor.execute("truncate table TestTempTable") int_var = self.cursor.var(int) str_var = self.cursor.var(str, 2) self.cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (1, 'Longer than two chars') returning IntCol, StringCol1 into :int_var, :str_var""", int_var=int_var, str_var=str_var) error_obj, = cm.exception.args self.assertEqual(error_obj.full_code, "DPI-1037") if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_1800_interval_var.py000066400000000000000000000200731434177474600225470ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 1800 - Module for testing interval variables """ import datetime import oracledb import test_env class TestCase(test_env.BaseTestCase): def setUp(self): super().setUp() self.raw_data = [] self.data_by_key = {} for i in range(1, 11): delta = datetime.timedelta(days=i, hours=i, minutes=i * 2, seconds=i * 3) if i % 2 == 0: nullable_delta = None else: nullable_delta = datetime.timedelta(days=i + 5, hours=i + 2, minutes=i * 2 + 5, seconds=i * 3 + 5) data_tuple = (i, delta, nullable_delta) self.raw_data.append(data_tuple) self.data_by_key[i] = data_tuple def test_1800_bind_interval(self): "1800 - test binding in an interval" self.cursor.setinputsizes(value=oracledb.DB_TYPE_INTERVAL_DS) value = datetime.timedelta(days=5, hours=5, minutes=10, seconds=15) self.cursor.execute(""" select * from TestIntervals where IntervalCol = :value""", value=value) self.assertEqual(self.cursor.fetchall(), [self.data_by_key[5]]) def test_1801_bind_null(self): "1801 - test binding in a null" self.cursor.setinputsizes(value=oracledb.DB_TYPE_INTERVAL_DS) self.cursor.execute(""" select * from TestIntervals where IntervalCol = :value""", value=None) self.assertEqual(self.cursor.fetchall(), []) def test_1802_bind_out_set_input_sizes(self): "1802 - test binding out with set input sizes defined" bind_vars = \ self.cursor.setinputsizes(value=oracledb.DB_TYPE_INTERVAL_DS) self.cursor.execute(""" begin :value := to_dsinterval('8 09:24:18.123789'); end;""") expected_value = datetime.timedelta(days=8, hours=9, minutes=24, seconds=18, microseconds=123789) self.assertEqual(bind_vars["value"].getvalue(), expected_value) def test_1803_bind_in_out_set_input_sizes(self): "1803 - test binding in/out with set input sizes defined" bind_vars = \ self.cursor.setinputsizes(value=oracledb.DB_TYPE_INTERVAL_DS) self.cursor.execute(""" begin :value := :value + to_dsinterval('5 08:30:00'); end;""", value=datetime.timedelta(days=5, hours=2, minutes=15)) expected_value = datetime.timedelta(days=10, hours=10, minutes=45) self.assertEqual(bind_vars["value"].getvalue(), expected_value) def test_1804_bind_in_out_fractional_second(self): "1804 - test binding in/out with set input sizes defined" bind_vars = \ self.cursor.setinputsizes(value=oracledb.DB_TYPE_INTERVAL_DS) self.cursor.execute(""" begin :value := :value + to_dsinterval('5 08:30:00'); end;""", value=datetime.timedelta(days=5, seconds=12.123789)) expected_value = datetime.timedelta(days=10, hours=8, minutes=30, seconds=12, microseconds=123789) self.assertEqual(bind_vars["value"].getvalue(), expected_value) def test_1805_bind_out_var(self): "1805 - test binding out with cursor.var() method" var = self.cursor.var(oracledb.DB_TYPE_INTERVAL_DS) self.cursor.execute(""" begin :value := to_dsinterval('15 18:35:45.586'); end;""", value=var) expected_value = datetime.timedelta(days=15, hours=18, minutes=35, seconds=45, milliseconds=586) self.assertEqual(var.getvalue(), expected_value) def test_1806_bind_in_out_var_direct_set(self): "1806 - test binding in/out with cursor.var() method" var = self.cursor.var(oracledb.DB_TYPE_INTERVAL_DS) var.setvalue(0, datetime.timedelta(days=1, minutes=50)) self.cursor.execute(""" begin :value := :value + to_dsinterval('8 05:15:00'); end;""", value=var) expected_value = datetime.timedelta(days=9, hours=6, minutes=5) self.assertEqual(var.getvalue(), expected_value) def test_1807_cursor_description(self): "1807 - test cursor description is accurate" self.cursor.execute("select * from TestIntervals") expected_value = [ ('INTCOL', oracledb.DB_TYPE_NUMBER, 10, None, 9, 0, False), ('INTERVALCOL', oracledb.DB_TYPE_INTERVAL_DS, None, None, 2, 6, False), ('NULLABLECOL', oracledb.DB_TYPE_INTERVAL_DS, None, None, 2, 6, True) ] self.assertEqual(self.cursor.description, expected_value) def test_1808_fetchall(self): "1808 - test that fetching all of the data returns the correct results" self.cursor.execute("select * From TestIntervals order by IntCol") self.assertEqual(self.cursor.fetchall(), self.raw_data) self.assertEqual(self.cursor.fetchall(), []) def test_1809_fetchmany(self): "1809 - test that fetching data in chunks returns the correct results" self.cursor.execute("select * From TestIntervals order by IntCol") self.assertEqual(self.cursor.fetchmany(3), self.raw_data[0:3]) self.assertEqual(self.cursor.fetchmany(2), self.raw_data[3:5]) self.assertEqual(self.cursor.fetchmany(4), self.raw_data[5:9]) self.assertEqual(self.cursor.fetchmany(3), self.raw_data[9:]) self.assertEqual(self.cursor.fetchmany(3), []) def test_1810_fetchone(self): "1810 - test that fetching a single row returns the correct results" self.cursor.execute(""" select * from TestIntervals where IntCol in (3, 4) order by IntCol""") self.assertEqual(self.cursor.fetchone(), self.data_by_key[3]) self.assertEqual(self.cursor.fetchone(), self.data_by_key[4]) self.assertEqual(self.cursor.fetchone(), None) def test_1811_bind_and_fetch_negative_interval(self): "1811 - test binding and fetching a negative interval" value = datetime.timedelta(days=-1, seconds=86314, microseconds=431152) self.cursor.execute("select :1 from dual", [value]) result, = self.cursor.fetchone() self.assertEqual(result, value) def test_1812_unsupported_year_to_month_interval(self): "1812 - test year to month interval not supported" statement = "select INTERVAL '10-2' YEAR TO MONTH from dual" self.assertRaisesRegex(oracledb.NotSupportedError, "^DPY-3007:", self.cursor.execute, statement) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_1900_lob_var.py000066400000000000000000000470671434177474600215140ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 1900 - Module for testing LOB (CLOB and BLOB) variables """ import pickle import oracledb import test_env class TestCase(test_env.BaseTestCase): def __get_temp_lobs(self, sid): cursor = self.connection.cursor() cursor.execute(""" select cache_lobs + nocache_lobs + abstract_lobs from v$temporary_lobs where sid = :sid""", sid = sid) row = cursor.fetchone() if row is None: return 0 return int(row[0]) def __perform_test(self, lob_type, input_type): long_string = "" db_type = getattr(oracledb, "DB_TYPE_" + lob_type) self.cursor.execute(f"truncate table Test{lob_type}s") for i in range(0, 11): if i > 0: char = chr(ord('A') + i - 1) long_string += char * 25000 elif input_type is not db_type: continue self.cursor.setinputsizes(long_string=input_type) if lob_type == "BLOB": bind_value = long_string.encode() else: bind_value = long_string self.cursor.execute(f""" insert into Test{lob_type}s ( IntCol, {lob_type}Col ) values ( :integer_value, :long_string )""", integer_value=i, long_string=bind_value) self.connection.commit() self.cursor.execute(f""" select IntCol, {lob_type}Col from Test{lob_type}s order by IntCol""") self.__validate_query(self.cursor, lob_type) def __test_bind_ordering(self, lob_type): main_col = "A" * 32768 extra_col_1 = "B" * 65536 extra_col_2 = "C" * 131072 if lob_type == "BLOB": main_col = main_col.encode() extra_col_1 = extra_col_1.encode() extra_col_2 = extra_col_2.encode() self.connection.stmtcachesize = 0 self.cursor.execute(f"truncate table Test{lob_type}s") data = (1, main_col, 8, extra_col_1, 15, extra_col_2) self.cursor.execute(f""" insert into Test{lob_type}s ( IntCol, {lob_type}Col, ExtraNumCol1, Extra{lob_type}Col1, ExtraNumCol2, Extra{lob_type}Col2 ) values (:1, :2, :3, :4, :5, :6)""", data) with test_env.FetchLobsContextManager(False): self.cursor.execute(f"select * from Test{lob_type}s") fetched_data = self.cursor.fetchone() self.assertEqual(fetched_data, data) def __test_fetch_lobs_direct(self, lob_type): self.cursor.execute(f"truncate table Test{lob_type}s") data = [] long_string = "" for i in range(1, 11): if i > 0: char = chr(ord('A') + i - 1) long_string += char * 25000 if lob_type == "BLOB": data.append((i, long_string.encode())) else: data.append((i, long_string)) self.cursor.executemany(f""" insert into Test{lob_type}s ( IntCol, {lob_type}Col ) values ( :1, :2 )""", data) with test_env.FetchLobsContextManager(False): self.cursor.execute(f""" select IntCol, {lob_type}Col from Test{lob_type}s order by IntCol""") self.assertEqual(data, self.cursor.fetchall()) def __test_lob_operations(self, lob_type): self.cursor.execute(f"truncate table Test{lob_type}s") self.cursor.setinputsizes(long_string=getattr(oracledb, lob_type)) long_string = "X" * 75000 write_value = "TEST" if lob_type == "BLOB": long_string = long_string.encode("ascii") write_value = write_value.encode("ascii") self.cursor.execute(f""" insert into Test{lob_type}s ( IntCol, {lob_type}Col ) values ( :integer_value, :long_string )""", integer_value=1, long_string=long_string) self.cursor.execute(f""" select {lob_type}Col from Test{lob_type}s where IntCol = 1""") lob, = self.cursor.fetchone() self.assertEqual(lob.isopen(), False) lob.open() self.assertEqual(lob.isopen(), True) lob.close() self.assertEqual(lob.isopen(), False) self.assertEqual(lob.size(), 75000) lob.write(write_value, 75001) self.assertEqual(lob.size(), 75000 + len(write_value)) self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-2030:", lob.read, 0) self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-2030:", lob.read, -25) self.assertEqual(lob.read(), long_string + write_value) lob.write(write_value, 1) self.assertEqual(lob.read(), write_value + long_string[4:] + write_value) lob.trim(25000) self.assertEqual(lob.size(), 25000) lob.trim(newSize=10000) self.assertEqual(lob.size(), 10000) self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-2014:", lob.trim, new_size=50, newSize=60) lob.trim() self.assertEqual(lob.size(), 0) self.assertIsInstance(lob.getchunksize(), int) def __test_pickle(self, lob_type): value = "A test string value for pickling" if lob_type == "BLOB": value = value.encode("ascii") db_type = getattr(oracledb, "DB_TYPE_" + lob_type) lob = self.connection.createlob(db_type) lob.write(value) pickled_data = pickle.dumps(lob) unpickled_value = pickle.loads(pickled_data) self.assertEqual(unpickled_value, value) def __test_temporary_lob(self, lob_type): self.cursor.execute(f"truncate table Test{lob_type}s") value = "A test string value" if lob_type == "BLOB": value = value.encode("ascii") db_type = getattr(oracledb, "DB_TYPE_" + lob_type) lob = self.connection.createlob(db_type) lob.write(value) self.cursor.execute(""" insert into Test%ss (IntCol, %sCol) values (:int_val, :lob_val)""" % (lob_type, lob_type), int_val=1, lob_val=lob) self.cursor.execute("select %sCol from Test%ss" % (lob_type, lob_type)) lob, = self.cursor.fetchone() self.assertEqual(lob.read(), value) def __validate_query(self, rows, lob_type): long_string = "" db_type = getattr(oracledb, "DB_TYPE_" + lob_type) for row in rows: integer_value, lob = row self.assertEqual(lob.type, db_type) if integer_value == 0: self.assertEqual(lob.size(), 0) expected_value = "" if lob_type == "BLOB": expected_value = expected_value.encode() self.assertEqual(lob.read(), expected_value) else: char = chr(ord('A') + integer_value - 1) prev_char = chr(ord('A') + integer_value - 2) long_string += char * 25000 if lob_type == "BLOB": expected_value = long_string.encode("ascii") char = char.encode("ascii") prev_char = prev_char.encode("ascii") else: expected_value = long_string self.assertEqual(lob.size(), len(expected_value)) self.assertEqual(lob.read(), expected_value) if lob_type == "CLOB": self.assertEqual(str(lob), expected_value) self.assertEqual(lob.read(len(expected_value)), char) if integer_value > 1: offset = (integer_value - 1) * 25000 - 4 string = prev_char * 5 + char * 5 self.assertEqual(lob.read(offset, 10), string) def test_1900_bind_lob_value(self): "1900 - test binding a LOB value directly" self.cursor.execute("truncate table TestCLOBs") self.cursor.execute(""" insert into TestCLOBs (IntCol, ClobCol) values (1, 'Short value')""") self.cursor.execute("select ClobCol from TestCLOBs") lob, = self.cursor.fetchone() self.cursor.execute(""" insert into TestCLOBs (IntCol, ClobCol) values (2, :value)""", value=lob) def test_1901_blob_cursor_description(self): "1901 - test cursor description is accurate for BLOBs" self.cursor.execute("select IntCol, BlobCol from TestBLOBs") expected_value = [ ('INTCOL', oracledb.DB_TYPE_NUMBER, 10, None, 9, 0, 0), ('BLOBCOL', oracledb.DB_TYPE_BLOB, None, None, None, None, 0) ] self.assertEqual(self.cursor.description, expected_value) def test_1902_blob_direct(self): "1902 - test binding and fetching BLOB data (directly)" self.__perform_test("BLOB", oracledb.DB_TYPE_BLOB) def test_1903_blob_indirect(self): "1903 - test binding and fetching BLOB data (indirectly)" self.__perform_test("BLOB", oracledb.DB_TYPE_LONG_RAW) def test_1904_blob_operations(self): "1904 - test operations on BLOBs" self.__test_lob_operations("BLOB") def test_1905_clob_cursor_description(self): "1905 - test cursor description is accurate for CLOBs" self.cursor.execute("select IntCol, ClobCol from TestCLOBs") expected_value = [ ('INTCOL', oracledb.DB_TYPE_NUMBER, 10, None, 9, 0, False), ('CLOBCOL', oracledb.DB_TYPE_CLOB, None, None, None, None, False) ] self.assertEqual(self.cursor.description, expected_value) def test_1906_clob_direct(self): "1906 - test binding and fetching CLOB data (directly)" self.__perform_test("CLOB", oracledb.DB_TYPE_CLOB) def test_1907_clob_indirect(self): "1907 - test binding and fetching CLOB data (indirectly)" self.__perform_test("CLOB", oracledb.DB_TYPE_LONG) def test_1908_clob_operations(self): "1908 - test operations on CLOBs" self.__test_lob_operations("CLOB") def test_1909_create_temp_blob(self): "1909 - test creating a temporary BLOB" self.__test_temporary_lob("BLOB") def test_1910_create_temp_clob(self): "1910 - test creating a temporary CLOB" self.__test_temporary_lob("CLOB") def test_1911_create_temp_nclob(self): "1911 - test creating a temporary NCLOB" self.__test_temporary_lob("NCLOB") def test_1912_multiple_fetch(self): "1912 - test retrieving data from a CLOB after multiple fetches" self.cursor.arraysize = 1 self.cursor.execute("select * from TestCLOBS") rows = self.cursor.fetchall() self.__validate_query(rows, "CLOB") def test_1913_nclob_cursor_description(self): "1913 - test cursor description is accurate for NCLOBs" self.cursor.execute("select IntCol, NClobCol from TestNCLOBs") expected_value = [ ('INTCOL', oracledb.DB_TYPE_NUMBER, 10, None, 9, 0, 0), ('NCLOBCOL', oracledb.DB_TYPE_NCLOB, None, None, None, None, 0) ] self.assertEqual(self.cursor.description, expected_value) def test_1914_nclob_direct(self): "1914 - test binding and fetching NCLOB data (directly)" self.__perform_test("NCLOB", oracledb.DB_TYPE_NCLOB) def test_1915_nclob_non_ascii_chars(self): "1915 - test binding and fetching NCLOB data (with non-ASCII chars)" value = "\u03b4\u4e2a" self.cursor.execute("truncate table TestNCLOBs") self.cursor.setinputsizes(val=oracledb.DB_TYPE_NVARCHAR) self.cursor.execute(""" insert into TestNCLOBs (IntCol, NClobCol) values (1, :val)""", val=value) self.cursor.execute("select NCLOBCol from TestNCLOBs") nclob, = self.cursor.fetchone() self.cursor.setinputsizes(val=oracledb.DB_TYPE_NVARCHAR) self.cursor.execute("update TestNCLOBs set NCLOBCol = :val", val=nclob.read() + value) self.cursor.execute("select NCLOBCol from TestNCLOBs") nclob, = self.cursor.fetchone() self.assertEqual(nclob.read(), value + value) def test_1916_nclob_indirect(self): "1916 - test binding and fetching NCLOB data (indirectly)" self.__perform_test("NCLOB", oracledb.DB_TYPE_LONG) def test_1917_nclob_operations(self): "1917 - test operations on NCLOBs" self.__test_lob_operations("NCLOB") def test_1918_temporary_lobs(self): "1918 - test temporary LOBs" self.cursor.execute(""" select sys_context('USERENV', 'SID') from dual""") sid, = self.cursor.fetchone() temp_lobs = self.__get_temp_lobs(sid) with self.connection.cursor() as cursor: cursor.arraysize = 27 self.assertEqual(temp_lobs, 0) cursor.execute(""" select extract(xmlcol, '/').getclobval() from TestXML""") for lob, in cursor: value = lob.read() del lob temp_lobs = self.__get_temp_lobs(sid) self.assertEqual(temp_lobs, 0) def test_1919_assign_string_beyond_array_size(self): "1919 - test assign string to NCLOB beyond array size" nclobVar = self.cursor.var(oracledb.DB_TYPE_NCLOB) self.assertRaises(IndexError, nclobVar.setvalue, 1, "test char") def test_1920_supplemental_characters(self): "1920 - test read/write temporary LOBs using supplemental characters" self.cursor.execute(""" select value from nls_database_parameters where parameter = 'NLS_CHARACTERSET'""") charset, = self.cursor.fetchone() if charset != "AL32UTF8": self.skipTest("Database character set must be AL32UTF8") supplemental_chars = "𠜎 𠜱 𠝹 𠱓 𠱸 𠲖 𠳏 𠳕 𠴕 𠵼 𠵿 𠸎 𠸏 𠹷 𠺝 " \ "𠺢 𠻗 𠻹 𠻺 𠼭 𠼮 𠽌 𠾴 𠾼 𠿪 𡁜 𡁯 𡁵 𡁶 𡁻 𡃁 𡃉 𡇙 𢃇 " \ "𢞵 𢫕 𢭃 𢯊 𢱑 𢱕 𢳂 𢴈 𢵌 𢵧 𢺳 𣲷 𤓓 𤶸 𤷪 𥄫 𦉘 𦟌 𦧲 " \ "𦧺 𧨾 𨅝 𨈇 𨋢 𨳊 𨳍 𨳒 𩶘" self.cursor.execute("truncate table TestCLOBs") lob = self.connection.createlob(oracledb.DB_TYPE_CLOB) lob.write(supplemental_chars) self.cursor.execute(""" insert into TestCLOBs (IntCol, ClobCol) values (1, :val)""", [lob]) self.connection.commit() self.cursor.execute("select ClobCol from TestCLOBs") lob, = self.cursor.fetchone() self.assertEqual(lob.read(), supplemental_chars) def test_1921_plsql_auto_conversion_clob(self): "1921 - test automatic conversion to CLOB for PL/SQL" var = self.cursor.var(str, outconverter=lambda v: v[-15:]) var.setvalue(0, "A" * 50000) self.cursor.execute(""" declare t_Clob clob; begin t_Clob := :data; dbms_lob.copy(:data, t_Clob, 50000); dbms_lob.writeappend(:data, 5, 'BBBBB'); end;""", data=var) result = var.getvalue() self.assertEqual(result, "A" * 10 + "B" * 5) def test_1922_plsql_auto_conversion_nclob(self): "1922 - test automatic conversion to NCLOB for PL/SQL" var = self.cursor.var(oracledb.DB_TYPE_NCHAR, outconverter=lambda v: v[-12:]) var.setvalue(0, "N" * 51234) self.cursor.execute(""" declare t_Clob nclob; begin t_Clob := :data; dbms_lob.copy(:data, t_Clob, 51234); dbms_lob.writeappend(:data, 7, 'PPPPPPP'); end;""", data=var) result = var.getvalue() self.assertEqual(result, "N" * 5 + "P" * 7) def test_1923_plsql_auto_conversion_blob(self): "1923 - test automatic conversion to BLOB for PL/SQL" var = self.cursor.var(bytes, outconverter=lambda v: v[-14:]) var.setvalue(0, b"L" * 52345) self.cursor.execute(""" declare t_Blob blob; begin t_Blob := :data; dbms_lob.copy(:data, t_Blob, 52345); dbms_lob.writeappend(:data, 6, '515151515151'); end;""", data=var) result = var.getvalue() self.assertEqual(result, b"L" * 8 + b"Q" * 6) def test_1924_pickle_blob(self): "1924 - test pickling of BLOB" self.__test_pickle("BLOB") def test_1925_pickle_clob(self): "1925 - test pickling of CLOB" self.__test_pickle("CLOB") def test_1926_pickle_nclob(self): "1925 - test pickling of NCLOB" self.__test_pickle("NCLOB") def test_1927_fetch_blob_as_bytes(self): "1927 - test fetching BLOB as bytes" self.__test_fetch_lobs_direct("BLOB") def test_1928_fetch_clob_as_str(self): "1928 - test fetching CLOB as str" self.__test_fetch_lobs_direct("CLOB") def test_1929_fetch_nclob_as_str(self): "1929 - test fetching NCLOB as str" self.__test_fetch_lobs_direct("NCLOB") def test_1930_bind_order_blob(self): "1930 - test bind ordering with BLOB" self.__test_bind_ordering("BLOB") def test_1931_bind_order_clob(self): "1931 - test bind ordering with CLOB" self.__test_bind_ordering("CLOB") def test_1932_bind_order_nclob(self): "1932 - test bind ordering with NCLOB" self.__test_bind_ordering("NCLOB") if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_2000_long_var.py000066400000000000000000000122721434177474600216550ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 2000 - Module for testing long and long raw variables """ import unittest import oracledb import test_env class TestCase(test_env.BaseTestCase): def __perform_test(self, typ): name_part = "Long" if typ is oracledb.DB_TYPE_LONG else "LongRaw" self.cursor.execute(f"truncate table Test{name_part}s") self.cursor.setinputsizes(long_string=typ) long_string = "" for i in range(1, 11): char = chr(ord('A') + i - 1) long_string += char * 25000 if i % 3 == 1: bind_value = None else: if typ is oracledb.DB_TYPE_LONG_RAW: bind_value = long_string.encode() else: bind_value = long_string self.cursor.execute(f""" insert into Test{name_part}s ( IntCol, {name_part}Col ) values ( :integer_value, :long_string )""", integer_value=i, long_string=bind_value) self.connection.commit() self.cursor.execute(""" select * from Test%ss order by IntCol""" % name_part) long_string = "" for integer_value, fetched_value in self.cursor: char = chr(ord('A') + integer_value - 1) long_string += char * 25000 if integer_value % 3 == 1: expected_value = None else: if typ is oracledb.DB_TYPE_LONG_RAW: expected_value = long_string.encode() else: expected_value = long_string if fetched_value is not None: self.assertEqual(len(fetched_value), integer_value * 25000) self.assertEqual(fetched_value, expected_value) def test_2000_longs(self): "2000 - test binding and fetching long data" self.__perform_test(oracledb.DB_TYPE_LONG) def test_2001_long_with_execute_many(self): "2001 - test binding long data with executemany()" data = [] self.cursor.execute("truncate table TestLongs") for i in range(5): char = chr(ord('A') + i) long_str = char * (32768 * (i + 1)) data.append((i + 1, long_str)) self.cursor.executemany("insert into TestLongs values (:1, :2)", data) self.connection.commit() self.cursor.execute("select * from TestLongs order by IntCol") fetched_data = self.cursor.fetchall() self.assertEqual(fetched_data, data) def test_2002_long_raws(self): "2002 - test binding and fetching long raw data" self.__perform_test(oracledb.DB_TYPE_LONG_RAW) def test_2003_long_cursor_description(self): "2003 - test cursor description is accurate for longs" self.cursor.execute("select * from TestLongs") expected_value = [ ('INTCOL', oracledb.DB_TYPE_NUMBER, 10, None, 9, 0, False), ('LONGCOL', oracledb.DB_TYPE_LONG, None, None, None, None, True) ] self.assertEqual(self.cursor.description, expected_value) def test_2004_long_raw_cursor_description(self): "2004 - test cursor description is accurate for long raws" self.cursor.execute("select * from TestLongRaws") expected_value = [ ('INTCOL', oracledb.DB_TYPE_NUMBER, 10, None, 9, 0, False), ('LONGRAWCOL', oracledb.DB_TYPE_LONG_RAW, None, None, None, None, True) ] self.assertEqual(self.cursor.description, expected_value) @unittest.skipIf(test_env.get_is_thin(), "not relevant for thin mode") def test_2005_array_size_too_large(self): "2005 - test array size too large generates an exception" self.cursor.arraysize = 268435456 self.assertRaisesRegex(oracledb.DatabaseError, "^DPI-1015:", self.cursor.execute, "select * from TestLongRaws") if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_2100_nchar_var.py000066400000000000000000000261351434177474600220150ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 2100 - Module for testing NCHAR variables """ import oracledb import test_env class TestCase(test_env.BaseTestCase): def setUp(self): super().setUp() self.raw_data = [] self.data_by_key = {} for i in range(1, 11): unicode_col = "Unicode \u3042 %d" % i fixed_char_col = ("Fixed Unicode %d" % i).ljust(40) if i % 2: nullable_col = "Nullable %d" % i else: nullable_col = None data_tuple = (i, unicode_col, fixed_char_col, nullable_col) self.raw_data.append(data_tuple) self.data_by_key[i] = data_tuple def test_2100_unicode_length(self): "2100 - test value length" return_value = self.cursor.var(int) self.cursor.execute(""" begin :retval := LENGTH(:value); end;""", value="InVal \u3042", retval=return_value) self.assertEqual(return_value.getvalue(), 7) def test_2101_bind_unicode(self): "2101 - test binding in a unicode" self.cursor.setinputsizes(value=oracledb.DB_TYPE_NVARCHAR) self.cursor.execute(""" select * from TestUnicodes where UnicodeCol = :value""", value="Unicode \u3042 5") self.assertEqual(self.cursor.fetchall(), [self.data_by_key[5]]) def test_2102_bind_different_var(self): "2102 - test binding a different variable on second execution" retval_1 = self.cursor.var(oracledb.DB_TYPE_NVARCHAR, 30) retval_2 = self.cursor.var(oracledb.DB_TYPE_NVARCHAR, 30) self.cursor.execute(r"begin :retval := unistr('Called \3042'); end;", retval=retval_1) self.assertEqual(retval_1.getvalue(), "Called \u3042") self.cursor.execute("begin :retval := 'Called'; end;", retval=retval_2) self.assertEqual(retval_2.getvalue(), "Called") def test_2103_bind_unicode_after_number(self): "2103 - test binding in a string after setting input sizes to a number" unicode_val = self.cursor.var(oracledb.DB_TYPE_NVARCHAR) unicode_val.setvalue(0, "Unicode \u3042 6") self.cursor.setinputsizes(value=oracledb.NUMBER) self.cursor.execute(""" select * from TestUnicodes where UnicodeCol = :value""", value=unicode_val) self.assertEqual(self.cursor.fetchall(), [self.data_by_key[6]]) def test_2104_bind_unicode_array_direct(self): "2104 - test binding in a unicode array" return_value = self.cursor.var(oracledb.NUMBER) array = [r[1] for r in self.raw_data] array_var = self.cursor.arrayvar(oracledb.DB_TYPE_NVARCHAR, array) statement = """ begin :retval := pkg_TestUnicodeArrays.TestInArrays( :integer_value, :array); end;""" self.cursor.execute(statement, retval=return_value, integer_value=5, array=array_var) self.assertEqual(return_value.getvalue(), 116) array = ["Unicode - \u3042 %d" % i for i in range(15)] array_var = self.cursor.arrayvar(oracledb.DB_TYPE_NVARCHAR, array) self.cursor.execute(statement, integer_value=8, array=array_var) self.assertEqual(return_value.getvalue(), 208) def test_2105_bind_unicode_array_by_sizes(self): "2105 - test binding in a unicode array (with setinputsizes)" return_value = self.cursor.var(oracledb.NUMBER) self.cursor.setinputsizes(array = [oracledb.DB_TYPE_NVARCHAR, 10]) array = [r[1] for r in self.raw_data] self.cursor.execute(""" begin :retval := pkg_TestUnicodeArrays.TestInArrays(:integer_value, :array); end;""", retval=return_value, integer_value=6, array=array) self.assertEqual(return_value.getvalue(), 117) def test_2106_bind_unicode_array_by_var(self): "2106 - test binding in a unicode array (with arrayvar)" return_value = self.cursor.var(oracledb.NUMBER) array = self.cursor.arrayvar(oracledb.DB_TYPE_NVARCHAR, 10, 20) array.setvalue(0, [r[1] for r in self.raw_data]) self.cursor.execute(""" begin :retval := pkg_TestUnicodeArrays.TestInArrays(:integer_value, :array); end;""", retval=return_value, integer_value=7, array=array) self.assertEqual(return_value.getvalue(), 118) def test_2107_bind_in_out_unicode_array_by_var(self): "2107 - test binding in/out a unicode array (with arrayvar)" array = self.cursor.arrayvar(oracledb.DB_TYPE_NVARCHAR, 10, 100) original_data = [r[1] for r in self.raw_data] fmt = "Converted element \u3042 # %d originally had length %d" expected_data = [fmt % (i, len(original_data[i - 1])) \ for i in range(1, 6)] + original_data[5:] array.setvalue(0, original_data) self.cursor.execute(""" begin pkg_TestUnicodeArrays.TestInOutArrays(:numElems, :array); end;""", numElems = 5, array = array) self.assertEqual(array.getvalue(), expected_data) def test_2108_bind_out_unicode_array_by_var(self): "2108 - test binding out a unicode array (with arrayvar)" array = self.cursor.arrayvar(oracledb.DB_TYPE_NVARCHAR, 6, 100) fmt = "Test out element \u3042 # %d" expected_data = [fmt % i for i in range(1, 7)] self.cursor.execute(""" begin pkg_TestUnicodeArrays.TestOutArrays(:numElems, :array); end;""", numElems = 6, array = array) self.assertEqual(array.getvalue(), expected_data) def test_2109_bind_null(self): "2109 - test binding in a null" self.cursor.execute(""" select * from TestUnicodes where UnicodeCol = :value""", value = None) self.assertEqual(self.cursor.fetchall(), []) def test_2110_bind_out_set_input_sizes_by_type(self): "2110 - test binding out with set input sizes defined (by type)" bind_vars = self.cursor.setinputsizes(value=oracledb.DB_TYPE_NVARCHAR) self.cursor.execute(r""" begin :value := unistr('TSI \3042'); end;""") self.assertEqual(bind_vars["value"].getvalue(), "TSI \u3042") def test_2111_bind_in_out_set_input_sizes_by_type(self): "2111 - test binding in/out with set input sizes defined (by type)" bind_vars = self.cursor.setinputsizes(value=oracledb.DB_TYPE_NVARCHAR) self.cursor.execute(r""" begin :value := :value || unistr(' TSI \3042'); end;""", value = "InVal \u3041") self.assertEqual(bind_vars["value"].getvalue(), "InVal \u3041 TSI \u3042") def test_2112_bind_out_var(self): "2112 - test binding out with cursor.var() method" var = self.cursor.var(oracledb.DB_TYPE_NVARCHAR) self.cursor.execute(r""" begin :value := unistr('TSI (VAR) \3042'); end;""", value=var) self.assertEqual(var.getvalue(), "TSI (VAR) \u3042") def test_2113_bind_in_out_var_direct_set(self): "2113 - test binding in/out with cursor.var() method" var = self.cursor.var(oracledb.DB_TYPE_NVARCHAR) var.setvalue(0, "InVal \u3041") self.cursor.execute(r""" begin :value := :value || unistr(' TSI (VAR) \3042'); end;""", value = var) self.assertEqual(var.getvalue(), "InVal \u3041 TSI (VAR) \u3042") def test_2114_cursor_description(self): "2114 - test cursor description is accurate" self.cursor.execute("select * from TestUnicodes") varchar_ratio, nvarchar_ratio = test_env.get_charset_ratios() expected_value = [ ('INTCOL', oracledb.DB_TYPE_NUMBER, 10, None, 9, 0, False), ('UNICODECOL', oracledb.DB_TYPE_NVARCHAR, 20, 20 * nvarchar_ratio, None, None, False), ('FIXEDUNICODECOL', oracledb.DB_TYPE_NCHAR, 40, 40 * nvarchar_ratio, None, None, False), ('NULLABLECOL', oracledb.DB_TYPE_NVARCHAR, 50, 50 * nvarchar_ratio, None, None, True) ] self.assertEqual(self.cursor.description, expected_value) def test_2115_fetchall(self): "2115 - test that fetching all of the data returns the correct results" self.cursor.execute("select * From TestUnicodes order by IntCol") self.assertEqual(self.cursor.fetchall(), self.raw_data) self.assertEqual(self.cursor.fetchall(), []) def test_2116_fetchmany(self): "2116 - test that fetching data in chunks returns the correct results" self.cursor.execute("select * From TestUnicodes order by IntCol") self.assertEqual(self.cursor.fetchmany(3), self.raw_data[0:3]) self.assertEqual(self.cursor.fetchmany(2), self.raw_data[3:5]) self.assertEqual(self.cursor.fetchmany(4), self.raw_data[5:9]) self.assertEqual(self.cursor.fetchmany(3), self.raw_data[9:]) self.assertEqual(self.cursor.fetchmany(3), []) def test_2117_fetchone(self): "2117 - test that fetching a single row returns the correct results" self.cursor.execute(""" select * from TestUnicodes where IntCol in (3, 4) order by IntCol""") self.assertEqual(self.cursor.fetchone(), self.data_by_key[3]) self.assertEqual(self.cursor.fetchone(), self.data_by_key[4]) self.assertEqual(self.cursor.fetchone(), None) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_2200_number_var.py000066400000000000000000000516671434177474600222230ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 2200 - Module for testing number variables """ import decimal import oracledb import test_env class TestCase(test_env.BaseTestCase): def output_type_handler_binary_int(self, cursor, name, default_type, size, precision, scale): return cursor.var(oracledb.DB_TYPE_BINARY_INTEGER, arraysize=cursor.arraysize) def output_type_handler_decimal(self, cursor, name, default_type, size, precision, scale): if default_type == oracledb.NUMBER: return cursor.var(str, 255, outconverter=decimal.Decimal, arraysize=cursor.arraysize) def output_type_handler_str(self, cursor, name, default_type, size, precision, scale): return cursor.var(str, 255, arraysize=cursor.arraysize) def setUp(self): super().setUp() self.raw_data = [] self.data_by_key = {} for i in range(1, 11): number_col = i + i * 0.25 float_col = i + i * 0.75 unconstrained_col = i ** 3 + i * 0.5 if i % 2: nullable_col = 143 ** i else: nullable_col = None data_tuple = (i, 38 ** i, number_col, float_col, unconstrained_col, nullable_col) self.raw_data.append(data_tuple) self.data_by_key[i] = data_tuple def test_2200_bind_boolean(self): "2200 - test binding in a boolean" result = self.cursor.callfunc("pkg_TestBooleans.GetStringRep", str, (True,)) self.assertEqual(result, "TRUE") def test_2201_bind_boolean_as_number(self): "2201 - test binding in a boolean as a number" var = self.cursor.var(oracledb.NUMBER) var.setvalue(0, True) self.cursor.execute("select :1 from dual", [var]) result, = self.cursor.fetchone() self.assertEqual(result, 1) var.setvalue(0, False) self.cursor.execute("select :1 from dual", [var]) result, = self.cursor.fetchone() self.assertEqual(result, 0) def test_2202_bind_decimal(self): "2202 - test binding in a decimal.Decimal" self.cursor.execute(""" select * from TestNumbers where NumberCol - :value1 - :value2 = trunc(NumberCol)""", value1=decimal.Decimal("0.20"), value2=decimal.Decimal("0.05")) expected_data = [self.data_by_key[1], self.data_by_key[5], self.data_by_key[9]] self.assertEqual(self.cursor.fetchall(), expected_data) def test_2203_bind_float(self): "2203 - test binding in a float" self.cursor.execute(""" select * from TestNumbers where NumberCol - :value = trunc(NumberCol)""", value=0.25) expected_data = [self.data_by_key[1], self.data_by_key[5], self.data_by_key[9]] self.assertEqual(self.cursor.fetchall(), expected_data) def test_2204_bind_integer(self): "2204 - test binding in an integer" self.cursor.execute(""" select * from TestNumbers where IntCol = :value""", value = 2) self.assertEqual(self.cursor.fetchall(), [self.data_by_key[2]]) def test_2205_bind_large_long_as_oracle_number(self): "2205 - test binding in a large long integer as Oracle number" in_val = 6088343244 value_var = self.cursor.var(oracledb.NUMBER) value_var.setvalue(0, in_val) self.cursor.execute(""" begin :value := :value + 5; end;""", value=value_var) value = value_var.getvalue() self.assertEqual(value, in_val + 5) def test_2206_bind_large_long_as_integer(self): "2206 - test binding in a large long integer as Python integer" long_value = -9999999999999999999 self.cursor.execute("select :value from dual", value=long_value) result, = self.cursor.fetchone() self.assertEqual(result, long_value) def test_2207_bind_integer_after_string(self): "2207 - test binding in an integer after setting input sizes to string" self.cursor.setinputsizes(value=15) self.cursor.execute(""" select * from TestNumbers where IntCol = :value""", value=3) self.assertEqual(self.cursor.fetchall(), [self.data_by_key[3]]) def test_2208_bind_decimal_after_number(self): "2208 - test binding in a decimal after setting input sizes to number" cursor = self.connection.cursor() value = decimal.Decimal("319438950232418390.273596") cursor.setinputsizes(value=oracledb.NUMBER) cursor.outputtypehandler = self.output_type_handler_decimal cursor.execute("select :value from dual", value=value) out_value, = cursor.fetchone() self.assertEqual(out_value, value) def test_2209_bind_null(self): "2209 - test binding in a null" self.cursor.execute(""" select * from TestNumbers where IntCol = :value""", value=None) self.assertEqual(self.cursor.fetchall(), []) def test_2210_bind_number_array_direct(self): "2210 - test binding in a number array" return_value = self.cursor.var(oracledb.NUMBER) array = [r[2] for r in self.raw_data] statement = """ begin :return_value := pkg_TestNumberArrays.TestInArrays( :start_value, :array); end;""" self.cursor.execute(statement, return_value=return_value, start_value=5, array=array) self.assertEqual(return_value.getvalue(), 73.75) array = list(range(15)) self.cursor.execute(statement, start_value=10, array=array) self.assertEqual(return_value.getvalue(), 115.0) def test_2211_bind_number_array_by_sizes(self): "2211 - test binding in a number array (with setinputsizes)" return_value = self.cursor.var(oracledb.NUMBER) self.cursor.setinputsizes(array = [oracledb.NUMBER, 10]) array = [r[2] for r in self.raw_data] self.cursor.execute(""" begin :return_value := pkg_TestNumberArrays.TestInArrays( :start_value, :array); end;""", return_value=return_value, start_value=6, array=array) self.assertEqual(return_value.getvalue(), 74.75) def test_2212_bind_number_array_by_var(self): "2212 - test binding in a number array (with arrayvar)" return_value = self.cursor.var(oracledb.NUMBER) array = self.cursor.arrayvar(oracledb.NUMBER, [r[2] for r in self.raw_data]) self.cursor.execute(""" begin :return_value := pkg_TestNumberArrays.TestInArrays( :integer_value, :array); end;""", return_value = return_value, integer_value = 7, array = array) self.assertEqual(return_value.getvalue(), 75.75) def test_2213_bind_zero_length_number_array_by_var(self): "2213 - test binding in a zero length number array (with arrayvar)" return_value = self.cursor.var(oracledb.NUMBER) array = self.cursor.arrayvar(oracledb.NUMBER, 0) self.cursor.execute(""" begin :return_value := pkg_TestNumberArrays.TestInArrays( :integer_value, :array); end;""", return_value=return_value, integer_value=8, array=array) self.assertEqual(return_value.getvalue(), 8.0) self.assertEqual(array.getvalue(), []) def test_2214_bind_in_out_number_array_by_var(self): "2214 - test binding in/out a number array (with arrayvar)" array = self.cursor.arrayvar(oracledb.NUMBER, 10) original_data = [r[2] for r in self.raw_data] expected_data = [original_data[i - 1] * 10 for i in range(1, 6)] + \ original_data[5:] array.setvalue(0, original_data) self.cursor.execute(""" begin pkg_TestNumberArrays.TestInOutArrays(:num_elems, :array); end;""", num_elems=5, array=array) self.assertEqual(array.getvalue(), expected_data) def test_2215_bind_out_number_array_by_var(self): "2215 - test binding out a Number array (with arrayvar)" array = self.cursor.arrayvar(oracledb.NUMBER, 6) expected_data = [i * 100 for i in range(1, 7)] self.cursor.execute(""" begin pkg_TestNumberArrays.TestOutArrays(:num_elems, :array); end;""", num_elems=6, array=array) self.assertEqual(array.getvalue(), expected_data) def test_2216_bind_out_set_input_sizes(self): "2216 - test binding out with set input sizes defined" bind_vars = self.cursor.setinputsizes(value = oracledb.NUMBER) self.cursor.execute(""" begin :value := 5; end;""") self.assertEqual(bind_vars["value"].getvalue(), 5) def test_2217_bind_in_out_set_input_sizes(self): "2217 - test binding in/out with set input sizes defined" bind_vars = self.cursor.setinputsizes(value = oracledb.NUMBER) self.cursor.execute(""" begin :value := :value + 5; end;""", value = 1.25) self.assertEqual(bind_vars["value"].getvalue(), 6.25) def test_2218_bind_out_var(self): "2218 - test binding out with cursor.var() method" var = self.cursor.var(oracledb.NUMBER) self.cursor.execute(""" begin :value := 5; end;""", value = var) self.assertEqual(var.getvalue(), 5) def test_2219_bind_in_out_var_direct_set(self): "2219 - test binding in/out with cursor.var() method" var = self.cursor.var(oracledb.NUMBER) var.setvalue(0, 2.25) self.cursor.execute(""" begin :value := :value + 5; end;""", value = var) self.assertEqual(var.getvalue(), 7.25) def test_2220_cursor_description(self): "2220 - test cursor description is accurate" self.cursor.execute("select * from TestNumbers") expected_value = [ ('INTCOL', oracledb.DB_TYPE_NUMBER, 10, None, 9, 0, False), ('LONGINTCOL', oracledb.DB_TYPE_NUMBER, 17, None, 16, 0, False), ('NUMBERCOL', oracledb.DB_TYPE_NUMBER, 13, None, 9, 2, False), ('FLOATCOL', oracledb.DB_TYPE_NUMBER, 127, None, 126, -127, False), ('UNCONSTRAINEDCOL', oracledb.DB_TYPE_NUMBER, 127, None, 0, -127, False), ('NULLABLECOL', oracledb.DB_TYPE_NUMBER, 39, None, 38, 0, True) ] self.assertEqual(self.cursor.description, expected_value) def test_2221_fetchall(self): "2221 - test that fetching all of the data returns the correct results" self.cursor.execute("select * From TestNumbers order by IntCol") self.assertEqual(self.cursor.fetchall(), self.raw_data) self.assertEqual(self.cursor.fetchall(), []) def test_2222_fetchmany(self): "2222 - test that fetching data in chunks returns the correct results" self.cursor.execute("select * From TestNumbers order by IntCol") self.assertEqual(self.cursor.fetchmany(3), self.raw_data[0:3]) self.assertEqual(self.cursor.fetchmany(2), self.raw_data[3:5]) self.assertEqual(self.cursor.fetchmany(4), self.raw_data[5:9]) self.assertEqual(self.cursor.fetchmany(3), self.raw_data[9:]) self.assertEqual(self.cursor.fetchmany(3), []) def test_2223_fetchone(self): "2223 - test that fetching a single row returns the correct results" self.cursor.execute(""" select * from TestNumbers where IntCol in (3, 4) order by IntCol""") self.assertEqual(self.cursor.fetchone(), self.data_by_key[3]) self.assertEqual(self.cursor.fetchone(), self.data_by_key[4]) self.assertEqual(self.cursor.fetchone(), None) def test_2224_return_as_long(self): "2224 - test that fetching a long integer returns such in Python" self.cursor.execute(""" select NullableCol from TestNumbers where IntCol = 9""") col, = self.cursor.fetchone() self.assertEqual(col, 25004854810776297743) def test_2225_return_constant_float(self): "2225 - test fetching a floating point number returns such in Python" self.cursor.execute("select 1.25 from dual") result, = self.cursor.fetchone() self.assertEqual(result, 1.25) def test_2226_return_constant_integer(self): "2226 - test that fetching an integer returns such in Python" self.cursor.execute("select 148 from dual") result, = self.cursor.fetchone() self.assertEqual(result, 148) self.assertTrue(isinstance(result, int), "integer not returned") def test_2227_acceptable_boundary_numbers(self): "2227 - test that acceptable boundary numbers are handled properly" in_values = [decimal.Decimal("9.99999999999999e+125"), decimal.Decimal("-9.99999999999999e+125"), 0.0, 1e-130, -1e-130] out_values = [int("9" * 15 + "0" * 111), -int("9" * 15 + "0" * 111), 0, 1e-130, -1e-130] for in_value, out_value in zip(in_values, out_values): self.cursor.execute("select :1 from dual", (in_value,)) result, = self.cursor.fetchone() self.assertEqual(result, out_value) def test_2228_unacceptable_boundary_numbers(self): "2228 - test that unacceptable boundary numbers are rejected" in_values = [1e126, -1e126, float("inf"), float("-inf"), float("NaN"), decimal.Decimal("1e126"), decimal.Decimal("-1e126"), decimal.Decimal("inf"), decimal.Decimal("-inf"), decimal.Decimal("NaN")] no_rep_err = "^DPY-4003:" invalid_err = "^DPY-4004:" expected_errors = [no_rep_err, no_rep_err, invalid_err, invalid_err, invalid_err, no_rep_err, no_rep_err, invalid_err, invalid_err, invalid_err] for in_value, error in zip(in_values, expected_errors): self.assertRaisesRegex(oracledb.DatabaseError, error, self.cursor.execute, "select :1 from dual", (in_value,)) def test_2229_return_float_from_division(self): "2229 - test that fetching the result of division returns a float" self.cursor.execute(""" select IntCol / 7 from TestNumbers where IntCol = 1""") result, = self.cursor.fetchone() self.assertEqual(result, 1.0 / 7.0) self.assertTrue(isinstance(result, float), "float not returned") def test_2230_string_format(self): "2230 - test that string format is returned properly" var = self.cursor.var(oracledb.NUMBER) self.assertIs(var.type, oracledb.DB_TYPE_NUMBER) self.assertEqual(str(var), "") var.setvalue(0, 4.5) self.assertEqual(str(var), "") def test_2231_bind_binary_double(self): "2231 - test that binding binary double is possible" statement = "select :1 from dual" self.cursor.setinputsizes(oracledb.DB_TYPE_BINARY_DOUBLE) self.cursor.execute(statement, (5,)) self.assertEqual(self.cursor.bindvars[0].type, oracledb.DB_TYPE_BINARY_DOUBLE) value, = self.cursor.fetchone() self.assertEqual(value, 5) self.cursor.execute(statement, (1.5,)) self.assertEqual(self.cursor.bindvars[0].type, oracledb.DB_TYPE_BINARY_DOUBLE) value, = self.cursor.fetchone() self.assertEqual(value, 1.5) self.cursor.execute(statement, (decimal.Decimal("NaN"),)) self.assertEqual(self.cursor.bindvars[0].type, oracledb.DB_TYPE_BINARY_DOUBLE) value, = self.cursor.fetchone() self.assertEqual(str(value), str(float("NaN"))) def test_2232_fetch_binary_int(self): "2232 - test fetching numbers as binary integers" self.cursor.outputtypehandler = self.output_type_handler_binary_int for value in (1, 2 ** 31, 2 ** 63 - 1, -1, -2 ** 31, -2 ** 63 + 1): self.cursor.execute("select :1 from dual", [str(value)]) fetched_value, = self.cursor.fetchone() self.assertEqual(value, fetched_value) def test_2233_out_bind_binary_int(self): "2233 - test binding native integer as an out bind" statement = "begin :value := 2.9; end;" simple_var = self.cursor.var(oracledb.DB_TYPE_BINARY_INTEGER) self.cursor.execute(statement, [simple_var]) self.assertEqual(simple_var.getvalue(), 2) statement = "begin :value := 1.5; end;" simple_var = self.cursor.var(oracledb.DB_TYPE_BINARY_INTEGER) self.cursor.execute(statement, [simple_var]) self.assertEqual(simple_var.getvalue(), 1) def test_2234_in_bind_binary_int(self): "2234 - test binding in a native integer" statement = "begin :value := :value + 2.5; end;" simple_var = self.cursor.var(oracledb.DB_TYPE_BINARY_INTEGER) simple_var.setvalue(0, 0) self.cursor.execute(statement, [simple_var]) self.assertEqual(simple_var.getvalue(), 2) simple_var.setvalue(0, -5) self.cursor.execute(statement, [simple_var]) self.assertEqual(simple_var.getvalue(), -2) def test_2235_setting_decimal_value_binary_int(self): "2235 - test setting decimal value for binary int" statement = "begin :value := :value + 2.5; end;" simple_var = self.cursor.var(oracledb.DB_TYPE_BINARY_INTEGER) simple_var.setvalue(0, 2.5) self.cursor.execute(statement, [simple_var]) self.assertEqual(simple_var.getvalue(), 4) def test_2236_out_bind_binary_int_with_large_value(self): "2236 - bind a large value to binary int" statement = "begin :value := POWER(2, 31) - 1; end;" simple_var = self.cursor.var(oracledb.DB_TYPE_BINARY_INTEGER) self.cursor.execute(statement, [simple_var]) self.assertEqual(simple_var.getvalue(), 2**31 - 1) statement = "begin :value := POWER(-2, 31) - 1; end;" self.cursor.execute(statement, [simple_var]) self.assertEqual(simple_var.getvalue(), -2**31 - 1) def test_2237_fetch_number_with_lobs_default_false(self): "2237 - fetch a number with oracledb.defaults.fetch_lobs = False" with test_env.FetchLobsContextManager(False): self.cursor.execute("select 1 from dual") result, = self.cursor.fetchone() self.assertEqual(type(result), int) def test_2238_fetch_small_constant_with_decimal_point(self): "2238 - fetch a small constant with a decimal point" self.cursor.outputtypehandler = self.output_type_handler_str self.cursor.execute("select 3 / 2 from dual") result, = self.cursor.fetchone() self.assertTrue(len(result) == 3 and result[0] == "1" \ and result[-1] == "5") if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_2300_object_var.py000066400000000000000000000504101434177474600221630ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 2300 - Module for testing object variables """ import datetime import decimal import unittest import oracledb import test_env class TestCase(test_env.BaseTestCase): def __test_data(self, expected_int_value, expected_obj_value, expected_array_value): int_value, object_value, array_value = self.cursor.fetchone() if object_value is not None: object_value = self.get_db_object_as_plain_object(object_value) if array_value is not None: array_value = array_value.aslist() self.assertEqual(int_value, expected_int_value) self.assertEqual(object_value, expected_obj_value) self.assertEqual(array_value, expected_array_value) def test_2300_bind_null_in(self): "2300 - test binding a null value (IN)" var = self.cursor.var(oracledb.DB_TYPE_OBJECT, typename="UDT_OBJECT") result = self.cursor.callfunc("pkg_TestBindObject.GetStringRep", str, (var,)) self.assertEqual(result, "null") def test_2301_bind_object_in(self): "2301 - test binding an object (IN)" type_obj = self.connection.gettype("UDT_OBJECT") obj = type_obj.newobject() obj.NUMBERVALUE = 13 obj.STRINGVALUE = "Test String" result = self.cursor.callfunc("pkg_TestBindObject.GetStringRep", str, (obj,)) exp = "udt_Object(13, 'Test String', null, null, null, null, null)" self.assertEqual(result, exp) obj.NUMBERVALUE = None obj.STRINGVALUE = "Test With Dates" obj.DATEVALUE = datetime.datetime(2016, 2, 10) obj.TIMESTAMPVALUE = datetime.datetime(2016, 2, 10, 14, 13, 50) result = self.cursor.callfunc("pkg_TestBindObject.GetStringRep", str, (obj,)) self.assertEqual(result, "udt_Object(null, 'Test With Dates', null, " \ "to_date('2016-02-10', 'YYYY-MM-DD'), " \ "to_timestamp('2016-02-10 14:13:50', " \ "'YYYY-MM-DD HH24:MI:SS'), " \ "null, null)") obj.DATEVALUE = None obj.TIMESTAMPVALUE = None sub_type_obj = self.connection.gettype("UDT_SUBOBJECT") sub_obj = sub_type_obj.newobject() sub_obj.SUBNUMBERVALUE = decimal.Decimal("18.25") sub_obj.SUBSTRINGVALUE = "Sub String" obj.SUBOBJECTVALUE = sub_obj result = self.cursor.callfunc("pkg_TestBindObject.GetStringRep", str, (obj,)) self.assertEqual(result, "udt_Object(null, 'Test With Dates', null, null, " \ "null, udt_SubObject(18.25, 'Sub String'), null)") def test_2302_copy_object(self): "2302 - test copying an object" type_obj = self.connection.gettype("UDT_OBJECT") obj = type_obj() obj.NUMBERVALUE = 5124 obj.STRINGVALUE = "A test string" obj.DATEVALUE = datetime.datetime(2016, 2, 24) obj.TIMESTAMPVALUE = datetime.datetime(2016, 2, 24, 13, 39, 10) copied_obj = obj.copy() self.assertEqual(obj.NUMBERVALUE, copied_obj.NUMBERVALUE) self.assertEqual(obj.STRINGVALUE, copied_obj.STRINGVALUE) self.assertEqual(obj.DATEVALUE, copied_obj.DATEVALUE) self.assertEqual(obj.TIMESTAMPVALUE, copied_obj.TIMESTAMPVALUE) def test_2303_empty_collection_as_list(self): "2303 - test getting an empty collection as a list" type_obj = self.connection.gettype("UDT_ARRAY") obj = type_obj.newobject() self.assertEqual(obj.aslist(), []) def test_2304_fetch_data(self): "2304 - test fetching objects" self.cursor.execute("alter session set time_zone = 'UTC'") self.cursor.execute(""" select IntCol, ObjectCol, ArrayCol from TestObjects order by IntCol""") expected_value = [ ('INTCOL', oracledb.DB_TYPE_NUMBER, 10, None, 9, 0, False), ('OBJECTCOL', oracledb.DB_TYPE_OBJECT, None, None, None, None, True), ('ARRAYCOL', oracledb.DB_TYPE_OBJECT, None, None, None, None, True) ] self.assertEqual(self.cursor.description, expected_value) expected_value = ( 1, 'First row', 'First ', 'N First Row', 'N First ', b'Raw Data 1', 2, 5, 12.125, 0.5, 12.5, 25.25, 50.125, oracledb.Timestamp(2007, 3, 6, 0, 0, 0), oracledb.Timestamp(2008, 9, 12, 16, 40), oracledb.Timestamp(2009, 10, 13, 17, 50), oracledb.Timestamp(2010, 11, 14, 18, 55), 'Short CLOB value', 'Short NCLOB Value', b'Short BLOB value', (11, 'Sub object 1'), [(5, 'first element'), (6, 'second element')] ) self.__test_data(1, expected_value, [5, 10, None, 20]) self.__test_data(2, None, [3, None, 9, 12, 15]) expected_value = ( 3, 'Third row', 'Third ', 'N Third Row', 'N Third ', b'Raw Data 3', 4, 10, 6.5, 0.75, 43.25, 86.5, 192.125, oracledb.Timestamp(2007, 6, 21, 0, 0, 0), oracledb.Timestamp(2007, 12, 13, 7, 30, 45), oracledb.Timestamp(2017, 6, 21, 23, 18, 45), oracledb.Timestamp(2017, 7, 21, 8, 27, 13), 'Another short CLOB value', 'Another short NCLOB Value', b'Yet another short BLOB value', (13, 'Sub object 3'), [ (10, 'element #1'), (20, 'element #2'), (30, 'element #3'), (40, 'element #4') ] ) self.__test_data(3, expected_value, None) def test_2305_get_object_type(self): "2305 - test getting object type" type_obj = self.connection.gettype("UDT_OBJECT") self.assertEqual(type_obj.iscollection, False) self.assertEqual(type_obj.schema, self.connection.username.upper()) self.assertEqual(type_obj.name, "UDT_OBJECT") sub_object_value_type = self.connection.gettype("UDT_SUBOBJECT") sub_object_array_type = self.connection.gettype("UDT_OBJECTARRAY") expected_attr_names = [ "NUMBERVALUE", "STRINGVALUE", "FIXEDCHARVALUE", "NSTRINGVALUE", "NFIXEDCHARVALUE", "RAWVALUE", "INTVALUE", "SMALLINTVALUE", "REALVALUE", "DOUBLEPRECISIONVALUE", "FLOATVALUE", "BINARYFLOATVALUE", "BINARYDOUBLEVALUE", "DATEVALUE", "TIMESTAMPVALUE", "TIMESTAMPTZVALUE", "TIMESTAMPLTZVALUE", "CLOBVALUE", "NCLOBVALUE", "BLOBVALUE", "SUBOBJECTVALUE", "SUBOBJECTARRAY" ] actual_attr_names = [a.name for a in type_obj.attributes] self.assertEqual(actual_attr_names, expected_attr_names) expected_attr_types = [ oracledb.DB_TYPE_NUMBER, oracledb.DB_TYPE_VARCHAR, oracledb.DB_TYPE_CHAR, oracledb.DB_TYPE_NVARCHAR, oracledb.DB_TYPE_NCHAR, oracledb.DB_TYPE_RAW, oracledb.DB_TYPE_NUMBER, oracledb.DB_TYPE_NUMBER, oracledb.DB_TYPE_NUMBER, oracledb.DB_TYPE_NUMBER, oracledb.DB_TYPE_NUMBER, oracledb.DB_TYPE_BINARY_FLOAT, oracledb.DB_TYPE_BINARY_DOUBLE, oracledb.DB_TYPE_DATE, oracledb.DB_TYPE_TIMESTAMP, oracledb.DB_TYPE_TIMESTAMP_TZ, oracledb.DB_TYPE_TIMESTAMP_LTZ, oracledb.DB_TYPE_CLOB, oracledb.DB_TYPE_NCLOB, oracledb.DB_TYPE_BLOB, sub_object_value_type, sub_object_array_type ] actual_attr_types = [a.type for a in type_obj.attributes] self.assertEqual(actual_attr_types, expected_attr_types) self.assertEqual(sub_object_array_type.iscollection, True) self.assertEqual(sub_object_array_type.attributes, []) def test_2306_object_type(self): "2306 - test object type data" self.cursor.execute(""" select ObjectCol from TestObjects where ObjectCol is not null and rownum <= 1""") obj, = self.cursor.fetchone() self.assertEqual(obj.type.schema, self.connection.username.upper()) self.assertEqual(obj.type.name, "UDT_OBJECT") self.assertEqual(obj.type.attributes[0].name, "NUMBERVALUE") def test_2307_round_trip_object(self): "2307 - test inserting and then querying object with all data types" self.cursor.execute("alter session set time_zone = 'UTC'") self.cursor.execute("truncate table TestClobs") self.cursor.execute("truncate table TestNClobs") self.cursor.execute("truncate table TestBlobs") self.cursor.execute(""" insert into TestClobs (IntCol, ClobCol) values (1, 'A short CLOB')""") self.cursor.execute(""" insert into TestNClobs (IntCol, NClobCol) values (1, 'A short NCLOB')""") self.cursor.execute(""" insert into TestBlobs (IntCol, BlobCol) values (1, utl_raw.cast_to_raw('A short BLOB'))""") self.connection.commit() self.cursor.execute("select CLOBCol from TestClobs") clob, = self.cursor.fetchone() self.cursor.execute("select NCLOBCol from TestNClobs") nclob, = self.cursor.fetchone() self.cursor.execute("select BLOBCol from TestBlobs") blob, = self.cursor.fetchone() type_obj = self.connection.gettype("UDT_OBJECT") obj = type_obj.newobject() obj.NUMBERVALUE = 5 obj.STRINGVALUE = "A string" obj.FIXEDCHARVALUE = "Fixed str" obj.NSTRINGVALUE = "A NCHAR string" obj.NFIXEDCHARVALUE = "Fixed N" obj.RAWVALUE = b"Raw Value" obj.INTVALUE = 27 obj.SMALLINTVALUE = 13 obj.REALVALUE = 184.875 obj.DOUBLEPRECISIONVALUE = 1.375 obj.FLOATVALUE = 23.75 obj.DATEVALUE = datetime.date(2017, 5, 9) obj.TIMESTAMPVALUE = datetime.datetime(2017, 5, 9, 9, 41, 13) obj.TIMESTAMPTZVALUE = datetime.datetime(1986, 8, 2, 15, 27, 38) obj.TIMESTAMPLTZVALUE = datetime.datetime(1999, 11, 12, 23, 5, 2) obj.BINARYFLOATVALUE = 14.25 obj.BINARYDOUBLEVALUE = 29.1625 obj.CLOBVALUE = clob obj.NCLOBVALUE = nclob obj.BLOBVALUE = blob sub_type_obj = self.connection.gettype("UDT_SUBOBJECT") sub_obj = sub_type_obj.newobject() sub_obj.SUBNUMBERVALUE = 23 sub_obj.SUBSTRINGVALUE = "Substring value" obj.SUBOBJECTVALUE = sub_obj self.cursor.execute("insert into TestObjects (IntCol, ObjectCol) " \ "values (4, :obj)", obj=obj) self.cursor.execute(""" select IntCol, ObjectCol, ArrayCol from TestObjects where IntCol = 4""") expected_value = ( 5, 'A string', 'Fixed str ', 'A NCHAR string', 'Fixed N ', b'Raw Value', 27, 13, 184.875, 1.375, 23.75, 14.25, 29.1625, oracledb.Timestamp(2017, 5, 9, 0, 0, 0), oracledb.Timestamp(2017, 5, 9, 9, 41, 13), oracledb.Timestamp(1986, 8, 2, 15, 27, 38), oracledb.Timestamp(1999, 11, 12, 23, 5, 2), 'A short CLOB', 'A short NCLOB', b'A short BLOB', (23, 'Substring value'), None ) self.__test_data(4, expected_value, None) obj.CLOBVALUE = "A short CLOB (modified)" obj.NCLOBVALUE = "A short NCLOB (modified)" obj.BLOBVALUE = "A short BLOB (modified)" self.cursor.execute("insert into TestObjects (IntCol, ObjectCol) " \ "values (5, :obj)", obj = obj) self.cursor.execute(""" select IntCol, ObjectCol, ArrayCol from TestObjects where IntCol = 5""") expected_value = ( 5, 'A string', 'Fixed str ', 'A NCHAR string', 'Fixed N ', b'Raw Value', 27, 13, 184.875, 1.375, 23.75, 14.25, 29.1625, oracledb.Timestamp(2017, 5, 9, 0, 0, 0), oracledb.Timestamp(2017, 5, 9, 9, 41, 13), oracledb.Timestamp(1986, 8, 2, 15, 27, 38), oracledb.Timestamp(1999, 11, 12, 23, 5, 2), 'A short CLOB (modified)', 'A short NCLOB (modified)', b'A short BLOB (modified)', (23, 'Substring value'), None ) self.__test_data(5, expected_value, None) self.connection.rollback() def test_2308_invalid_type_object(self): "2308 - test trying to find an object type that does not exist" self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-2035:", self.connection.gettype, "A TYPE THAT DOES NOT EXIST") def test_2309_appending_wrong_object_type(self): "2309 - test appending an object of the wrong type to a collection" collection_obj_type = self.connection.gettype("UDT_OBJECTARRAY") collection_obj = collection_obj_type.newobject() array_obj_type = self.connection.gettype("UDT_ARRAY") array_obj = array_obj_type.newobject() self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-2008:", collection_obj.append, array_obj) def test_2310_referencing_sub_obj(self): "2310 - test that referencing a sub object affects the parent object" obj_type = self.connection.gettype("UDT_OBJECT") sub_obj_type = self.connection.gettype("UDT_SUBOBJECT") obj = obj_type.newobject() obj.SUBOBJECTVALUE = sub_obj_type.newobject() obj.SUBOBJECTVALUE.SUBNUMBERVALUE = 5 obj.SUBOBJECTVALUE.SUBSTRINGVALUE = "Substring" self.assertEqual(obj.SUBOBJECTVALUE.SUBNUMBERVALUE, 5) self.assertEqual(obj.SUBOBJECTVALUE.SUBSTRINGVALUE, "Substring") def test_2311_access_sub_object_parent_object_destroyed(self): "2311 - test accessing sub object after parent object destroyed" obj_type = self.connection.gettype("UDT_OBJECT") sub_obj_type = self.connection.gettype("UDT_SUBOBJECT") array_type = self.connection.gettype("UDT_OBJECTARRAY") sub_obj1 = sub_obj_type.newobject() sub_obj1.SUBNUMBERVALUE = 2 sub_obj1.SUBSTRINGVALUE = "AB" sub_obj2 = sub_obj_type.newobject() sub_obj2.SUBNUMBERVALUE = 3 sub_obj2.SUBSTRINGVALUE = "CDE" obj = obj_type.newobject() obj.SUBOBJECTARRAY = array_type.newobject([sub_obj1, sub_obj2]) sub_obj_array = obj.SUBOBJECTARRAY del obj self.assertEqual(self.get_db_object_as_plain_object(sub_obj_array), [(2, "AB"), (3, "CDE")]) def test_2312_setting_attr_wrong_object_type(self): "2312 - test assigning an object of wrong type to an object attribute" obj_type = self.connection.gettype("UDT_OBJECT") obj = obj_type.newobject() wrong_obj_type = self.connection.gettype("UDT_OBJECTARRAY") wrong_obj = wrong_obj_type.newobject() self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-2008:", setattr, obj, "SUBOBJECTVALUE", wrong_obj) def test_2313_setting_var_wrong_object_type(self): "2313 - test setting value of object variable to wrong object type" wrong_obj_type = self.connection.gettype("UDT_OBJECTARRAY") wrong_obj = wrong_obj_type.newobject() var = self.cursor.var(oracledb.DB_TYPE_OBJECT, typename="UDT_OBJECT") self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2008:", var.setvalue, 0, wrong_obj) def test_2314_string_format(self): "2314 - test object string format" obj_type = self.connection.gettype("UDT_OBJECT") user = test_env.get_main_user() self.assertEqual(str(obj_type), "" % user.upper()) self.assertEqual(str(obj_type.attributes[0]), "") def test_2315_trim_collection_list(self): "2315 - test Trim number of elements from collection" sub_obj_type = self.connection.gettype("UDT_SUBOBJECT") array_type = self.connection.gettype("UDT_OBJECTARRAY") data = [(1, "AB"), (2, "CDE"), (3, "FGH"), (4, "IJK"), (5, "LMN")] array_obj = array_type() for num_val, str_val in data: subObj = sub_obj_type() subObj.SUBNUMBERVALUE = num_val subObj.SUBSTRINGVALUE = str_val array_obj.append(subObj) self.assertEqual(self.get_db_object_as_plain_object(array_obj), data) array_obj.trim(2) self.assertEqual(self.get_db_object_as_plain_object(array_obj), data[:3]) array_obj.trim(1) self.assertEqual(self.get_db_object_as_plain_object(array_obj), data[:2]) array_obj.trim(0) self.assertEqual(self.get_db_object_as_plain_object(array_obj), data[:2]) array_obj.trim(2) self.assertEqual(self.get_db_object_as_plain_object(array_obj), []) def test_2316_sql_type_metadata(self): "2316 - test the metadata of a SQL type" user = test_env.get_main_user() typ = self.connection.gettype("UDT_OBJECTARRAY") self.assertEqual(typ.schema, user.upper()) self.assertEqual(typ.name, "UDT_OBJECTARRAY") self.assertEqual(typ.package_name, None) self.assertEqual(typ.element_type.schema, user.upper()) self.assertEqual(typ.element_type.name, "UDT_SUBOBJECT") self.assertEqual(typ.element_type.package_name, None) def test_2317_plsql_type_metadata(self): "2317 - test the metadata of a PL/SQL type" user = test_env.get_main_user() typ = self.connection.gettype("PKG_TESTSTRINGARRAYS.UDT_STRINGLIST") self.assertEqual(typ.schema, user.upper()) self.assertEqual(typ.name, "UDT_STRINGLIST") self.assertEqual(typ.package_name, "PKG_TESTSTRINGARRAYS") self.assertEqual(typ.element_type, oracledb.DB_TYPE_VARCHAR) def test_2318_negative_create_object_var_no_type_name(self): "2318 - test creating an object variable without a type name" self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-2037:", self.cursor.var, oracledb.DB_TYPE_OBJECT) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_2400_pool.py000066400000000000000000001031451434177474600210230ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 2400 - Module for testing pools """ import threading import unittest import oracledb import test_env class TestCase(test_env.BaseTestCase): require_connection = False def __connect_and_drop(self): with self.pool.acquire() as connection: cursor = connection.cursor() cursor.execute("select count(*) from TestNumbers") count, = cursor.fetchone() self.assertEqual(count, 10) def __connect_and_generate_error(self): with self.pool.acquire() as connection: cursor = connection.cursor() self.assertRaisesRegex(oracledb.DatabaseError,"^ORA-01476:", cursor.execute, "select 1 / 0 from dual") def __callable_session_callback(self, conn, requested_tag): self.session_called = True supported_formats = { "SIMPLE" : "'YYYY-MM-DD HH24:MI'", "FULL" : "'YYYY-MM-DD HH24:MI:SS'" } supported_time_zones = { "UTC" : "'UTC'", "MST" : "'-07:00'" } supported_keys = { "NLS_DATE_FORMAT" : supported_formats, "TIME_ZONE" : supported_time_zones } if requested_tag is not None: state_parts = [] for directive in requested_tag.split(";"): parts = directive.split("=") if len(parts) != 2: raise ValueError("Tag must contain key=value pairs") key, value = parts value_dict = supported_keys.get(key) if value_dict is None: raise ValueError("Tag only supports keys: %s" % \ (", ".join(supported_keys))) actual_value = value_dict.get(value) if actual_value is None: raise ValueError("Key %s only supports values: %s" % \ (key, ", ".join(value_dict))) state_parts.append("%s = %s" % (key, actual_value)) sql = "alter session set %s" % " ".join(state_parts) cursor = conn.cursor() cursor.execute(sql) conn.tag = requested_tag def __perform_reconfigure_test(self, parameter_name, parameter_value, min=3, max=30, increment=4, timeout=5, wait_timeout=5000, stmtcachesize=25, max_lifetime_session=1000, max_sessions_per_shard=3, ping_interval=30, getmode=oracledb.POOL_GETMODE_WAIT): creation_args = dict(min=min, max=max, increment=increment, timeout=timeout, wait_timeout=wait_timeout, stmtcachesize=stmtcachesize, max_lifetime_session=max_lifetime_session, max_sessions_per_shard=max_sessions_per_shard, ping_interval=ping_interval, getmode=getmode) reconfigure_args = {} reconfigure_args[parameter_name] = parameter_value pool = test_env.get_pool(**creation_args) connection = pool.acquire() pool.reconfigure(**reconfigure_args) actual_args = dict(min=pool.min, max=pool.max, increment=pool.increment, timeout=pool.timeout, wait_timeout=pool.wait_timeout, stmtcachesize=pool.stmtcachesize, max_lifetime_session=pool.max_lifetime_session, max_sessions_per_shard=pool.max_sessions_per_shard, ping_interval=pool.ping_interval, getmode=pool.getmode) expected_args = creation_args.copy() expected_args.update(reconfigure_args) self.assertEqual(actual_args, expected_args) def __verify_connection(self, connection, expected_user, expected_proxy_user=None): cursor = connection.cursor() cursor.execute(""" select sys_context('userenv', 'session_user'), sys_context('userenv', 'proxy_user') from dual""") actual_user, actual_proxy_user = cursor.fetchone() self.assertEqual(actual_user, expected_user.upper()) self.assertEqual(actual_proxy_user, expected_proxy_user and expected_proxy_user.upper()) def test_2400_pool(self): "2400 - test that the pool is created and has the right attributes" pool = test_env.get_pool(min=2, max=8, increment=3, getmode=oracledb.POOL_GETMODE_WAIT) self.assertEqual(pool.username, test_env.get_main_user(), "user name differs") self.assertEqual(pool.dsn, test_env.get_connect_string(), "dsn differs") self.assertEqual(pool.max, 8, "max differs") self.assertEqual(pool.min, 2, "min differs") self.assertEqual(pool.increment, 3, "increment differs") self.assertEqual(pool.busy, 0, "busy not 0 at start") connection_1 = pool.acquire() self.assertEqual(pool.busy, 1, "busy not 1 after acquire") connection_2 = oracledb.connect(pool=pool) self.assertEqual(pool.busy, 2, "busy not 2 after acquire") self.assertEqual(pool.opened, 2, "opened differs") connection_3 = pool.acquire() self.assertEqual(pool.busy, 3, "busy not 3 after acquire") pool.release(connection_3) self.assertEqual(pool.busy, 2, "busy not 2 after release") pool.release(connection_1) connection_2.close() self.assertEqual(pool.busy, 0, "busy not 0 after release") pool.getmode = oracledb.POOL_GETMODE_NOWAIT self.assertEqual(pool.getmode, oracledb.POOL_GETMODE_NOWAIT) if test_env.get_client_version() >= (12, 2): pool.getmode = oracledb.POOL_GETMODE_TIMEDWAIT self.assertEqual(pool.getmode, oracledb.POOL_GETMODE_TIMEDWAIT) pool.stmtcachesize = 50 self.assertEqual(pool.stmtcachesize, 50) pool.timeout = 10 self.assertEqual(pool.timeout, 10) if test_env.get_client_version() >= (12, 1): pool.max_lifetime_session = 10 self.assertEqual(pool.max_lifetime_session, 10) @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support proxy users yet") def test_2401_proxy_auth(self): "2401 - test that proxy authentication is possible" pool = test_env.get_pool(min=2, max=8, increment=3, getmode=oracledb.POOL_GETMODE_WAIT) self.assertEqual(pool.homogeneous, True, "homogeneous should be True by default") self.assertRaisesRegex(oracledb.DatabaseError, "^DPI-1012:", pool.acquire, user="missing_proxyuser") pool = test_env.get_pool(min=2, max=8, increment=3, getmode=oracledb.POOL_GETMODE_WAIT, homogeneous=False) msg = "homogeneous should be False after setting it in the constructor" self.assertEqual(pool.homogeneous, False, msg) connection = pool.acquire(user=test_env.get_proxy_user()) cursor = connection.cursor() cursor.execute('select user from dual') result, = cursor.fetchone() self.assertEqual(result, test_env.get_proxy_user().upper()) connection.close() def test_2403_rollback_on_release(self): "2403 - connection rolls back before released back to the pool" pool = test_env.get_pool(getmode=oracledb.POOL_GETMODE_WAIT) connection = pool.acquire() cursor = connection.cursor() cursor.execute("truncate table TestTempTable") cursor.execute("insert into TestTempTable (IntCol) values (1)") cursor.close() pool.release(connection) pool = test_env.get_pool(getmode=oracledb.POOL_GETMODE_WAIT) connection = pool.acquire() cursor = connection.cursor() cursor.execute("select count(*) from TestTempTable") count, = cursor.fetchone() self.assertEqual(count, 0) connection.close() def test_2404_threading(self): "2404 - test session pool with multiple threads" self.pool = test_env.get_pool(min=5, max=20, increment=2, getmode=oracledb.POOL_GETMODE_WAIT) threads = [] for i in range(20): thread = threading.Thread(None, self.__connect_and_drop) threads.append(thread) thread.start() for thread in threads: thread.join() def test_2405_threading_with_errors(self): "2405 - test session pool with multiple threads (with errors)" self.pool = test_env.get_pool(min=5, max=20, increment=2, getmode=oracledb.POOL_GETMODE_WAIT) threads = [] for i in range(20): thread = threading.Thread(None, self.__connect_and_generate_error) threads.append(thread) thread.start() for thread in threads: thread.join() def test_2406_purity(self): "2406 - test session pool with various types of purity" pool = test_env.get_pool(min=1, max=8, increment=1, getmode=oracledb.POOL_GETMODE_WAIT) # get connection and set the action action = "TEST_ACTION" connection = pool.acquire() connection.action = action cursor = connection.cursor() cursor.execute("select 1 from dual") cursor.close() pool.release(connection) self.assertEqual(pool.opened, 1, "opened (1)") # verify that the connection still has the action set on it connection = pool.acquire() cursor = connection.cursor() cursor.execute("select sys_context('userenv', 'action') from dual") result, = cursor.fetchone() self.assertEqual(result, action) cursor.close() pool.release(connection) self.assertEqual(pool.opened, 1, "opened (2)") # get a new connection with new purity (should not have state) connection = pool.acquire(purity=oracledb.ATTR_PURITY_NEW) cursor = connection.cursor() cursor.execute("select sys_context('userenv', 'action') from dual") result, = cursor.fetchone() self.assertEqual(result, None) cursor.close() pool.release(connection) @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support proxy users yet") def test_2407_heterogeneous(self): "2407 - test heterogeneous pool with user and password specified" pool = test_env.get_pool(min=2, max=8, increment=3, homogeneous=False, getmode=oracledb.POOL_GETMODE_WAIT) self.assertEqual(pool.homogeneous, 0) conn = pool.acquire() self.__verify_connection(pool.acquire(), test_env.get_main_user()) conn.close() conn = pool.acquire(test_env.get_main_user(), test_env.get_main_password()) self.__verify_connection(conn, test_env.get_main_user()) conn.close() conn = pool.acquire(test_env.get_proxy_user(), test_env.get_proxy_password()) self.__verify_connection(conn, test_env.get_proxy_user()) conn.close() user_str = "%s[%s]" % \ (test_env.get_main_user(), test_env.get_proxy_user()) conn = pool.acquire(user_str, test_env.get_main_password()) self.__verify_connection(conn, test_env.get_proxy_user(), test_env.get_main_user()) conn.close() @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support proxy users yet") def test_2408_heterogenous_without_user(self): "2408 - test heterogeneous pool without user and password specified" pool = test_env.get_pool(user="", password="", min=2, max=8, increment=3, getmode=oracledb.POOL_GETMODE_WAIT, homogeneous=False) conn = pool.acquire(test_env.get_main_user(), test_env.get_main_password()) self.__verify_connection(conn, test_env.get_main_user()) conn.close() conn = pool.acquire(test_env.get_proxy_user(), test_env.get_proxy_password()) self.__verify_connection(conn, test_env.get_proxy_user()) conn.close() user_str = "%s[%s]" % \ (test_env.get_main_user(), test_env.get_proxy_user()) conn = pool.acquire(user_str, test_env.get_main_password()) self.__verify_connection(conn, test_env.get_proxy_user(), test_env.get_main_user()) @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support proxy users yet") def test_2409_heterogeneous_wrong_password(self): "2409 - test heterogeneous pool with wrong password specified" pool = test_env.get_pool(min=2, max=8, increment=3, getmode=oracledb.POOL_GETMODE_WAIT, homogeneous=False) self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-01017:", pool.acquire, test_env.get_proxy_user(), "this is the wrong password") @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support tagging yet") def test_2410_tagging_session(self): "2410 - test tagging a session" pool = test_env.get_pool(min=2, max=8, increment=3, getmode=oracledb.POOL_GETMODE_NOWAIT) tag_mst = "TIME_ZONE=MST" tag_utc = "TIME_ZONE=UTC" conn = pool.acquire() self.assertEqual(conn.tag, None) pool.release(conn, tag=tag_mst) conn = pool.acquire() self.assertEqual(conn.tag, None) conn.tag = tag_utc conn.close() conn = pool.acquire(tag=tag_mst) self.assertEqual(conn.tag, tag_mst) conn.close() conn = pool.acquire(tag=tag_utc) self.assertEqual(conn.tag, tag_utc) conn.close() @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support session callbacks yet") def test_2411_plsql_session_callbacks(self): "2411 - test PL/SQL session callbacks" if test_env.get_client_version() < (12, 2): self.skipTest("PL/SQL session callbacks not supported before 12.2") callback = "pkg_SessionCallback.TheCallback" pool = test_env.get_pool(min=2, max=8, increment=3, getmode=oracledb.POOL_GETMODE_NOWAIT, session_callback=callback) tags = [ "NLS_DATE_FORMAT=SIMPLE", "NLS_DATE_FORMAT=FULL;TIME_ZONE=UTC", "NLS_DATE_FORMAT=FULL;TIME_ZONE=MST" ] actual_tags = [None, None, "NLS_DATE_FORMAT=FULL;TIME_ZONE=UTC"] # truncate PL/SQL session callback log conn = pool.acquire() cursor = conn.cursor() cursor.execute("truncate table PLSQLSessionCallbacks") conn.close() # request sessions with each of the first two tags for tag in tags[:2]: conn = pool.acquire(tag=tag) conn.close() # for the last tag, use the matchanytag flag conn = pool.acquire(tag=tags[2], matchanytag=True) conn.close() # verify the PL/SQL session callback log is accurate conn = pool.acquire() cursor = conn.cursor() cursor.execute(""" select RequestedTag, ActualTag from PLSQLSessionCallbacks order by FixupTimestamp""") results = cursor.fetchall() expected_results = list(zip(tags, actual_tags)) self.assertEqual(results, expected_results) conn.close() @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support tagging yet") def test_2412_tagging_invalid_key(self): "2412 - testTagging with Invalid key" pool = test_env.get_pool(getmode=oracledb.POOL_GETMODE_NOWAIT) conn = pool.acquire() self.assertRaises(TypeError, pool.release, conn, tag=12345) if test_env.get_client_version() >= (12, 2): self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-24488:", pool.release, conn, tag="INVALID_TAG") def test_2413_close_and_drop_connection_from_pool(self): "2413 - test dropping/closing a connection from the pool" pool = test_env.get_pool(min=1, max=8, increment=1, getmode=oracledb.POOL_GETMODE_WAIT) conn = pool.acquire() self.assertEqual(pool.busy, 1, "busy (1)") self.assertEqual(pool.opened, 1, "opened (1)") pool.drop(conn) self.assertEqual(pool.busy, 0, "busy (2)") self.assertEqual(pool.opened, 0, "opened (2)") conn = pool.acquire() self.assertEqual(pool.busy, 1, "busy (3)") self.assertEqual(pool.opened, 1, "opened (3)") conn.close() self.assertEqual(pool.busy, 0, "busy (4)") self.assertEqual(pool.opened, 1, "opened (4)") def test_2414_create_new_pure_connection(self): "2414 - test to ensure pure connections are being created correctly" pool = test_env.get_pool(min=1, max=2, increment=1, getmode=oracledb.POOL_GETMODE_WAIT) connection_1 = pool.acquire() connection_2 = pool.acquire() self.assertEqual(pool.opened, 2, "opened (1)") pool.release(connection_1) pool.release(connection_2) connection_3 = pool.acquire(purity=oracledb.ATTR_PURITY_NEW) self.assertEqual(pool.opened, 2, "opened (2)") pool.release(connection_3) @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support pool reconfigure yet") def test_2415_reconfigure_pool(self): "2415 - test to ensure reconfigure() updates pool properties" pool = test_env.get_pool(min=1, max=2, increment=1, getmode=oracledb.POOL_GETMODE_WAIT) self.assertEqual(pool.min, 1, "min (1)") self.assertEqual(pool.max, 2, "max (2)") self.assertEqual(pool.increment, 1, "increment (1)") self.assertEqual(pool.getmode, oracledb.POOL_GETMODE_WAIT, "getmode differs") self.assertEqual(pool.timeout, 0, "timeout (0)") self.assertEqual(pool.wait_timeout, 5000, "wait_timeout (5000)") self.assertEqual(pool.max_lifetime_session, 0, "max_lifetime_sessionmeout (0)") self.assertEqual(pool.max_sessions_per_shard, 0, "max_sessions_per_shard (0)") self.assertEqual(pool.stmtcachesize, 20, "stmtcachesize (20)") self.assertEqual(pool.ping_interval, 60, "ping_interval (60)") pool.reconfigure(min=2, max=5, increment=2, timeout=30, getmode=oracledb.POOL_GETMODE_TIMEDWAIT, wait_timeout=3000, max_lifetime_session=20, max_sessions_per_shard=2, stmtcachesize=30, ping_interval=30) self.assertEqual(pool.min, 2, "min (2)") self.assertEqual(pool.max, 5, "max (5)") self.assertEqual(pool.increment, 2, "increment (2)") self.assertEqual(pool.getmode, oracledb.POOL_GETMODE_TIMEDWAIT, "getmode differs") self.assertEqual(pool.timeout, 30, "timeout (30)") self.assertEqual(pool.wait_timeout, 3000, "wait_timeout (3000)") self.assertEqual(pool.max_lifetime_session, 20, "max_lifetime_sessionmeout (20)") self.assertEqual(pool.max_sessions_per_shard, 2, "max_sessions_per_shard (2)") self.assertEqual(pool.stmtcachesize, 30, "stmtcachesize (30)") self.assertEqual(pool.ping_interval, 30, "ping_interval (30)") @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support all the pool params yet") def test_2416_test_reconfigure_pool_with_missing_values(self): "2416 - test the reconfigure values are changed and rest unchanged" self.__perform_reconfigure_test("min", 5) self.__perform_reconfigure_test("max", 20) self.__perform_reconfigure_test("increment", 5) self.__perform_reconfigure_test("timeout", 10) self.__perform_reconfigure_test("wait_timeout", 8000) self.__perform_reconfigure_test("stmtcachesize", 40) self.__perform_reconfigure_test("max_lifetime_session", 2000) self.__perform_reconfigure_test("max_sessions_per_shard", 5) self.__perform_reconfigure_test("ping_interval", 50) self.__perform_reconfigure_test("getmode", oracledb.POOL_GETMODE_NOWAIT) @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support all the pool params yet") def test_2417_setting_each_pool_param(self): "2417 - test to see if specified parameters are set during creation" pool = test_env.get_pool(min=1, max=2, increment=1, timeout=10, wait_timeout=10, max_lifetime_session=20, max_sessions_per_shard=1, stmtcachesize=25, ping_interval=25, getmode=oracledb.POOL_GETMODE_WAIT) self.assertEqual(pool.min, 1, "min (1)") self.assertEqual(pool.max, 2, "max (2)") self.assertEqual(pool.increment, 1, "increment (1)") self.assertEqual(pool.getmode, oracledb.POOL_GETMODE_WAIT, "getmode differs") self.assertEqual(pool.timeout, 10, "timeout (10)") self.assertEqual(pool.wait_timeout, 10, "wait_timeout (10)") self.assertEqual(pool.max_lifetime_session, 20, "max_lifetime_sessionmeout (20)") self.assertEqual(pool.max_sessions_per_shard, 1, "max_sessions_per_shard (1)") self.assertEqual(pool.stmtcachesize, 25, "stmtcachesize (25)") self.assertEqual(pool.ping_interval, 25, "ping_interval (25)") def test_2418_deprecations(self): "2418 - test to verify deprecations" callback = "pkg_SessionCallback.TheCallback" self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2014:", test_env.get_pool, min=1, max=2, increment=1, wait_timeout=10, waitTimeout=10) self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2014:", test_env.get_pool, min=1, max=2, increment=1, max_lifetime_session=20, maxLifetimeSession=20) self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2014:", test_env.get_pool, min=1, max=2, increment=1, max_sessions_per_shard=1, maxSessionsPerShard=1) self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2014:", test_env.get_pool, min=2, max=8, increment=3, getmode=oracledb.POOL_GETMODE_NOWAIT, session_callback=callback, sessionCallback=callback) def test_2419_statement_cache_size(self): "2419 - test to verify statement cache size is retained" pool = test_env.get_pool(min=1, max=2, increment=1, getmode=oracledb.POOL_GETMODE_WAIT, stmtcachesize=25) self.assertEqual(pool.stmtcachesize, 25, "stmtcachesize (25)") pool.stmtcachesize = 35 self.assertEqual(pool.stmtcachesize, 35, "stmtcachesize (35)") @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support tagging yet") def test_2420_callable_session_callbacks(self): "2420 - test that session callbacks are being called correctly" pool = test_env.get_pool(min=2, max=5, increment=1, session_callback=self.__callable_session_callback) # new connection with a tag should invoke the session callback with pool.acquire(tag="NLS_DATE_FORMAT=SIMPLE") as conn: cursor = conn.cursor() cursor.execute("select to_char(2021-05-20) from dual") result, = cursor.fetchone() self.assertEqual(self.session_called, True) # acquiring a connection with the same tag should not invoke the # session callback self.session_called = False with pool.acquire(tag="NLS_DATE_FORMAT=SIMPLE") as conn: cursor = conn.cursor() cursor.execute("select to_char(2021-05-20) from dual") result, = cursor.fetchone() self.assertEqual(self.session_called, False) # acquiring a connection with a new tag should invoke the session # callback self.session_called = False with pool.acquire(tag="NLS_DATE_FORMAT=FULL;TIME_ZONE=UTC") as conn: cursor = conn.cursor() cursor.execute("select to_char(current_date) from dual") result, = cursor.fetchone() self.assertEqual(self.session_called, True) # acquiring a connection with a new tag and specifying that a # connection with any tag can be acquired should invoke the session # callback self.session_called = False with pool.acquire(tag="NLS_DATE_FORMAT=FULL;TIME_ZONE=MST", \ matchanytag=True) as conn: cursor = conn.cursor() cursor.execute("select to_char(current_date) from dual") result, = cursor.fetchone() self.assertEqual(self.session_called, True) # new connection with no tag should invoke the session callback self.session_called = False with pool.acquire() as conn: cursor = conn.cursor() cursor.execute("select to_char(current_date) from dual") result, = cursor.fetchone() self.assertEqual(self.session_called, True) def test_2421_pool_close_normal_no_connections(self): "2421 - test closing a pool normally with no connections checked out" pool = test_env.get_pool(min=1, max=8, increment=1, getmode=oracledb.POOL_GETMODE_WAIT) pool.close() def test_2422_pool_close_normal_with_connections(self): "2422 - test closing a pool normally with connections checked out" pool = test_env.get_pool(min=1, max=8, increment=1, getmode=oracledb.POOL_GETMODE_WAIT) conn = pool.acquire() self.assertRaisesRegex(oracledb.InterfaceError, "^DPY-1005:", pool.close) def test_2423_pool_close_force(self): "2423 - test closing a pool forcibly" pool = test_env.get_pool(min=1, max=8, increment=1, getmode=oracledb.POOL_GETMODE_WAIT) conn = pool.acquire() pool.close(force=True) def test_2424_exception_on_acquire_after_pool_closed(self): "2424 - using the pool after it is closed raises an exception" pool = test_env.get_pool(min=1, max=8, increment=1, getmode=oracledb.POOL_GETMODE_WAIT) pool.close() self.assertRaisesRegex(oracledb.InterfaceError, "^DPY-1002:", pool.acquire) def test_2425_pool_with_no_connections(self): "2425 - using the pool beyond max limit raises an error" pool = test_env.get_pool(min=1, max=2, increment=1, getmode=oracledb.POOL_GETMODE_WAIT) connection_1 = pool.acquire() connection_2 = pool.acquire() pool.getmode = oracledb.POOL_GETMODE_NOWAIT self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4005:", pool.acquire) def test_2426_session_callback_for_new_connections(self): "2426 - callable session callback is executed for new connections" class Counter: num_calls = 0 @classmethod def session_callback(cls, conn, requested_tag): cls.num_calls += 1 pool = test_env.get_pool(min=1, max=2, increment=1, session_callback=Counter.session_callback) with pool.acquire() as conn1: with pool.acquire() as conn2: pass with pool.acquire() as conn1: with pool.acquire() as conn2: pass self.assertEqual(Counter.num_calls, 2) def test_2427_drop_dead_connection_from_pool(self): "2427 - drop the pooled connection on receiving dead connection error" admin_conn = test_env.get_admin_connection() pool = test_env.get_pool(min=2, max=2, increment=2) # acquire connections from the pool and kill all the sessions with admin_conn.cursor() as admin_cursor: for conn in [pool.acquire() for i in range(2)]: with conn.cursor() as cursor: cursor.execute(""" select dbms_debug_jdwp.current_session_id, dbms_debug_jdwp.current_session_serial from dual""") sid, serial = cursor.fetchone() sql = f"alter system kill session '{sid},{serial}'" admin_cursor.execute(sql) conn.close() self.assertEqual(pool.opened, 2) # when try to re-use the killed sessions error will be raised; # release all such connections for conn in [pool.acquire() for i in range(2)]: with conn.cursor() as cursor: self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4011:", cursor.execute, "select user from dual") conn.close() self.assertEqual(pool.opened, 0) # if a free connection is available, it can be used; otherwise a new # connection will be created for conn in [pool.acquire() for i in range(2)]: with conn.cursor() as cursor: cursor.execute("select user from dual") user, = cursor.fetchone() self.assertEqual(user, test_env.get_main_user().upper()) conn.close() self.assertEqual(pool.opened, 2) def test_2428_acquire_conn_from_empty_pool(self): "2428 - acquire a connection from an empty pool (min=0)" pool = test_env.get_pool(min=0, max=2, increment=2) with pool.acquire() as conn: with conn.cursor() as cursor: cursor.execute("select user from dual") result, = cursor.fetchone() self.assertEqual(result, test_env.get_main_user().upper()) @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support soda_metadata_cache" \ "parameter yet") def test_2429_soda_metadata_cache(self): "2429 - test soda_metadata_cache parameter" self.get_soda_database(minclient=(19, 11)) pool = test_env.get_pool(min=0, max=2, increment=2) self.assertEqual(pool.soda_metadata_cache, False) pool = test_env.get_pool(min=0, max=2, increment=2, soda_metadata_cache=True) self.assertEqual(pool.soda_metadata_cache, True) pool.soda_metadata_cache = False self.assertEqual(pool.soda_metadata_cache, False) def test_2430_get_different_types_from_pooled_connections(self): "2430 - get different object types from different connections" pool = test_env.get_pool(min=1, max=2, increment=1) with pool.acquire() as conn: typ = conn.gettype("UDT_SUBOBJECT") self.assertEqual(typ.name, "UDT_SUBOBJECT") with pool.acquire() as conn: typ = conn.gettype("UDT_OBJECTARRAY") self.assertEqual(typ.name, "UDT_OBJECTARRAY") def test_2431_proxy_user_in_create(self): "2431 - test creating a pool using a proxy user" user_str = "%s[%s]" % \ (test_env.get_main_user(), test_env.get_proxy_user()) pool = test_env.get_pool(user=user_str) self.__verify_connection(pool.acquire(), test_env.get_proxy_user(), test_env.get_main_user()) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_2500_string_var.py000066400000000000000000000515331434177474600222340ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 2500 - Module for testing string variables """ import random import string import unittest import oracledb import test_env class TestCase(test_env.BaseTestCase): def setUp(self): super().setUp() self.raw_data = [] self.data_by_key = {} for i in range(1, 11): string_col = "String %d" % i fixed_char_col = ("Fixed Char %d" % i).ljust(40) raw_col = ("Raw %d" % i).encode("ascii") if i % 2: nullable_col = "Nullable %d" % i else: nullable_col = None data_tuple = (i, string_col, raw_col, fixed_char_col, nullable_col) self.raw_data.append(data_tuple) self.data_by_key[i] = data_tuple def __return_strings_as_bytes(self, cursor, name, default_type, size, precision, scale): if default_type == oracledb.DB_TYPE_VARCHAR: return cursor.var(str, arraysize=cursor.arraysize, bypass_decode=True) def test_2500_array_with_increased_size(self): "2500 - test creating array var and then increasing the internal size" val = ["12345678901234567890"] * 3 var = self.cursor.arrayvar(str, len(val), 4) var.setvalue(0, val) self.assertEqual(var.getvalue(), val) def test_2501_bind_string(self): "2501 - test binding in a string" self.cursor.execute(""" select * from TestStrings where StringCol = :value""", value="String 5") self.assertEqual(self.cursor.fetchall(), [self.data_by_key[5]]) def test_2502_bind_different_var(self): "2502 - test binding a different variable on second execution" retval_1 = self.cursor.var(oracledb.STRING, 30) retval_2 = self.cursor.var(oracledb.STRING, 30) self.cursor.execute("begin :retval := 'Called'; end;", retval=retval_1) self.assertEqual(retval_1.getvalue(), "Called") self.cursor.execute("begin :retval := 'Called'; end;", retval=retval_2) self.assertEqual(retval_2.getvalue(), "Called") def test_2503_exceeds_num_elements(self): "2503 - test exceeding the number of elements returns IndexError" var = self.cursor.var(str) self.assertRaises(IndexError, var.getvalue, 1) def test_2504_bind_string_after_number(self): "2504 - test binding in a string after setting input sizes to a number" self.cursor.setinputsizes(value = oracledb.NUMBER) self.cursor.execute(""" select * from TestStrings where StringCol = :value""", value="String 6") self.assertEqual(self.cursor.fetchall(), [self.data_by_key[6]]) def test_2505_bind_string_array_direct(self): "2505 - test binding in a string array" return_value = self.cursor.var(oracledb.NUMBER) array = [r[1] for r in self.raw_data] statement = """ begin :return_value := pkg_TestStringArrays.TestInArrays( :integer_value, :array); end;""" self.cursor.execute(statement, return_value=return_value, integer_value=5, array=array) self.assertEqual(return_value.getvalue(), 86) array = [ "String - %d" % i for i in range(15) ] self.cursor.execute(statement, integer_value=8, array=array) self.assertEqual(return_value.getvalue(), 163) def test_2506_bind_string_array_by_sizes(self): "2506 - test binding in a string array (with setinputsizes)" return_value = self.cursor.var(oracledb.NUMBER) self.cursor.setinputsizes(array=[oracledb.STRING, 10]) array = [r[1] for r in self.raw_data] self.cursor.execute(""" begin :return_value := pkg_TestStringArrays.TestInArrays( :integer_value, :array); end;""", return_value=return_value, integer_value=6, array=array) self.assertEqual(return_value.getvalue(), 87) def test_2507_bind_string_array_by_var(self): "2507 - test binding in a string array (with arrayvar)" return_value = self.cursor.var(oracledb.NUMBER) array = self.cursor.arrayvar(oracledb.STRING, 10, 20) array.setvalue(0, [r[1] for r in self.raw_data]) self.cursor.execute(""" begin :return_value := pkg_TestStringArrays.TestInArrays( :integer_value, :array); end;""", return_value=return_value, integer_value=7, array=array) self.assertEqual(return_value.getvalue(), 88) def test_2508_bind_in_out_string_array_by_var(self): "2508 - test binding in/out a string array (with arrayvar)" array = self.cursor.arrayvar(oracledb.STRING, 10, 100) original_data = [r[1] for r in self.raw_data] expected_data = ["Converted element # %d originally had length %d" % \ (i, len(original_data[i - 1])) \ for i in range(1, 6)] + original_data[5:] array.setvalue(0, original_data) self.cursor.execute(""" begin pkg_TestStringArrays.TestInOutArrays(:num_elems, :array); end;""", num_elems=5, array=array) self.assertEqual(array.getvalue(), expected_data) def test_2509_bind_out_string_array_by_var(self): "2509 - test binding out a string array (with arrayvar)" array = self.cursor.arrayvar(oracledb.STRING, 6, 100) expected_data = ["Test out element # %d" % i for i in range(1, 7)] self.cursor.execute(""" begin pkg_TestStringArrays.TestOutArrays(:num_elems, :array); end;""", num_elems=6, array=array) self.assertEqual(array.getvalue(), expected_data) def test_2510_bind_raw(self): "2510 - test binding in a raw" self.cursor.setinputsizes(value = oracledb.BINARY) self.cursor.execute(""" select * from TestStrings where RawCol = :value""", value="Raw 4".encode()) self.assertEqual(self.cursor.fetchall(), [self.data_by_key[4]]) def test_2511_bind_and_fetch_rowid(self): "2511 - test binding (and fetching) a rowid" self.cursor.execute(""" select rowid from TestStrings where IntCol = 3""") rowid, = self.cursor.fetchone() self.cursor.execute(""" select * from TestStrings where rowid = :value""", value=rowid) self.assertEqual(self.cursor.fetchall(), [self.data_by_key[3]]) def test_2513_bind_null(self): "2513 - test binding in a null" self.cursor.execute(""" select * from TestStrings where StringCol = :value""", value=None) self.assertEqual(self.cursor.fetchall(), []) def test_2514_bind_out_set_input_sizes_by_type(self): "2514 - test binding out with set input sizes defined (by type)" bind_vars = self.cursor.setinputsizes(value=oracledb.STRING) self.cursor.execute(""" begin :value := 'TSI'; end;""") self.assertEqual(bind_vars["value"].getvalue(), "TSI") def test_2515_bind_out_set_input_sizes_by_integer(self): "2515 - test binding out with set input sizes defined (by integer)" bind_vars = self.cursor.setinputsizes(value=30) self.cursor.execute(""" begin :value := 'TSI (I)'; end;""") self.assertEqual(bind_vars["value"].getvalue(), "TSI (I)") def test_2516_bind_in_out_set_input_sizes_by_type(self): "2516 - test binding in/out with set input sizes defined (by type)" bind_vars = self.cursor.setinputsizes(value=oracledb.STRING) self.cursor.execute(""" begin :value := :value || ' TSI'; end;""", value="InVal") self.assertEqual(bind_vars["value"].getvalue(), "InVal TSI") def test_2517_bind_in_out_set_input_sizes_by_integer(self): "2517 - test binding in/out with set input sizes defined (by integer)" bind_vars = self.cursor.setinputsizes(value=30) self.cursor.execute(""" begin :value := :value || ' TSI (I)'; end;""", value="InVal") self.assertEqual(bind_vars["value"].getvalue(), "InVal TSI (I)") def test_2518_bind_out_var(self): "2518 - test binding out with cursor.var() method" var = self.cursor.var(oracledb.STRING) self.cursor.execute(""" begin :value := 'TSI (VAR)'; end;""", value=var) self.assertEqual(var.getvalue(), "TSI (VAR)") def test_2519_bind_in_out_var_direct_set(self): "2519 - test binding in/out with cursor.var() method" var = self.cursor.var(oracledb.STRING) var.setvalue(0, "InVal") self.cursor.execute(""" begin :value := :value || ' TSI (VAR)'; end;""", value=var) self.assertEqual(var.getvalue(), "InVal TSI (VAR)") def test_2520_bind_long_string(self): "2520 - test that binding a long string succeeds" self.cursor.setinputsizes(big_string=oracledb.DB_TYPE_LONG) self.cursor.execute(""" declare t_Temp varchar2(20000); begin t_Temp := :big_string; end;""", big_string="X" * 10000) def test_2521_bind_long_string_after_setting_size(self): "2521 - test that setinputsizes() returns a long variable" var = self.cursor.setinputsizes(test=90000)["test"] in_string = "1234567890" * 9000 var.setvalue(0, in_string) out_string = var.getvalue() msg = f"output does not match: in was {len(in_string)}, " \ f"out was {len(out_string)}" self.assertEqual(in_string, out_string, msg) def test_2522_cursor_description(self): "2522 - test cursor description is accurate" self.cursor.execute("select * from TestStrings") varchar_ratio, nvarchar_ratio = test_env.get_charset_ratios() expected_value = [ ('INTCOL', oracledb.DB_TYPE_NUMBER, 10, None, 9, 0, False), ('STRINGCOL', oracledb.DB_TYPE_VARCHAR, 20, 20 * varchar_ratio, None, None, False), ('RAWCOL', oracledb.DB_TYPE_RAW, 30, 30, None, None, False), ('FIXEDCHARCOL', oracledb.DB_TYPE_CHAR, 40, 40 * varchar_ratio, None, None, False), ('NULLABLECOL', oracledb.DB_TYPE_VARCHAR, 50, 50 * varchar_ratio, None, None, True) ] self.assertEqual(self.cursor.description, expected_value) def test_2523_fetchall(self): "2523 - test that fetching all of the data returns the correct results" self.cursor.execute("select * From TestStrings order by IntCol") self.assertEqual(self.cursor.fetchall(), self.raw_data) self.assertEqual(self.cursor.fetchall(), []) def test_2524_fetchmany(self): "2524 - test that fetching data in chunks returns the correct results" self.cursor.execute("select * From TestStrings order by IntCol") self.assertEqual(self.cursor.fetchmany(3), self.raw_data[0:3]) self.assertEqual(self.cursor.fetchmany(2), self.raw_data[3:5]) self.assertEqual(self.cursor.fetchmany(4), self.raw_data[5:9]) self.assertEqual(self.cursor.fetchmany(3), self.raw_data[9:]) self.assertEqual(self.cursor.fetchmany(3), []) def test_2525_fetchone(self): "2525 - test that fetching a single row returns the correct results" self.cursor.execute(""" select * from TestStrings where IntCol in (3, 4) order by IntCol""") self.assertEqual(self.cursor.fetchone(), self.data_by_key[3]) self.assertEqual(self.cursor.fetchone(), self.data_by_key[4]) self.assertEqual(self.cursor.fetchone(), None) def test_2526_supplemental_characters(self): "2526 - test binding and fetching supplemental charcters" self.cursor.execute(""" select value from nls_database_parameters where parameter = 'NLS_CHARACTERSET'""") charset, = self.cursor.fetchone() if charset != "AL32UTF8": self.skipTest("Database character set must be AL32UTF8") supplemental_chars = "𠜎 𠜱 𠝹 𠱓 𠱸 𠲖 𠳏 𠳕 𠴕 𠵼 𠵿 𠸎 𠸏 𠹷 𠺝 " \ "𠺢 𠻗 𠻹 𠻺 𠼭 𠼮 𠽌 𠾴 𠾼 𠿪 𡁜 𡁯 𡁵 𡁶 𡁻 𡃁 𡃉 𡇙 𢃇 " \ "𢞵 𢫕 𢭃 𢯊 𢱑 𢱕 𢳂 𢴈 𢵌 𢵧 𢺳 𣲷 𤓓 𤶸 𤷪 𥄫 𦉘 𦟌 𦧲 " \ "𦧺 𧨾 𨅝 𨈇 𨋢 𨳊 𨳍 𨳒 𩶘" self.cursor.execute("truncate table TestTempTable") self.cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (:1, :2)""", (1, supplemental_chars)) self.connection.commit() self.cursor.execute("select StringCol1 from TestTempTable") value, = self.cursor.fetchone() self.assertEqual(value, supplemental_chars) def test_2527_bind_twice_with_large_string_second(self): "2527 - test binding twice with a larger string the second time" self.cursor.execute("truncate table TestTempTable") sql = "insert into TestTempTable (IntCol, StringCol1) values (:1, :2)" short_string = "short string" long_string = "long string " * 30 self.cursor.execute(sql, (1, short_string)) self.cursor.execute(sql, (2, long_string)) self.connection.commit() self.cursor.execute(""" select IntCol, StringCol1 from TestTempTable order by IntCol""") self.assertEqual(self.cursor.fetchall(), [(1, short_string), (2, long_string)]) def test_2528_issue_50(self): "2528 - test issue 50 - avoid error ORA-24816" cursor = self.connection.cursor() try: cursor.execute("drop table issue_50 purge") except oracledb.DatabaseError: pass cursor.execute(""" create table issue_50 ( Id number(11) primary key, Str1 nvarchar2(256), Str2 nvarchar2(256), Str3 nvarchar2(256), NClob1 nclob, NClob2 nclob )""") id_var = cursor.var(oracledb.NUMBER) cursor.execute(""" insert into issue_50 (Id, Str2, Str3, NClob1, NClob2, Str1) values (:arg0, :arg1, :arg2, :arg3, :arg4, :arg5) returning id into :arg6""", [1, '555a4c78', 'f319ef0e', '23009914', '', '', id_var]) cursor = self.connection.cursor() cursor.execute(""" insert into issue_50 (Id, Str2, Str3, NClob1, NClob2, Str1) values (:arg0, :arg1, :arg2, :arg3, :arg4, :arg5) returning id into :arg6""", [2, 'd5ff845a', '94275767', 'bf161ff6', '', '', id_var]) cursor.execute("drop table issue_50 purge") def test_2529_set_rowid_to_string(self): "2529 - test assigning a string to rowid" var = self.cursor.var(oracledb.ROWID) self.assertRaisesRegex(oracledb.NotSupportedError, "^DPY-3004:", var.setvalue, 0, "ABDHRYTHFJGKDKKDH") def test_2530_short_xml_as_string(self): "2530 - test fetching XMLType (< 1K) as a string" self.cursor.execute(""" select XMLElement("string", stringCol) from TestStrings where intCol = 1""") actual_value, = self.cursor.fetchone() expected_value = "String 1" self.assertEqual(actual_value, expected_value) def test_2531_long_xml_as_string(self): "2531 - test inserting and fetching XMLType (1K) as a string" chars = string.ascii_uppercase + string.ascii_lowercase random_string = ''.join(random.choice(chars) for _ in range(1024)) int_val = 2531 xml_string = '' + random_string + '' self.cursor.execute("truncate table TestTempXML") self.cursor.execute(""" insert into TestTempXML (IntCol, XMLCol) values (:1, :2)""", (int_val, xml_string)) self.cursor.execute("select XMLCol from TestTempXML where intCol = :1", (int_val,)) actual_value, = self.cursor.fetchone() self.assertEqual(actual_value.strip(), xml_string) def test_2532_fetch_null_values(self): "2532 - fetching null and not null values can use optimised path" sql = """ select * from TestStrings where IntCol between :start_value and :end_value""" self.cursor.execute(sql, start_value=2, end_value=5) self.assertEqual(self.cursor.fetchall(), self.raw_data[1:5]) self.cursor.execute(sql, start_value=5, end_value=8) self.assertEqual(self.cursor.fetchall(), self.raw_data[4:8]) self.cursor.execute(sql, start_value=8, end_value=10) self.assertEqual(self.cursor.fetchall(), self.raw_data[7:10]) def test_2533_bypass_decode(self): "2533 - test bypass string decode" self.cursor.execute("truncate table TestTempTable") string_val = "I bought a cafetière on the Champs-Élysées" sql = "insert into TestTempTable (IntCol, StringCol1) values (:1, :2)" with self.connection.cursor() as cursor: cursor.execute(sql, (1, string_val)) cursor.execute("select IntCol, StringCol1 from TestTempTable") self.assertEqual(cursor.fetchone(), (1, string_val)) with self.connection.cursor() as cursor: cursor.outputtypehandler = self.__return_strings_as_bytes cursor.execute("select IntCol, StringCol1 from TestTempTable") self.assertEqual(cursor.fetchone(), (1, string_val.encode())) with self.connection.cursor() as cursor: cursor.outputtypehandler = None cursor.execute("select IntCol, StringCol1 from TestTempTable") self.assertEqual(cursor.fetchone(), (1, string_val)) @unittest.skipIf(not test_env.get_is_thin(), "thick mode doesn't support fetching XMLType > VARCHAR2") def test_2534_very_long_xml_as_string(self): "2534 - test inserting and fetching XMLType (32K) as a string" chars = string.ascii_uppercase + string.ascii_lowercase random_string = ''.join(random.choice(chars) for _ in range(32768)) int_val = 2534 xml_string = f"{random_string}" lob = self.connection.createlob(oracledb.DB_TYPE_CLOB) lob.write(xml_string) self.cursor.execute("truncate table TestTempXML") self.cursor.execute(""" insert into TestTempXML (IntCol, XMLCol) values (:1, sys.xmltype(:2))""", (int_val, lob)) self.cursor.execute("select XMLCol from TestTempXML where intCol = :1", (int_val,)) actual_value, = self.cursor.fetchone() self.assertEqual(actual_value.strip(), xml_string) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_2600_timestamp_var.py000066400000000000000000000173301434177474600227270ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 2600 - Module for testing timestamp variables """ import time import oracledb import test_env class TestCase(test_env.BaseTestCase): def setUp(self): super().setUp() self.raw_data = [] self.data_by_key = {} for i in range(1, 11): time_tuple = (2002, 12, 9, 0, 0, 0, 0, 0, -1) time_in_ticks = time.mktime(time_tuple) + i * 86400 date_value = oracledb.TimestampFromTicks(int(time_in_ticks)) str_value = str(i * 50) fsecond = int(str_value + "0" * (6 - len(str_value))) date_col = oracledb.Timestamp(date_value.year, date_value.month, date_value.day, date_value.hour, date_value.minute, i * 2, fsecond) if i % 2: time_in_ticks = time.mktime(time_tuple) + i * 86400 + 86400 date_value = oracledb.TimestampFromTicks(int(time_in_ticks)) str_value = str(i * 125) fsecond = int(str_value + "0" * (6 - len(str_value))) nullable_col = oracledb.Timestamp(date_value.year, date_value.month, date_value.day, date_value.hour, date_value.minute, i * 3, fsecond) else: nullable_col = None data_tuple = (i, date_col, nullable_col) self.raw_data.append(data_tuple) self.data_by_key[i] = data_tuple def test_2600_bind_timestamp(self): "2600 - test binding in a timestamp" self.cursor.setinputsizes(value=oracledb.DB_TYPE_TIMESTAMP) self.cursor.execute(""" select * from TestTimestamps where TimestampCol = :value""", value=oracledb.Timestamp(2002, 12, 14, 0, 0, 10, 250000)) self.assertEqual(self.cursor.fetchall(), [self.data_by_key[5]]) def test_2601_bind_null(self): "2601 - test binding in a null" self.cursor.setinputsizes(value=oracledb.DB_TYPE_TIMESTAMP) self.cursor.execute(""" select * from TestTimestamps where TimestampCol = :value""", value=None) self.assertEqual(self.cursor.fetchall(), []) def test_2602_bind_out_set_input_sizes(self): "2602 - test binding out with set input sizes defined" bind_vars = self.cursor.setinputsizes(value=oracledb.DB_TYPE_TIMESTAMP) self.cursor.execute(""" begin :value := to_timestamp('20021209', 'YYYYMMDD'); end;""") self.assertEqual(bind_vars["value"].getvalue(), oracledb.Timestamp(2002, 12, 9)) def test_2603_bind_in_out_set_input_sizes(self): "2603 - test binding in/out with set input sizes defined" bind_vars = self.cursor.setinputsizes(value=oracledb.DB_TYPE_TIMESTAMP) self.cursor.execute(""" begin :value := :value + 5.25; end;""", value = oracledb.Timestamp(2002, 12, 12, 10, 0, 0)) self.assertEqual(bind_vars["value"].getvalue(), oracledb.Timestamp(2002, 12, 17, 16, 0, 0)) def test_2604_bind_out_var(self): "2604 - test binding out with cursor.var() method" var = self.cursor.var(oracledb.DB_TYPE_TIMESTAMP) self.cursor.execute(""" begin :value := to_date('20021231 12:31:00', 'YYYYMMDD HH24:MI:SS'); end;""", value=var) self.assertEqual(var.getvalue(), oracledb.Timestamp(2002, 12, 31, 12, 31, 0)) def test_2605_bind_in_out_var_direct_set(self): "2605 - test binding in/out with cursor.var() method" var = self.cursor.var(oracledb.DB_TYPE_TIMESTAMP) var.setvalue(0, oracledb.Timestamp(2002, 12, 9, 6, 0, 0)) self.cursor.execute(""" begin :value := :value + 5.25; end;""", value = var) self.assertEqual(var.getvalue(), oracledb.Timestamp(2002, 12, 14, 12, 0, 0)) def test_2606_cursor_description(self): "2606 - test cursor description is accurate" self.cursor.execute("select * from TestTimestamps") expected_value = [ ('INTCOL', oracledb.DB_TYPE_NUMBER, 10, None, 9, 0, False), ('TIMESTAMPCOL', oracledb.DB_TYPE_TIMESTAMP, 23, None, 0, 6, False), ('NULLABLECOL', oracledb.DB_TYPE_TIMESTAMP, 23, None, 0, 6, True) ] self.assertEqual(self.cursor.description, expected_value) def test_2607_fetchall(self): "2607 - test that fetching all of the data returns the correct results" self.cursor.execute("select * From TestTimestamps order by IntCol") self.assertEqual(self.cursor.fetchall(), self.raw_data) self.assertEqual(self.cursor.fetchall(), []) def test_2608_fetchmany(self): "2608 - test that fetching data in chunks returns the correct results" self.cursor.execute("select * From TestTimestamps order by IntCol") self.assertEqual(self.cursor.fetchmany(3), self.raw_data[0:3]) self.assertEqual(self.cursor.fetchmany(2), self.raw_data[3:5]) self.assertEqual(self.cursor.fetchmany(4), self.raw_data[5:9]) self.assertEqual(self.cursor.fetchmany(3), self.raw_data[9:]) self.assertEqual(self.cursor.fetchmany(3), []) def test_2609_fetchone(self): "2609 - test that fetching a single row returns the correct results" self.cursor.execute(""" select * from TestTimestamps where IntCol in (3, 4) order by IntCol""") self.assertEqual(self.cursor.fetchone(), self.data_by_key[3]) self.assertEqual(self.cursor.fetchone(), self.data_by_key[4]) self.assertEqual(self.cursor.fetchone(), None) def test_2610_bind_timestamp_with_zero_fseconds(self): "2610 - test binding a timestamp with zero fractional seconds" self.cursor.setinputsizes(value=oracledb.DB_TYPE_TIMESTAMP) self.cursor.execute(""" select * from TestTimestamps where trunc(TimestampCol) = :value""", value=oracledb.Timestamp(2002, 12, 14)) self.assertEqual(self.cursor.fetchall(), [self.data_by_key[5]]) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_2700_aq.py000066400000000000000000000551441434177474600204630ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 2700 - Module for testing AQ """ import datetime import decimal import threading import unittest import oracledb import test_env @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support AQ yet") class TestCase(test_env.BaseTestCase): book_type_name = "UDT_BOOK" book_queue_name = "TEST_BOOK_QUEUE" book_data = [ ("Wings of Fire", "A.P.J. Abdul Kalam", decimal.Decimal("15.75")), ("The Story of My Life", "Hellen Keller", decimal.Decimal("10.50")), ("The Chronicles of Narnia", "C.S. Lewis", decimal.Decimal("25.25")) ] json_queue_name = "TEST_JSON_QUEUE" json_data = [ [ 2.75, True, 'Ocean Beach', b'Some bytes', {'keyA': 1.0, 'KeyB': 'Melbourne'}, datetime.datetime(2022, 8, 1, 0, 0) ], [ True, False, 'String', b'Some Bytes', {}, {"name": None}, {"name": "John"}, {"age": 30}, {"Permanent": True}, { "employee": { "name":"John", "age": 30, "city": "Delhi", "Parmanent": True } }, { "employees": ["John", "Matthew", "James"] }, { "employees": [ { "employee1": {"name": "John", "city": "Delhi"} }, { "employee2": {"name": "Matthew", "city": "Mumbai"} }, { "employee3": {"name": "James", "city": "Bangalore"} } ] } ], [ datetime.datetime.today(), datetime.datetime(2004, 2, 1, 3, 4, 5), datetime.datetime(2020, 12, 2, 13, 29, 14), datetime.timedelta(8.5), datetime.datetime(2002, 12, 13, 9, 36, 0), oracledb.Timestamp(2002, 12, 13, 9, 36, 0), datetime.datetime(2002, 12, 13) ], dict(name="John", age=30, city="New York"), [ 0, 1, 25.25, 6088343244, -9999999999999999999, decimal.Decimal("0.25"), decimal.Decimal("10.25"), decimal.Decimal("319438950232418390.273596") ] ] def __deq_in_thread(self, results): with test_env.get_connection() as connection: books_type = connection.gettype(self.book_type_name) queue = connection.queue(self.book_queue_name, books_type) queue.deqoptions.wait = 10 props = queue.deqone() if props is not None: book = props.payload results.append((book.TITLE, book.AUTHORS, book.PRICE)) connection.commit() def __verify_attr(self, obj, attrName, value): setattr(obj, attrName, value) self.assertEqual(getattr(obj, attrName), value) def test_2700_deq_empty(self): "2700 - test dequeuing an empty queue" queue = self.get_and_clear_queue(self.book_queue_name, self.book_type_name) queue.deqoptions.wait = oracledb.DEQ_NO_WAIT props = queue.deqone() self.assertTrue(props is None) def test_2701_deq_enq(self): "2701 - test enqueuing and dequeuing multiple messages" queue = self.get_and_clear_queue(self.book_queue_name, self.book_type_name) props = self.connection.msgproperties() for title, authors, price in self.book_data: props.payload = book = queue.payload_type.newobject() book.TITLE = title book.AUTHORS = authors book.PRICE = price queue.enqone(props) queue.deqoptions.navigation = oracledb.DEQ_FIRST_MSG queue.deqoptions.wait = oracledb.DEQ_NO_WAIT results = [] while True: props = queue.deqone() if props is None: break book = props.payload row = (book.TITLE, book.AUTHORS, book.PRICE) results.append(row) self.connection.commit() self.assertEqual(results, self.book_data) def test_2702_deq_mode_remove_no_data(self): "2702 - test dequeuing with DEQ_REMOVE_NODATA option" queue = self.get_and_clear_queue(self.book_queue_name, self.book_type_name) book = queue.payload_type.newobject() title, authors, price = self.book_data[1] book.TITLE = title book.AUTHORS = authors book.PRICE = price props = self.connection.msgproperties(payload=book) queue.enqone(props) queue.deqoptions.navigation = oracledb.DEQ_FIRST_MSG queue.deqoptions.wait = oracledb.DEQ_NO_WAIT queue.deqoptions.mode = oracledb.DEQ_REMOVE_NODATA props = queue.deqone() self.assertTrue(props is not None) self.assertTrue(props.payload.TITLE is None) def test_2703_deq_options(self): "2703 - test getting/setting dequeue options attributes" queue = self.get_and_clear_queue(self.book_queue_name, self.book_type_name) options = queue.deqoptions self.__verify_attr(options, "condition", "TEST_CONDITION") self.__verify_attr(options, "consumername", "TEST_CONSUMERNAME") self.__verify_attr(options, "correlation", "TEST_CORRELATION") self.__verify_attr(options, "mode", oracledb.DEQ_LOCKED) self.__verify_attr(options, "navigation", oracledb.DEQ_NEXT_TRANSACTION) self.__verify_attr(options, "transformation", "TEST_TRANSFORMATION") self.__verify_attr(options, "visibility", oracledb.ENQ_IMMEDIATE) self.__verify_attr(options, "wait", 1287) self.__verify_attr(options, "msgid", b'mID') def test_2704_deq_with_wait(self): "2704 - test waiting for dequeue" queue = self.get_and_clear_queue(self.book_queue_name, self.book_type_name) results = [] thread = threading.Thread(target=self.__deq_in_thread, args=(results,)) thread.start() book = queue.payload_type.newobject() title, authors, price = self.book_data[0] book.TITLE = title book.AUTHORS = authors book.PRICE = price props = self.connection.msgproperties(payload=book) queue.enqone(props) self.connection.commit() thread.join() self.assertEqual(results, [(title, authors, price)]) def test_2705_enq_options(self): "2705 - test getting/setting enqueue options attributes" queue = self.get_and_clear_queue(self.book_queue_name, self.book_type_name) options = queue.enqoptions self.__verify_attr(options, "visibility", oracledb.ENQ_IMMEDIATE) def test_2706_errors_for_invalid_values(self): "2706 - test errors for invalid values for enqueue" queue = self.get_and_clear_queue(self.book_queue_name, self.book_type_name) book = queue.payload_type.newobject() self.assertRaises(TypeError, queue.enqone, book) def test_2707_msg_props(self): "2707 - test getting/setting message properties attributes" props = self.connection.msgproperties() self.__verify_attr(props, "correlation", "TEST_CORRELATION") self.__verify_attr(props, "delay", 60) self.__verify_attr(props, "exceptionq", "TEST_EXCEPTIONQ") self.__verify_attr(props, "expiration", 30) self.assertEqual(props.attempts, 0) self.__verify_attr(props, "priority", 1) self.assertEqual(props.state, oracledb.MSG_READY) self.assertEqual(props.deliverymode, 0) def test_2708_visibility_mode_commit(self): "2708 - test enqueue visibility option - ENQ_ON_COMMIT" queue = self.get_and_clear_queue(self.book_queue_name, self.book_type_name) book = queue.payload_type.newobject() book.TITLE, book.AUTHORS, book.PRICE = self.book_data[0] queue.enqoptions.visibility = oracledb.ENQ_ON_COMMIT props = self.connection.msgproperties(payload=book) queue.enqone(props) other_connection = test_env.get_connection() books_type = other_connection.gettype(self.book_type_name) queue = other_connection.queue(self.book_queue_name, books_type) queue.deqoptions.navigation = oracledb.DEQ_FIRST_MSG queue.deqoptions.wait = oracledb.DEQ_NO_WAIT props = queue.deqone() self.assertTrue(props is None) self.connection.commit() props = queue.deqone() self.assertTrue(props is not None) def test_2709_visibility_mode_immediate(self): "2709 - test enqueue visibility option - ENQ_IMMEDIATE" queue = self.get_and_clear_queue(self.book_queue_name, self.book_type_name) book = queue.payload_type.newobject() book.TITLE, book.AUTHORS, book.PRICE = self.book_data[0] queue.enqoptions.visibility = oracledb.ENQ_IMMEDIATE props = self.connection.msgproperties(payload=book) queue.enqone(props) other_connection = test_env.get_connection() books_type = other_connection.gettype(self.book_type_name) queue = other_connection.queue(self.book_queue_name, books_type) queue.deqoptions.navigation = oracledb.DEQ_FIRST_MSG queue.deqoptions.visibility = oracledb.DEQ_ON_COMMIT queue.deqoptions.wait = oracledb.DEQ_NO_WAIT props = queue.deqone() book = props.payload results = (book.TITLE, book.AUTHORS, book.PRICE) other_connection.commit() self.assertEqual(results, self.book_data[0]) def test_2710_delivery_mode_same_buffered(self): "2710 - test enqueue/dequeue delivery modes identical - buffered" queue = self.get_and_clear_queue(self.book_queue_name, self.book_type_name) book = queue.payload_type.newobject() book.TITLE, book.AUTHORS, book.PRICE = self.book_data[0] queue.enqoptions.deliverymode = oracledb.MSG_BUFFERED queue.enqoptions.visibility = oracledb.ENQ_IMMEDIATE props = self.connection.msgproperties(payload=book) queue.enqone(props) other_connection = test_env.get_connection() books_type = other_connection.gettype(self.book_type_name) queue = other_connection.queue(self.book_queue_name, books_type) queue.deqoptions.deliverymode = oracledb.MSG_BUFFERED queue.deqoptions.navigation = oracledb.DEQ_FIRST_MSG queue.deqoptions.visibility = oracledb.DEQ_IMMEDIATE queue.deqoptions.wait = oracledb.DEQ_NO_WAIT props = queue.deqone() book = props.payload results = (book.TITLE, book.AUTHORS, book.PRICE) other_connection.commit() self.assertEqual(results, self.book_data[0]) def test_2711_delivery_mode_same_persistent(self): "2711 - test enqueue/dequeue delivery modes identical - persistent" queue = self.get_and_clear_queue(self.book_queue_name, self.book_type_name) book = queue.payload_type.newobject() book.TITLE, book.AUTHORS, book.PRICE = self.book_data[0] queue.enqoptions.deliverymode = oracledb.MSG_PERSISTENT queue.enqoptions.visibility = oracledb.ENQ_IMMEDIATE props = self.connection.msgproperties(payload=book) queue.enqone(props) other_connection = test_env.get_connection() books_type = other_connection.gettype(self.book_type_name) queue = other_connection.queue(self.book_queue_name, books_type) queue.deqoptions.deliverymode = oracledb.MSG_PERSISTENT queue.deqoptions.navigation = oracledb.DEQ_FIRST_MSG queue.deqoptions.visibility = oracledb.DEQ_IMMEDIATE queue.deqoptions.wait = oracledb.DEQ_NO_WAIT props = queue.deqone() book = props.payload results = (book.TITLE, book.AUTHORS, book.PRICE) other_connection.commit() self.assertEqual(results, self.book_data[0]) def test_2712_delivery_mode_same_persistent_buffered(self): "2712 - test enqueue/dequeue delivery modes the same" queue = self.get_and_clear_queue(self.book_queue_name, self.book_type_name) book = queue.payload_type.newobject() book.TITLE, book.AUTHORS, book.PRICE = self.book_data[0] queue.enqoptions.deliverymode = oracledb.MSG_PERSISTENT_OR_BUFFERED queue.enqoptions.visibility = oracledb.ENQ_IMMEDIATE props = self.connection.msgproperties(payload=book) queue.enqone(props) other_connection = test_env.get_connection() books_type = other_connection.gettype(self.book_type_name) queue = other_connection.queue(self.book_queue_name, books_type) queue.deqoptions.deliverymode = oracledb.MSG_PERSISTENT_OR_BUFFERED queue.deqoptions.navigation = oracledb.DEQ_FIRST_MSG queue.deqoptions.visibility = oracledb.DEQ_IMMEDIATE queue.deqoptions.wait = oracledb.DEQ_NO_WAIT props = queue.deqone() book = props.payload results = (book.TITLE, book.AUTHORS, book.PRICE) other_connection.commit() self.assertEqual(results, self.book_data[0]) def test_2713_delivery_mode_different(self): "2713 - test enqueue/dequeue delivery modes different" queue = self.get_and_clear_queue(self.book_queue_name, self.book_type_name) book = queue.payload_type.newobject() book.TITLE, book.AUTHORS, book.PRICE = self.book_data[0] queue.enqoptions.deliverymode = oracledb.MSG_BUFFERED queue.enqoptions.visibility = oracledb.ENQ_IMMEDIATE props = self.connection.msgproperties(payload=book) queue.enqone(props) other_connection = test_env.get_connection() books_type = other_connection.gettype(self.book_type_name) queue = other_connection.queue(self.book_queue_name, books_type) queue.deqoptions.deliverymode = oracledb.MSG_PERSISTENT queue.deqoptions.navigation = oracledb.DEQ_FIRST_MSG queue.deqoptions.visibility = oracledb.DEQ_IMMEDIATE queue.deqoptions.wait = oracledb.DEQ_NO_WAIT props = queue.deqone() self.assertTrue(props is None) def test_2714_dequeue_transformation(self): "2714 - test dequeue transformation" queue = self.get_and_clear_queue(self.book_queue_name, self.book_type_name) book = queue.payload_type.newobject() book.TITLE, book.AUTHORS, book.PRICE = self.book_data[0] expected_price = book.PRICE + 10 props = self.connection.msgproperties(payload=book) queue.enqone(props) self.connection.commit() other_connection = test_env.get_connection() books_type = other_connection.gettype(self.book_type_name) queue = other_connection.queue(self.book_queue_name, books_type) queue.deqoptions.navigation = oracledb.DEQ_FIRST_MSG queue.deqoptions.visibility = oracledb.DEQ_IMMEDIATE queue.deqoptions.transformation = \ "%s.transform2" % self.connection.username queue.deqoptions.wait = oracledb.DEQ_NO_WAIT props = queue.deqone() self.assertEqual(props.payload.PRICE, expected_price) def test_2715_enqueue_transformation(self): "2715 - test enqueue transformation" queue = self.get_and_clear_queue(self.book_queue_name, self.book_type_name) book = queue.payload_type.newobject() book.TITLE, book.AUTHORS, book.PRICE = self.book_data[0] expected_price = book.PRICE + 5 queue.enqoptions.transformation = \ "%s.transform1" % self.connection.username props = self.connection.msgproperties(payload=book) queue.enqone(props) self.connection.commit() other_connection = test_env.get_connection() books_type = other_connection.gettype(self.book_type_name) queue = other_connection.queue(self.book_queue_name, books_type) queue.deqoptions.navigation = oracledb.DEQ_FIRST_MSG queue.deqoptions.visibility = oracledb.DEQ_IMMEDIATE queue.deqoptions.wait = oracledb.DEQ_NO_WAIT props = queue.deqone() self.assertEqual(props.payload.PRICE, expected_price) def test_2716_payloadType_deprecation(self): "2716 - test to verify payloadType is deprecated" books_type = self.connection.gettype(self.book_type_name) self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2014:", self.connection.queue, self.book_queue_name, books_type, payloadType=books_type) def test_2717_message_with_no_payload(self): "2717 - test error for message with no payload" books_type = self.connection.gettype(self.book_type_name) queue = self.connection.queue(self.book_queue_name, books_type) props = self.connection.msgproperties() self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2000:", queue.enqone, props) def test_2718_verify_msgid(self): "2718 - verify that the msgid property is returned correctly" queue = self.get_and_clear_queue(self.book_queue_name, self.book_type_name) book = queue.payload_type.newobject() book.TITLE, book.AUTHORS, book.PRICE = self.book_data[0] props = self.connection.msgproperties(payload=book) self.assertEqual(props.msgid, None) queue.enqone(props) self.cursor.execute("select msgid from book_queue_tab") actual_msgid, = self.cursor.fetchone() self.assertEqual(props.msgid, actual_msgid) props = queue.deqone() self.assertEqual(props.msgid, actual_msgid) def test_2719_recipients_list(self): "2719 - verify use of recipients property" books_type = self.connection.gettype(self.book_type_name) book = books_type.newobject() book.TITLE, book.AUTHORS, book.PRICE = self.book_data[0] queue = self.connection.queue("BOOK_QUEUE_MULTI", books_type) props = self.connection.msgproperties(payload=book, recipients=["sub2", "sub3"]) self.assertEqual(props.recipients, ["sub2", "sub3"]) queue.enqone(props) self.connection.commit() queue.deqoptions.wait = oracledb.DEQ_NO_WAIT queue.deqoptions.navigation = oracledb.DEQ_FIRST_MSG queue.deqoptions.consumername = "sub3" props1 = queue.deqone() book = props1.payload results = (book.TITLE, book.AUTHORS, book.PRICE) self.assertEqual(results, self.book_data[0]) queue.deqoptions.consumername = "sub1" props1 = queue.deqone() self.assertTrue(props1 is None) def test_2720_aq_notification(self): "2720 - verify msgid of aq message which spawned notification " if self.is_on_oracle_cloud(self.connection): self.skipTest("AQ notification not supported on the cloud") queue = self.get_and_clear_queue(self.book_queue_name, self.book_type_name) condition = threading.Condition() connection = test_env.get_connection(events=True) def notification_callback(message): self.cursor.execute("select msgid from book_queue_tab") actual_msgid, = self.cursor.fetchone() self.assertEqual(actual_msgid, message.msgid) with condition: condition.notify() sub = connection.subscribe(namespace=oracledb.SUBSCR_NAMESPACE_AQ, name=self.book_queue_name, callback=notification_callback, timeout=300) book = queue.payload_type.newobject() book.TITLE, book.AUTHORS, book.PRICE = self.book_data[0] props = self.connection.msgproperties(payload=book) queue.enqone(props) self.connection.commit() with condition: self.assertTrue(condition.wait(5)) def test_2721_json_enq_deq(self): "2721 - test enqueuing and dequeuing JSON payloads" queue = self.get_and_clear_queue(self.json_queue_name, "JSON") self.assertEqual(queue.payload_type, "JSON") for data in self.json_data: props = self.connection.msgproperties(payload=data) queue.enqone(props) self.connection.commit() queue.deqoptions.wait = oracledb.DEQ_NO_WAIT results = [] while True: props = queue.deqone() if props is None: break results.append(props.payload) self.connection.commit() self.assertEqual(results, self.json_data) def test_2722_no_json_payload(self): "2722 - test enqueuing to a JSON queue without a JSON payload" queue = self.get_and_clear_queue(self.json_queue_name, "JSON") random_string = "This is a string message" props = self.connection.msgproperties(payload=random_string) self.assertRaisesRegex(oracledb.DatabaseError, "^DPI-1071:", queue.enqone, props) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_2800_bulk_aq.py000066400000000000000000000167701434177474600215030ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 2800 - Module for testing AQ Bulk enqueue/dequeue """ import datetime import threading import unittest import oracledb import test_env RAW_QUEUE_NAME = "TEST_RAW_QUEUE" JSON_QUEUE_NAME = "TEST_JSON_QUEUE" RAW_PAYLOAD_DATA = [ "The first message", "The second message", "The third message", "The fourth message", "The fifth message", "The sixth message", "The seventh message", "The eighth message", "The ninth message", "The tenth message", "The eleventh message", "The twelfth and final message" ] JSON_DATA_PAYLOAD = [ [ 2.75, True, 'Ocean Beach', b'Some bytes', {'keyA': 1.0, 'KeyB': 'Melbourne'}, datetime.datetime(2022, 8, 1, 0, 0) ], dict(name="John", age=30, city="New York") ] @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support AQ yet") class TestCase(test_env.BaseTestCase): def __deq_in_thread(self, results): with test_env.get_connection() as connection: queue = connection.queue(RAW_QUEUE_NAME) queue.deqoptions.wait = 10 queue.deqoptions.navigation = oracledb.DEQ_FIRST_MSG while len(results) < len(RAW_PAYLOAD_DATA): messages = queue.deqmany(5) if not messages: break for m in messages: results.append(m.payload.decode()) connection.commit() def test_2800_enq_and_deq(self): "2800 - test bulk enqueue and dequeue" queue = self.get_and_clear_queue(RAW_QUEUE_NAME) messages = [self.connection.msgproperties(payload=d) \ for d in RAW_PAYLOAD_DATA] queue.enqmany(messages) messages = queue.deqmany(len(RAW_PAYLOAD_DATA)) data = [m.payload.decode() for m in messages] self.connection.commit() self.assertEqual(data, RAW_PAYLOAD_DATA) def test_2801_dequeue_empty(self): "2801 - test empty bulk dequeue" queue = self.get_and_clear_queue(RAW_QUEUE_NAME) queue.deqoptions.wait = oracledb.DEQ_NO_WAIT messages = queue.deqmany(5) self.connection.commit() self.assertEqual(messages, []) def test_2802_deq_with_wait(self): "2802 - test bulk dequeue with wait" queue = self.get_and_clear_queue(RAW_QUEUE_NAME) results = [] thread = threading.Thread(target=self.__deq_in_thread, args=(results,)) thread.start() messages = [self.connection.msgproperties(payload=d) \ for d in RAW_PAYLOAD_DATA] queue.enqoptions.visibility = oracledb.ENQ_IMMEDIATE queue.enqmany(messages) thread.join() self.assertEqual(results, RAW_PAYLOAD_DATA) def test_2803_enq_and_deq_multiple_times(self): "2803 - test enqueue and dequeue multiple times" queue = self.get_and_clear_queue(RAW_QUEUE_NAME) data_to_enqueue = RAW_PAYLOAD_DATA for num in (2, 6, 4): messages = [self.connection.msgproperties(payload=d) \ for d in data_to_enqueue[:num]] data_to_enqueue = data_to_enqueue[num:] queue.enqmany(messages) self.connection.commit() all_data = [] for num in (3, 5, 10): messages = queue.deqmany(num) all_data.extend(m.payload.decode() for m in messages) self.connection.commit() self.assertEqual(all_data, RAW_PAYLOAD_DATA) def test_2804_enq_and_deq_visibility(self): "2804 - test visibility option for enqueue and dequeue" queue = self.get_and_clear_queue(RAW_QUEUE_NAME) # first test with ENQ_ON_COMMIT (commit required) queue.enqoptions.visibility = oracledb.ENQ_ON_COMMIT props1 = self.connection.msgproperties(payload="A first message") props2 = self.connection.msgproperties(payload="A second message") queue.enqmany([props1, props2]) other_connection = test_env.get_connection() other_queue = other_connection.queue(RAW_QUEUE_NAME) other_queue.deqoptions.wait = oracledb.DEQ_NO_WAIT other_queue.deqoptions.visibility = oracledb.DEQ_ON_COMMIT messages = other_queue.deqmany(5) self.assertEqual(len(messages), 0) self.connection.commit() messages = other_queue.deqmany(5) self.assertEqual(len(messages), 2) other_connection.rollback() # second test with ENQ_IMMEDIATE (no commit required) queue.enqoptions.visibility = oracledb.ENQ_IMMEDIATE other_queue.deqoptions.visibility = oracledb.DEQ_IMMEDIATE queue.enqmany([props1, props2]) messages = other_queue.deqmany(5) self.assertEqual(len(messages), 4) other_connection.rollback() messages = other_queue.deqmany(5) self.assertEqual(len(messages), 0) def test_2805_messages_with_no_payload(self): "2805 - test error for messages with no payload" queue = self.get_and_clear_queue(RAW_QUEUE_NAME) messages = [self.connection.msgproperties() \ for d in RAW_PAYLOAD_DATA] self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2000:", queue.enqmany, messages) def test_2806_verify_msgid(self): "2806 - verify that the msgid property is returned correctly" queue = self.get_and_clear_queue(RAW_QUEUE_NAME) messages = [self.connection.msgproperties(payload=d) \ for d in RAW_PAYLOAD_DATA] queue.enqmany(messages) self.cursor.execute("select msgid from raw_queue_tab") actual_msgids = set(m for m, in self.cursor) msgids = set(m.msgid for m in messages) self.assertEqual(msgids, actual_msgids) messages = queue.deqmany(len(RAW_PAYLOAD_DATA)) msgids = set(m.msgid for m in messages) self.assertEqual(msgids, actual_msgids) def test_2807_json_enq_deq(self): "4800 - test enqueuing and dequeuing JSON message" queue = self.get_and_clear_queue(JSON_QUEUE_NAME, "JSON") props = [self.connection.msgproperties(payload=d) \ for d in JSON_DATA_PAYLOAD] queue.enqmany(props) self.connection.commit() queue.deqoptions.wait = oracledb.DEQ_NO_WAIT messages = queue.deqmany(5) actual_data = [m.payload for m in messages] self.assertEqual(actual_data, JSON_DATA_PAYLOAD) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_2900_rowid.py000066400000000000000000000172051434177474600212040ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 2900 - Module for testing Rowids """ import datetime import unittest import oracledb import test_env class TestCase(test_env.BaseTestCase): def __populate_test_universal_rowids(self): self.cursor.execute("truncate table TestUniversalRowids") data = [ (1, "ABC" * 75, datetime.datetime(2017, 4, 11)), (2, "DEF" * 80, datetime.datetime(2017, 4, 12)) ] self.cursor.executemany(""" insert into TestUniversalRowids values (:1, :2, :3)""", data) self.connection.commit() def __test_select_rowids(self, table_name): self.cursor.execute(f"select rowid, IntCol from {table_name}") sql = f"select IntCol from {table_name} where rowid = :val" for rowid, int_val in self.cursor.fetchall(): self.cursor.execute(sql, val=rowid) self.assertEqual(self.cursor.fetchall(), [(int_val,)]) def test_2900_select_rowids_regular(self): "2900 - test selecting all rowids from a regular table" self.__test_select_rowids("TestNumbers") def test_2901_select_rowids_index_organised(self): "2901 - test selecting all rowids from an index organised table" self.__populate_test_universal_rowids() self.__test_select_rowids("TestUniversalRowids") def test_2902_insert_invalid_rowid(self): "2902 - test inserting an invalid rowid" sql = "insert into TestRowids (IntCol, RowidCol) values (1, :rid)" self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-00932:", self.cursor.execute, sql, rid=12345) self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-01410:", self.cursor.execute, sql, rid="523lkhlf") def test_2903_select_rowids_as_urowids(self): "2903 - test selecting regular rowids stored in a urowid column" self.cursor.execute("truncate table TestRowids") self.cursor.execute(""" insert into TestRowids (IntCol, UrowidCol) select IntCol, rowid from TestNumbers""") self.connection.commit() self.cursor.execute("select IntCol, UrowidCol from TestRowids") for int_val, rowid in self.cursor.fetchall(): self.cursor.execute(""" select IntCol from TestNumbers where rowid = :val""", val=rowid) self.assertEqual(self.cursor.fetchall(), [(int_val,)]) def test_2904_select_rowids_as_rowids(self): "2904 - test selecting regular rowids stored in a rowid column" self.cursor.execute("truncate table TestRowids") self.cursor.execute(""" insert into TestRowids (IntCol, RowidCol) select IntCol, rowid from TestNumbers""") self.connection.commit() self.cursor.execute("select IntCol, RowidCol from TestRowids") for int_val, rowid in self.cursor.fetchall(): self.cursor.execute(""" select IntCol from TestNumbers where rowid = :val""", val=rowid) self.assertEqual(self.cursor.fetchall(), [(int_val,)]) def test_2905_test_bind_and_insert_rowid(self): "2905 - binding and inserting a rowid" self.cursor.execute("truncate table TestRowids") insert_data = [ (1, "String #1"), (2, "String #2"), (3, "String #3"), (4, "String #4") ] self.cursor.execute("truncate table TestTempTable") sql = "insert into TestTempTable (IntCol, StringCol1) values (:1, :2)" self.cursor.executemany(sql, insert_data) self.connection.commit() ridvar = self.cursor.var(oracledb.ROWID) self.cursor.execute(""" begin select rowid into :rid from TestTempTable where IntCol = 3; end;""", rid=ridvar) self.cursor.setinputsizes(r1=oracledb.ROWID) self.cursor.execute(""" insert into TestRowids (IntCol, RowidCol) values(1, :r1)""", r1=ridvar) self.connection.commit() self.cursor.execute("select IntCol, RowidCol from TestRowids") int_val, rowid = self.cursor.fetchone() self.cursor.execute(""" select IntCol, StringCol1 from TestTempTable where rowid = :val""", val=rowid) self.assertEqual(self.cursor.fetchone(), (3, "String #3")) @unittest.skipIf(not test_env.get_is_thin(), "thick mode doesn't support DB_TYPE_UROWID") def test_2906_test_bind_and_insert_rowid_as_urowid(self): "2906 - binding and inserting a rowid as urowid" self.cursor.execute("truncate table TestRowids") insert_data = [ (1, "String #1", datetime.datetime(2017, 4, 4)), (2, "String #2", datetime.datetime(2017, 4, 5)), (3, "String #3", datetime.datetime(2017, 4, 6)), (4, "A" * 250, datetime.datetime(2017, 4, 7)) ] self.cursor.execute("truncate table TestUniversalRowids") sql = "insert into TestUniversalRowids values (:1, :2, :3)" self.cursor.executemany(sql, insert_data) self.connection.commit() ridvar = self.cursor.var(oracledb.DB_TYPE_UROWID) self.cursor.execute(""" begin select rowid into :rid from TestUniversalRowids where IntCol = 3; end;""", rid=ridvar) self.cursor.setinputsizes(r1=oracledb.DB_TYPE_UROWID) self.cursor.execute(""" insert into TestRowids (IntCol, UrowidCol) values(1, :r1)""", r1=ridvar) self.connection.commit() self.cursor.execute("select IntCol, UrowidCol from TestRowids") int_val, rowid = self.cursor.fetchone() self.cursor.execute(""" select IntCol, StringCol, DateCol from TestUniversalRowids where rowid = :val""", val=rowid) self.assertEqual(self.cursor.fetchone(), (3, "String #3", datetime.datetime(2017, 4, 6))) def test_2907_test_fetch_null_rowids(self): "2907 - fetching a null rowid" self.cursor.execute("truncate table TestRowids") self.cursor.execute("insert into TestRowids (IntCol) values (1)") self.connection.commit() self.cursor.execute("select * from TestRowids") self.assertEqual(self.cursor.fetchone(), (1, None, None)) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_3000_subscription.py000066400000000000000000000174431434177474600226000ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 3000 - Module for testing subscriptions """ import threading import unittest import oracledb import test_env class SubscriptionData: def __init__(self, num_messages_expected): self.condition = threading.Condition() self.num_messages_expected = num_messages_expected self.num_messages_received = 0 def _process_message(self, message): pass def callback_handler(self, message): if message.type != oracledb.EVENT_DEREG: self._process_message(message) self.num_messages_received += 1 if message.type == oracledb.EVENT_DEREG or \ self.num_messages_received == self.num_messages_expected: with self.condition: self.condition.notify() def wait_for_messages(self): if self.num_messages_received < self.num_messages_expected: with self.condition: self.condition.wait(10) class AQSubscriptionData(SubscriptionData): pass class DMLSubscriptionData(SubscriptionData): def __init__(self, num_messages_expected): super().__init__(num_messages_expected) self.table_operations = [] self.row_operations = [] self.rowids = [] def _process_message(self, message): table, = message.tables self.table_operations.append(table.operation) for row in table.rows: self.row_operations.append(row.operation) self.rowids.append(row.rowid) @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support subscriptions") class TestCase(test_env.BaseTestCase): @unittest.skip("FIXME: threaded mode fails on close of connection?") def test_3000_dml_subscription(self): "3000 - test subscription for insert, update, delete and truncate" # skip if running on the Oracle Cloud, which does not support # subscriptions currently if self.is_on_oracle_cloud(): message = "Oracle Cloud does not support subscriptions currently" self.skipTest(message) # truncate table in order to run test in known state self.cursor.execute("truncate table TestTempTable") # expected values table_operations = [ oracledb.OPCODE_INSERT, oracledb.OPCODE_UPDATE, oracledb.OPCODE_INSERT, oracledb.OPCODE_DELETE, oracledb.OPCODE_ALTER | oracledb.OPCODE_ALLROWS ] row_operations = [ oracledb.OPCODE_INSERT, oracledb.OPCODE_UPDATE, oracledb.OPCODE_INSERT, oracledb.OPCODE_DELETE ] rowids = [] # set up subscription data = DMLSubscriptionData(5) connection = test_env.get_connection(events=True) sub = connection.subscribe(callback=data.callback_handler, timeout=10, qos=oracledb.SUBSCR_QOS_ROWIDS) sub.registerquery("select * from TestTempTable") connection.autocommit = True cursor = connection.cursor() # insert statement cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (1, 'test')""") cursor.execute("select rowid from TestTempTable where IntCol = 1") rowids.extend(r for r, in cursor) # update statement cursor.execute(""" update TestTempTable set StringCol1 = 'update' where IntCol = 1""") cursor.execute("select rowid from TestTempTable where IntCol = 1") rowids.extend(r for r, in cursor) # second insert statement cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (2, 'test2')""") cursor.execute("select rowid from TestTempTable where IntCol = 2") rowids.extend(r for r, in cursor) # delete statement cursor.execute("delete TestTempTable where IntCol = 2") rowids.append(rowids[-1]) # truncate table cursor.execute("truncate table TestTempTable") # wait for all messages to be sent data.wait_for_messages() # verify the correct messages were sent self.assertEqual(data.table_operations, table_operations) self.assertEqual(data.row_operations, row_operations) self.assertEqual(data.rowids, rowids) # test string format of subscription object is as expected fmt = ">" expected = fmt % \ (test_env.get_main_user(), test_env.get_connect_string()) self.assertEqual(str(sub), expected) def test_3001_deprecations(self): "3001 - test to verify deprecations" self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2014:", self.connection.subscribe, ip_address='www.oracle.in', ipAddress='www.oracle.in') self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2014:", self.connection.subscribe, grouping_class=1, groupingClass=1) self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2014:", self.connection.subscribe, grouping_value=3, groupingValue=3) self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2014:", self.connection.subscribe, grouping_type=2, groupingType=2) self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2014:", self.connection.subscribe, client_initiated=True, clientInitiated=True) @unittest.skip("multiple subscriptions cannot be created simultaneously") def test_3002_aq_subscription(self): "3002 - test subscription for AQ" # create queue and clear it of all messages queue = self.connection.queue("TEST_RAW_QUEUE") queue.deqoptions.wait = oracledb.DEQ_NO_WAIT while queue.deqone(): pass self.connection.commit() # set up subscription data = AQSubscriptionData(1) connection = test_env.get_connection(events=True) sub = connection.subscribe(namespace=oracledb.SUBSCR_NAMESPACE_AQ, name=queue.name, timeout=10, callback=data.callback_handler) # enqueue a message queue.enqone(self.connection.msgproperties(payload="Some data")) self.connection.commit() # wait for all messages to be sent data.wait_for_messages() if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_3100_boolean_var.py000066400000000000000000000071501434177474600223360ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 3100 - Module for testing boolean variables """ import unittest import oracledb import test_env @unittest.skipUnless(test_env.get_client_version() >= (12, 1), "unsupported client") class TestCase(test_env.BaseTestCase): def __test_bind_value_as_boolean(self, value): expected_result = str(bool(value)).upper() var = self.cursor.var(bool) var.setvalue(0, value) result = self.cursor.callfunc("pkg_TestBooleans.GetStringRep", str, (var,)) self.assertEqual(result, expected_result) def test_3100_bind_false(self): "3100 - test binding in a False value" result = self.cursor.callfunc("pkg_TestBooleans.GetStringRep", str, (False,)) self.assertEqual(result, "FALSE") def test_3101_bind_float_as_boolean(self): "3101 - test binding in a float as a boolean" self.__test_bind_value_as_boolean(0.0) self.__test_bind_value_as_boolean(1.0) def test_3102_bind_integer_as_boolean(self): "3102 - test binding in an integer as a boolean" self.__test_bind_value_as_boolean(0) self.__test_bind_value_as_boolean(1) def test_3103_bind_null(self): "3103 - test binding in a null value" self.cursor.setinputsizes(None, bool) result = self.cursor.callfunc("pkg_TestBooleans.GetStringRep", str, (None,)) self.assertEqual(result, "NULL") def test_3104_bind_out_false(self): "3104 - test binding out a boolean value (False)" result = self.cursor.callfunc("pkg_TestBooleans.IsLessThan10", oracledb.DB_TYPE_BOOLEAN, (15,)) self.assertEqual(result, False) def test_3105_bind_out_true(self): "3105 - test binding out a boolean value (True)" result = self.cursor.callfunc("pkg_TestBooleans.IsLessThan10", bool, (5,)) self.assertEqual(result, True) def test_3106_bind_string_as_boolean(self): "3106 - test binding in a string as a boolean" self.__test_bind_value_as_boolean("") self.__test_bind_value_as_boolean("0") def test_3107_bind_true(self): "3107 - test binding in a True value" result = self.cursor.callfunc("pkg_TestBooleans.GetStringRep", str, (True,)) self.assertEqual(result, "TRUE") if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_3200_features_12_1.py000066400000000000000000000561701434177474600224160ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 3200 - Module for testing features introduced in 12.1 """ import datetime import unittest import oracledb import test_env @unittest.skipUnless(test_env.get_client_version() >= (12, 1), "unsupported client") class TestCase(test_env.BaseTestCase): def test_3200_array_dml_row_counts_off(self): "3200 - test executing with arraydmlrowcounts mode disabled" self.cursor.execute("truncate table TestArrayDML") rows = [(1, "First"), (2, "Second")] sql = "insert into TestArrayDML (IntCol,StringCol) values (:1,:2)" self.cursor.executemany(sql, rows, arraydmlrowcounts=False) self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4006:", self.cursor.getarraydmlrowcounts) rows = [(3, "Third"), (4, "Fourth")] self.cursor.executemany(sql, rows) self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4006:", self.cursor.getarraydmlrowcounts) def test_3201_array_dml_row_counts_on(self): "3201 - test executing with arraydmlrowcounts mode enabled" self.cursor.execute("truncate table TestArrayDML") rows = [ (1, "First", 100), (2, "Second", 200), (3, "Third", 300), (4, "Fourth", 300), (5, "Fifth", 300) ] sql = "insert into TestArrayDML (IntCol,StringCol,IntCol2) " \ "values (:1,:2,:3)" self.cursor.executemany(sql, rows, arraydmlrowcounts=True) self.connection.commit() self.assertEqual(self.cursor.getarraydmlrowcounts(), [1, 1, 1, 1, 1]) self.cursor.execute("select count(*) from TestArrayDML") count, = self.cursor.fetchone() self.assertEqual(count, len(rows)) def test_3202_bind_plsql_boolean_collection_in(self): "3202 - test binding a boolean collection (in)" type_obj = self.connection.gettype("PKG_TESTBOOLEANS.UDT_BOOLEANLIST") obj = type_obj.newobject() obj.setelement(1, True) obj.extend([True, False, True, True, False, True]) result = self.cursor.callfunc("pkg_TestBooleans.TestInArrays", int, (obj,)) self.assertEqual(result, 5) def test_3203_bind_plsql_boolean_collection_out(self): "3203 - test binding a boolean collection (out)" type_obj = self.connection.gettype("PKG_TESTBOOLEANS.UDT_BOOLEANLIST") obj = type_obj.newobject() self.cursor.callproc("pkg_TestBooleans.TestOutArrays", (6, obj)) self.assertEqual(obj.aslist(), [True, False, True, False, True, False]) def test_3204_bind_plql_date_collection_in(self): "3204 - test binding a PL/SQL date collection (in)" type_obj = self.connection.gettype("PKG_TESTDATEARRAYS.UDT_DATELIST") obj = type_obj.newobject() obj.setelement(1, datetime.datetime(2016, 2, 5)) obj.append(datetime.datetime(2016, 2, 8, 12, 15, 30)) obj.append(datetime.datetime(2016, 2, 12, 5, 44, 30)) result = self.cursor.callfunc("pkg_TestDateArrays.TestInArrays", oracledb.NUMBER, (2, datetime.datetime(2016, 2, 1), obj)) self.assertEqual(result, 24.75) def test_3205_bind_plqsl_date_collection_in_out(self): "3205 - test binding a PL/SQL date collection (in/out)" type_obj = self.connection.gettype("PKG_TESTDATEARRAYS.UDT_DATELIST") obj = type_obj.newobject() obj.setelement(1, datetime.datetime(2016, 1, 1)) obj.append(datetime.datetime(2016, 1, 7)) obj.append(datetime.datetime(2016, 1, 13)) obj.append(datetime.datetime(2016, 1, 19)) self.cursor.callproc("pkg_TestDateArrays.TestInOutArrays", (4, obj)) expected_values = [ datetime.datetime(2016, 1, 8), datetime.datetime(2016, 1, 14), datetime.datetime(2016, 1, 20), datetime.datetime(2016, 1, 26) ] self.assertEqual(obj.aslist(), expected_values) def test_3206_bind_plsql_date_collection_out(self): "3206 - test binding a PL/SQL date collection (out)" type_obj = self.connection.gettype("PKG_TESTDATEARRAYS.UDT_DATELIST") obj = type_obj.newobject() self.cursor.callproc("pkg_TestDateArrays.TestOutArrays", (3, obj)) expected_values = [ datetime.datetime(2002, 12, 13, 4, 48), datetime.datetime(2002, 12, 14, 9, 36), datetime.datetime(2002, 12, 15, 14, 24) ] self.assertEqual(obj.aslist(), expected_values) def test_3207_bind_plsql_number_collection_in(self): "3207 - test binding a PL/SQL number collection (in)" type_name = "PKG_TESTNUMBERARRAYS.UDT_NUMBERLIST" type_obj = self.connection.gettype(type_name) obj = type_obj.newobject() obj.setelement(1, 10) obj.extend([20, 30, 40, 50]) result = self.cursor.callfunc("pkg_TestNumberArrays.TestInArrays", int, (5, obj)) self.assertEqual(result, 155) def test_3208_bind_plsql_number_collection_in_out(self): "3208 - test binding a PL/SQL number collection (in/out)" type_name = "PKG_TESTNUMBERARRAYS.UDT_NUMBERLIST" type_obj = self.connection.gettype(type_name) obj = type_obj.newobject() obj.setelement(1, 5) obj.extend([8, 3, 2]) self.cursor.callproc("pkg_TestNumberArrays.TestInOutArrays", (4, obj)) self.assertEqual(obj.aslist(), [50, 80, 30, 20]) def test_3209_bind_plsql_number_collection_out(self): "3209 - test binding a PL/SQL number collection (out)" type_name = "PKG_TESTNUMBERARRAYS.UDT_NUMBERLIST" type_obj = self.connection.gettype(type_name) obj = type_obj.newobject() self.cursor.callproc("pkg_TestNumberArrays.TestOutArrays", (3, obj)) self.assertEqual(obj.aslist(), [100, 200, 300]) def test_3210_bind_plsql_record_array(self): "3210 - test binding an array of PL/SQL records (in)" rec_type = self.connection.gettype("PKG_TESTRECORDS.UDT_RECORD") array_type = self.connection.gettype("PKG_TESTRECORDS.UDT_RECORDARRAY") array_obj = array_type.newobject() for i in range(3): obj = rec_type.newobject() obj.NUMBERVALUE = i + 1 obj.STRINGVALUE = "String in record #%d" % (i + 1) obj.DATEVALUE = datetime.datetime(2017, i + 1, 1) obj.TIMESTAMPVALUE = datetime.datetime(2017, 1, i + 1) obj.BOOLEANVALUE = (i % 2) == 1 obj.PLSINTEGERVALUE = i * 5 obj.BINARYINTEGERVALUE = i * 2 array_obj.append(obj) result = self.cursor.callfunc("pkg_TestRecords.TestInArrays", str, (array_obj,)) self.assertEqual(result, "udt_Record(1, 'String in record #1', " \ "to_date('2017-01-01', 'YYYY-MM-DD'), " \ "to_timestamp('2017-01-01 00:00:00', " \ "'YYYY-MM-DD HH24:MI:SS'), false, 0, 0); " \ "udt_Record(2, 'String in record #2', " \ "to_date('2017-02-01', 'YYYY-MM-DD'), " \ "to_timestamp('2017-01-02 00:00:00', " \ "'YYYY-MM-DD HH24:MI:SS'), true, 5, 2); " \ "udt_Record(3, 'String in record #3', " \ "to_date('2017-03-01', 'YYYY-MM-DD'), " \ "to_timestamp('2017-01-03 00:00:00', " \ "'YYYY-MM-DD HH24:MI:SS'), false, 10, 4)") def test_3211_bind_plsql_record_in(self): "3211 - test binding a PL/SQL record (in)" type_obj = self.connection.gettype("PKG_TESTRECORDS.UDT_RECORD") obj = type_obj.newobject() obj.NUMBERVALUE = 18 obj.STRINGVALUE = "A string in a record" obj.DATEVALUE = datetime.datetime(2016, 2, 15) obj.TIMESTAMPVALUE = datetime.datetime(2016, 2, 12, 14, 25, 36) obj.BOOLEANVALUE = False obj.PLSINTEGERVALUE = 21 obj.BINARYINTEGERVALUE = 5 result = self.cursor.callfunc("pkg_TestRecords.GetStringRep", str, (obj,)) self.assertEqual(result, "udt_Record(18, 'A string in a record', " \ "to_date('2016-02-15', 'YYYY-MM-DD'), " \ "to_timestamp('2016-02-12 14:25:36', " \ "'YYYY-MM-DD HH24:MI:SS'), false, 21, 5)") def test_3212_bind_plsql_record_out(self): "3212 - test binding a PL/SQL record (out)" type_obj = self.connection.gettype("PKG_TESTRECORDS.UDT_RECORD") obj = type_obj.newobject() obj.NUMBERVALUE = 5 obj.STRINGVALUE = "Test value" obj.DATEVALUE = datetime.datetime.today() obj.TIMESTAMPVALUE = datetime.datetime.today() obj.BOOLEANVALUE = False obj.PLSINTEGERVALUE = 23 obj.BINARYINTEGERVALUE = 9 self.cursor.callproc("pkg_TestRecords.TestOut", (obj,)) self.assertEqual(obj.NUMBERVALUE, 25) self.assertEqual(obj.STRINGVALUE, "String in record") self.assertEqual(obj.DATEVALUE, datetime.datetime(2016, 2, 16)) self.assertEqual(obj.TIMESTAMPVALUE, datetime.datetime(2016, 2, 16, 18, 23, 55)) self.assertEqual(obj.BOOLEANVALUE, True) self.assertEqual(obj.PLSINTEGERVALUE, 45) self.assertEqual(obj.BINARYINTEGERVALUE, 10) def test_3213_bind_plsql_string_collection_in(self): "3213 - test binding a PL/SQL string collection (in)" type_name = "PKG_TESTSTRINGARRAYS.UDT_STRINGLIST" type_obj = self.connection.gettype(type_name) obj = type_obj.newobject() obj.setelement(1, "First element") obj.setelement(2, "Second element") obj.setelement(3, "Third element") result = self.cursor.callfunc("pkg_TestStringArrays.TestInArrays", int, (5, obj)) self.assertEqual(result, 45) def test_3214_bind_plsql_string_collection_in_out(self): "3214 - test binding a PL/SQL string collection (in/out)" type_name = "PKG_TESTSTRINGARRAYS.UDT_STRINGLIST" type_obj = self.connection.gettype(type_name) obj = type_obj.newobject() obj.setelement(1, "The first element") obj.append("The second element") obj.append("The third and final element") self.cursor.callproc("pkg_TestStringArrays.TestInOutArrays", (3, obj)) expected_values = [ 'Converted element # 1 originally had length 17', 'Converted element # 2 originally had length 18', 'Converted element # 3 originally had length 27' ] self.assertEqual(obj.aslist(), expected_values) def test_3215_bind_plsql_string_collection_out(self): "3215 - test binding a PL/SQL string collection (out)" type_name = "PKG_TESTSTRINGARRAYS.UDT_STRINGLIST" type_obj = self.connection.gettype(type_name) obj = type_obj.newobject() self.cursor.callproc("pkg_TestStringArrays.TestOutArrays", (4, obj)) expected_values = [ 'Test out element # 1', 'Test out element # 2', 'Test out element # 3', 'Test out element # 4' ] self.assertEqual(obj.aslist(), expected_values) def test_3216_bind_plsql_string_collection_out_with_holes(self): "3216 - test binding a PL/SQL string collection (out with holes)" type_name = "PKG_TESTSTRINGARRAYS.UDT_STRINGLIST" type_obj = self.connection.gettype(type_name) obj = type_obj.newobject() self.cursor.callproc("pkg_TestStringArrays.TestIndexBy", (obj,)) self.assertEqual(obj.first(), -1048576) self.assertEqual(obj.last(), 8388608) self.assertEqual(obj.next(-576), 284) self.assertEqual(obj.prev(284), -576) self.assertEqual(obj.size(), 4) self.assertEqual(obj.exists(-576), True) self.assertEqual(obj.exists(-577), False) self.assertEqual(obj.getelement(284), 'Third element') expected_list = [ "First element", "Second element", "Third element", "Fourth element" ] self.assertEqual(obj.aslist(), expected_list) expected_dict = { -1048576: 'First element', -576: 'Second element', 284: 'Third element', 8388608: 'Fourth element' } self.assertEqual(obj.asdict(), expected_dict) obj.delete(-576) obj.delete(284) expected_list.pop(2) expected_list.pop(1) self.assertEqual(obj.aslist(), expected_list) expected_dict.pop(-576) expected_dict.pop(284) self.assertEqual(obj.asdict(), expected_dict) def test_3217_exception_in_iteration(self): "3217 - test executing with arraydmlrowcounts with exception" self.cursor.execute("truncate table TestArrayDML") rows = [ (1, "First"), (2, "Second"), (2, "Third"), (4, "Fourth") ] sql = "insert into TestArrayDML (IntCol,StringCol) values (:1,:2)" self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-00001:", self.cursor.executemany, sql, rows, arraydmlrowcounts=True) self.assertEqual(self.cursor.getarraydmlrowcounts(), [1, 1]) def test_3218_executing_delete(self): "3218 - test executing delete statement with arraydmlrowcount mode" self.cursor.execute("truncate table TestArrayDML") rows = [ (1, "First", 100), (2, "Second", 200), (3, "Third", 300), (4, "Fourth", 300), (5, "Fifth", 300), (6, "Sixth", 400), (7, "Seventh", 400), (8, "Eighth", 500) ] sql = "insert into TestArrayDML (IntCol,StringCol,IntCol2) " \ "values (:1, :2, :3)" self.cursor.executemany(sql, rows) rows = [(200,), (300,), (400,)] statement = "delete from TestArrayDML where IntCol2 = :1" self.cursor.executemany(statement, rows, arraydmlrowcounts=True) self.assertEqual(self.cursor.getarraydmlrowcounts(), [1, 3, 2]) self.assertEqual(self.cursor.rowcount, 6) def test_3219_executing_update(self): "3219 - test executing update statement with arraydmlrowcount mode" self.cursor.execute("truncate table TestArrayDML") rows = [ (1, "First",100), (2, "Second",200), (3, "Third",300), (4, "Fourth",300), (5, "Fifth",300), (6, "Sixth",400), (7, "Seventh",400), (8, "Eighth",500) ] sql = "insert into TestArrayDML (IntCol,StringCol,IntCol2) " \ "values (:1, :2, :3)" self.cursor.executemany(sql, rows) rows = [ ("One", 100), ("Two", 200), ("Three", 300), ("Four", 400) ] sql = "update TestArrayDML set StringCol = :1 where IntCol2 = :2" self.cursor.executemany(sql, rows, arraydmlrowcounts=True) self.assertEqual(self.cursor.getarraydmlrowcounts(), [1, 1, 3, 2]) self.assertEqual(self.cursor.rowcount, 7) def test_3220_implicit_results(self): "3220 - test getimplicitresults() returns the correct data" self.cursor.execute(""" declare c1 sys_refcursor; c2 sys_refcursor; begin open c1 for select NullableCol from TestNumbers where IntCol between 3 and 5; dbms_sql.return_result(c1); open c2 for select NullableCol from TestNumbers where IntCol between 7 and 10; dbms_sql.return_result(c2); end;""") results = self.cursor.getimplicitresults() self.assertEqual(len(results), 2) self.assertEqual([n for n, in results[0]], [2924207, None, 59797108943]) self.assertEqual([n for n, in results[1]], [1222791080775407, None, 25004854810776297743, None]) def test_3221_implicit_results_no_statement(self): "3221 - test getimplicitresults() without executing a statement" self.assertRaisesRegex(oracledb.InterfaceError, "^DPY-1004:", self.cursor.getimplicitresults) def test_3222_insert_with_batch_error(self): "3222 - test executing insert with multiple distinct batch errors" self.cursor.execute("truncate table TestArrayDML") rows = [ (1, "First", 100), (2, "Second", 200), (2, "Third", 300), (4, "Fourth", 400), (5, "Fourth", 1000) ] sql = "insert into TestArrayDML (IntCol, StringCol, IntCol2) " \ "values (:1, :2, :3)" self.cursor.executemany(sql, rows, batcherrors=True, arraydmlrowcounts=True) user = test_env.get_main_user() expected_errors = [ (4, "ORA-01438"), (2, "ORA-00001") ] actual_errors = [(e.offset, e.full_code) \ for e in self.cursor.getbatcherrors()] self.assertEqual(actual_errors, expected_errors) self.assertEqual(self.cursor.getarraydmlrowcounts(), [1, 1, 0, 1, 0]) def test_3223_batch_error_false(self): "3223 - test batcherrors mode set to False" self.cursor.execute("truncate table TestArrayDML") rows = [ (1, "First", 100), (2, "Second", 200), (2, "Third", 300) ] sql = "insert into TestArrayDML (IntCol, StringCol, IntCol2) " \ "values (:1, :2, :3)" self.assertRaisesRegex(oracledb.IntegrityError, "^ORA-00001:", self.cursor.executemany, sql, rows, batcherrors=False) def test_3224_update_with_batch_error(self): "3224 - test executing in succession with batch error" self.cursor.execute("truncate table TestArrayDML") rows = [ (1, "First", 100), (2, "Second", 200), (3, "Third", 300), (4, "Second", 300), (5, "Fifth", 300), (6, "Sixth", 400), (6, "Seventh", 400), (8, "Eighth", 100) ] sql = "insert into TestArrayDML (IntCol, StringCol, IntCol2) " \ "values (:1, :2, :3)" self.cursor.executemany(sql, rows, batcherrors=True) expected_errors = [(6, "ORA-00001")] actual_errors = [(e.offset, e.full_code) \ for e in self.cursor.getbatcherrors()] self.assertEqual(actual_errors, expected_errors) rows = [ (101, "First"), (201, "Second"), (3000, "Third"), (900, "Ninth"), (301, "Third") ] sql = "update TestArrayDML set IntCol2 = :1 where StringCol = :2" self.cursor.executemany(sql, rows, arraydmlrowcounts=True, batcherrors=True) expected_errors = [(2, "ORA-01438")] actual_errors = [(e.offset, e.full_code) \ for e in self.cursor.getbatcherrors()] self.assertEqual(actual_errors, expected_errors) self.assertEqual(self.cursor.getarraydmlrowcounts(), [1, 2, 0, 0, 1]) self.assertEqual(self.cursor.rowcount, 4) def test_3225_implicit_results(self): "3225 - test using implicit cursors to execute new statements" cursor = self.connection.cursor() cursor.execute(""" declare c1 sys_refcursor; begin open c1 for select NumberCol from TestNumbers where IntCol between 3 and 5; dbms_sql.return_result(c1); end;""") results = cursor.getimplicitresults() self.assertEqual(len(results), 1) self.assertEqual([n for n, in results[0]], [3.75, 5, 6.25]) results[0].execute("select :1 from dual", (7,)) row, = results[0].fetchone() self.assertEqual(row, 7) def test_3226_batch_error_no_errors(self): "3226 - test batcherrors mode without any errors produced" self.cursor.execute("truncate table TestArrayDML") rows = [ (1, "First", 100), (2, "Second", 200), (3, "Third", 300) ] sql = """ insert into TestArrayDML (IntCol, StringCol, IntCol2) values (:1, :2, :3)""" self.cursor.executemany(sql, rows, batcherrors=True) self.assertEqual(self.cursor.getbatcherrors(), []) def test_3227_batch_error_multiple_execute(self): "3227 - test batcherrors mode with multiple executes" self.cursor.execute("truncate table TestArrayDML") rows_1 = [ (1, "Value 1", 100), (2, "Value 2", 200), (2, "Value 2", 200) ] rows_2 = [ (3, "Value 3", 300), (3, "Value 3", 300), (4, "Value 4", 400) ] sql = """ insert into TestArrayDML (IntCol, StringCol, IntCol2) values (:1, :2, :3)""" self.cursor.executemany(sql, rows_1, batcherrors=True) expected_errors = [(2, "ORA-00001")] actual_errors = [(e.offset, e.full_code) \ for e in self.cursor.getbatcherrors()] self.assertEqual(actual_errors, expected_errors) self.cursor.executemany(sql, rows_2, batcherrors=True) expected_errors = [(1, "ORA-00001")] actual_errors = [(e.offset, e.full_code) \ for e in self.cursor.getbatcherrors()] self.assertEqual(actual_errors, expected_errors) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_3300_soda_database.py000066400000000000000000000130461434177474600226240ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 3300 - Module for testing Simple Oracle Document Access (SODA) Database """ import json import unittest import oracledb import test_env @unittest.skipIf(test_env.skip_soda_tests(), "unsupported client/server combination") class TestCase(test_env.BaseTestCase): def __drop_existing_collections(self, soda_db): for name in soda_db.getCollectionNames(): soda_db.openCollection(name).drop() def __verify_doc(self, doc, raw_content, str_content=None, content=None, key=None, media_type='application/json'): self.assertEqual(doc.getContentAsBytes(), raw_content) if str_content is not None: self.assertEqual(doc.getContentAsString(), str_content) if content is not None: self.assertEqual(doc.getContent(), content) self.assertEqual(doc.key, key) self.assertEqual(doc.mediaType, media_type) def test_3300_create_document_with_json(self): "3300 - test creating documents with JSON data" soda_db = self.connection.getSodaDatabase() val = {"testKey1": "testValue1", "testKey2": "testValue2"} str_val = json.dumps(val) bytes_val = str_val.encode() key = "MyKey" media_type = "text/plain" doc = soda_db.createDocument(val) self.__verify_doc(doc, bytes_val, str_val, val) doc = soda_db.createDocument(str_val, key) self.__verify_doc(doc, bytes_val, str_val, val, key) doc = soda_db.createDocument(bytes_val, key, media_type) self.__verify_doc(doc, bytes_val, str_val, val, key, media_type) def test_3301_create_document_with_raw(self): "3301 - test creating documents with raw data" soda_db = self.connection.getSodaDatabase() val = b"" key = "MyRawKey" media_type = "text/html" doc = soda_db.createDocument(val) self.__verify_doc(doc, val) doc = soda_db.createDocument(val, key) self.__verify_doc(doc, val, key=key) doc = soda_db.createDocument(val, key, media_type) self.__verify_doc(doc, val, key=key, media_type=media_type) def test_3302_get_collection_names(self): "3302 - test getting collection names from the database" soda_db = self.connection.getSodaDatabase() self.__drop_existing_collections(soda_db) self.assertEqual(soda_db.getCollectionNames(), []) names = ["zCol", "dCol", "sCol", "aCol", "gCol"] sorted_names = list(sorted(names)) for name in names: soda_db.createCollection(name) self.assertEqual(soda_db.getCollectionNames(), sorted_names) self.assertEqual(soda_db.getCollectionNames(limit=2), sorted_names[:2]) self.assertEqual(soda_db.getCollectionNames("a"), sorted_names) self.assertEqual(soda_db.getCollectionNames("C"), sorted_names) self.assertEqual(soda_db.getCollectionNames("b", limit=3), sorted_names[1:4]) self.assertEqual(soda_db.getCollectionNames("z"), sorted_names[-1:]) def test_3303_open_collection(self): "3303 - test opening a collection" soda_db = self.connection.getSodaDatabase() self.__drop_existing_collections(soda_db) coll = soda_db.openCollection("CollectionThatDoesNotExist") self.assertEqual(coll, None) created_coll = soda_db.createCollection("TestOpenCollection") coll = soda_db.openCollection(created_coll.name) self.assertEqual(coll.name, created_coll.name) coll.drop() def test_3304_repr(self): "3304 - test SodaDatabase representation" con1 = self.connection con2 = test_env.get_connection() soda_db1 = self.connection.getSodaDatabase() soda_db2 = con1.getSodaDatabase() soda_db3 = con2.getSodaDatabase() self.assertEqual(str(soda_db1), str(soda_db2)) self.assertEqual(str(soda_db2), str(soda_db3)) def test_3305_negative(self): "3305 - test negative cases for SODA database methods" soda_db = self.connection.getSodaDatabase() self.assertRaises(TypeError, soda_db.createCollection) self.assertRaises(TypeError, soda_db.createCollection, 1) self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-40658:", soda_db.createCollection, None) self.assertRaises(TypeError, soda_db.getCollectionNames, 1) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_3400_soda_collection.py000066400000000000000000000517431434177474600232220ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 3400 - Module for testing Simple Oracle Document Access (SODA) Collections """ import unittest import json import oracledb import test_env @unittest.skipIf(test_env.skip_soda_tests(), "unsupported client/server combination") class TestCase(test_env.BaseTestCase): def __test_skip(self, coll, num_to_skip, expected_content): filter_spec = {'$orderby': [{'path': 'name', 'order': 'desc'}]} doc = coll.find().filter(filter_spec).skip(num_to_skip).getOne() content = doc.getContent() if doc is not None else None self.assertEqual(content, expected_content) def test_3400_invalid_json(self): "3400 - test inserting invalid JSON value into SODA collection" invalid_json = "{testKey:testValue}" soda_db = self.connection.getSodaDatabase() coll = soda_db.createCollection("InvalidJSON") doc = soda_db.createDocument(invalid_json) self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-40780:|^ORA-02290:", coll.insertOne, doc) coll.drop() def test_3401_insert_documents(self): "3401 - test inserting documents into a SODA collection" soda_db = self.connection.getSodaDatabase() coll = soda_db.createCollection("TestInsertDocs") coll.find().remove() values_to_insert = [ {"name": "George", "age": 47}, {"name": "Susan", "age": 39}, {"name": "John", "age": 50}, {"name": "Jill", "age": 54} ] inserted_keys = [] for value in values_to_insert: doc = coll.insertOneAndGet(value) inserted_keys.append(doc.key) self.connection.commit() self.assertEqual(coll.find().count(), len(values_to_insert)) for key, value in zip(inserted_keys, values_to_insert): doc = coll.find().key(key).getOne() self.assertEqual(doc.getContent(), value) coll.drop() def test_3402_skip_documents(self): "3402 - test skipping documents in a SODA collection" soda_db = self.connection.getSodaDatabase() coll = soda_db.createCollection("TestSkipDocs") coll.find().remove() values_to_insert = [ {"name": "Anna", "age": 62}, {"name": "Mark", "age": 37}, {"name": "Martha", "age": 43}, {"name": "Matthew", "age": 28} ] for value in values_to_insert: coll.insertOne(value) self.connection.commit() self.__test_skip(coll, 0, values_to_insert[3]) self.__test_skip(coll, 1, values_to_insert[2]) self.__test_skip(coll, 3, values_to_insert[0]) self.__test_skip(coll, 4, None) self.__test_skip(coll, 125, None) def test_3403_replace_document(self): "3403 - test replace documents in SODA collection" soda_db = self.connection.getSodaDatabase() coll = soda_db.createCollection("TestReplaceDoc") coll.find().remove() content = {'name': 'John', 'address': {'city': 'Sydney'}} doc = coll.insertOneAndGet(content) new_content = {'name': 'John', 'address': {'city':'Melbourne'}} coll.find().key(doc.key).replaceOne(new_content) self.connection.commit() self.assertEqual(coll.find().key(doc.key).getOne().getContent(), new_content) coll.drop() def test_3404_search_documents_with_content(self): "3404 - test search documents with content using $like and $regex" soda_db = self.connection.getSodaDatabase() coll = soda_db.createCollection("TestSearchDocContent") coll.find().remove() data = [ {'name': 'John', 'address': {'city': 'Bangalore'}}, {'name': 'Johnson', 'address': {'city': 'Banaras'}}, {'name': 'Joseph', 'address': {'city': 'Bangalore'}}, {'name': 'Jibin', 'address': {'city': 'Secunderabad'}}, {'name': 'Andrew', 'address': {'city': 'Hyderabad'}}, {'name': 'Matthew', 'address': {'city': 'Mumbai'}} ] for value in data: coll.insertOne(value) self.connection.commit() filter_specs = [ ({'name': {'$like': 'And%'}}, 1), ({'name': {'$like': 'J%n'}}, 3), ({'name': {'$like': '%hn%'}}, 2), ({'address.city': {'$like': 'Ban%'}}, 3), ({'address.city': {'$like': '%bad'}}, 2), ({'address.city': {'$like': 'Hyderabad'}}, 1), ({'address.city': {'$like': 'China%'}}, 0), ({'name': {'$regex': 'Jo.*'}}, 3), ({'name': {'$regex': '.*[ho]n'}}, 2), ({'name': {'$regex': 'J.*h'}}, 1), ({'address.city': {'$regex': 'Ba.*'}}, 3), ({'address.city': {'$regex': '.*bad'}}, 2), ({'address.city': {'$regex': 'Hyderabad'}}, 1), ({'name': {'$regex': 'Js.*n'}}, 0) ] for filter_spec, expected_count in filter_specs: self.assertEqual(coll.find().filter(filter_spec).count(), expected_count, filter_spec) coll.drop() def test_3405_document_remove(self): "3405 - test removing documents" soda_db = self.connection.getSodaDatabase() coll = soda_db.createCollection("TestRemoveDocs") coll.find().remove() data = [ {'name': 'John', 'address': {'city': 'Bangalore'}}, {'name': 'Johnson', 'address': {'city': 'Banaras'}}, {'name': 'Joseph', 'address': {'city': 'Mangalore'}}, {'name': 'Jibin', 'address': {'city': 'Secunderabad'}}, {'name': 'Andrew', 'address': {'city': 'Hyderabad'}}, {'name': 'Matthew', 'address': {'city': 'Mumbai'}} ] docs = [coll.insertOneAndGet(v) for v in data] coll.find().key(docs[3].key).remove() self.assertEqual(coll.find().count(), len(data) - 1) searchResults = coll.find().filter({'name': {'$like': 'Jibin'}}) self.assertEqual(searchResults.count(), 0) coll.find().filter({'name': {'$like': 'John%'}}).remove() self.assertEqual(coll.find().count(), len(data) - 3) coll.find().filter({'name': {'$regex': 'J.*'}}).remove() self.assertEqual(coll.find().count(), len(data) - 4) self.connection.commit() coll.drop() def test_3406_create_and_drop_index(self): "3406 - test create and drop Index" index_name = "TestIndexes_ix_1" index_spec = { 'name': index_name, 'fields': [ { 'path': 'address.city', 'datatype': 'string', 'order': 'asc' } ] } soda_db = self.connection.getSodaDatabase() coll = soda_db.createCollection("TestIndexes") coll.find().remove() self.connection.commit() coll.dropIndex(index_name) coll.createIndex(index_spec) self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-40733:", coll.createIndex, index_spec) self.assertEqual(coll.dropIndex(index_name), True) self.assertEqual(coll.dropIndex(index_name), False) coll.drop() def test_3407_get_documents(self): "3407 - test getting documents from Collection" self.connection.autocommit = True soda_db = self.connection.getSodaDatabase() coll = soda_db.createCollection("TestGetDocs") coll.find().remove() data = [ {'name': 'John', 'address': {'city': 'Bangalore'}}, {'name': 'Johnson', 'address': {'city': 'Banaras'}}, {'name': 'Joseph', 'address': {'city': 'Mangalore'}}, {'name': 'Jibin', 'address': {'city': 'Secunderabad'}}, {'name': 'Andrew', 'address': {'city': 'Hyderabad'}} ] inserted_keys = list(sorted(coll.insertOneAndGet(v).key for v in data)) fetched_keys = list(sorted(d.key for d in coll.find().getDocuments())) self.assertEqual(fetched_keys, inserted_keys) coll.drop() def test_3408_cursor(self): "3408 - test fetching documents from a cursor" self.connection.autocommit = True soda_db = self.connection.getSodaDatabase() coll = soda_db.createCollection("TestFindViaCursor") coll.find().remove() data = [ {'name': 'John', 'address': {'city': 'Bangalore'}}, {'name': 'Johnson', 'address': {'city': 'Banaras'}}, {'name': 'Joseph', 'address': {'city': 'Mangalore'}}, ] inserted_keys = list(sorted(coll.insertOneAndGet(v).key for v in data)) fetched_keys = list(sorted(d.key for d in coll.find().getCursor())) self.assertEqual(fetched_keys, inserted_keys) coll.drop() def test_3409_multiple_document_remove(self): "3409 - test removing multiple documents using multiple keys" soda_db = self.connection.getSodaDatabase() coll = soda_db.createCollection("TestRemoveMultipleDocs") coll.find().remove() data = [ {'name': 'John', 'address': {'city': 'Bangalore'}}, {'name': 'Johnson', 'address': {'city': 'Banaras'}}, {'name': 'Joseph', 'address': {'city': 'Mangalore'}}, {'name': 'Jibin', 'address': {'city': 'Secunderabad'}}, {'name': 'Andrew', 'address': {'city': 'Hyderabad'}}, {'name': 'Matthew', 'address': {'city': 'Mumbai'}} ] docs = [coll.insertOneAndGet(v) for v in data] keys = [docs[i].key for i in (1, 3, 5)] num_removed = coll.find().keys(keys).remove() self.assertEqual(num_removed, len(keys)) self.assertEqual(coll.find().count(), len(data) - len(keys)) self.connection.commit() coll.drop() def test_3410_document_version(self): "3410 - test using version to get documents and remove them" soda_db = self.connection.getSodaDatabase() coll = soda_db.createCollection("TestDocumentVersion") coll.find().remove() content = {'name': 'John', 'address': {'city': 'Bangalore'}} inserted_doc = coll.insertOneAndGet(content) key = inserted_doc.key version = inserted_doc.version doc = coll.find().key(key).version(version).getOne() self.assertEqual(doc.getContent(), content) new_content = {'name': 'James', 'address': {'city': 'Delhi'}} replacedDoc = coll.find().key(key).replaceOneAndGet(new_content) new_version = replacedDoc.version doc = coll.find().key(key).version(version).getOne() self.assertEqual(doc, None) doc = coll.find().key(key).version(new_version).getOne() self.assertEqual(doc.getContent(), new_content) self.assertEqual(coll.find().key(key).version(version).remove(), 0) self.assertEqual(coll.find().key(key).version(new_version).remove(), 1) self.assertEqual(coll.find().count(), 0) self.connection.commit() coll.drop() def test_3411_get_cursor(self): "3411 - test keys with GetCursor" soda_db = self.connection.getSodaDatabase() coll = soda_db.createCollection("TestKeysWithGetCursor") coll.find().remove() data = [ {'name': 'John', 'address': {'city': 'Bangalore'}}, {'name': 'Johnson', 'address': {'city': 'Banaras'}}, {'name': 'Joseph', 'address': {'city': 'Mangalore'}}, {'name': 'Jibin', 'address': {'city': 'Secunderabad'}}, {'name': 'Andrew', 'address': {'city': 'Hyderabad'}}, {'name': 'Matthew', 'address': {'city': 'Mumbai'}} ] docs = [coll.insertOneAndGet(v) for v in data] keys = [docs[i].key for i in (2, 4, 5)] fetched_keys = [d.key for d in coll.find().keys(keys).getCursor()] self.assertEqual(list(sorted(fetched_keys)), list(sorted(keys))) self.connection.commit() coll.drop() def test_3412_created_on(self): "3412 - test createdOn attribute of Document" soda_db = self.connection.getSodaDatabase() coll = soda_db.createCollection("CreatedOn") coll.find().remove() data = {'name': 'John', 'address': {'city': 'Bangalore'}} doc = coll.insertOneAndGet(data) self.assertEqual(doc.createdOn, doc.lastModified) @unittest.skipIf(test_env.get_client_version() < (20, 1), "unsupported client") def test_3413_soda_truncate(self): "3413 - test Soda truncate" soda_db = self.connection.getSodaDatabase() coll = soda_db.createCollection("TestTruncateDocs") coll.find().remove() values_to_insert = [ {"name": "George", "age": 47}, {"name": "Susan", "age": 39}, {"name": "John", "age": 50}, {"name": "Jill", "age": 54} ] for value in values_to_insert: coll.insertOne(value) self.connection.commit() self.assertEqual(coll.find().count(), len(values_to_insert)) coll.truncate() self.assertEqual(coll.find().count(), 0) coll.drop() def test_3414_soda_hint(self): "3414 - verify hints are reflected in the executed SQL statement" soda_db = self.connection.getSodaDatabase() cursor = self.connection.cursor() statement = """ SELECT ( SELECT t2.sql_fulltext FROM v$sql t2 WHERE t2.sql_id = t1.prev_sql_id AND t2.child_number = t1.prev_child_number ) FROM v$session t1 WHERE t1.audsid = sys_context('userenv', 'sessionid')""" coll = soda_db.createCollection("TestSodaHint") coll.find().remove() values_to_insert = [ {"name": "George", "age": 47}, {"name": "Susan", "age": 39}, ] coll.insertOneAndGet(values_to_insert[0], hint="MONITOR") cursor.execute(statement) result, = cursor.fetchone() self.assertTrue('MONITOR' in result.read()) coll.find().hint("MONITOR").getOne().getContent() cursor.execute(statement) result, = cursor.fetchone() self.assertTrue('MONITOR' in result.read()) coll.insertOneAndGet(values_to_insert[1], hint="NO_MONITOR") cursor.execute(statement) result, = cursor.fetchone() self.assertTrue('NO_MONITOR' in result.read()) def test_3415_soda_hint_with_invalid_type(self): "3415 - test error for invalid type for soda hint" soda_db = self.connection.getSodaDatabase() coll = soda_db.createCollection("InvalidSodaHint") self.assertRaises(TypeError, coll.insertOneAndGet, dict(name="Fred", age=16), hint=5) self.assertRaises(TypeError, coll.insertManyAndGet, dict(name="George", age=25), hint=10) self.assertRaises(TypeError, coll.saveAndGet, dict(name="Sally", age=36), hint=5) def test_3416_collection_name_and_metadata(self): "3416 - test name and metadata attribute" soda_db = self.connection.getSodaDatabase() collection_name = "TestCollectionMetadata" coll = soda_db.createCollection(collection_name) self.assertEqual(coll.name, collection_name) self.assertEqual(coll.metadata["tableName"], collection_name) coll.drop() def test_3417_insert_many(self): "3417 - test insertMany" soda_db = self.get_soda_database(minclient=(18, 5)) coll = soda_db.createCollection("TestInsertMany") values_to_insert = [ dict(name="George", age=25), soda_db.createDocument(dict(name="Lucas", age=47)) ] coll.insertMany(values_to_insert) self.connection.commit() fetched_docs = list(coll.find().getCursor()) for fetched_doc, expected_doc in zip(fetched_docs, values_to_insert): if isinstance(expected_doc, dict): expected_doc = soda_db.createDocument(expected_doc) self.assertEqual(fetched_doc.getContent(), expected_doc.getContent()) self.assertRaisesRegex(oracledb.DatabaseError, "^DPI-1031:", coll.insertMany, []) coll.drop() def test_3418_save(self): "3418 - test save" soda_db = self.get_soda_database(minclient=(19, 9)) coll = soda_db.createCollection("TestSodaSave") values_to_save = [ dict(name="Jill", age=37), soda_db.createDocument(dict(name="John", age=7)), soda_db.createDocument(dict(name="Charles", age=24)) ] for value in values_to_save: coll.save(value) self.connection.commit() fetched_docs = coll.find().getDocuments() for fetched_doc, expected_doc in zip(fetched_docs, values_to_save): if isinstance(expected_doc, dict): expected_doc = soda_db.createDocument(expected_doc) self.assertEqual(fetched_doc.getContent(), expected_doc.getContent()) coll.drop() def test_3419_save_and_get_with_hint(self): "3419 - test saveAndGet with hint" soda_db = self.get_soda_database(minclient=(19, 11)) cursor = self.connection.cursor() statement = """ SELECT ( SELECT t2.sql_fulltext FROM v$sql t2 WHERE t2.sql_id = t1.prev_sql_id AND t2.child_number = t1.prev_child_number ) FROM v$session t1 WHERE t1.audsid = sys_context('userenv', 'sessionid')""" coll = soda_db.createCollection("TestSodaSaveWithHint") coll.find().remove() values_to_save = [ dict(name="Jordan", age=59), dict(name="Curry", age=34) ] hints = ["MONITOR", "NO_MONITOR"] for value, hint in zip(values_to_save, hints): coll.saveAndGet(value, hint=hint) coll.find().hint(hint).getOne().getContent() cursor.execute(statement) result, = cursor.fetchone() self.assertTrue(hint in result.read()) def test_3420_save_and_get(self): "3420 - test saveAndGet" soda_db = self.get_soda_database(minclient=(19, 9)) coll = soda_db.createCollection("TestSodaSaveAndGet") coll.find().remove() values_to_save = [ dict(name="John", age=50), soda_db.createDocument(dict(name="Mark", age=45)), soda_db.createDocument(dict(name="Jill", age=32)) ] inserted_keys = [] for value in values_to_save: doc = coll.saveAndGet(value) inserted_keys.append(doc.key) fetched_docs = coll.find().getDocuments() self.connection.commit() self.assertEqual(coll.find().count(), len(values_to_save)) for key, fetched_doc in zip(inserted_keys, fetched_docs): doc = coll.find().key(key).getOne() self.assertEqual(doc.getContent(), fetched_doc.getContent()) coll.drop() def test_3421_insert_many_and_get(self): "3421 - test insert many and get" soda_db = self.get_soda_database(minclient=(18, 5)) coll = soda_db.createCollection("TestInsertManyAndGet") values_to_insert = [ dict(name="George", age=25), soda_db.createDocument(dict(name="Lucas", age=47)) ] docs = coll.insertManyAndGet(values_to_insert) inserted_keys = [i.key for i in docs] self.connection.commit() self.assertEqual(coll.find().count(), len(values_to_insert)) for key, expected_doc in zip(inserted_keys, values_to_insert): if isinstance(expected_doc, dict): expected_doc = soda_db.createDocument(expected_doc) doc = coll.find().key(key).getOne() self.assertEqual(doc.getContent(), expected_doc.getContent()) coll.drop() if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_3500_json.py000066400000000000000000000166501434177474600210310ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 3500 - Module for testing the JSON data type. """ import datetime import decimal import unittest import oracledb import test_env @unittest.skipUnless(test_env.get_client_version() >= (21, 0), "unsupported client") @unittest.skipUnless(test_env.get_server_version() >= (21, 0), "unsupported server") @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support JSON yet") class TestCase(test_env.BaseTestCase): json_data = [ True, False, 'String', b'Some Bytes', {}, {"name": None}, {"name": "John"}, {"age": 30}, {"Permanent": True}, { "employee": { "name":"John", "age": 30, "city": "Delhi", "Parmanent": True } }, { "employees": ["John", "Matthew", "James"] }, { "employees": [ { "employee1": {"name": "John", "city": "Delhi"} }, { "employee2": {"name": "Matthew", "city": "Mumbai"} }, { "employee3": {"name": "James", "city": "Bangalore"} } ] } ] def __bind_scalar_as_json(self, data): self.cursor.execute("truncate table TestJson") out_var = self.cursor.var(oracledb.DB_TYPE_JSON, arraysize=len(data)) self.cursor.setinputsizes(None, oracledb.DB_TYPE_JSON, out_var) bind_data = list(enumerate(data)) self.cursor.executemany(""" insert into TestJson values (:1, :2) returning JsonCol into :json_out""", bind_data) self.connection.commit() self.assertEqual(out_var.values, [[v] for v in data]) def test_3500_insert_and_fetch_single_json(self): "3500 - insert and fetch single row with JSON" self.cursor.execute("truncate table TestJson") self.cursor.setinputsizes(None, oracledb.DB_TYPE_JSON) self.cursor.execute("insert into TestJson values (:1, :2)", [1, self.json_data]) self.cursor.execute("select JsonCol from TestJson") result, = self.cursor.fetchone() self.assertEqual(result, self.json_data) def test_3501_execute_with_dml_returning(self): "3501 - inserting single rows with JSON and DML returning" json_val = self.json_data[11] self.cursor.execute("truncate table TestJson") json_out = self.cursor.var(oracledb.DB_TYPE_JSON) self.cursor.setinputsizes(None, oracledb.DB_TYPE_JSON, json_out) self.cursor.execute(""" insert into TestJson values (:1, :2) returning JsonCol into :json_out""", [1, json_val]) self.assertEqual(json_out.getvalue(0), [json_val]) def test_3502_insert_and_fetch_multiple_json(self): "3502 - insert and fetch multiple rows with JSON" self.cursor.execute("truncate table TestJson") self.cursor.setinputsizes(None, oracledb.DB_TYPE_JSON) data = list(enumerate(self.json_data)) self.cursor.executemany("insert into TestJson values(:1, :2)", data) self.cursor.execute("select * from TestJson") fetched_data = self.cursor.fetchall() self.assertEqual(fetched_data, data) def test_3503_executemany_with_dml_returning(self): "3503 - inserting multiple rows with JSON and DML returning" self.cursor.execute("truncate table TestJson") int_values = [i for i in range(len(self.json_data))] out_int_var = self.cursor.var(int, arraysize=len(int_values)) out_json_var = self.cursor.var(oracledb.DB_TYPE_JSON, arraysize=len(int_values)) self.cursor.setinputsizes(None, oracledb.DB_TYPE_JSON, out_int_var, out_json_var) data = list(zip(int_values, self.json_data)) self.cursor.executemany(""" insert into TestJson values(:int_val, :json_val) returning IntCol, JsonCol into :int_var, :json_var""", data) self.assertEqual(out_int_var.values, [[v] for v in int_values]) self.assertEqual(out_json_var.values, [[v] for v in self.json_data]) def test_3504_boolean(self): "3504 - test binding boolean values as scalar JSON values" data = [ True, False, True, True, False, True ] self.__bind_scalar_as_json(data) def test_3505_strings_and_bytes(self): "3505 - test binding strings/bytes values as scalar JSON values" data = [ "String 1", b"A raw value", "A much longer string", b"A much longer RAW value", "Short string", b"Y" ] self.__bind_scalar_as_json(data) def test_3506_datetime(self): "3506 - test binding dates/intervals as scalar JSON values" data = [ datetime.datetime.today(), datetime.datetime(2004, 2, 1, 3, 4, 5), datetime.datetime(2020, 12, 2, 13, 29, 14), datetime.timedelta(8.5), datetime.datetime(2002, 12, 13, 9, 36, 0), oracledb.Timestamp(2002, 12, 13, 9, 36, 0), datetime.datetime(2002, 12, 13) ] self.__bind_scalar_as_json(data) def test_3507_bind_number(self): "3507 - test binding number in json values" data = [ 0, 1, 25.25, 6088343244, -9999999999999999999, decimal.Decimal("0.25"), decimal.Decimal("10.25"), decimal.Decimal("319438950232418390.273596") ] self.__bind_scalar_as_json(data) def test_3508_unsupported_python_type(self): "3508 - test unsupported python type with JSON" self.cursor.execute("truncate table TestJson") self.cursor.setinputsizes(None, oracledb.DB_TYPE_JSON) insert_sql = "insert into TestJson values (:1, :2)" self.assertRaisesRegex(oracledb.NotSupportedError, "^DPY-3003:", self.cursor.execute, insert_sql, [1, list]) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_3600_outputtypehandler.py000066400000000000000000000607451434177474600236650ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 3600 - Module for testing the conversions of outputtype handler. """ import datetime import decimal import unittest import oracledb import test_env class TestCase(test_env.BaseTestCase): def __test_type_handler(self, input_type, output_type, in_value, expected_out_value): def type_handler(cursor, name, default_type, size, precision, scale): return cursor.var(output_type, arraysize=cursor.arraysize) self.cursor.outputtypehandler = type_handler var = self.cursor.var(input_type) var.setvalue(0, in_value) self.cursor.execute("select :1 from dual", [var]) fetched_value, = self.cursor.fetchone() self.assertEqual(type(fetched_value), type(expected_out_value)) self.assertEqual(fetched_value, expected_out_value) def __test_type_handler_lob(self, lob_type, output_type): db_type = getattr(oracledb, lob_type) def type_handler(cursor, name, default_type, size, precision, scale): if default_type == db_type: return cursor.var(output_type, arraysize=cursor.arraysize) self.cursor.outputtypehandler = type_handler in_value = f"Some {lob_type} data" if lob_type == "BLOB": in_value = in_value.encode() self.cursor.execute(f"truncate table Test{lob_type}s") self.cursor.execute(f""" insert into Test{lob_type}s (IntCol, {lob_type}Col) values(1, :val)""", val=in_value) self.connection.commit() self.cursor.execute(f""" select {lob_type}Col, IntCol, {lob_type}Col from Test{lob_type}s""") self.assertEqual(self.cursor.fetchone(), (in_value, 1, in_value)) def setUp(self): super().setUp() stmt = "ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'" \ "NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF6'" \ "NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF6'" \ "time_zone='Europe/London'" self.cursor.execute(stmt) def test_3600_VARCHAR_to_NUMBER(self): "3600 - output type handler conversion from VARCHAR to NUMBER" self.__test_type_handler(oracledb.DB_TYPE_VARCHAR, oracledb.DB_TYPE_NUMBER, "31.5", 31.5) def test_3601_CHAR_to_NUMBER(self): "3601 - output type handler conversion from CHAR to NUMBER" self.__test_type_handler(oracledb.DB_TYPE_CHAR, oracledb.DB_TYPE_NUMBER, "31.5", 31.5) def test_3602_LONG_to_NUMBER(self): "3602 - output type handler conversion from LONG to NUMBER" self.__test_type_handler(oracledb.DB_TYPE_LONG, oracledb.DB_TYPE_NUMBER, "31.5", 31.5) def test_3603_BINT_to_NUMBER(self): "3603 - test output type handler conversion from INTEGER to NUMBER" self.__test_type_handler(oracledb.DB_TYPE_BINARY_INTEGER, oracledb.DB_TYPE_NUMBER, 31, 31.0) def test_3604_VARCHAR_to_BINT(self): "3604 - output type handler conversion from VARCHAR to INTEGER" self.__test_type_handler(oracledb.DB_TYPE_VARCHAR, oracledb.DB_TYPE_BINARY_INTEGER, "31.5", 31) def test_3605_CHAR_to_BINT(self): "3605 - output type handler conversion from CHAR to INTEGER" self.__test_type_handler(oracledb.DB_TYPE_CHAR, oracledb.DB_TYPE_BINARY_INTEGER, "31.5", 31) def test_3606_LONG_to_BINT(self): "3606 - output type handler conversion from LONG to INTEGER" self.__test_type_handler(oracledb.DB_TYPE_LONG, oracledb.DB_TYPE_BINARY_INTEGER, "31.5", 31) def test_3607_NUMBER_to_BINT(self): "3607 - output type handler conversion from NUMBER to INTEGER" self.__test_type_handler(oracledb.DB_TYPE_NUMBER, oracledb.DB_TYPE_BINARY_INTEGER, 31.5, 31) def test_3608_BINARY_DOUBLE_to_BINT(self): "3608 - output type handler conversion from DOUBLE to INTEGER" self.__test_type_handler(oracledb.DB_TYPE_BINARY_DOUBLE, oracledb.DB_TYPE_BINARY_INTEGER, 31.5, 31) def test_3609_BINARY_FLOAT_to_BINT(self): "3609 - output type handler conversion from FLOAT to INTEGER" self.__test_type_handler(oracledb.DB_TYPE_BINARY_FLOAT, oracledb.DB_TYPE_BINARY_INTEGER, 31.5, 31) def test_3610_DATE_to_VARCHAR(self): "3610 - output type handler conversion from DATE to VARCHAR" in_val = datetime.date(2021, 2, 1) out_val = "2021-02-01 00:00:00" self.__test_type_handler(oracledb.DB_TYPE_DATE, oracledb.DB_TYPE_VARCHAR, in_val, out_val) def test_3611_DATE_to_CHAR(self): "3611 - output type handler conversion from DATE to CHAR" in_val = datetime.date(2021, 2, 1) out_val = "2021-02-01 00:00:00" self.__test_type_handler(oracledb.DB_TYPE_DATE, oracledb.DB_TYPE_CHAR, in_val, out_val) def test_3612_DATE_to_LONG(self): "3612 - output type handler conversion from DATE to LONG" in_val = datetime.date(2021, 2, 1) out_val = "2021-02-01 00:00:00" self.__test_type_handler(oracledb.DB_TYPE_DATE, oracledb.DB_TYPE_LONG, in_val , out_val) def test_3613_NUMBER_to_VARCHAR(self): "3613 - output type handler conversion from NUMBER to VARCHAR" self.__test_type_handler(oracledb.DB_TYPE_NUMBER, oracledb.DB_TYPE_VARCHAR, 31.5, "31.5") self.__test_type_handler(oracledb.DB_TYPE_NUMBER, oracledb.DB_TYPE_VARCHAR, 0, "0") def test_3614_NUMBER_to_CHAR(self): "3614 - output type handler conversion from NUMBER to CHAR" self.__test_type_handler(oracledb.DB_TYPE_NUMBER, oracledb.DB_TYPE_CHAR, 31.5, "31.5") self.__test_type_handler(oracledb.DB_TYPE_NUMBER, oracledb.DB_TYPE_CHAR, 0, "0") def test_3615_NUMBER_to_LONG(self): "3615 - output type handler conversion from NUMBER to LONG" self.__test_type_handler(oracledb.DB_TYPE_NUMBER, oracledb.DB_TYPE_LONG, 31.5, "31.5") def test_3616_INTERVAL_to_VARCHAR(self): "3616 - output type handler conversion from INTERVAL to VARCHAR" in_val = datetime.timedelta(days=-1, seconds=86314, microseconds=431152) if test_env.get_is_thin(): out_val = str(in_val) else: out_val = "-000000001 23:58:34.431152000" self.__test_type_handler(oracledb.DB_TYPE_INTERVAL_DS, oracledb.DB_TYPE_VARCHAR, in_val, out_val) def test_3617_INTERVAL_to_CHAR(self): "3617 - output type handler conversion from INTERVAL to CHAR" in_val = datetime.timedelta(days=-1, seconds=86314, microseconds=431152) if test_env.get_is_thin(): out_val = str(in_val) else: out_val = "-000000001 23:58:34.431152000" self.__test_type_handler(oracledb.DB_TYPE_INTERVAL_DS, oracledb.DB_TYPE_CHAR, in_val, out_val) def test_3618_INTERVAL_to_LONG(self): "3618 - output type handler conversion from INTERVAL to LONG" in_val = datetime.timedelta(days=-1, seconds=86314, microseconds=431152) if test_env.get_is_thin(): out_val = str(in_val) else: out_val = "-000000001 23:58:34.431152000" self.__test_type_handler(oracledb.DB_TYPE_INTERVAL_DS, oracledb.DB_TYPE_LONG, in_val, out_val) def test_3619_TIMESTAMP_to_VARCHAR(self): "3619 - output type handler conversion from TIMESTAMP to VARCHAR" in_val = datetime.datetime(2002, 12, 17, 1, 2, 16, 400000) self.__test_type_handler(oracledb.DB_TYPE_TIMESTAMP, oracledb.DB_TYPE_VARCHAR, in_val, str(in_val)) def test_3620_TIMESTAMP_to_CHAR(self): "3620 - output type handler conversion from TIMESTAMP to CHAR" in_val = datetime.datetime(2002, 12, 17, 1, 2, 16, 400000) self.__test_type_handler(oracledb.DB_TYPE_TIMESTAMP, oracledb.DB_TYPE_CHAR, in_val, str(in_val)) def test_3621_TIMESTAMP_to_LONG(self): "3621 - output type handler conversion from TIMESTAMP to LONG" in_val = datetime.datetime(2002, 12, 17, 1, 2, 16, 400000) self.__test_type_handler(oracledb.DB_TYPE_TIMESTAMP, oracledb.DB_TYPE_LONG, in_val, str(in_val)) def test_3622_TIMESTAMP_TZ_to_VARCHAR(self): "3622 - output type handler conversion from TIMESTAMP_TZ to VARCHAR" in_val = datetime.datetime(2002, 12, 17, 1, 2, 16, 400000) self.__test_type_handler(oracledb.DB_TYPE_TIMESTAMP_TZ, oracledb.DB_TYPE_VARCHAR, in_val, str(in_val)) def test_3623_TIMESTAMP_TZ_to_CHAR(self): "3623 - output type handler conversion from TIMESTAMP_TZ to CHAR" in_val = datetime.datetime(2002, 12, 17, 1, 2, 16, 400000) self.__test_type_handler(oracledb.DB_TYPE_TIMESTAMP_TZ, oracledb.DB_TYPE_CHAR, in_val, str(in_val)) def test_3624_TIMESTAMP_TZ_to_LONG(self): "3624 - output type handler conversion from TIMESTAMP_TZ to LONG" in_val = datetime.datetime(2002, 12, 17, 1, 2, 16, 400000) self.__test_type_handler(oracledb.DB_TYPE_TIMESTAMP_TZ, oracledb.DB_TYPE_LONG, in_val, str(in_val)) def test_3625_TIMESTAMP_LTZ_to_VARCHAR(self): "3625 - output type handler conversion from TIMESTAMP_LTZ to VARCHAR" in_val = datetime.datetime(2002, 12, 17, 1, 2, 16, 400000) self.__test_type_handler(oracledb.DB_TYPE_TIMESTAMP_LTZ, oracledb.DB_TYPE_VARCHAR, in_val, str(in_val)) def test_3626_TIMESTAMP_LTZ_to_CHAR(self): "3626 - output type handler conversion from TIMESTAMP_LTZ to CHAR" in_val = datetime.datetime(2002, 12, 17, 1, 2, 16, 400000) self.__test_type_handler(oracledb.DB_TYPE_TIMESTAMP_LTZ, oracledb.DB_TYPE_CHAR, in_val, str(in_val)) def test_3627_TIMESTAMP_LTZ_to_LONG(self): "3627 - output type handler conversion from TIMESTAMP_LTZ to LONG" in_val = datetime.datetime(2002, 12, 17, 1, 2, 16, 400000) self.__test_type_handler(oracledb.DB_TYPE_TIMESTAMP_LTZ, oracledb.DB_TYPE_LONG, in_val, str(in_val)) def test_3628_BINT_to_VARCHAR(self): "3628 - output type handler conversion from INTEGER to VARCHAR" self.__test_type_handler(oracledb.DB_TYPE_BINARY_INTEGER, oracledb.DB_TYPE_VARCHAR, 31, "31") def test_3629_BINT_to_CHAR(self): "3629 - output type handler conversion from INTEGER to CHAR" self.__test_type_handler(oracledb.DB_TYPE_BINARY_INTEGER, oracledb.DB_TYPE_CHAR, 31, "31") def test_3630_BINT_to_LONG(self): "3630 - output type handler conversion from INTEGER to LONG" self.__test_type_handler(oracledb.DB_TYPE_BINARY_INTEGER, oracledb.DB_TYPE_LONG, 31, "31") def test_3631_NUMBER_to_BINARY_DOUBLE(self): "3631 - output type handler conversion from NUMBER to DOUBLE" self.__test_type_handler(oracledb.DB_TYPE_NUMBER, oracledb.DB_TYPE_BINARY_DOUBLE, 31.5, 31.5) def test_3632_BINARY_FLOAT_to_BINARY_DOUBLE(self): "3632 - output type handler conversion from FLOAT to DOUBLE" self.__test_type_handler(oracledb.DB_TYPE_BINARY_FLOAT, oracledb.DB_TYPE_BINARY_DOUBLE, 31.5, 31.5) def test_3633_VARCHAR_to_BINARY_DOUBLE(self): "3633 - output type handler conversion from VARCHAR to DOUBLE" self.__test_type_handler(oracledb.DB_TYPE_VARCHAR, oracledb.DB_TYPE_BINARY_DOUBLE, "31.5", 31.5) def test_3634_CHAR_to_BINARY_DOUBLE(self): "3634 - output type handler conversion from CHAR to DOUBLE" self.__test_type_handler(oracledb.DB_TYPE_CHAR, oracledb.DB_TYPE_BINARY_DOUBLE, "31.5", 31.5) def test_3635_LONG_to_BINARY_DOUBLE(self): "3635 - output type handler conversion from LONG to DOUBLE" self.__test_type_handler(oracledb.DB_TYPE_LONG, oracledb.DB_TYPE_BINARY_DOUBLE, "31.5", 31.5) def test_3636_NUMBER_to_BINARY_FLOAT(self): "3636 - output type handler conversion from NUMBER to FLOAT" self.__test_type_handler(oracledb.DB_TYPE_NUMBER, oracledb.DB_TYPE_BINARY_FLOAT, 31.5, 31.5) def test_3637_BINARY_DOUBLE_to_BINARY_FLOAT(self): "3637 - output type handler conversion from DOUBLE to FLOAT" self.__test_type_handler(oracledb.DB_TYPE_BINARY_DOUBLE, oracledb.DB_TYPE_BINARY_FLOAT, 31.5, 31.5) def test_3638_VARCHAR_to_BINARY_FLOAT(self): "3638 - output type handler conversion from VARCHAR to FLOAT" self.__test_type_handler(oracledb.DB_TYPE_VARCHAR, oracledb.DB_TYPE_BINARY_FLOAT, "31.5", 31.5) def test_3639_CHAR_to_BINARY_FLOAT(self): "3639 - output type handler conversion from CHAR to FLOAT" self.__test_type_handler(oracledb.DB_TYPE_CHAR, oracledb.DB_TYPE_BINARY_FLOAT, "31.5", 31.5) def test_3640_LONG_to_BINARY_FLOAT(self): "3640 - output type handler conversion from LONG to FLOAT" self.__test_type_handler(oracledb.DB_TYPE_LONG, oracledb.DB_TYPE_BINARY_FLOAT, "31.5", 31.5) def test_3641_VARCHAR_to_CHAR(self): "3641 - output type handler conversion from VARCHAR to CHAR" self.__test_type_handler(oracledb.DB_TYPE_VARCHAR, oracledb.DB_TYPE_CHAR, "31.5", "31.5") def test_3642_VARCHAR_to_LONG(self): "3642 - output type handler conversion from VARCHAR to LONG" self.__test_type_handler(oracledb.DB_TYPE_VARCHAR, oracledb.DB_TYPE_LONG, "31.5", "31.5") def test_3643_LONG_to_VARCHAR(self): "3643 - output type handler conversion from LONG to VARCHAR" self.__test_type_handler(oracledb.DB_TYPE_LONG, oracledb.DB_TYPE_VARCHAR, "31.5", "31.5") def test_3644_LONG_to_CHAR(self): "3644 - output type handler conversion from LONG to CHAR" self.__test_type_handler(oracledb.DB_TYPE_LONG, oracledb.DB_TYPE_CHAR, "31.5", "31.5") def test_3645_CHAR_to_VARCHAR(self): "3645 - output type handler conversion from CHAR to VARCHAR" self.__test_type_handler(oracledb.DB_TYPE_CHAR, oracledb.DB_TYPE_VARCHAR, "31.5", "31.5") def test_3646_CHAR_to_LONG(self): "3646 - output type handler conversion from CHAR to LONG" self.__test_type_handler(oracledb.DB_TYPE_CHAR, oracledb.DB_TYPE_LONG, "31.5", "31.5") def test_3647_TIMESTAMP_to_TIMESTAMP_TZ(self): "3647 - output type handler conversion from TIMESTAMP to TIMESTAMP_TZ" val = datetime.datetime(2002, 12, 17, 0, 0, 16, 400000) self.__test_type_handler(oracledb.DB_TYPE_TIMESTAMP, oracledb.DB_TYPE_TIMESTAMP_TZ, val, val) def test_3648_TIMESTAMP_to_TIMESTAMP_LTZ(self): "3648 - output type handler conversion from TIMESTAMP to TIMESTAMP_LTZ" val = datetime.datetime(2002, 12, 17, 0, 0, 16, 400000) self.__test_type_handler(oracledb.DB_TYPE_TIMESTAMP, oracledb.DB_TYPE_TIMESTAMP_LTZ, val, val) def test_3649_TIMESTAMP_TZ_to_TIMESTAMP(self): "3649 - output type handler from conversion TIMESTAMP_TZ to TIMESTAMP" val = datetime.datetime(2002, 12, 17, 0, 0, 16, 400000) self.__test_type_handler(oracledb.DB_TYPE_TIMESTAMP_TZ, oracledb.DB_TYPE_TIMESTAMP, val, val) def test_3650_NUMBER_TO_DATE(self): "3650 - output type handler conversion from NUMBER to DATE is invalid" self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4007:|^ORA-00932:", self.__test_type_handler, oracledb.DB_TYPE_NUMBER, oracledb.DB_TYPE_DATE, 3, 3) def test_3651_CLOB_TO_CHAR(self): "3651 - output type handler conversion from CLOB to CHAR" val = "Some Clob String" self.__test_type_handler(oracledb.DB_TYPE_CLOB, oracledb.DB_TYPE_CHAR, val, val) def test_3652_CLOB_TO_VARCHAR(self): "3652 - output type handler conversion from CLOB to VARCHAR" val = "Some Clob String" self.__test_type_handler(oracledb.DB_TYPE_CLOB, oracledb.DB_TYPE_VARCHAR, val, val) def test_3653_CLOB_TO_LONG(self): "3653 - output type handler conversion from CLOB to LONG" val = "Some Clob String" self.__test_type_handler(oracledb.DB_TYPE_CLOB, oracledb.DB_TYPE_LONG, val, val) def test_3654_BLOB_TO_RAW(self): "3654 - output type handler conversion from BLOB to RAW" val = b"Some binary data" self.__test_type_handler(oracledb.DB_TYPE_BLOB, oracledb.DB_TYPE_RAW, val, val) def test_3655_BLOB_TO_LONG_RAW(self): "3655 - output type handler conversion from BLOB to LONGRAW" val = b"Some binary data" self.__test_type_handler(oracledb.DB_TYPE_BLOB, oracledb.DB_TYPE_LONG_RAW, val, val) def test_3656_BLOB_TO_LONG_RAW(self): "3656 - output type handler conversion from permanent BLOBs to LONG_RAW" self.__test_type_handler_lob("BLOB", oracledb.DB_TYPE_LONG_RAW) def test_3657_BLOB_TO_RAW(self): "3657 - output type handler conversion from permanent BLOBs to RAW" self.__test_type_handler_lob("BLOB", oracledb.DB_TYPE_RAW) def test_3658_CLOB_TO_VARCHAR(self): "3658 - output type handler conversion from permanent CLOBs to VARCHAR" self.__test_type_handler_lob("CLOB", oracledb.DB_TYPE_VARCHAR) def test_3659_CLOB_TO_CHAR(self): "3659 - output type handler conversion from permanent CLOBs to CHAR" self.__test_type_handler_lob("CLOB", oracledb.DB_TYPE_CHAR) def test_3660_CLOB_TO_LONG(self): "3660 - output type handler conversion from permanent CLOBs to LONG" self.__test_type_handler_lob("CLOB", oracledb.DB_TYPE_LONG) def test_3661_NCLOB_TO_CHAR(self): "3661 - output type handler conversion from NCLOB to CHAR" val = "Some nclob data" self.__test_type_handler(oracledb.DB_TYPE_NCLOB, oracledb.DB_TYPE_CHAR, val, val) def test_3662_NCLOB_TO_VARCHAR(self): "3662 - output type handler conversion from NCLOB to VARCHAR" val = "Some nclob data" self.__test_type_handler(oracledb.DB_TYPE_NCLOB, oracledb.DB_TYPE_VARCHAR, val, val) def test_3663_NCLOB_TO_LONG(self): "3663 - output type handler conversion from NCLOB to LONG" val = "Some nclob data" self.__test_type_handler(oracledb.DB_TYPE_NCLOB, oracledb.DB_TYPE_LONG, val, val) def test_3664_NCLOB_TO_VARCHAR(self): "3664 - output type handler conversion from permanent NCLOBs to VARCHAR" self.__test_type_handler_lob("NCLOB", oracledb.DB_TYPE_VARCHAR) def test_3665_NCLOB_TO_CHAR(self): "3665 - output type handler conversion from permanent NCLOBs to CHAR" self.__test_type_handler_lob("NCLOB", oracledb.DB_TYPE_CHAR) def test_3666_NCLOB_TO_LONG(self): "3666 - output type handler conversion from permanent NCLOBs to LONG" self.__test_type_handler_lob("NCLOB", oracledb.DB_TYPE_LONG) def test_3667_NVARCHAR_to_VARCHAR(self): "3667 - output type handler conversion from NVARCHAR to VARCHAR" self.__test_type_handler(oracledb.DB_TYPE_NVARCHAR, oracledb.DB_TYPE_VARCHAR, "31.5", "31.5") def test_3668_VARCHAR_to_NVARCHAR(self): "3668 - output type handler conversion from VARCHAR to NVARCHAR" self.__test_type_handler(oracledb.DB_TYPE_VARCHAR, oracledb.DB_TYPE_NVARCHAR, "31.5", "31.5") def test_3669_NCHAR_to_CHAR(self): "3669 - output type handler conversion from NCHAR to CHAR" self.__test_type_handler(oracledb.DB_TYPE_NCHAR, oracledb.DB_TYPE_CHAR, "31.5", "31.5") def test_3670_CHAR_to_NCHAR(self): "3670 - output type handler conversion from CHAR to NCHAR" self.__test_type_handler(oracledb.DB_TYPE_CHAR, oracledb.DB_TYPE_NCHAR, "31.5", "31.5") def test_3671_incorrect_arraysize(self): "3671 - execute raises an error if an incorrect arraysize is used" def type_handler(cursor, name, default_type, size, precision, scale): return cursor.var(str) cursor = self.connection.cursor() cursor.arraysize = 100 cursor.outputtypehandler = type_handler self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2016:", cursor.execute, "select :1 from dual", [5]) def test_3672_incorrect_outputtypehandler_return_type(self): "3672 - execute raises an error if a var is not returned" def type_handler(cursor, name, default_type, size, precision, scale): return "incorrect_return" cursor = self.connection.cursor() cursor.outputtypehandler = type_handler self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2015:", cursor.execute, "select :1 from dual", [5]) def test_3673_NUMBER_to_DECIMAL(self): "3673 - output type handler conversion from NUMBER to decimal.Decimal" self.__test_type_handler(oracledb.DB_TYPE_NUMBER, decimal.Decimal, 31.5, decimal.Decimal("31.5")) self.__test_type_handler(oracledb.DB_TYPE_NUMBER, decimal.Decimal, 0, decimal.Decimal("0")) def test_3674_cursor_description_unchanged(self): "3674 - use of output type handler does not affect description" def type_handler(cursor, name, default_type, size, precision, scale): return cursor.var(str, arraysize=cursor.arraysize) with self.connection.cursor() as cursor: cursor.execute("select user from dual") desc_before = cursor.description with self.connection.cursor() as cursor: cursor.outputtypehandler = type_handler cursor.execute("select user from dual") self.assertEqual(cursor.description, desc_before) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_3700_var.py000066400000000000000000000474111434177474600206510ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 3700 - Module for testing all variable types. """ import datetime import decimal import unittest import oracledb import test_env class TestCase(test_env.BaseTestCase): def _test_positive_set_and_get(self, var_type, value_to_set, expected_value, type_name=None): var = self.cursor.var(var_type, typename=type_name) var.setvalue(0, value_to_set) result = var.getvalue() if isinstance(result, oracledb.LOB): result = result.read() elif isinstance(result, oracledb.DbObject): result = self.get_db_object_as_plain_object(result) if isinstance(expected_value, datetime.date) \ and not isinstance(expected_value, datetime.datetime): if isinstance(result, datetime.datetime): result = result.date() self.assertEqual(type(result), type(expected_value)) self.assertEqual(result, expected_value) def _test_negative_set_and_get(self, var_type, value_to_set, type_name=None): var = self.cursor.var(var_type, typename=type_name) self.assertRaises((TypeError, oracledb.DatabaseError), var.setvalue, 0, value_to_set) def test_3700_DB_TYPE_NUMBER(self): "3700 - setting values on variables of type DB_TYPE_NUMBER" self._test_positive_set_and_get(int, 5, 5) self._test_positive_set_and_get(oracledb.DB_TYPE_NUMBER, 3.5, 3.5) self._test_positive_set_and_get(decimal.Decimal, decimal.Decimal("24.8"), decimal.Decimal("24.8")) self._test_positive_set_and_get(int, True, 1) self._test_positive_set_and_get(int, False, 0) self._test_positive_set_and_get(oracledb.DB_TYPE_NUMBER, None, None) self._test_negative_set_and_get(oracledb.DB_TYPE_NUMBER, "abc") def test_3701_DB_TYPE_BINARY_INTEGER(self): "3701 - setting values on variables of type DB_TYPE_BINARY_INTEGER" self._test_positive_set_and_get(oracledb.DB_TYPE_BINARY_INTEGER, 5, 5) self._test_positive_set_and_get(oracledb.DB_TYPE_BINARY_INTEGER, 3.5, 3) self._test_positive_set_and_get(oracledb.DB_TYPE_BINARY_INTEGER, decimal.Decimal("24.8"), 24) self._test_positive_set_and_get(oracledb.DB_TYPE_BINARY_INTEGER, True, 1) self._test_positive_set_and_get(oracledb.DB_TYPE_BINARY_INTEGER, False, 0) self._test_positive_set_and_get(oracledb.DB_TYPE_BINARY_INTEGER, None, None) self._test_negative_set_and_get(oracledb.DB_TYPE_BINARY_INTEGER, "abc") def test_3702_DB_TYPE_VARCHAR(self): "3702 - setting values on variables of type DB_TYPE_VARCHAR" value = "A VARCHAR string" self._test_positive_set_and_get(oracledb.DB_TYPE_VARCHAR, value, value) value = b"A raw string for VARCHAR" self._test_positive_set_and_get(oracledb.DB_TYPE_VARCHAR, value, value.decode()) self._test_positive_set_and_get(oracledb.DB_TYPE_VARCHAR, None, None) self._test_negative_set_and_get(oracledb.DB_TYPE_VARCHAR, 5) def test_3703_DB_TYPE_NVARCHAR(self): "3703 - setting values on variables of type DB_TYPE_NVARCHAR" value = "A NVARCHAR string" self._test_positive_set_and_get(oracledb.DB_TYPE_NVARCHAR, value, value) value = b"A raw string for NVARCHAR" self._test_positive_set_and_get(oracledb.DB_TYPE_NVARCHAR, value, value.decode()) self._test_positive_set_and_get(oracledb.DB_TYPE_NVARCHAR, None, None) self._test_negative_set_and_get(oracledb.DB_TYPE_NVARCHAR, 5) def test_3704_DB_TYPE_CHAR(self): "3704 - setting values on variables of type DB_TYPE_CHAR" value = "A CHAR string" self._test_positive_set_and_get(oracledb.DB_TYPE_CHAR, value, value) value = b"A raw string for CHAR" self._test_positive_set_and_get(oracledb.DB_TYPE_CHAR, value, value.decode()) self._test_positive_set_and_get(oracledb.DB_TYPE_CHAR, None, None) self._test_negative_set_and_get(oracledb.DB_TYPE_CHAR, 5) def test_3705_DB_TYPE_NCHAR(self): "3705 - setting values on variables of type DB_TYPE_NCHAR" value = "A NCHAR string" self._test_positive_set_and_get(oracledb.DB_TYPE_NCHAR, value, value) value = b"A raw string for NCHAR" self._test_positive_set_and_get(oracledb.DB_TYPE_CHAR, value, value.decode()) self._test_positive_set_and_get(oracledb.DB_TYPE_NCHAR, None, None) self._test_negative_set_and_get(oracledb.DB_TYPE_NCHAR, 5) def test_3706_DB_TYPE_LONG(self): "3706 - setting values on variables of type DB_TYPE_LONG" value = "Long Data" * 15000 self._test_positive_set_and_get(oracledb.DB_TYPE_LONG, value, value) value = b"Raw data for LONG" * 15000 self._test_positive_set_and_get(oracledb.DB_TYPE_LONG, value, value.decode()) self._test_positive_set_and_get(oracledb.DB_TYPE_LONG, None, None) self._test_negative_set_and_get(oracledb.DB_TYPE_LONG, 5) def test_3707_DB_TYPE_RAW(self): "3707 - setting values on variables of type DB_TYPE_RAW" value = b'Raw Data' self._test_positive_set_and_get(oracledb.DB_TYPE_RAW, value, value) value = "String data for RAW" self._test_positive_set_and_get(oracledb.DB_TYPE_RAW, value, value.encode()) self._test_positive_set_and_get(oracledb.DB_TYPE_RAW, None, None) self._test_negative_set_and_get(oracledb.DB_TYPE_RAW, 5) def test_3708_DB_TYPE_LONG_RAW(self): "3708 - setting values on variables of type DB_TYPE_LONG_RAW" value = b'Long Raw Data' * 15000 self._test_positive_set_and_get(oracledb.DB_TYPE_LONG_RAW, value, value) value = "String data for LONG RAW" * 15000 self._test_positive_set_and_get(oracledb.DB_TYPE_LONG_RAW, value, value.encode()) self._test_positive_set_and_get(oracledb.DB_TYPE_LONG_RAW, None, None) self._test_negative_set_and_get(oracledb.DB_TYPE_LONG_RAW, 5) def test_3709_DB_TYPE_DATE(self): "3709 - setting values on variables of type DB_TYPE_DATE" value = datetime.date(2017, 5, 6) self._test_positive_set_and_get(oracledb.DB_TYPE_DATE, value, value) value = datetime.datetime(2017, 5, 6, 9, 36, 0) self._test_positive_set_and_get(oracledb.DB_TYPE_DATE, value, value) self._test_positive_set_and_get(oracledb.DB_TYPE_DATE, None, None) self._test_negative_set_and_get(oracledb.DB_TYPE_DATE, 5) def test_3710_DB_TYPE_TIMESTAMP(self): "3710 - setting values on variables of type DB_TYPE_TIMESTAMP" value = datetime.date(2017, 5, 6) self._test_positive_set_and_get(oracledb.DB_TYPE_TIMESTAMP, value, value) value = datetime.datetime(2017, 5, 6, 9, 36, 0, 300000) self._test_positive_set_and_get(oracledb.DB_TYPE_TIMESTAMP, value, value) self._test_positive_set_and_get(oracledb.DB_TYPE_TIMESTAMP, None, None) self._test_negative_set_and_get(oracledb.DB_TYPE_TIMESTAMP, 5) def test_3711_DB_TYPE_TIMESTAMP_TZ(self): "3711 - setting values on variables of type DB_TYPE_TIMESTAMP_TZ" value = datetime.date(2017, 5, 6) self._test_positive_set_and_get(oracledb.DB_TYPE_TIMESTAMP_TZ, value, value) value = datetime.datetime(2017, 5, 6, 9, 36, 0, 300000) self._test_positive_set_and_get(oracledb.DB_TYPE_TIMESTAMP_TZ, value, value) self._test_positive_set_and_get(oracledb.DB_TYPE_TIMESTAMP_TZ, None, None) self._test_negative_set_and_get(oracledb.DB_TYPE_TIMESTAMP_TZ, 5) def test_3712_DB_TYPE_TIMESTAMP_LTZ(self): "3712 - setting values on variables of type DB_TYPE_TIMESTAMP_LTZ" value = datetime.date(2017, 5, 6) self._test_positive_set_and_get(oracledb.DB_TYPE_TIMESTAMP_LTZ, value, value) value = datetime.datetime(2017, 5, 6, 9, 36, 0, 300000) self._test_positive_set_and_get(oracledb.DB_TYPE_TIMESTAMP_LTZ, value, value) self._test_positive_set_and_get(oracledb.DB_TYPE_TIMESTAMP_LTZ, None, None) self._test_negative_set_and_get(oracledb.DB_TYPE_TIMESTAMP_LTZ, 5) def test_3713_DB_TYPE_BLOB(self): "3713 - setting values on variables of type DB_TYPE_BLOB" value = b'Short temp BLOB value' temp_blob = self.connection.createlob(oracledb.DB_TYPE_BLOB) temp_blob.write(value) self._test_positive_set_and_get(oracledb.DB_TYPE_BLOB, temp_blob, value) self._test_negative_set_and_get(oracledb.DB_TYPE_CLOB, temp_blob) self._test_negative_set_and_get(oracledb.DB_TYPE_NCLOB, temp_blob) value = b'Short BLOB value' self._test_positive_set_and_get(oracledb.DB_TYPE_BLOB, value, value) self._test_positive_set_and_get(oracledb.DB_TYPE_BLOB, None, None) self._test_negative_set_and_get(oracledb.DB_TYPE_BLOB, 5) def test_3714_DB_TYPE_CLOB(self): "3714 - setting values on variables of type DB_TYPE_CLOB" value = 'Short temp CLOB value' temp_clob = self.connection.createlob(oracledb.DB_TYPE_CLOB) temp_clob.write(value) self._test_positive_set_and_get(oracledb.DB_TYPE_CLOB, temp_clob, value) self._test_negative_set_and_get(oracledb.DB_TYPE_BLOB, temp_clob) self._test_negative_set_and_get(oracledb.DB_TYPE_NCLOB, temp_clob) value = 'Short CLOB value' self._test_positive_set_and_get(oracledb.DB_TYPE_CLOB, value, value) self._test_positive_set_and_get(oracledb.DB_TYPE_CLOB, None, None) self._test_negative_set_and_get(oracledb.DB_TYPE_CLOB, 5) def test_3715_DB_TYPE_NCLOB(self): "3715 - setting values on variables of type DB_TYPE_NCLOB" value = 'Short temp NCLOB value' temp_nclob = self.connection.createlob(oracledb.DB_TYPE_NCLOB) temp_nclob.write(value) self._test_positive_set_and_get(oracledb.DB_TYPE_NCLOB, temp_nclob, value) self._test_negative_set_and_get(oracledb.DB_TYPE_BLOB, temp_nclob) self._test_negative_set_and_get(oracledb.DB_TYPE_CLOB, temp_nclob) value = 'Short NCLOB Value' self._test_positive_set_and_get(oracledb.DB_TYPE_NCLOB, value, value) self._test_positive_set_and_get(oracledb.DB_TYPE_NCLOB, None, None) self._test_negative_set_and_get(oracledb.DB_TYPE_NCLOB, 5) def test_3716_DB_TYPE_BINARY_FLOAT(self): "3716 - setting values on variables of type DB_TYPE_BINARY_FLOAT" self._test_positive_set_and_get(oracledb.DB_TYPE_BINARY_FLOAT, 5, 5.0) self._test_positive_set_and_get(oracledb.DB_TYPE_BINARY_FLOAT, 3.5, 3.5) self._test_positive_set_and_get(oracledb.DB_TYPE_BINARY_FLOAT, decimal.Decimal("24.5"), 24.5) self._test_positive_set_and_get(oracledb.DB_TYPE_BINARY_FLOAT, True, 1.0) self._test_positive_set_and_get(oracledb.DB_TYPE_BINARY_FLOAT, False, 0.0) self._test_positive_set_and_get(oracledb.DB_TYPE_BINARY_FLOAT, None, None) self._test_negative_set_and_get(oracledb.DB_TYPE_BINARY_FLOAT, "abc") def test_3717_DB_TYPE_BINARY_DOUBLE(self): "3717 - setting values on variables of type DB_TYPE_BINARY_DOUBLE" self._test_positive_set_and_get(oracledb.DB_TYPE_BINARY_DOUBLE, 5, 5.0) self._test_positive_set_and_get(oracledb.DB_TYPE_BINARY_DOUBLE, 3.5, 3.5) self._test_positive_set_and_get(oracledb.DB_TYPE_BINARY_DOUBLE, decimal.Decimal("192.125"), 192.125) self._test_positive_set_and_get(oracledb.DB_TYPE_BINARY_DOUBLE, True, 1.0) self._test_positive_set_and_get(oracledb.DB_TYPE_BINARY_DOUBLE, False, 0.0) self._test_positive_set_and_get(oracledb.DB_TYPE_BINARY_DOUBLE, None, None) self._test_negative_set_and_get(oracledb.DB_TYPE_BINARY_DOUBLE, "abc") def test_3718_DB_TYPE_BOOLEAN(self): "3718 - setting values on variables of type DB_TYPE_BOOLEAN" self._test_positive_set_and_get(oracledb.DB_TYPE_BOOLEAN, 5, True) self._test_positive_set_and_get(oracledb.DB_TYPE_BOOLEAN, 2.0, True) self._test_positive_set_and_get(oracledb.DB_TYPE_BOOLEAN, "abc", True) self._test_positive_set_and_get(oracledb.DB_TYPE_BOOLEAN, decimal.Decimal("24.8"), True) self._test_positive_set_and_get(oracledb.DB_TYPE_BOOLEAN, 0.0, False) self._test_positive_set_and_get(oracledb.DB_TYPE_BOOLEAN, 0, False) self._test_positive_set_and_get(oracledb.DB_TYPE_BOOLEAN, None, None) def test_3719_DB_TYPE_INTERVAL_DS(self): "3719 - setting values on variables of type DB_TYPE_INTERVAL_DS" value = datetime.timedelta(days=5, seconds=56000, microseconds=123780) self._test_positive_set_and_get(oracledb.DB_TYPE_INTERVAL_DS, value, value) self._test_positive_set_and_get(oracledb.DB_TYPE_INTERVAL_DS, None, None) self._test_negative_set_and_get(oracledb.DB_TYPE_INTERVAL_DS, 5) def test_3720_DB_TYPE_ROWID(self): "3720 - setting values on variables of type DB_TYPE_ROWID" self._test_negative_set_and_get(oracledb.DB_TYPE_ROWID, 12345) self._test_negative_set_and_get(oracledb.DB_TYPE_ROWID, "523lkhlf") def test_3721_DB_TYPE_OBJECT(self): "3721 - setting values on variables of type DB_TYPE_OBJECT" obj_type = self.connection.gettype("UDT_OBJECT") obj = obj_type.newobject() plain_obj = self.get_db_object_as_plain_object(obj) self._test_positive_set_and_get(oracledb.DB_TYPE_OBJECT, obj, plain_obj, "UDT_OBJECT") self._test_positive_set_and_get(obj_type, obj, plain_obj) self._test_positive_set_and_get(oracledb.DB_TYPE_OBJECT, None, None, "UDT_OBJECT") self._test_positive_set_and_get(obj_type, None, None) self._test_negative_set_and_get(oracledb.DB_TYPE_OBJECT, "abc", "UDT_OBJECT") self._test_negative_set_and_get(oracledb.DB_TYPE_OBJECT, obj, "UDT_OBJECTARRAY") wrong_obj_type = self.connection.gettype("UDT_OBJECTARRAY") self._test_negative_set_and_get(wrong_obj_type, obj) @unittest.skipIf(test_env.get_client_version() < (21, 0), "unsupported client") @unittest.skipIf(test_env.get_server_version() < (21, 0), "unsupported server") @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support JSON yet") def test_3722_DB_TYPE_JSON(self): "3722 - setting values on variables of type DB_TYPE_JSON" json_data = [ 5, 25.25, decimal.Decimal("10.25"), True, False, datetime.datetime(2017, 5, 6), datetime.datetime(2017, 5, 6, 9, 36, 0, 300000), datetime.timedelta(days=5, seconds=56000, microseconds=123780), {}, 'String', b'Some bytes', {'keyA': 1, 'KeyB': 'Melbourne'}, [], [1, "A"], {"name": None}, {"name": "John"}, {"age": 30}, {"Permanent": True}, { "employee": { "name":"John", "age": 30, "city": "Delhi", "Parmanent": True } }, { "employees": ["John", "Matthew", "James"] }, { "employees": [ { "employee1": {"name": "John", "city": "Delhi"} }, { "employee2": {"name": "Matthew", "city": "Mumbai"} }, { "employee3": {"name": "James", "city": "Bangalore"} } ] } ] self._test_positive_set_and_get(oracledb.DB_TYPE_JSON, json_data, json_data) self._test_positive_set_and_get(oracledb.DB_TYPE_JSON, None, None) def test_3723_DB_TYPE_CURSOR(self): "3723 - test setting values on variables of type DB_TYPE_CURSOR" cursor = self.connection.cursor() self._test_positive_set_and_get(oracledb.DB_TYPE_CURSOR, None, None) self._test_negative_set_and_get(oracledb.DB_TYPE_CURSOR, 5) def test_3724_fetch_null_column(self): "3724 - test fetching columns containing all null values" self.cursor.execute(""" select null, to_char(null), to_number(null), to_date(null), to_timestamp(null), to_clob(null), to_blob(null) from dual""") results = self.cursor.fetchall() self.assertEqual(results, [(None, None, None, None, None, None, None)]) @unittest.skipIf(not test_env.get_is_thin(), "thick mode doesn't support DB_TYPE_UROWID") def test_3725_DB_TYPE_UROWID(self): "3725 - setting values on variables of type DB_TYPE_UROWID" self._test_negative_set_and_get(oracledb.DB_TYPE_UROWID, 12345) self._test_negative_set_and_get(oracledb.DB_TYPE_UROWID, "523lkhlf") if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_3800_typehandler.py000066400000000000000000000237251434177474600224030ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 3800 - Module for testing the input and output type handlers. """ import json import unittest import oracledb import test_env class Building(object): def __init__(self, building_id, description, num_floors): self.building_id = building_id self.description = description self.num_floors = num_floors def __repr__(self): return "" % (self.building_id, self.description) def __eq__(self, other): if isinstance(other, Building): return other.building_id == self.building_id \ and other.description == self.description \ and other.num_floors == self.num_floors return NotImplemented def to_json(self): return json.dumps(self.__dict__) @classmethod def from_json(cls, value): result = json.loads(value) return cls(**result) class TestCase(test_env.BaseTestCase): def building_in_converter(self, value): return value.to_json() def input_type_handler(self, cursor, value, num_elements): if isinstance(value, Building): return cursor.var(oracledb.STRING, arraysize=num_elements, inconverter=self.building_in_converter) def output_type_handler(self, cursor, name, default_type, size, precision, scale): if default_type == oracledb.STRING: return cursor.var(default_type, arraysize=cursor.arraysize, outconverter=Building.from_json) def test_3800(self): "3800 - binding unsupported python object without input type handler" self.cursor.execute("truncate table TestTempTable") sql = "insert into TestTempTable (IntCol, StringCol1) values (:1, :2)" building = Building(1, "The First Building", 5) self.assertRaisesRegex(oracledb.NotSupportedError, "^DPY-3002:", self.cursor.execute, sql, (building.building_id, building)) def test_3801(self): "3801 - not callable input type handler" self.cursor.execute("truncate table TestTempTable") building = Building(1, "The First Building", 5) sql = "insert into TestTempTable (IntCol, StringCol1) values (:1, :2)" self.cursor.inputtypehandler = 5 self.assertRaises(TypeError, self.cursor.execute, sql, (building.building_id, building)) def test_3802(self): "3802 - binding unsupported python object with input type handler" self.cursor.execute("truncate table TestTempTable") building = Building(1, "The First Building", 5) sql = "insert into TestTempTable (IntCol, StringCol1) values (:1, :2)" self.cursor.inputtypehandler = self.input_type_handler self.cursor.execute(sql, (building.building_id, building)) self.connection.commit() self.cursor.execute("select IntCol, StringCol1 from TestTempTable") self.assertEqual(self.cursor.fetchall(), [(building.building_id, building.to_json())]) def test_3803(self): "3803 - input type handler and output type handler on cursor level" self.cursor.execute("truncate table TestTempTable") building_one = Building(1, "The First Building", 5) building_two = Building(2, "The Second Building", 87) sql = "insert into TestTempTable (IntCol, StringCol1) values (:1, :2)" cursor_one = self.connection.cursor() cursor_two = self.connection.cursor() cursor_one.inputtypehandler = self.input_type_handler cursor_one.execute(sql, (building_one.building_id, building_one)) self.connection.commit() cursor_one.execute("select IntCol, StringCol1 from TestTempTable") self.assertEqual(cursor_one.fetchall(), [(building_one.building_id, building_one.to_json())]) self.assertRaisesRegex(oracledb.NotSupportedError, "^DPY-3002:", cursor_two.execute, sql, (building_two.building_id, building_two)) cursor_two.outputtypehandler = self.output_type_handler cursor_two.execute("select IntCol, StringCol1 from TestTempTable") self.assertEqual(cursor_two.fetchall(), [(building_one.building_id, building_one)]) def test_3804(self): "3804 - input type handler and output type handler on connection level" self.cursor.execute("truncate table TestTempTable") building_one = Building(1, "The First Building", 5) building_two = Building(2, "The Second Building", 87) sql = "insert into TestTempTable (IntCol, StringCol1) values (:1, :2)" connection = test_env.get_connection() connection.inputtypehandler = self.input_type_handler cursor_one = connection.cursor() cursor_two = connection.cursor() cursor_one.execute(sql, (building_one.building_id, building_one)) cursor_two.execute(sql, (building_two.building_id, building_two)) connection.commit() expected_data = [ (building_one.building_id, building_one), (building_two.building_id, building_two) ] connection.outputtypehandler = self.output_type_handler cursor_one.execute("select IntCol, StringCol1 from TestTempTable") self.assertEqual(cursor_one.fetchall(), expected_data) cursor_two.execute("select IntCol, StringCol1 from TestTempTable") self.assertEqual(cursor_two.fetchall(), expected_data) other_cursor = self.connection.cursor() self.assertRaisesRegex(oracledb.NotSupportedError, "^DPY-3002:", other_cursor.execute, sql, (building_one.building_id, building_one)) def test_3805(self): "3805 - output type handler with outconvert and null values" self.cursor.execute("truncate table TestTempTable") sql = "insert into TestTempTable (IntCol, StringCol1) values (:1, :2)" data_to_insert = [ (1, "String 1"), (2, None), (3, "String 2") ] self.cursor.executemany(sql, data_to_insert) self.connection.commit() def converter(value): return "CONVERTED" def output_type_handler(cursor, name, default_type, size, precision, scale): if default_type is oracledb.DB_TYPE_VARCHAR: return cursor.var(str, outconverter=converter, arraysize=cursor.arraysize) self.cursor.outputtypehandler = output_type_handler self.cursor.execute(""" select IntCol, StringCol1 from TestTempTable order by IntCol""") expected_data = [ (1, "CONVERTED"), (2, None), (3, "CONVERTED") ] self.assertEqual(self.cursor.fetchall(), expected_data) @unittest.skipUnless(test_env.get_server_version() >= (21, 0), "unsupported server") def test_3806(self): "3806 - output type handler for fetching 21c JSON" self.cursor.execute("truncate table TestJson") insert_sql = "insert into TestJson values (:1, :2)" json_data = [ dict(name="John", city="Delhi"), dict(name="George", city="Bangalore"), dict(name="Sam", city="Mumbai") ] data_to_insert = list(enumerate(json_data)) json_as_string = self.connection.thin or \ test_env.get_client_version() < (21, 0) if json_as_string: # insert data as JSON string self.cursor.executemany(insert_sql, [(i, json.dumps(j)) for i, j in data_to_insert]) else: # take advantage of direct binding self.cursor.setinputsizes(None, oracledb.DB_TYPE_JSON) self.cursor.executemany(insert_sql, data_to_insert) def output_type_handler(cursor, name, default_type, size, precision, scale): # fetch 21c JSON datatype when using python-oracledb thin mode if default_type == oracledb.DB_TYPE_JSON: return cursor.var(str, arraysize=cursor.arraysize, outconverter=json.loads) # if using Oracle Client version < 21, then database returns BLOB # data type instead of JSON data type elif default_type == oracledb.DB_TYPE_BLOB: return cursor.var(default_type, arraysize=cursor.arraysize, outconverter=lambda v: json.loads(v.read())) if json_as_string: self.cursor.outputtypehandler = output_type_handler self.cursor.execute("select * from TestJson") self.assertEqual(self.cursor.fetchall(), data_to_insert) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_3900_cursor_execute.py000066400000000000000000000400521434177474600231140ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 3900 - Module for testing the cursor execute() method """ import oracledb import test_env class TestCase(test_env.BaseTestCase): def test_3900_execute_no_args(self): "3900 - test executing a statement without any arguments" result = self.cursor.execute("begin null; end;") self.assertEqual(result, None) def test_3901_execute_no_statement_with_args(self): "3901 - test executing a None statement with bind variables" self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2001:", self.cursor.execute, None, x=5) def test_3902_execute_empty_keyword_args(self): "3902 - test executing a statement with args and empty keyword args" simple_var = self.cursor.var(oracledb.NUMBER) args = [simple_var] kwargs = {} result = self.cursor.execute("begin :1 := 25; end;", args, **kwargs) self.assertEqual(result, None) self.assertEqual(simple_var.getvalue(), 25) def test_3903_execute_keyword_args(self): "3903 - test executing a statement with keyword arguments" simple_var = self.cursor.var(oracledb.NUMBER) result = self.cursor.execute("begin :value := 5; end;", value=simple_var) self.assertEqual(result, None) self.assertEqual(simple_var.getvalue(), 5) def test_3904_execute_dictionary_arg(self): "3904 - test executing a statement with a dictionary argument" simple_var = self.cursor.var(oracledb.NUMBER) dict_arg = dict(value=simple_var) result = self.cursor.execute("begin :value := 10; end;", dict_arg) self.assertEqual(result, None) self.assertEqual(simple_var.getvalue(), 10) def test_3905_execute_multiple_arg_types(self): "3905 - test executing a statement with both a dict and keyword args" simple_var = self.cursor.var(oracledb.NUMBER) dict_arg = dict(value=simple_var) self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2005:", self.cursor.execute, "begin :value := 15; end;", dict_arg, value=simple_var) def test_3906_execute_and_modify_array_size(self): "3906 - test executing a statement and then changing the array size" self.cursor.execute("select IntCol from TestNumbers") self.cursor.arraysize = 20 self.assertEqual(len(self.cursor.fetchall()), 10) def test_3907_bad_execute(self): "3907 - test that subsequent executes succeed after bad execute" sql = "begin raise_application_error(-20000, 'this); end;" self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-06550:|^ORA-01756:", self.cursor.execute, sql) self.cursor.execute("begin null; end;") def test_3908_fetch_after_bad_execute(self): "3908 - test that subsequent fetches fail after bad execute" self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-00904:", self.cursor.execute, "select y from dual") self.assertRaisesRegex(oracledb.InterfaceError, "^DPY-1003:", self.cursor.fetchall) def test_3909_execute_bind_names_with_incorrect_bind(self): "3909 - test executing a statement with an incorrect named bind" statement = "select * from TestStrings where IntCol = :value" self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4008:|^ORA-01036:", self.cursor.execute, statement, value2=3) def test_3910_execute_with_named_binds(self): "3910 - test executing a statement with named binds" statement = "select * from TestNumbers where IntCol = :value1 " + \ "and LongIntCol = :value2" result = self.cursor.execute(statement, value1=1, value2=38) self.assertEqual(len(result.fetchall()), 1) def test_3911_execute_bind_position_with_incorrect_bind(self): "3911 - test executing a statement with an incorrect positional bind" statement = "select * from TestNumbers where IntCol = :value " + \ "and LongIntCol = :value2" self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4009:|^ORA-01008:", self.cursor.execute, statement, [3]) def test_3912_execute_with_positional_binds(self): "3912 - test executing a statement with positional binds" statement = "select * from TestNumbers where IntCol = :value " + \ "and LongIntCol = :value2" result = self.cursor.execute(statement, [1,38]) self.assertEqual(len(result.fetchall()), 1) def test_3913_execute_with_rebinding_bind_name(self): "3913 - test executing a statement after rebinding a named bind" statement = "begin :value := :value2 + 5; end;" simple_var = self.cursor.var(oracledb.NUMBER) simple_var2 = self.cursor.var(oracledb.NUMBER) simple_var2.setvalue(0, 5) result = self.cursor.execute(statement, value=simple_var, value2=simple_var2) self.assertEqual(result, None) self.assertEqual(simple_var.getvalue(), 10) simple_var = self.cursor.var(oracledb.NATIVE_FLOAT) simple_var2 = self.cursor.var(oracledb.NATIVE_FLOAT) simple_var2.setvalue(0, 10) result = self.cursor.execute(statement, value=simple_var, value2=simple_var2) self.assertEqual(result, None) self.assertEqual(simple_var.getvalue(), 15) def test_3914_bind_by_name_with_duplicates(self): "3914 - test executing a PL/SQL statement with duplicate binds" statement = "begin :value := :value + 5; end;" simple_var = self.cursor.var(oracledb.NUMBER) simple_var.setvalue(0, 5) result = self.cursor.execute(statement, value=simple_var) self.assertEqual(result, None) self.assertEqual(simple_var.getvalue(), 10) def test_3915_positional_bind_with_duplicates(self): "3915 - test executing a PL/SQL statement with duplicate binds" statement = "begin :value := :value + 5; end;" simple_var = self.cursor.var(oracledb.NUMBER) simple_var.setvalue(0, 5) self.cursor.execute(statement, [simple_var]) self.assertEqual(simple_var.getvalue(), 10) def test_3916_execute_with_incorrect_bind_values(self): "3916 - test executing a statement with an incorrect number of binds" statement = "begin :value := :value2 + 5; end;" var = self.cursor.var(oracledb.NUMBER) var.setvalue(0, 5) self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4010:|^ORA-01008:", self.cursor.execute, statement) self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4010:|^ORA-01008:", self.cursor.execute, statement, value=var) self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4008:|^ORA-01036:", self.cursor.execute, statement, value=var, value2=var, value3=var) def test_3917_change_in_size_on_successive_bind(self): "3917 - change in size on subsequent binds does not use optimised path" self.cursor.execute("truncate table TestTempTable") data = [ (1, "Test String #1"), (2, "ABC" * 100) ] sql = "insert into TestTempTable (IntCol, StringCol1) values (:1, :2)" for row in data: self.cursor.execute(sql, row) self.connection.commit() self.cursor.execute("select IntCol, StringCol1 from TestTempTable") self.assertEqual(self.cursor.fetchall(), data) def test_3918_dml_can_use_optimised_path(self): "3918 - test that dml can use optimised path" data_to_insert = [ (1, "Test String #1"), (2, "Test String #2"), (3, "Test String #3") ] self.cursor.execute("truncate table TestTempTable") sql = "insert into TestTempTable (IntCol, StringCol1) values (:1, :2)" for row in data_to_insert: with self.connection.cursor() as cursor: cursor.execute(sql, row) self.connection.commit() self.cursor.execute(""" select IntCol, StringCol1 from TestTempTable order by IntCol""") self.assertEqual(self.cursor.fetchall(), data_to_insert) def test_3919_execute_with_invalid_parameters(self): "3919 - test calling execute() with invalid parameters" sql = "insert into TestTempTable (IntCol, StringCol1) values (:1, :2)" self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2003:", self.cursor.execute, sql, "These are not valid parameters") def test_3920_execute_with_mixed_binds(self): "3920 - test calling execute() with mixed binds" self.cursor.execute("truncate table TestTempTable") self.cursor.setinputsizes(None, None, str) data = dict(val1=1, val2="Test String 1") self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2006:", self.cursor.execute, """ insert into TestTempTable (IntCol, StringCol1) values (:1, :2) returning StringCol1 into :out_var""", data) def test_3921_bind_by_name_with_double_quotes(self): "3921 - test binding by name with double quotes" sql = 'select :"_value1" + :"VaLue_2" + :"3VALUE" from dual' data = {'"_value1"': 1, '"VaLue_2"': 2, '"3VALUE"': 3} self.cursor.execute(sql, data) result, = self.cursor.fetchone() self.assertEqual(result, 6) def test_3922_resize_buffer(self): "3922 - test executing a statement with different input buffer sizes" sql = """ insert into TestTempTable (IntCol, StringCol1, StringCol2) values (:int_col, :str_val1, :str_val2) returning IntCol into :ret_data""" values1 = {"int_col" : 1, "str_val1": '{"a", "b"}', "str_val2" : None} values2 = {"int_col" : 2, "str_val1": None, "str_val2" : '{"a", "b"}'} values3 = {"int_col" : 3, "str_val1": '{"a"}', "str_val2" : None} self.cursor.execute("truncate table TestTempTable") ret_bind = self.cursor.var(oracledb.DB_TYPE_VARCHAR, arraysize=1) self.cursor.setinputsizes(ret_data=ret_bind) self.cursor.execute(sql, values1) self.assertEqual(ret_bind.values, [['1']]) ret_bind = self.cursor.var(oracledb.DB_TYPE_VARCHAR, arraysize=1) self.cursor.setinputsizes(ret_data=ret_bind) self.cursor.execute(sql, values2) self.assertEqual(ret_bind.values, [['2']]) ret_bind = self.cursor.var(oracledb.DB_TYPE_VARCHAR, arraysize=1) self.cursor.setinputsizes(ret_data=ret_bind) self.cursor.execute(sql, values3) self.assertEqual(ret_bind.values, [['3']]) def test_3923_rowfactory_callable(self): "3923 - test using rowfactory" self.cursor.execute("truncate table TestTempTable") self.cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (1, 'Test 1')""") self.connection.commit() self.cursor.execute("select IntCol, StringCol1 from TestTempTable") column_names = [col[0] for col in self.cursor.description] self.cursor.rowfactory = lambda *row: dict(zip(column_names, row)) self.assertEqual(self.cursor.fetchall(), [{'INTCOL': 1, 'STRINGCOL1': 'Test 1'}]) def test_3924_rowfactory_execute_same_sql(self): "3924 - test executing same query after setting rowfactory" self.cursor.execute("truncate table TestTempTable") data = [ (1, 'Test 1'), (2, 'Test 2') ] self.cursor.executemany(""" insert into TestTempTable (IntCol, StringCol1) values (:1, :2)""", data) self.connection.commit() self.cursor.execute("select IntCol, StringCol1 from TestTempTable") column_names = [col[0] for col in self.cursor.description] self.cursor.rowfactory = lambda *row: dict(zip(column_names, row)) results_1 = self.cursor.fetchall() self.cursor.execute("select IntCol, StringCol1 from TestTempTable") results_2 = self.cursor.fetchall() self.assertEqual(results_1, results_2) def test_3925_rowfactory_execute_different_sql(self): "3925 - test executing different query after setting rowfactory" self.cursor.execute("truncate table TestTempTable") data = [ (1, 'Test 1'), (2, 'Test 2') ] self.cursor.executemany(""" insert into TestTempTable (IntCol, StringCol1) values (:1, :2)""", data) self.connection.commit() self.cursor.execute("select IntCol, StringCol1 from TestTempTable") column_names = [col[0] for col in self.cursor.description] self.cursor.rowfactory = lambda *row: dict(zip(column_names, row)) self.cursor.execute(""" select IntCol, StringCol from TestSTrings where IntCol between 1 and 3 order by IntCol""") expected_data = [ (1, 'String 1'), (2, 'String 2'), (3, 'String 3') ] self.assertEqual(self.cursor.fetchall(), expected_data) def test_3926_rowfactory_on_refcursor(self): "3926 - test setting rowfactory on a REF cursor" with self.connection.cursor() as cursor: ref_cursor = cursor.callfunc("pkg_TestRefCursors.TestReturnCursor", oracledb.DB_TYPE_CURSOR, [2]) column_names = [col[0] for col in ref_cursor.description] ref_cursor.rowfactory = lambda *row: dict(zip(column_names, row)) self.assertEqual(ref_cursor.fetchall(), [{'INTCOL': 1, 'STRINGCOL': 'String 1'}, {'INTCOL': 2, 'STRINGCOL': 'String 2'}]) def test_3927_subclassed_string(self): "3927 - test using a subclassed string as bind parameter keys" class my_str(str): pass self.cursor.execute("truncate table TestTempTable") keys = {my_str("str_val"): oracledb.DB_TYPE_VARCHAR} self.cursor.setinputsizes(**keys) values = { my_str("int_val"): 3927, my_str("str_val"): "3927 - String Value" } self.cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (:int_val, :str_val)""", values) self.cursor.execute("select IntCol, StringCol1 from TestTempTable") self.assertEqual(self.cursor.fetchall(), [(3927, "3927 - String Value")]) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_4000_cursor_executemany.py000066400000000000000000000327601434177474600240000ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 4000 - Module for testing the cursor executemany() method """ import decimal import oracledb import test_env class TestCase(test_env.BaseTestCase): def test_4000_executemany_by_name(self): "4000 - test executing a statement multiple times (named args)" self.cursor.execute("truncate table TestTempTable") rows = [{"value": n} for n in range(250)] self.cursor.arraysize = 100 statement = "insert into TestTempTable (IntCol) values (:value)" self.cursor.executemany(statement, rows) self.connection.commit() self.cursor.execute("select count(*) from TestTempTable") count, = self.cursor.fetchone() self.assertEqual(count, len(rows)) def test_4001_executemany_by_position(self): "4001 - test executing a statement multiple times (positional args)" self.cursor.execute("truncate table TestTempTable") rows = [[n] for n in range(230)] self.cursor.arraysize = 100 statement = "insert into TestTempTable (IntCol) values (:1)" self.cursor.executemany(statement, rows) self.connection.commit() self.cursor.execute("select count(*) from TestTempTable") count, = self.cursor.fetchone() self.assertEqual(count, len(rows)) def test_4002_executemany_with_prepare(self): "4002 - test executing a statement multiple times (with prepare)" self.cursor.execute("truncate table TestTempTable") rows = [[n] for n in range(225)] self.cursor.arraysize = 100 statement = "insert into TestTempTable (IntCol) values (:1)" self.cursor.prepare(statement) self.cursor.executemany(None, rows) self.connection.commit() self.cursor.execute("select count(*) from TestTempTable") count, = self.cursor.fetchone() self.assertEqual(count, len(rows)) def test_4003_executemany_with_rebind(self): "4003 - test executing a statement multiple times (with rebind)" self.cursor.execute("truncate table TestTempTable") rows = [[n] for n in range(235)] self.cursor.arraysize = 100 statement = "insert into TestTempTable (IntCol) values (:1)" self.cursor.executemany(statement, rows[:50]) self.cursor.executemany(statement, rows[50:]) self.connection.commit() self.cursor.execute("select count(*) from TestTempTable") count, = self.cursor.fetchone() self.assertEqual(count, len(rows)) def test_4004_executemany_with_input_sizes_wrong(self): "4004 - test executing multiple times (with input sizes wrong)" cursor = self.connection.cursor() cursor.setinputsizes(oracledb.NUMBER) data = [[decimal.Decimal("25.8")], [decimal.Decimal("30.0")]] cursor.executemany("declare t number; begin t := :1; end;", data) def test_4005_executemany_with_multiple_batches(self): "4005 - test executing multiple times (with multiple batches)" self.cursor.execute("truncate table TestTempTable") sql = "insert into TestTempTable (IntCol, StringCol1) values (:1, :2)" self.cursor.executemany(sql, [(1, None), (2, None)]) self.cursor.executemany(sql, [(3, None), (4, "Testing")]) def test_4006_executemany_numeric(self): "4006 - test executemany() with various numeric types" self.cursor.execute("truncate table TestTempTable") data = [ (1, 5), (2, 7.0), (3, 6.5), (4, 2 ** 65), (5, decimal.Decimal("24.5")) ] sql = "insert into TestTempTable (IntCol, NumberCol) values (:1, :2)" self.cursor.executemany(sql, data) self.cursor.execute(""" select IntCol, NumberCol from TestTempTable order by IntCol""") self.assertEqual(self.cursor.fetchall(), data) def test_4007_executemany_with_resize(self): "4007 - test executing a statement multiple times (with resize)" self.cursor.execute("truncate table TestTempTable") rows = [ (1, "First"), (2, "Second"), (3, "Third"), (4, "Fourth"), (5, "Fifth"), (6, "Sixth"), (7, "Seventh and the longest one") ] sql = "insert into TestTempTable (IntCol, StringCol1) values (:1, :2)" self.cursor.executemany(sql, rows) self.cursor.execute(""" select IntCol, StringCol1 from TestTempTable order by IntCol""") fetched_rows = self.cursor.fetchall() self.assertEqual(fetched_rows, rows) def test_4008_executemany_with_exception(self): "4008 - test executing a statement multiple times (with exception)" self.cursor.execute("truncate table TestTempTable") rows = [{"value": n} for n in (1, 2, 3, 2, 5)] statement = "insert into TestTempTable (IntCol) values (:value)" self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-00001:", self.cursor.executemany, statement, rows) self.assertEqual(self.cursor.rowcount, 3) def test_4009_executemany_with_invalid_parameters(self): "4009 - test calling executemany() with invalid parameters" sql = "insert into TestTempTable (IntCol, StringCol1) values (:1, :2)" self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2004:", self.cursor.executemany, sql, "These are not valid parameters") def test_4010_executemany_no_parameters(self): "4010 - test calling executemany() without any bind parameters" num_rows = 5 self.cursor.execute("truncate table TestTempTable") self.cursor.executemany(""" declare t_Id number; begin select nvl(count(*), 0) + 1 into t_Id from TestTempTable; insert into TestTempTable (IntCol, StringCol1) values (t_Id, 'Test String ' || t_Id); end;""", num_rows) self.assertEqual(self.cursor.rowcount, 0) self.cursor.execute("select count(*) from TestTempTable") count, = self.cursor.fetchone() self.assertEqual(count, num_rows) def test_4011_executemany_bound_earlier(self): "4011 - test calling executemany() with binds performed earlier" num_rows = 9 self.cursor.execute("truncate table TestTempTable") var = self.cursor.var(int, arraysize=num_rows) self.cursor.setinputsizes(var) self.cursor.executemany(""" declare t_Id number; begin select nvl(count(*), 0) + 1 into t_Id from TestTempTable; insert into TestTempTable (IntCol, StringCol1) values (t_Id, 'Test String ' || t_Id); select sum(IntCol) into :1 from TestTempTable; end;""", num_rows) self.assertEqual(self.cursor.rowcount, 0) expected_data = [1, 3, 6, 10, 15, 21, 28, 36, 45] self.assertEqual(var.values, expected_data) def test_4012_executemany_with_plsql_binds(self): "4012 - test executing plsql statements multiple times (with binds)" var = self.cursor.var(int, arraysize=5) self.cursor.setinputsizes(var) data = [[25], [30], [None], [35], [None]] exepected_data = [25, 30, None, 35, None] self.cursor.executemany("declare t number; begin t := :1; end;", data) self.assertEqual(var.values, exepected_data) def test_4013_executemany_with_incorrect_params(self): "4013 - test executemany with incorrect parameters" self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2004:", self.cursor.executemany, "select :1 from dual", [1]) def test_4014_executemany_with_mixed_binds_pos_first(self): "4014 - test executemany with mixed binds (pos first)" rows = [["test"], {"value": 1}] self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2006:", self.cursor.executemany, "select :1 from dual", rows) def test_4015_executemany_with_mixed_binds_name_first(self): "4015 - test executemany with mixed binds (name first)" rows = [{"value": 1}, ["test"]] self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2006:", self.cursor.executemany, "select :value from dual", rows) def test_4016_executemany_plsql_dml_returning(self): "4016 - test executemany() with a pl/sql statement with dml returning" num_rows = 5 self.cursor.execute("truncate table TestTempTable") out_var = self.cursor.var(oracledb.NUMBER, arraysize=5) self.cursor.setinputsizes(out_var) self.cursor.executemany(""" declare t_Id number; begin select nvl(count(*), 0) + 1 into t_Id from TestTempTable; insert into TestTempTable (IntCol, StringCol1) values (t_Id, 'Test String ' || t_Id) returning IntCol into :out_bind; end;""", num_rows) self.assertEqual(out_var.values, [1, 2, 3, 4, 5]) def test_4017_executemany_pl_sql_with_in_and_out_binds(self): "4017 - test executemany() with pl/sql in binds and out binds" self.cursor.execute("truncate table TestTempTable") int_values = [5, 8, 17, 24, 6] str_values = ["Test 5", "Test 8", "Test 17", "Test 24", "Test 6"] out_bind = self.cursor.var(oracledb.NUMBER, arraysize=5) self.cursor.setinputsizes(None, None, out_bind) data = list(zip(int_values, str_values)) self.cursor.executemany(""" begin insert into TestTempTable (IntCol, StringCol1) values (:int_val, :str_val) returning IntCol into :out_bind; end;""", data) self.assertEqual(out_bind.values, [5, 8, 17, 24, 6]) def test_4018_executemany_pl_sql_out_bind(self): "4018 - test executemany() with pl/sql outbinds" self.cursor.execute("truncate table TestTempTable") out_bind = self.cursor.var(oracledb.NUMBER, arraysize=5) self.cursor.setinputsizes(out_bind) self.cursor.executemany(""" begin :out_var := 5; end;""", 5) self.assertEqual(out_bind.values, [5, 5, 5, 5, 5]) def test_4019_re_executemany_pl_sql_with_in_and_out_binds(self): "4019 - test re-executemany() with pl/sql in binds and out binds" int_values = [5, 8, 17, 24, 6] str_values = ["Test 5", "Test 8", "Test 17", "Test 24", "Test 6"] data = list(zip(int_values, str_values)) for i in range(2): self.cursor.execute("truncate table TestTempTable") out_bind = self.cursor.var(oracledb.NUMBER, arraysize=5) self.cursor.setinputsizes(None, None, out_bind) self.cursor.executemany(""" begin insert into TestTempTable (IntCol, StringCol1) values (:int_val, :str_val) returning IntCol into :out_bind; end;""", data) self.assertEqual(out_bind.values, [5, 8, 17, 24, 6]) def test_4020_executemany_with_plsql_single_row(self): "4020 - test PL/SQL statement with single row bind" value = 4020 var = self.cursor.var(int) data = [[var, value]] self.cursor.executemany("begin :1 := :2; end;", data) self.assertEqual(var.values, [value]) def test_4021_defer_type_assignment(self): "4021 - test deferral of type assignment" self.cursor.execute("truncate table TestTempTable") data = [(1, None), (2, 25)] self.cursor.executemany(""" insert into TestTempTable (IntCol, NumberCol) values (:1, :2)""", data) self.connection.commit() self.cursor.execute(""" select IntCol, NumberCol from TestTempTable order by IntCol""") fetched_data = self.cursor.fetchall() self.assertEqual(data, fetched_data) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_4100_cursor_callproc.py000066400000000000000000000142401434177474600232420ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 4100 - Module for testing the methods for calling stored procedures and functions (callproc() and callfunc()) """ import oracledb import test_env class TestCase(test_env.BaseTestCase): def test_4100_callproc(self): "4100 - test executing a stored procedure" var = self.cursor.var(oracledb.NUMBER) results = self.cursor.callproc("proc_Test", ("hi", 5, var)) self.assertEqual(results, ["hi", 10, 2.0]) def test_4101_callproc_all_keywords(self): "4101 - test executing a stored procedure with all args keyword args" inout_value = self.cursor.var(oracledb.NUMBER) inout_value.setvalue(0, 5) out_value = self.cursor.var(oracledb.NUMBER) kwargs = dict(a_InOutValue=inout_value, a_InValue="hi", a_OutValue=out_value) results = self.cursor.callproc("proc_Test", [], kwargs) self.assertEqual(results, []) self.assertEqual(inout_value.getvalue(), 10) self.assertEqual(out_value.getvalue(), 2.0) def test_4102_callproc_only_last_keyword(self): "4102 - test executing a stored procedure with last arg as keyword arg" out_value = self.cursor.var(oracledb.NUMBER) kwargs = dict(a_OutValue=out_value) results = self.cursor.callproc("proc_Test", ("hi", 5), kwargs) self.assertEqual(results, ["hi", 10]) self.assertEqual(out_value.getvalue(), 2.0) def test_4103_callproc_repeated_keyword_parameters(self): "4103 - test executing a stored procedure, repeated keyword arg" kwargs = dict(a_InValue="hi", a_OutValue=self.cursor.var(oracledb.NUMBER)) self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-06550:", self.cursor.callproc, "proc_Test", ("hi", 5), kwargs) def test_4104_callproc_no_args(self): "4104 - test executing a stored procedure without any arguments" results = self.cursor.callproc("proc_TestNoArgs") self.assertEqual(results, []) def test_4105_callfunc(self): "4105 - test executing a stored function" results = self.cursor.callfunc("func_Test", oracledb.NUMBER, ("hi", 5)) self.assertEqual(results, 7) def test_4106_callfunc_no_args(self): "4106 - test executing a stored function without any arguments" results = self.cursor.callfunc("func_TestNoArgs", oracledb.NUMBER) self.assertEqual(results, 712) def test_4107_callfunc_negative(self): "4107 - test executing a stored function with wrong parameters" func_name = "func_Test" self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2007:", self.cursor.callfunc, oracledb.NUMBER, func_name, ("hi", 5)) self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-06550:", self.cursor.callfunc, func_name, oracledb.NUMBER, ("hi", 5, 7)) self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2012:", self.cursor.callfunc, func_name, oracledb.NUMBER, "hi", 7) self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-06502:", self.cursor.callfunc, func_name, oracledb.NUMBER, [5, "hi"]) self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-06550:", self.cursor.callfunc, func_name, oracledb.NUMBER) self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2012:", self.cursor.callfunc, func_name, oracledb.NUMBER, 5) def test_4108_keywordParameters_deprecation(self): "4108 - test to verify keywordParameters is deprecated" out_value = self.cursor.var(oracledb.NUMBER) kwargs = dict(a_OutValue=out_value) self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2014:", self.cursor.callproc, "proc_Test", ("hi", 5), kwargs, keywordParameters=kwargs) extra_amount = self.cursor.var(oracledb.NUMBER) extra_amount.setvalue(0, 5) kwargs = dict(a_ExtraAmount=extra_amount, a_String="hi") self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2014:", self.cursor.callfunc, "func_Test", oracledb.NUMBER, [], kwargs, keywordParameters=kwargs) def test_4109_keyword_args_with_invalid_type(self): "4109 - test error for keyword args with invalid type" kwargs = [5] self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2013:", self.cursor.callproc, "proc_Test", [], kwargs) self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2013:", self.cursor.callfunc, "func_Test", oracledb.NUMBER, [], kwargs) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_4200_cursor_scrollable.py000066400000000000000000000236121434177474600235710ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 4200 - Module for testing scrollable cursors """ import unittest import oracledb import test_env @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support scrollable cursors yet") class TestCase(test_env.BaseTestCase): def test_4200_create_scrollable_cursor(self): "4200 - test creating a scrollable cursor" cursor = self.connection.cursor() self.assertEqual(cursor.scrollable, False) cursor = self.connection.cursor(True) self.assertEqual(cursor.scrollable, True) cursor = self.connection.cursor(scrollable=True) self.assertEqual(cursor.scrollable, True) cursor.scrollable = False self.assertEqual(cursor.scrollable, False) def test_4201_scroll_absolute_exception_after(self): "4201 - test scrolling absolute yields an exception (after result set)" cursor = self.connection.cursor(scrollable=True) cursor.arraysize = self.cursor.arraysize cursor.execute(""" select NumberCol from TestNumbers order by IntCol""") self.assertRaisesRegex(oracledb.DatabaseError, "^DPI-1027:", cursor.scroll, 12, "absolute") def test_4202_scroll_absolute_in_buffer(self): "4202 - test scrolling absolute (when in buffers)" cursor = self.connection.cursor(scrollable=True) cursor.arraysize = self.cursor.arraysize cursor.execute(""" select NumberCol from TestNumbers order by IntCol""") cursor.fetchmany() self.assertTrue(cursor.arraysize > 1, "array size must exceed 1 for this test to work correctly") cursor.scroll(1, mode="absolute") row = cursor.fetchone() self.assertEqual(row[0], 1.25) self.assertEqual(cursor.rowcount, 1) def test_4203_scroll_absolute_not_in_buffer(self): "4203 - test scrolling absolute (when not in buffers)" cursor = self.connection.cursor(scrollable=True) cursor.arraysize = self.cursor.arraysize cursor.execute(""" select NumberCol from TestNumbers order by IntCol""") cursor.scroll(6, mode="absolute") row = cursor.fetchone() self.assertEqual(row[0], 7.5) self.assertEqual(cursor.rowcount, 6) def test_4204_scroll_first_in_buffer(self): "4204 - test scrolling to first row in result set (in buffers)" cursor = self.connection.cursor(scrollable=True) cursor.arraysize = self.cursor.arraysize cursor.execute(""" select NumberCol from TestNumbers order by IntCol""") cursor.fetchmany() cursor.scroll(mode="first") row = cursor.fetchone() self.assertEqual(row[0], 1.25) self.assertEqual(cursor.rowcount, 1) def test_4205_scroll_first_not_in_buffer(self): "4205 - test scrolling to first row in result set (not in buffers)" cursor = self.connection.cursor(scrollable=True) cursor.arraysize = self.cursor.arraysize cursor.execute(""" select NumberCol from TestNumbers order by IntCol""") cursor.fetchmany() cursor.fetchmany() cursor.scroll(mode="first") row = cursor.fetchone() self.assertEqual(row[0], 1.25) self.assertEqual(cursor.rowcount, 1) def test_4206_scroll_last(self): "4206 - test scrolling to last row in result set" cursor = self.connection.cursor(scrollable=True) cursor.arraysize = self.cursor.arraysize cursor.execute(""" select NumberCol from TestNumbers order by IntCol""") cursor.scroll(mode="last") row = cursor.fetchone() self.assertEqual(row[0], 12.5) self.assertEqual(cursor.rowcount, 10) def test_4207_scroll_relative_exception_after(self): "4207 - test scrolling relative yields an exception (after result set)" cursor = self.connection.cursor(scrollable=True) cursor.arraysize = self.cursor.arraysize cursor.execute(""" select NumberCol from TestNumbers order by IntCol""") self.assertRaisesRegex(oracledb.DatabaseError, "^DPI-1027:", cursor.scroll, 15) def test_4208_scroll_relative_exception_before(self): "4208 - test scrolling relative yields exception (before result set)" cursor = self.connection.cursor(scrollable=True) cursor.arraysize = self.cursor.arraysize cursor.execute(""" select NumberCol from TestNumbers order by IntCol""") self.assertRaisesRegex(oracledb.DatabaseError, "^DPI-1027:", cursor.scroll, -5) def test_4209_scroll_relative_in_buffer(self): "4209 - test scrolling relative (when in buffers)" cursor = self.connection.cursor(scrollable=True) cursor.arraysize = self.cursor.arraysize cursor.execute(""" select NumberCol from TestNumbers order by IntCol""") cursor.fetchmany() message = "array size must exceed 1 for this test to work correctly" self.assertTrue(cursor.arraysize > 1, message) cursor.scroll(2 - cursor.rowcount) row = cursor.fetchone() self.assertEqual(row[0], 2.5) self.assertEqual(cursor.rowcount, 2) def test_4210_scroll_relative_not_in_buffer(self): "4210 - test scrolling relative (when not in buffers)" cursor = self.connection.cursor(scrollable=True) cursor.arraysize = self.cursor.arraysize cursor.execute(""" select NumberCol from TestNumbers order by IntCol""") cursor.fetchmany() cursor.fetchmany() message = "array size must exceed 1 for this test to work correctly" self.assertTrue(cursor.arraysize > 1, message) cursor.scroll(3 - cursor.rowcount) row = cursor.fetchone() self.assertEqual(row[0], 3.75) self.assertEqual(cursor.rowcount, 3) def test_4211_scroll_no_rows(self): "4211 - test scrolling when there are no rows" self.cursor.execute("truncate table TestTempTable") cursor = self.connection.cursor(scrollable=True) cursor.execute("select * from TestTempTable") cursor.scroll(mode="last") self.assertEqual(cursor.fetchall(), []) cursor.scroll(mode="first") self.assertEqual(cursor.fetchall(), []) self.assertRaisesRegex(oracledb.DatabaseError, "^DPI-1027:", cursor.scroll, 1, mode="absolute") def test_4212_scroll_differing_array_and_fetch_sizes(self): "4212 - test scrolling with differing array and fetch array sizes" self.cursor.execute("truncate table TestTempTable") for i in range(30): self.cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (:1, null)""", (i + 1,)) for arraysize in range(1, 6): cursor = self.connection.cursor(scrollable = True) cursor.arraysize = arraysize cursor.execute("select IntCol from TestTempTable order by IntCol") for num_rows in range(1, arraysize + 1): cursor.scroll(15, "absolute") rows = cursor.fetchmany(num_rows) self.assertEqual(rows[0][0], 15) self.assertEqual(cursor.rowcount, 15 + num_rows - 1) cursor.scroll(9) rows = cursor.fetchmany(num_rows) num_rows_fetched = len(rows) self.assertEqual(rows[0][0], 15 + num_rows + 8) self.assertEqual(cursor.rowcount, 15 + num_rows + num_rows_fetched + 7) cursor.scroll(-12) rows = cursor.fetchmany(num_rows) count = 15 + num_rows + num_rows_fetched - 5 self.assertEqual(rows[0][0], count) count = 15 + num_rows + num_rows_fetched + num_rows - 6 self.assertEqual(cursor.rowcount, count) def test_4213_scroll_with_invalid_mode(self): "4213 - test calling scroll() with invalid mode" cursor = self.connection.cursor(scrollable=True) cursor.arraysize = self.cursor.arraysize cursor.execute(""" select NumberCol from TestNumbers order by IntCol""") cursor.fetchmany() self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2009:", cursor.scroll, mode="middle") if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_4300_cursor_other.py000066400000000000000000001077671434177474600226070ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 4300 - Module for testing other cursor methods and attributes. """ import decimal import unittest import oracledb import test_env class TestCase(test_env.BaseTestCase): def test_4300_prepare(self): "4300 - test preparing a statement and executing it multiple times" self.assertEqual(self.cursor.statement, None) statement = "begin :value := :value + 5; end;" self.cursor.prepare(statement) var = self.cursor.var(oracledb.NUMBER) self.assertEqual(self.cursor.statement, statement) var.setvalue(0, 2) self.cursor.execute(None, value = var) self.assertEqual(var.getvalue(), 7) self.cursor.execute(None, value = var) self.assertEqual(var.getvalue(), 12) self.cursor.execute("begin :value2 := 3; end;", value2 = var) self.assertEqual(var.getvalue(), 3) def test_4301_exception_on_close(self): "4301 - confirm an exception is raised after closing a cursor" self.cursor.close() self.assertRaisesRegex(oracledb.InterfaceError, "^DPY-1006:", self.cursor.execute, "select 1 from dual") def test_4302_iterators(self): "4302 - test iterators" self.cursor.execute(""" select IntCol from TestNumbers where IntCol between 1 and 3 order by IntCol""") rows = [v for v, in self.cursor] self.assertEqual(rows, [1, 2, 3]) def test_4303_iterators_interrupted(self): "4303 - test iterators (with intermediate execute)" self.cursor.execute("truncate table TestTempTable") self.cursor.execute(""" select IntCol from TestNumbers where IntCol between 1 and 3 order by IntCol""") test_iter = iter(self.cursor) value, = next(test_iter) self.cursor.execute("insert into TestTempTable (IntCol) values (1)") self.assertRaisesRegex(oracledb.InterfaceError, "^DPY-1003:", next, test_iter) def test_4304_bind_names(self): "4304 - test that bindnames() works correctly." self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2002:", self.cursor.bindnames) self.cursor.prepare("begin null; end;") self.assertEqual(self.cursor.bindnames(), []) self.cursor.prepare("begin :retval := :inval + 5; end;") self.assertEqual(self.cursor.bindnames(), ["RETVAL", "INVAL"]) self.cursor.prepare("begin :retval := :a * :a + :b * :b; end;") self.assertEqual(self.cursor.bindnames(), ["RETVAL", "A", "B"]) self.cursor.prepare("begin :a := :b + :c + :d + :e + :f + :g + " + \ ":h + :i + :j + :k + :l; end;") names = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"] self.assertEqual(self.cursor.bindnames(), names) self.cursor.prepare("select :a * :a + :b * :b from dual") self.assertEqual(self.cursor.bindnames(), ["A", "B"]) self.cursor.prepare("select :value1 + :VaLue_2 from dual") self.assertEqual(self.cursor.bindnames(), ["VALUE1", "VALUE_2"]) def test_4305_set_input_sizes_negative(self): "4305 - test cursor.setinputsizes() with invalid parameters" val = decimal.Decimal(5) self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2005:", self.cursor.setinputsizes, val, x=val) self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2007:", self.cursor.setinputsizes, val) def test_4306_set_input_sizes_no_parameters(self): "4306 - test setting input sizes without any parameters" self.cursor.setinputsizes() self.cursor.execute("select :val from dual", val="Test Value") self.assertEqual(self.cursor.fetchall(), [("Test Value",)]) def test_4307_set_input_sizes_empty_dict(self): "4307 - test setting input sizes with an empty dictionary" empty_dict = {} self.cursor.prepare("select 236 from dual") self.cursor.setinputsizes(**empty_dict) self.cursor.execute(None, empty_dict) self.assertEqual(self.cursor.fetchall(), [(236,)]) def test_4308_set_input_sizes_empty_list(self): "4308 - test setting input sizes with an empty list" empty_list = [] self.cursor.prepare("select 239 from dual") self.cursor.setinputsizes(*empty_list) self.cursor.execute(None, empty_list) self.assertEqual(self.cursor.fetchall(), [(239,)]) def test_4309_set_input_sizes_by_position(self): "4309 - test setting input sizes with positional args" var = self.cursor.var(oracledb.STRING, 100) self.cursor.setinputsizes(None, 5, None, 10, None, oracledb.NUMBER) self.cursor.execute(""" begin :1 := :2 || to_char(:3) || :4 || to_char(:5) || to_char(:6); end;""", [var, 'test_', 5, '_second_', 3, 7]) self.assertEqual(var.getvalue(), "test_5_second_37") def test_4310_string_format(self): "4310 - test string format of cursor" format_string = ">" expected_value = format_string % \ (test_env.get_main_user(), test_env.get_connect_string()) self.assertEqual(str(self.cursor), expected_value) def test_4311_parse_query(self): "4311 - test parsing query statements" sql = "select LongIntCol from TestNumbers where IntCol = :val" self.cursor.parse(sql) self.assertEqual(self.cursor.statement, sql) self.assertEqual(self.cursor.description, [('LONGINTCOL', oracledb.DB_TYPE_NUMBER, 17, None, 16, 0, 0)]) def test_4312_set_output_size(self): "4312 - test cursor.setoutputsize() does not fail (but does nothing)" self.cursor.setoutputsize(100, 2) def test_4313_var_negative(self): "4313 - test cursor.var() with invalid parameters" self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2007:", self.cursor.var, 5) def test_4314_arrayvar_negative(self): "4314 - test cursor.arrayvar() with invalid parameters" self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2007:", self.cursor.arrayvar, 5, 1) def test_4315_boolean_without_plsql(self): "4315 - test binding boolean data without the use of PL/SQL" self.cursor.execute("truncate table TestTempTable") sql = "insert into TestTempTable (IntCol, StringCol1) values (:1, :2)" self.cursor.execute(sql, (False, "Value should be 0")) self.cursor.execute(sql, (True, "Value should be 1")) self.cursor.execute(""" select IntCol, StringCol1 from TestTempTable order by IntCol""") expected_value = [(0, "Value should be 0"), (1, "Value should be 1")] self.assertEqual(self.cursor.fetchall(), expected_value) def test_4316_as_context_manager(self): "4316 - test using a cursor as a context manager" with self.cursor as cursor: cursor.execute("truncate table TestTempTable") cursor.execute("select count(*) from TestTempTable") count, = cursor.fetchone() self.assertEqual(count, 0) self.assertRaisesRegex(oracledb.InterfaceError, "^DPY-1006:", self.cursor.close) def test_4317_query_row_count(self): "4317 - test that rowcount attribute is reset to zero on query execute" sql = "select * from dual where 1 = :s" self.cursor.execute(sql, [0]) self.cursor.fetchone() self.assertEqual(self.cursor.rowcount, 0) self.cursor.execute(sql, [1]) self.cursor.fetchone() self.assertEqual(self.cursor.rowcount, 1) self.cursor.execute(sql, [1]) self.cursor.fetchone() self.assertEqual(self.cursor.rowcount, 1) self.cursor.execute(sql, [0]) self.cursor.fetchone() self.assertEqual(self.cursor.rowcount, 0) def test_4318_var_type_name_none(self): "4318 - test that the typename attribute can be passed a value of None" value_to_set = 5 var = self.cursor.var(int, typename=None) var.setvalue(0, value_to_set) self.assertEqual(var.getvalue(), value_to_set) def test_4319_var_type_with_object_type(self): "4319 - test that an object type can be used as type in cursor.var()" obj_type = self.connection.gettype("UDT_OBJECT") var = self.cursor.var(obj_type) self.cursor.callproc("pkg_TestBindObject.BindObjectOut", (28, "Bind obj out", var)) obj = var.getvalue() result = self.cursor.callfunc("pkg_TestBindObject.GetStringRep", str, (obj,)) exp = "udt_Object(28, 'Bind obj out', null, null, null, null, null)" self.assertEqual(result, exp) def test_4320_fetch_xmltype(self): "4320 - test that fetching an XMLType returns a string" int_val = 5 label = "IntCol" expected_result = f"<{label}>{int_val}" self.cursor.execute(f""" select XMLElement("{label}", IntCol) from TestStrings where IntCol = :int_val""", int_val=int_val) result, = self.cursor.fetchone() self.assertEqual(result, expected_result) def test_4321_lastrowid(self): "4321 - test last rowid" # no statement executed: no rowid self.assertEqual(None, self.cursor.lastrowid) # DDL statement executed: no rowid self.cursor.execute("truncate table TestTempTable") self.assertEqual(None, self.cursor.lastrowid) # statement prepared: no rowid self.cursor.prepare("insert into TestTempTable (IntCol) values (:1)") self.assertEqual(None, self.cursor.lastrowid) # multiple rows inserted: rowid of last row inserted rows = [(n,) for n in range(225)] self.cursor.executemany(None, rows) rowid = self.cursor.lastrowid self.cursor.execute(""" select rowid from TestTempTable where IntCol = :1""", rows[-1]) self.assertEqual(rowid, self.cursor.fetchone()[0]) # statement executed but no rows updated: no rowid self.cursor.execute("delete from TestTempTable where 1 = 0") self.assertEqual(None, self.cursor.lastrowid) # stetement executed with one row updated: rowid of updated row self.cursor.execute(""" update TestTempTable set StringCol1 = 'Modified' where IntCol = :1""", rows[-2]) rowid = self.cursor.lastrowid self.cursor.execute(""" select rowid from TestTempTable where IntCol = :1""", rows[-2]) self.assertEqual(rowid, self.cursor.fetchone()[0]) # statement executed with many rows updated: rowid of last updated row self.cursor.execute(""" update TestTempTable set StringCol1 = 'Row ' || to_char(IntCol) where IntCol = :1""", rows[-3]) rowid = self.cursor.lastrowid self.cursor.execute(""" select StringCol1 from TestTempTable where rowid = :1""", [rowid]) self.assertEqual("Row %s" % rows[-3], self.cursor.fetchone()[0]) def test_4322_prefetchrows(self): "4322 - test prefetch rows" self.setup_round_trip_checker() # perform simple query and verify only one round trip is needed with self.connection.cursor() as cursor: cursor.execute("select sysdate from dual").fetchall() self.assertRoundTrips(1) # set prefetchrows to 1 and verify that two round trips are now needed with self.connection.cursor() as cursor: cursor.prefetchrows = 1 cursor.execute("select sysdate from dual").fetchall() self.assertRoundTrips(2) # simple DDL only requires a single round trip with self.connection.cursor() as cursor: cursor.execute("truncate table TestTempTable") self.assertRoundTrips(1) # array execution only requires a single round trip num_rows = 590 with self.connection.cursor() as cursor: sql = "insert into TestTempTable (IntCol) values (:1)" data = [(n + 1,) for n in range(num_rows)] cursor.executemany(sql, data) self.assertRoundTrips(1) # setting prefetch and array size to 1 requires a round-trip for each # row with self.connection.cursor() as cursor: cursor.prefetchrows = 1 cursor.arraysize = 1 cursor.execute("select IntCol from TestTempTable").fetchall() self.assertRoundTrips(num_rows + 1) # setting prefetch and array size to 300 requires 2 round-trips with self.connection.cursor() as cursor: cursor.prefetchrows = 300 cursor.arraysize = 300 cursor.execute("select IntCol from TestTempTable").fetchall() self.assertRoundTrips(2) def test_4323_existing_cursor_prefetchrows(self): "4323 - test prefetch rows using existing cursor" self.setup_round_trip_checker() # Set prefetch rows on an existing cursor num_rows = 590 with self.connection.cursor() as cursor: cursor.execute("truncate table TestTempTable") sql = "insert into TestTempTable (IntCol) values (:1)" data = [(n + 1,) for n in range(num_rows)] cursor.executemany(sql, data) cursor.prefetchrows = 300 cursor.arraysize = 300 cursor.execute("select IntCol from TestTempTable").fetchall() self.assertRoundTrips(4) def test_4324_bind_names_with_single_line_comments(self): "4324 - test bindnames() with single line comments" self.cursor.prepare("""--begin :value2 := :a + :b + :c +:a +3; end; begin :value2 := :a + :c +3; end; """) self.assertEqual(self.cursor.bindnames(), ["VALUE2", "A", "C"]) def test_4325_bind_names_with_multi_line_comments(self): "4325 - test bindnames() with multi line comments" self.cursor.prepare("""/*--select * from :a where :a = 1 select * from table_names where :a = 1*/ select * from :table_name where :value = 1 """) self.assertEqual(self.cursor.bindnames(), ["TABLE_NAME", "VALUE"]) def test_4326_bind_names_with_strings(self): "4326 - test bindnames() with strings in the statement" statement = """ begin :value := to_date('20021231 12:31:00', 'YYYYMMDD HH24:MI:SS'); end;""" self.cursor.prepare(statement) self.assertEqual(self.cursor.bindnames(), ["VALUE"]) def test_4327_parse_plsql(self): "4327 - test parsing plsql statements" sql = "begin :value := 5; end;" self.cursor.parse(sql) self.assertEqual(self.cursor.statement, sql) self.assertEqual(self.cursor.description, None) def test_4328_parse_ddl(self): "4328 - test parsing ddl statements" sql = "truncate table TestTempTable" self.cursor.parse(sql) self.assertEqual(self.cursor.statement, sql) self.assertEqual(self.cursor.description, None) def test_4329_parse_dml(self): "4329 - test parsing dml statements" sql = "insert into TestTempTable (IntCol) values (1)" self.cursor.parse(sql) self.assertEqual(self.cursor.statement, sql) self.assertEqual(self.cursor.description, None) def test_4330_encodingErrors_deprecation(self): "4330 - test to verify encodingErrors is deprecated" errors = 'strict' self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2014:", self.cursor.var, oracledb.NUMBER, encoding_errors=errors, encodingErrors=errors) def test_4331_unsupported_arrays_of_arrays(self): "4331 - test arrays of arrays not supported" simple_var = self.cursor.arrayvar(oracledb.NUMBER, 3) self.assertRaisesRegex(oracledb.NotSupportedError, "^DPY-3005:", simple_var.setvalue, 1, [1, 2, 3]) def test_4332_set_input_sizes_with_invalid_list_parameters(self): "4332 - test cursor.setinputsizes() with invalid list parameters" self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2011:", self.cursor.setinputsizes, [int, 2, 10]) def test_4333_unsupported_python_type(self): "4333 - test unsupported python type on cursor" self.assertRaisesRegex(oracledb.NotSupportedError, "^DPY-3003:", self.cursor.var, list) def test_4334_bind_by_name_with_leading_colon(self): "4334 - test binding by name with leading colon" sql = "select :arg1 from dual" params = {":arg1" : 5} self.cursor.execute(sql, params) result, = self.cursor.fetchone() self.assertEqual(result, params[":arg1"]) def test_4335_bind_out_mixed_null_not_null(self): "4335 - test binding mixed null and not null values in a PL/SQL block" sql = """ begin :1 := null; :2 := 'Value 1'; :3 := null; :4 := 'Value 2'; end;""" out_vars = [self.cursor.var(str) for i in range(4)] self.cursor.execute(sql, out_vars) values = [var.getvalue() for var in out_vars] self.assertEqual(values, [None, 'Value 1', None, 'Value 2']) def test_4336_bind_names_with_division_operators(self): "4336 - test bindnames() with multiple division operators" self.cursor.prepare(""" select :a / :b, :c / :d from dual""") self.assertEqual(self.cursor.bindnames(), ["A", "B", "C", "D"]) def test_4337_exclude_from_stmt_cache(self): "4337 - test excluding statement from statement cache" num_iters = 10 sql = "select user from dual" self.setup_parse_count_checker() # with statement cache enabled, only one parse should take place for i in range(num_iters): with self.connection.cursor() as cursor: cursor.execute(sql) self.assertParseCount(1) # with statement cache disabled for the statement, parse count should # be the same as the number of iterations for i in range(num_iters): with self.connection.cursor() as cursor: cursor.prepare(sql, cache_statement=False) cursor.execute(None) self.assertParseCount(num_iters - 1) def test_4338_bind_names_with_opening_parentheses(self): "4338 - test bindnames() with opening parentheses in statement" sql = "(select :a from dual) union (select :b from dual)" self.cursor.prepare(sql) self.assertEqual(self.cursor.bindnames(), ["A", "B"]) def test_4339_repeated_ddl(self): "4339 - test repeated DDL" self.cursor.execute("truncate table TestTempTable") self.cursor.execute("insert into TestTempTable (IntCol) values (1)") self.cursor.execute("truncate table TestTempTable") self.cursor.execute("insert into TestTempTable (IntCol) values (1)") def test_4340_sql_with_non_ascii_chars(self): "4340 - test executing SQL with non-ASCII characters" self.cursor.execute("select 'FÖÖ' from dual") result, = self.cursor.fetchone() self.assertTrue(result in ('FÖÖ', 'F¿¿')) def test_4341_unquoted_binds_case_sensitivity(self): "4341 - test case sensitivity of unquoted bind names" self.cursor.execute("select :test from dual", {"TEST": "a"}) result, = self.cursor.fetchone() self.assertEqual(result, "a") def test_4342_quoted_binds_case_sensitivity(self): "4342 - test case sensitivity of quoted bind names" sql = 'select :"test" from dual' params = {'"TEST"': "a"} self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-01036:|^DPY-4008:", self.cursor.execute, sql, params) def test_4343_reserved_keyword_as_bind_name(self): "4343 - test using a reserved keywords as a bind name" sql = 'select :ROWID from dual' self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-01745:", self.cursor.parse, sql) def test_4344_invalid_quoted_bind(self): "4344 - test using an invalid quoted bind" sql = 'select ":test" from dual' self.cursor.prepare(sql) self.assertEqual(self.cursor.bindnames(), []) def test_4345_non_ascii_bind_name(self): "4345 - test using a non-ascii character in the bind name" sql = 'select :méil$ from dual' self.cursor.prepare(sql) self.assertEqual(self.cursor.bindnames(), ['MÉIL$']) def test_4346_various_quoted_binds(self): "4346 - test various quoted bind names" self.cursor.prepare('select :"percent%" from dual') self.assertEqual(self.cursor.bindnames(), ["percent%"]) self.cursor.prepare('select :"q?marks" from dual') self.assertEqual(self.cursor.bindnames(), ["q?marks"]) self.cursor.prepare('select :"percent%(ens)yah" from dual') self.assertEqual(self.cursor.bindnames(), ["percent%(ens)yah"]) self.cursor.prepare('select :"per % cent" from dual') self.assertEqual(self.cursor.bindnames(), ["per % cent"]) self.cursor.prepare('select :"per cent" from dual') self.assertEqual(self.cursor.bindnames(), ["per cent"]) self.cursor.prepare('select :"par(ens)" from dual') self.assertEqual(self.cursor.bindnames(), ["par(ens)"]) self.cursor.prepare('select :"more/slashes" from dual') self.assertEqual(self.cursor.bindnames(), ["more/slashes"]) self.cursor.prepare('select :"%percent" from dual') self.assertEqual(self.cursor.bindnames(), ["%percent"]) self.cursor.prepare('select :"/slashes/" from dual') self.assertEqual(self.cursor.bindnames(), ["/slashes/"]) self.cursor.prepare('select :"1col:on" from dual') self.assertEqual(self.cursor.bindnames(), ["1col:on"]) self.cursor.prepare('select :"col:ons" from dual') self.assertEqual(self.cursor.bindnames(), ["col:ons"]) self.cursor.prepare('select :"more :: %colons%" from dual') self.assertEqual(self.cursor.bindnames(), ["more :: %colons%"]) self.cursor.prepare('select :"more/slashes" from dual') self.assertEqual(self.cursor.bindnames(), ["more/slashes"]) self.cursor.prepare('select :"spaces % more spaces" from dual') self.assertEqual(self.cursor.bindnames(), ["spaces % more spaces"]) self.cursor.prepare('select "col:ons", :"col:ons", :id from dual') self.assertEqual(self.cursor.bindnames(), ["col:ons", "ID"]) def test_4347_arraysize_lt_prefetchrows(self): "4347 - test array size less than prefetch rows" sql = "select 1 from dual union select 2 from dual" for i in range(2): with self.connection.cursor() as cursor: cursor.arraysize = 1 cursor.execute(sql) rows = cursor.fetchall() self.assertEqual(rows, [(1,), (2,)]) def test_4348_reexecute_query_with_blob_as_bytes(self): "4348 - test re-executing a query with blob as bytes" def type_handler(cursor, name, default_type, size, precision, scale): if default_type == oracledb.DB_TYPE_BLOB: return cursor.var(bytes, arraysize=cursor.arraysize) self.connection.outputtypehandler = type_handler blob_data = b"An arbitrary set of blob data for test case 4348" self.cursor.execute("truncate table TestBLOBs") self.cursor.execute(""" insert into TestBLOBs (IntCol, BlobCol) values (1, :data)""", [blob_data]) self.cursor.execute("select IntCol, BlobCol from TestBLOBs") self.assertEqual(self.cursor.fetchall(), [(1, blob_data)]) self.cursor.execute("truncate table TestBLOBs") self.cursor.execute(""" insert into TestBLOBs (IntCol, BlobCol) values (1, :data)""", [blob_data]) self.cursor.execute("select IntCol, BlobCol from TestBLOBs") self.assertEqual(self.cursor.fetchall(), [(1, blob_data)]) def test_4349_test_sql_with_quoted_identifiers_and_strings(self): "4349 - test parsing sql contaiting quoted identifiers and strings" sql = 'select "_value1" + : "VaLue_2" + :"3VALUE" from dual' self.cursor.prepare(sql) self.assertEqual(self.cursor.bindnames(), ["VaLue_2", "3VALUE"]) def test_4350_reexecute_after_error(self): "4350 - test re-executing a statement after raising an error" sql = "select * from TestFakeTable" self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-00942:", self.cursor.execute, sql) self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-00942:", self.cursor.execute, sql) sql = "insert into TestStrings (StringCol) values (NULL)" self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-01400:", self.cursor.execute, sql) self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-01400:", self.cursor.execute, sql) def test_4351_variable_not_in_select_list(self): "4351 - test executing a statement that raises ORA-01007" with self.connection.cursor() as cursor: cursor.execute(""" create or replace view ora_1007 as select 1 as SampleNumber, 'String' as SampleString, 'Another String' as AnotherString from dual""") with self.connection.cursor() as cursor: cursor.execute("select * from ora_1007") self.assertEqual(cursor.fetchone(), (1, 'String', 'Another String')) with self.connection.cursor() as cursor: cursor.execute(""" create or replace view ora_1007 as select 1 as SampleNumber, 'Another String' as AnotherString from dual""") with self.connection.cursor() as cursor: cursor.execute("select * from ora_1007") self.assertEqual(cursor.fetchone(), (1, 'Another String')) def test_4352_update_empty_row(self): "4352 - test updating an empty row" int_var = self.cursor.var(int) self.cursor.execute("truncate table TestTempTable") sql = """ begin update TestTempTable set IntCol = :1 where StringCol1 = :2 returning IntCol into :3; end;""" self.cursor.execute(sql, [1, "test string 4352", int_var]) self.assertEqual(int_var.values, [None]) def test_4353_single_quoted_strings(self): "4353 - test bindnames with statement containing strings" sql = '''select '"string_1"', :bind_1, 'string_2' from dual''' self.cursor.prepare(sql) self.assertEqual(self.cursor.bindnames(), ['BIND_1']) def test_4354_fetch_duplicate_data_twice(self): "4354 - fetch duplicate data from query in statement cache" sql = """ select 'A', 'B', 'C' from dual union all select 'A', 'B', 'C' from dual union all select 'A', 'B', 'C' from dual """ expected_data = [('A', 'B', 'C')] * 3 with self.connection.cursor() as cursor: cursor.prefetchrows = 0 cursor.execute(sql) self.assertEqual(cursor.fetchall(), expected_data) with self.connection.cursor() as cursor: cursor.prefetchrows = 0 cursor.execute(sql) self.assertEqual(cursor.fetchall(), expected_data) def test_4355_fetch_duplicate_data_with_out_converter(self): "4355 - fetch duplicate data with outconverter" def out_converter(value): self.assertIs(type(value), str) return int(value) def type_handler(cursor, name, default_type, size, precision, scale): if name == "COL_3": return cursor.var(str, arraysize=cursor.arraysize, outconverter=out_converter) self.cursor.outputtypehandler = type_handler self.cursor.execute(""" select 'A' as col_1, 2 as col_2, 3 as col_3 from dual union all select 'A' as col_1, 2 as col_2, 3 as col_3 from dual union all select 'A' as col_1, 2 as col_2, 3 as col_3 from dual""") expected_data = [('A', 2, 3)] * 3 self.assertEqual(self.cursor.fetchall(), expected_data) def test_4356_multiple_single_quoted_strings(self): "4356 - test bindnames with statement containing quoted strings" sql = "select :bind_4356, 'string_4356', ':string_4356' from dual" self.cursor.prepare(sql) self.assertEqual(self.cursor.bindnames(), ['BIND_4356']) def test_4357_setinputsizes_with_defaults(self): "4357 - test setinputsizes() with defaults specified" self.cursor.setinputsizes(None, str) self.assertIs(self.cursor.bindvars[0], None) self.assertIsInstance(self.cursor.bindvars[1], oracledb.Var) self.cursor.setinputsizes(a=None, b=str) self.assertIs(self.cursor.bindvars.get("a"), None) self.assertIsInstance(self.cursor.bindvars["b"], oracledb.Var) def test_4358_kill_conn_with_open_cursor(self): "4538 - kill connection with open cursor" admin_conn = test_env.get_admin_connection() conn = test_env.get_connection() self.assertEqual(conn.is_healthy(), True) cursor = conn.cursor() cursor.execute(""" select dbms_debug_jdwp.current_session_id, dbms_debug_jdwp.current_session_serial from dual""") sid, serial = cursor.fetchone() with admin_conn.cursor() as admin_cursor: sql = f"alter system kill session '{sid},{serial}'" admin_cursor.execute(sql) self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4011:", cursor.execute, "select user from dual") self.assertFalse(conn.is_healthy()) def test_4359_kill_conn_in_context_manager(self): "4359 - kill connection in cursor context manager" admin_conn = test_env.get_admin_connection() conn = test_env.get_connection() self.assertEqual(conn.is_healthy(), True) with conn.cursor() as cursor: cursor.execute(""" select dbms_debug_jdwp.current_session_id, dbms_debug_jdwp.current_session_serial from dual""") sid, serial = cursor.fetchone() with admin_conn.cursor() as admin_cursor: sql = f"alter system kill session '{sid},{serial}'" admin_cursor.execute(sql) self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4011:", cursor.execute, "select user from dual") self.assertEqual(conn.is_healthy(), False) def test_4360_fetchmany(self): "4360 - fetchmany() with and without parameters" sql_part = "select user from dual" sql = " union all ".join([sql_part] * 10) with self.connection.cursor() as cursor: cursor.arraysize = 6 cursor.execute(sql) rows = cursor.fetchmany() self.assertEqual(len(rows), cursor.arraysize) cursor.execute(sql) rows = cursor.fetchmany(size=2) self.assertEqual(len(rows), 2) cursor.execute(sql) rows = cursor.fetchmany(numRows=4) self.assertEqual(len(rows), 4) cursor.execute(sql) self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-2014:", cursor.fetchmany, size=2, numRows=4) def test_4361_rowcount_after_close(self): "4361 - access cursor.rowcount after closing cursor" with self.connection.cursor() as cursor: cursor.execute("select user from dual") cursor.fetchall() self.assertEqual(cursor.rowcount, 1) self.assertEqual(cursor.rowcount, -1) def test_4362_change_of_bind_type_with_define(self): "4362 - changing bind type with define needed" self.cursor.execute("truncate table TestClobs") row_for_1 = (1, "Short value 1") row_for_56 = (56, "Short value 56") for data in (row_for_1, row_for_56): self.cursor.execute(""" insert into TestClobs (IntCol, ClobCol) values (:1, :2)""", data) sql = "select IntCol, ClobCol from TestClobs where IntCol = :int_col" with test_env.FetchLobsContextManager(False): self.cursor.execute(sql, int_col="1") self.assertEqual(self.cursor.fetchone(), row_for_1) self.cursor.execute(sql, int_col="56") self.assertEqual(self.cursor.fetchone(), row_for_56) self.cursor.execute(sql, int_col=1) self.assertEqual(self.cursor.fetchone(), row_for_1) def test_4363_multiple_parse(self): "4363 - test calling cursor.parse() twice with the same statement" self.cursor.execute("truncate table TestTempTable") data = (4363, "Value for test 4363") self.cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (:1, :2)""", data) sql = "update TestTempTable set StringCol1 = :v where IntCol = :i" for i in range(2): self.cursor.parse(sql) self.cursor.execute(sql, ("Updated value", data[0])) def test_4364_binds_between_comment_blocks(self): "4364 - test bindnames() for bind variables between comment blocks" self.cursor.prepare(""" select /* comment 1 */ :a, /* comment 2 */ :b /* comment 3 */ from dual""") self.assertEqual(self.cursor.bindnames(), ["A", "B"]) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_4400_tpc.py000066400000000000000000000121201434177474600206320ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 4400 - Module for testing TPC (two-phase commit) transactions. """ import unittest import oracledb import test_env @unittest.skipIf(test_env.get_is_thin(), "thin mode doesn't support two-phase commit yet") class TestCase(test_env.BaseTestCase): def test_4400_tpc_with_rolback(self): "4400 - test begin, prepare, roll back global transaction" self.cursor.execute("truncate table TestTempTable") xid = self.connection.xid(3900, "txn3900", "branchId") self.connection.tpc_begin(xid) self.assertEqual(self.connection.tpc_prepare(), False) self.connection.tpc_begin(xid) self.cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (1, 'tesName')""") self.assertEqual(self.connection.tpc_prepare(), True) self.connection.tpc_rollback() self.cursor.execute("select count(*) from TestTempTable") count, = self.cursor.fetchone() self.assertEqual(count, 0) def test_4401_tpc_with_commit(self): "4401 - test begin, prepare, commit global transaction" self.cursor.execute("truncate table TestTempTable") xid = self.connection.xid(3901, "txn3901", "branchId") self.connection.tpc_begin(xid) self.cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (1, 'tesName')""") self.assertEqual(self.connection.tpc_prepare(), True) self.connection.tpc_commit() self.cursor.execute("select IntCol, StringCol1 from TestTempTable") self.assertEqual(self.cursor.fetchall(), [(1, 'tesName')]) def test_4402_tpc_multiple_transactions(self): "4402 - test multiple global transactions on the same connection" self.cursor.execute("truncate table TestTempTable") xid1 = self.connection.xid(3902, "txn3902", "branch1") xid2 = self.connection.xid(3902, "txn3902", "branch2") self.connection.tpc_begin(xid1) self.cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (1, 'tesName')""") self.connection.tpc_end() self.connection.tpc_begin(xid2) self.cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (2, 'tesName')""") self.connection.tpc_end() needs_commit1 = self.connection.tpc_prepare(xid1) needs_commit2 = self.connection.tpc_prepare(xid2) if needs_commit1: self.connection.tpc_commit(xid1) if needs_commit2: self.connection.tpc_commit(xid2) self.cursor.execute(""" select IntCol, StringCol1 from TestTempTable order by IntCol""") expected_rows = [(1, 'tesName'), (2, 'tesName')] self.assertEqual(self.cursor.fetchall(), expected_rows) def test_4403_rollback_with_xid(self): "4403 - test rollback with parameter xid" self.cursor.execute("truncate table TestTempTable") xid1 = self.connection.xid(3901, "txn3901", "branch1") xid2 = self.connection.xid(3902, "txn3902", "branch2") for count, xid in enumerate([xid1, xid2]): self.connection.tpc_begin(xid) self.cursor.execute(""" insert into TestTempTable (IntCol, StringCol1) values (:id, 'tesName')""", id=count) self.connection.tpc_end() self.connection.tpc_rollback(xid1) self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-24756", self.connection.tpc_prepare, xid1) needs_commit = self.connection.tpc_prepare(xid2) if needs_commit: self.connection.tpc_commit(xid2) self.cursor.execute(""" select IntCol, StringCol1 from TestTempTable order by IntCol""") self.assertEqual(self.cursor.fetchall(), [(1, 'tesName')]) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_4500_connect_params.py000066400000000000000000000712331434177474600230530ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 4500 - Module for testing connection parameters. """ import os import tempfile import oracledb import test_env class TestCase(test_env.BaseTestCase): requires_connection = False def __test_writable_parameter(self, name, value): """ Tests that a writable parameter can be written to and the modified value read back successfully. """ params = oracledb.ConnectParams() orig_value = getattr(params, name) copied_params = params.copy() args = {} args[name] = value params.set(**args) self.assertEqual(getattr(params, name), value) self.assertEqual(getattr(copied_params, name), orig_value) def test_4500_simple_easy_connect_with_port(self): "4500 - test simple EasyConnect string parsing with port specified" params = oracledb.ConnectParams() params.parse_connect_string("my_host:1578/my_service_name") self.assertEqual(params.host, "my_host") self.assertEqual(params.port, 1578) self.assertEqual(params.service_name, "my_service_name") def test_4501_simple_easy_connect_without_port(self): "4501 - test simple Easy Connect string parsing with no port specified" params = oracledb.ConnectParams() params.parse_connect_string("my_host2/my_service_name2") self.assertEqual(params.host, "my_host2") self.assertEqual(params.port, 1521) self.assertEqual(params.service_name, "my_service_name2") def test_4502_simple_easy_connect_with_server_type(self): "4502 - test simple EasyConnect string parsing with DRCP enabled" params = oracledb.ConnectParams() params.parse_connect_string("my_host3.org/my_service_name3:pooled") self.assertEqual(params.host, "my_host3.org") self.assertEqual(params.service_name, "my_service_name3") self.assertEqual(params.server_type, "pooled") params.parse_connect_string("my_host3/my_service_name3:ShArEd") self.assertEqual(params.server_type, "shared") def test_4503_simple_connect_descriptor(self): "4503 - test simple name-value pair format connect string" connect_string = \ "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=my_host4)(PORT=1589))" \ "(CONNECT_DATA=(SERVICE_NAME=my_service_name4)))" params = oracledb.ConnectParams() params.parse_connect_string(connect_string) self.assertEqual(params.host, "my_host4") self.assertEqual(params.port, 1589) self.assertEqual(params.service_name, "my_service_name4") def test_4504_search_tnsnames(self): "4504 - test simple tnsnames entry" connect_string = \ "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=my_host5)(PORT=1624))" \ "(CONNECT_DATA=(SERVICE_NAME=my_service_name5)))" alias = "tns_alias = " + connect_string with tempfile.TemporaryDirectory() as temp_dir: file_name = os.path.join(temp_dir, "tnsnames.ora") with open(file_name, "w") as f: f.write(alias) params = oracledb.ConnectParams(config_dir=temp_dir) params.parse_connect_string("tns_alias") self.assertEqual(params.host, "my_host5") self.assertEqual(params.port, 1624) self.assertEqual(params.service_name, "my_service_name5") def test_4505_easy_connect_with_protocol(self): "4505 - test EasyConnect with protocol" params = oracledb.ConnectParams() params.parse_connect_string("tcps://my_host6/my_service_name6") self.assertEqual(params.host, "my_host6") self.assertEqual(params.service_name, "my_service_name6") self.assertEqual(params.protocol, "tcps") def test_4506_easy_connect_with_invalid_protocol(self): "4506 - test EasyConnect with invalid protocol" params = oracledb.ConnectParams() self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4021:", params.parse_connect_string, "invalid_proto://my_host7/my_service_name7") def test_4507_exception_on_ipc_protocol(self): "4507 - confirm an exception is raised if using ipc protocol" connect_string = \ "(DESCRIPTION=(ADDRESS=(PROTOCOL=ipc)(KEY=my_view8))" \ "(CONNECT_DATA=(SERVICE_NAME=my_service_name8)))" params = oracledb.ConnectParams() self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4021:", params.parse_connect_string, connect_string) def test_4508_retry_count_and_delay(self): "4508 - connect descriptor with retry count and retry delay" connect_string = \ "(DESCRIPTION=(RETRY_COUNT=6)(RETRY_DELAY=5)" \ "(ADDRESS=(PROTOCOL=TCP)(HOST=my_host9)(PORT=1593))" \ "(CONNECT_DATA=(SERVICE_NAME=my_service_name9)))" params = oracledb.ConnectParams() params.parse_connect_string(connect_string) self.assertEqual(params.retry_count, 6) self.assertEqual(params.retry_delay, 5) def test_4509_connect_descriptor_expire_time(self): "4509 - connect descriptor with expire_time setting" connect_string = \ "(DESCRIPTION=(EXPIRE_TIME=12)" \ "(ADDRESS=(PROTOCOL=TCP)(HOST=my_host11)(PORT=1594))" \ "(CONNECT_DATA=(SERVICE_NAME=my_service_name11)))" params = oracledb.ConnectParams() params.parse_connect_string(connect_string) self.assertEqual(params.expire_time, 12) def test_4510_pool_parameters(self): "4510 - connect descriptor with pool parameters" connect_string = \ "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=my_host12)(PORT=694))" \ "(CONNECT_DATA=(SERVICE_NAME=my_service_name12)" \ "(POOL_CONNECTION_CLASS=cclass_12)(POOL_PURITY=SELF)))" params = oracledb.ConnectParams() params.parse_connect_string(connect_string) self.assertEqual(params.cclass, "cclass_12") self.assertEqual(params.purity, oracledb.PURITY_SELF) connect_string = connect_string.replace("SELF", "NEW") params.parse_connect_string(connect_string) self.assertEqual(params.purity, oracledb.PURITY_NEW) def test_4511_invalid_pool_purity(self): "4511 - connect descriptor with invalid pool purity" connect_string = \ "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=my_host13)(PORT=695))" \ "(CONNECT_DATA=(SERVICE_NAME=my_service_name13)" \ "(POOL_CONNECTION_CLASS=cclass_13)(POOL_PURITY=INVALID)))" params = oracledb.ConnectParams() self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4022:", params.parse_connect_string, connect_string) def test_4512_tcp_connect_timeout(self): "4512 - connect descriptor with transport connect timeout values" connect_string = \ "(DESCRIPTION=(TRANSPORT_CONNECT_TIMEOUT=500 ms)" \ "(ADDRESS=(PROTOCOL=TCP)(HOST=my_host14)(PORT=695))" \ "(CONNECT_DATA=(SERVICE_NAME=my_service_name14)))" params = oracledb.ConnectParams() params.parse_connect_string(connect_string) self.assertEqual(params.tcp_connect_timeout, 0.5) connect_string = connect_string.replace("500 ms", "15 SEC") params.parse_connect_string(connect_string) self.assertEqual(params.tcp_connect_timeout, 15) connect_string = connect_string.replace("15 SEC", "5 min") params.parse_connect_string(connect_string) self.assertEqual(params.tcp_connect_timeout, 300) connect_string = connect_string.replace("5 min", "34") params.parse_connect_string(connect_string) self.assertEqual(params.tcp_connect_timeout, 34) def test_4513_easy_connect_without_service_name(self): "4513 - test EasyConnect string parsing with no service name specified" params = oracledb.ConnectParams() params.parse_connect_string("my_host15:1578/") self.assertEqual(params.host, "my_host15") self.assertEqual(params.port, 1578) self.assertEqual(params.service_name, "") def test_4514_missing_entry_in_tnsnames(self): "4514 - test missing entry in tnsnames" with tempfile.TemporaryDirectory() as temp_dir: params = oracledb.ConnectParams(config_dir=temp_dir) file_name = os.path.join(temp_dir, "tnsnames.ora") with open(file_name, "w") as f: f.write("# no entries") self.assertRaisesRegex(oracledb.DatabaseError, "DPY-4000", params.parse_connect_string, "tns_alias") def test_4515_easy_connect_with_port_missing(self): "4515 - test EasyConnect string parsing with port value missing" params = oracledb.ConnectParams() params.parse_connect_string("my_host17:/my_service_name17") self.assertEqual(params.host, "my_host17") self.assertEqual(params.port, 1521) self.assertEqual(params.service_name, "my_service_name17") def test_4516_invalid_number_in_connect_string(self): "4516 - test connect descriptor with invalid number" params = oracledb.ConnectParams() connect_string = \ "(DESCRIPTION=(RETRY_COUNT=wrong)(RETRY_DELAY=5)" \ "(ADDRESS=(PROTOCOL=TCP)(HOST=my_host18)(PORT=1598))" \ "(CONNECT_DATA=(SERVICE_NAME=my_service_name18)))" self.assertRaisesRegex(oracledb.DatabaseError, "DPY-4018", params.parse_connect_string, connect_string) def test_4517_security_options(self): "4517 - test connect descriptor with security options" options = [ ("CN=unknown19a", "/tmp/wallet_loc19a", "On", True), ("CN=unknown19b", "/tmp/wallet_loc19b", "False", False), ("CN=unknown19c", "/tmp/wallet_loc19c", "Off", False), ("CN=unknown19d", "/tmp/wallet_loc19d", "True", True), ("CN=unknown19e", "/tmp/wallet_loc19e", "yes", True), ("CN=unknown19f", "/tmp/wallet_loc19f", "no", False) ] for dn, wallet_loc, match_option, match_value in options: params = oracledb.ConnectParams() connect_string = f""" (DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=my_host19)(PORT=872)) (CONNECT_DATA=(SERVICE_NAME=my_service_name19)) (SECURITY=(SSL_SERVER_CERT_DN="{dn}") (SSL_SERVER_DN_MATCH={match_option}) (MY_WALLET_DIRECTORY="{wallet_loc}")))""" params.parse_connect_string(connect_string) self.assertEqual(params.ssl_server_cert_dn, dn) self.assertEqual(params.wallet_location, wallet_loc) self.assertEqual(params.ssl_server_dn_match, match_value) def test_4518_easy_connect_security_options(self): "4518 - test easy connect string with security options" options = [ ("CN=unknown20a", "/tmp/wallet_loc20a", "On", True), ("CN=unknown20b", "/tmp/wallet_loc20b", "False", False), ("CN=unknown20c", "/tmp/wallet_loc20c", "Off", False), ("CN=unknown20d", "/tmp/wallet_loc20d", "True", True), ("CN=unknown20e", "/tmp/wallet_loc20e", "yes", True), ("CN=unknown20f", "/tmp/wallet_loc20f", "no", False) ] for dn, wallet_loc, match_option, match_value in options: params = oracledb.ConnectParams() connect_string = f''' my_host20/my_server_name20? ssl_server_cert_dn="{dn}"& ssl_server_dn_match= {match_option} & wallet_location = "{wallet_loc}"''' params = oracledb.ConnectParams() params.parse_connect_string(connect_string) self.assertEqual(params.ssl_server_cert_dn, dn) self.assertEqual(params.ssl_server_dn_match, match_value) self.assertEqual(params.wallet_location, wallet_loc) def test_4519_easy_connect_description_options(self): "4519 - test easy connect string with description options" params = oracledb.ConnectParams() connect_string = 'my_host21/my_server_name21?' \ 'expire_time=5&' \ 'retry_delay=10&' \ 'retry_count=12&' \ 'transport_connect_timeout=2.5' params.parse_connect_string(connect_string) self.assertEqual(params.expire_time, 5) self.assertEqual(params.retry_delay, 10) self.assertEqual(params.retry_count, 12) self.assertEqual(params.tcp_connect_timeout, 2.5) def test_4520_easy_connect_invalid_parameters(self): "4520 - test easy connect string with invalid parameters" params = oracledb.ConnectParams() connect_string_prefix = 'my_host22/my_server_name22?' suffixes = [ 'expire_time=invalid', 'expire_time' ] for suffix in suffixes: self.assertRaisesRegex(oracledb.DatabaseError, "DPY-4018", params.parse_connect_string, connect_string_prefix + suffix) def test_4521_connect_string_with_newlines_and_spaces(self): "4521 - test connect string containing spaces and newlines" params = oracledb.ConnectParams() connect_string = \ '(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP) \n(HOST=my_host23)\n' \ '(PORT=1560))(CONNECT_DATA= (SERVICE_NAME=my_service_name23))' \ '(SECURITY=(MY_WALLET_DIRECTORY="my wallet dir 23")))' params.parse_connect_string(connect_string) self.assertEqual(params.host, "my_host23") self.assertEqual(params.port, 1560) self.assertEqual(params.service_name, "my_service_name23") self.assertEqual(params.wallet_location, "my wallet dir 23") def test_4522_missing_tnsnames(self): "4522 - test missing tnsnames.ora in configuration directory" with tempfile.TemporaryDirectory() as temp_dir: params = oracledb.ConnectParams(config_dir=temp_dir) self.assertRaisesRegex(oracledb.DatabaseError, "DPY-4026:", params.parse_connect_string, "tns_alias") def test_4523_missing_config_dir(self): "4523 - test missing configuration directory" params = oracledb.ConnectParams(config_dir="/missing") self.assertRaisesRegex(oracledb.DatabaseError, "DPY-4026:", params.parse_connect_string, "tns_alias") def test_4524_invalid_entries_in_tnsnames(self): "4524 - test tnsnames.ora with invalid entries" connect_string = \ "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=my_host24)(PORT=1148))" \ "(CONNECT_DATA=(SERVICE_NAME=my_service_name24)))" alias = f"tns_alias24 = {connect_string}" with tempfile.TemporaryDirectory() as temp_dir: file_name = os.path.join(temp_dir, "tnsnames.ora") with open(file_name, "w") as f: print("invalid_alias = something to ignore", file=f) print("some garbage data which should be ignored", file=f) print(alias, file=f) params = oracledb.ConnectParams(config_dir=temp_dir) params.parse_connect_string("tns_alias24") self.assertEqual(params.host, "my_host24") self.assertEqual(params.port, 1148) self.assertEqual(params.service_name, "my_service_name24") def test_4525_single_address_list(self): "4525 - test connect string with an address list" params = oracledb.ConnectParams() connect_string = \ '(DESCRIPTION=(LOAD_BALANCE=ON)(RETRY_COUNT=5)(RETRY_DELAY=2)' \ '(ADDRESS_LIST=(LOAD_BALANCE=ON)' \ '(ADDRESS=(PROTOCOL=tcp)(PORT=1521)(HOST=my_host25))' \ '(ADDRESS=(PROTOCOL=tcps)(PORT=222)(HOST=my_host26)))' \ '(CONNECT_DATA=(SERVICE_NAME=my_service_name25)))' params.parse_connect_string(connect_string) self.assertEqual(params.host, ["my_host25", "my_host26"]) self.assertEqual(params.port, [1521, 222]) self.assertEqual(params.protocol, ["tcp", "tcps"]) self.assertEqual(params.service_name, "my_service_name25") self.assertEqual(params.retry_count, 5) self.assertEqual(params.retry_delay, 2) def test_4526_multiple_address_lists(self): "4526 - test connect string with multiple address lists" params = oracledb.ConnectParams() connect_string = \ '(DESCRIPTION=(LOAD_BALANCE=ON)(RETRY_COUNT=5)(RETRY_DELAY=2)' \ '(ADDRESS_LIST=(LOAD_BALANCE=ON)' \ '(ADDRESS=(PROTOCOL=tcp)(PORT=1521)(HOST=my_host26))' \ '(ADDRESS=(PROTOCOL=tcp)(PORT=222)(HOST=my_host27)))' \ '(ADDRESS_LIST=(LOAD_BALANCE=ON)' \ '(ADDRESS=(PROTOCOL=tcps)(PORT=5555)(HOST=my_host28))' \ '(ADDRESS=(PROTOCOL=tcps)(PORT=444)(HOST=my_host29)))' \ '(CONNECT_DATA=(SERVICE_NAME=my_service_name26)))' params.parse_connect_string(connect_string) hosts = ["my_host26", "my_host27", "my_host28", "my_host29"] self.assertEqual(params.host, hosts) self.assertEqual(params.port, [1521, 222, 5555, 444]) self.assertEqual(params.protocol, ["tcp", "tcp", "tcps", "tcps"]) self.assertEqual(params.service_name, "my_service_name26") self.assertEqual(params.retry_count, 5) self.assertEqual(params.retry_delay, 2) def test_4527_multiple_descriptions(self): "4527 - test connect string with multiple descriptions" params = oracledb.ConnectParams() connect_string = \ '(DESCRIPTION_LIST=(FAIL_OVER=ON)(LOAD_BALANCE=OFF)' \ '(DESCRIPTION=(LOAD_BALANCE=OFF)(RETRY_COUNT=1)(RETRY_DELAY=1)' \ '(ADDRESS_LIST=(ADDRESS=(PROTOCOL=tcp)(PORT=5001)' \ '(HOST=my_host30))' \ '(ADDRESS=(PROTOCOL=tcp)(PORT=1521)(HOST=my_host31)))' \ '(ADDRESS_LIST=(ADDRESS=(PROTOCOL=tcp)(PORT=5002) ' \ '(HOST=my_host32))' \ '(ADDRESS=(PROTOCOL=tcp)(PORT=5003)(HOST=my_host33)))' \ '(CONNECT_DATA=(SERVICE_NAME=my_service_name27)))' \ '(DESCRIPTION=(LOAD_BALANCE=OFF)(RETRY_COUNT=2)(RETRY_DELAY=3)' \ '(ADDRESS_LIST = (ADDRESS=(PROTOCOL=tcp)(PORT=5001)' \ '(HOST=my_host34))' \ '(ADDRESS=(PROTOCOL=tcp)(PORT=5001)(HOST=my_host35)))' \ '(ADDRESS_LIST=(ADDRESS=(PROTOCOL=tcp)(PORT=5001)' \ '(HOST=my_host36))' \ '(ADDRESS=(PROTOCOL=tcps)(HOST=my_host37)(PORT=1521)))' \ '(CONNECT_DATA=(SERVICE_NAME=my_service_name28))))' params.parse_connect_string(connect_string) hosts = ["my_host30", "my_host31", "my_host32", "my_host33", \ "my_host34", "my_host35", "my_host36", "my_host37"] ports = [5001, 1521, 5002, 5003, 5001, 5001, 5001, 1521] protocols = ["tcp", "tcp", "tcp", "tcp", "tcp", "tcp", "tcp", "tcps"] service_names = ["my_service_name27", "my_service_name28"] self.assertEqual(params.host, hosts) self.assertEqual(params.port, ports) self.assertEqual(params.protocol, protocols) self.assertEqual(params.service_name, service_names) self.assertEqual(params.retry_count, [1, 2]) self.assertEqual(params.retry_delay, [1, 3]) def test_4528_https_proxy(self): "4528 - test connect strings with https_proxy defined" params = oracledb.ConnectParams() connect_string = \ "(DESCRIPTION=" \ "(ADDRESS=(HTTPS_PROXY=proxy_4528a)(HTTPS_PROXY_PORT=4528)" \ "(PROTOCOL=TCP)(HOST=my_host4528a)(PORT=8528))" \ "(CONNECT_DATA=(SERVICE_NAME=my_service_name4528a)))" params.parse_connect_string(connect_string) self.assertEqual(params.https_proxy, "proxy_4528a") self.assertEqual(params.https_proxy_port, 4528) connect_string = "tcps://my_host_4528b/my_service_name_4528b?" \ "https_proxy=proxy_4528b&https_proxy_port=9528" params.parse_connect_string(connect_string) self.assertEqual(params.https_proxy, "proxy_4528b") self.assertEqual(params.https_proxy_port, 9528) def test_4529_server_type(self): "4529 - test connect strings with server_type defined" params = oracledb.ConnectParams() connect_string = \ "(DESCRIPTION=" \ "(ADDRESS=(PROTOCOL=TCP)(HOST=my_host4529)(PORT=4529))" \ "(CONNECT_DATA=(SERVER=DEDICATED)" \ "(SERVICE_NAME=my_service_name4529)))" params.parse_connect_string(connect_string) self.assertEqual(params.server_type, "dedicated") connect_string = connect_string.replace("DEDICATED", "INVALID") self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4028:", params.parse_connect_string, connect_string) def test_4530_writable_params(self): "4530 - test writable parameters" self.__test_writable_parameter("appcontext", [("a", "b", "c")]) self.__test_writable_parameter("config_dir", "config_dir_4530") self.__test_writable_parameter("disable_oob", True) self.__test_writable_parameter("edition", "edition_4530") self.__test_writable_parameter("events", True) self.__test_writable_parameter("matchanytag", True) self.__test_writable_parameter("mode", oracledb.AUTH_MODE_SYSDBA) self.__test_writable_parameter("shardingkey", [1, 2, 3]) self.__test_writable_parameter("stmtcachesize", 25) self.__test_writable_parameter("supershardingkey", [1, 2, 3]) self.__test_writable_parameter("tag", "tag_4530") self.__test_writable_parameter("debug_jdwp", "host=host;port=4530") self.__test_writable_parameter("externalauth", True) def test_4531_build_connect_string_with_tcp_connect_timeout(self): "4531 - test building connect string with TCP connect timeout" host = "my_host4531" service_name = "my_service4531" options = [ (25, "25"), (120, "2min"), (2.5, "2500ms"), (3.4328, "3432ms") ] for in_val, out_val in options: params = oracledb.ConnectParams(host=host, service_name=service_name, tcp_connect_timeout=in_val) tcp_timeout_val = f"(TRANSPORT_CONNECT_TIMEOUT={out_val})" connect_string = f"(DESCRIPTION={tcp_timeout_val}" + \ f"(ADDRESS_LIST=(ADDRESS=(PROTOCOL=tcp)" + \ f"(HOST={host})(PORT=1521)))(CONNECT_DATA=" + \ f"(SERVICE_NAME={service_name}))" + \ f"(SECURITY=(SSL_SERVER_DN_MATCH=ON)))" self.assertEqual(params.get_connect_string(), connect_string) def test_4532_multiple_alias_entry_tnsnames(self): "4532 - test tnsnames.ora with multiple aliases on one line" connect_string = \ "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=my_host32)(PORT=1132))" \ "(CONNECT_DATA=(SERVICE_NAME=my_service_name32)))" aliases = f"tns_alias32a,tns_alias32b = {connect_string}" with tempfile.TemporaryDirectory() as temp_dir: file_name = os.path.join(temp_dir, "tnsnames.ora") with open(file_name, "w") as f: print(aliases, file=f) params = oracledb.ConnectParams(config_dir=temp_dir) for name in ("tns_alias32a", "tns_alias32b"): params.parse_connect_string(name) self.assertEqual(params.host, "my_host32") self.assertEqual(params.port, 1132) self.assertEqual(params.service_name, "my_service_name32") def test_4533_easy_connect_with_pool_parameters(self): "4533 - test EasyConnect with pool parameters" options = [ ("cclass_33a", "self", oracledb.PURITY_SELF), ("cclass_33b", "new", oracledb.PURITY_NEW) ] for cclass, purity_str, purity_int in options: connect_string = f"my_host_33/my_service_name_33:pooled?" \ f"pool_connection_class={cclass}&" \ f"pool_purity={purity_str}" params = oracledb.ConnectParams() params.parse_connect_string(connect_string) self.assertEqual(params.host, "my_host_33") self.assertEqual(params.service_name, "my_service_name_33") self.assertEqual(params.port, 1521) self.assertEqual(params.server_type, "pooled") self.assertEqual(params.cclass, cclass) self.assertEqual(params.purity, purity_int) def test_4534_connect_descriptor_small_container_first(self): "4534 - test connect descriptor with different containers (small 1st)" connect_string = """ (DESCRIPTION= (ADDRESS=(PROTOCOL=tcp)(HOST=host1)(PORT=1521)) (ADDRESS_LIST= (ADDRESS=(PROTOCOL=tcp)(HOST=host2a)(PORT=1522)) (ADDRESS=(PROTOCOL=tcp)(HOST=host2b)(PORT=1523))) (ADDRESS=(PROTOCOL=tcp)(HOST=host3)(PORT=1524)) (CONNECT_DATA=(SERVICE_NAME=my_service_34)) )""" params = oracledb.ConnectParams() params.parse_connect_string(connect_string) self.assertEqual(params.host, ["host1", "host2a", "host2b", "host3"]) def test_4535_connect_descriptor_small_container_second(self): "4535 - test connect descriptor with different containers (small 2nd)" connect_string = """ (DESCRIPTION= (ADDRESS_LIST= (ADDRESS=(PROTOCOL=tcp)(HOST=host1a)(PORT=1532)) (ADDRESS=(PROTOCOL=tcp)(HOST=host1b)(PORT=1533))) (ADDRESS=(PROTOCOL=tcp)(HOST=host2)(PORT=1534)) (ADDRESS_LIST= (ADDRESS=(PROTOCOL=tcp)(HOST=host3a)(PORT=1535)) (ADDRESS=(PROTOCOL=tcp)(HOST=host3b)(PORT=1536))) (CONNECT_DATA=(SERVICE_NAME=my_service_34)) )""" params = oracledb.ConnectParams() params.parse_connect_string(connect_string) self.assertEqual(params.host, ["host1a", "host1b", "host2", "host3a", "host3b"]) def test_4536_build_connect_string_with_source_route(self): "4536 - test building connect string with source route designation" host = "my_host4536" service_name = "my_service4536" options = [ ("on", True), ("off", False), ("true", True), ("false", False), ("yes", True), ("no", False) ] for in_val, has_section in options: connect_string = f""" (DESCRIPTION= (SOURCE_ROUTE={in_val}) (ADDRESS=(PROTOCOL=tcp)(HOST=host1)(PORT=1521)) (ADDRESS=(PROTOCOL=tcp)(HOST=host2)(PORT=1522)) (CONNECT_DATA=(SERVICE_NAME=my_service_35)) )""" params = oracledb.ConnectParams() params.parse_connect_string(connect_string) source_route_clause = "(SOURCE_ROUTE=ON)" if has_section else "" connect_string = \ f"(DESCRIPTION={source_route_clause}" + \ f"(ADDRESS_LIST=" + \ f"(ADDRESS=(PROTOCOL=tcp)(HOST=host1)(PORT=1521))" + \ f"(ADDRESS=(PROTOCOL=tcp)(HOST=host2)(PORT=1522)))" + \ f"(CONNECT_DATA=(SERVICE_NAME=my_service_35))" + \ f"(SECURITY=(SSL_SERVER_DN_MATCH=ON)))" self.assertEqual(params.get_connect_string(), connect_string) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_4600_type_changes.py000066400000000000000000000215311434177474600225250ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 4600 - Module for testing the handling of type changes in queries. """ import datetime import unittest import oracledb import test_env class TestCase(test_env.BaseTestCase): def __test_type_change(self, query_frag_1, query_value_1, query_frag_2, query_value_2, table_name="dual"): self.cursor.execute(f""" create or replace view TestTypesChanged as select {query_frag_1} as value from {table_name}""") self.cursor.execute("select * from TestTypesChanged") self.assertEqual(self.cursor.fetchall(), [(query_value_1,)]) self.cursor.execute(f""" create or replace view TestTypesChanged as select {query_frag_2} as value from dual""") self.cursor.execute("select * from TestTypesChanged") self.assertEqual(self.cursor.fetchall(), [(query_value_2,)]) @unittest.skipIf(not test_env.get_is_thin(), "thick mode doesn't support this type change") def test_4600_VARCHAR_to_CLOB(self): "4600 - test data type changing from VARCHAR to CLOB" self.__test_type_change("cast('string_4600' as VARCHAR2(15))", "string_4600", "to_clob('clob_4600')", "clob_4600") @unittest.skipIf(not test_env.get_is_thin(), "thick mode doesn't support this type change") def test_4601_CHAR_to_CLOB(self): "4601 - test data type changing from CHAR to CLOB" self.__test_type_change("cast('string_4601' as CHAR(11))", "string_4601", "to_clob('clob_4601')", "clob_4601") @unittest.skipIf(not test_env.get_is_thin(), "thick mode doesn't support this type change") def test_4602_LONG_to_CLOB(self): "4602 - test data type changing from LONG to CLOB" self.cursor.execute("truncate table TestLongs") self.cursor.execute("insert into TestLongs values (1, 'string_4602')") self.__test_type_change("LongCol", "string_4602", "to_clob('clob_4602')", "clob_4602", "TestLongs") @unittest.skipIf(not test_env.get_is_thin(), "thick mode doesn't support this type change") def test_4603_NVARCHAR_to_CLOB(self): "4603 - test data type changing from NVARCHAR to CLOB" self.__test_type_change("cast('string_4603' as NVARCHAR2(15))", "string_4603", "to_clob('clob_4603')", "clob_4603") @unittest.skipIf(not test_env.get_is_thin(), "thick mode doesn't support this type change") def test_4604_NCHAR_to_CLOB(self): "4604 - test data type changing from NCHAR to CLOB" self.__test_type_change("cast('string_4604' as NCHAR(11))", "string_4604", "to_clob('clob_4604')", "clob_4604") @unittest.skipIf(not test_env.get_is_thin(), "thick mode doesn't support this type change") def test_4605_RAW_to_BLOB(self): "4605 - test data type changing from RAW to BLOB" self.__test_type_change("utl_raw.cast_to_raw('string_4605')", b"string_4605", "to_blob(utl_raw.cast_to_raw('blob_4605'))", b"blob_4605") @unittest.skipIf(not test_env.get_is_thin(), "thick mode doesn't support this type change") def test_4606_LONG_RAW_to_BLOB(self): "4606 - test data type changing from LONGRAW to BLOB" self.cursor.execute("truncate table TestLongRaws") data = [1, b"string_4606"] self.cursor.execute("insert into TestLongRaws values (:1, :2)", data) self.__test_type_change("LongRawCol", b"string_4606", "to_blob(utl_raw.cast_to_raw('blob_4606'))", b"blob_4606", "TestLongRaws") @unittest.skipIf(not test_env.get_is_thin(), "thick mode doesn't support this type change") def test_4607_VARCHAR_to_NCLOB(self): "4607 - test data type changing from VARCHAR to NCLOB" self.__test_type_change("cast('string_4607' as VARCHAR2(15))", "string_4607", "to_nclob('nclob_4607')", "nclob_4607") @unittest.skipIf(not test_env.get_is_thin(), "thick mode doesn't support this type change") def test_4608_CHAR_to_NCLOB(self): "4608 - test data type changing from CHAR to NCLOB" self.__test_type_change("cast('string_4608' as CHAR(11))", "string_4608", "to_nclob('nclob_4608')", "nclob_4608") @unittest.skipIf(not test_env.get_is_thin(), "thick mode doesn't support this type change") def test_4609_LONG_to_NCLOB(self): "4609 - test data type changing from LONG to NCLOB" self.cursor.execute("truncate table TestLongs") self.cursor.execute("insert into TestLongs values (1, 'string_4609')") self.__test_type_change("LongCol", "string_4609", "to_nclob('nclob_4609')", "nclob_4609", "TestLongs") @unittest.skipIf(not test_env.get_is_thin(), "thick mode doesn't support this type change") def test_4610_NVARCHAR_to_NCLOB(self): "4610 - test data type changing from NVARCHAR to NCLOB" self.__test_type_change("cast('string_4610' as NVARCHAR2(15))", "string_4610", "to_nclob('nclob_4610')", "nclob_4610") @unittest.skipIf(not test_env.get_is_thin(), "thick mode doesn't support this type change") def test_4611_NCHAR_to_NCLOB(self): "4611 - test data type changing from NCHAR to NCLOB" self.__test_type_change("cast('string_4611' as NCHAR(11))", "string_4611", "to_nclob('nclob_4611')", "nclob_4611") def test_4612_VARCHAR_to_NUMBER(self): "4612 - test data type changing from VARCHAR to NUMBER" self.__test_type_change("cast('string_4612' as VARCHAR2(15))", "string_4612", "to_number('4612')", 4612) def test_4613_NUMBER_to_VARCHAR(self): "4613 - test data type changing from NUMBER to VARCHAR" self.__test_type_change("to_number('4613')", 4613, "cast('string_4613' as VARCHAR2(15))", "string_4613") def test_4614_STRING_to_DATE(self): "4614 - test data type changing from STRING to DATE" self.__test_type_change("cast('string_4614' as VARCHAR2(15))", "string_4614", "to_date('04-JAN-2022')", datetime.datetime(2022, 1, 4, 0, 0)) def test_4615_DATE_to_STRING(self): "4615 - test data type changing from DATE to STRING" self.__test_type_change("to_date('04-JAN-2022')", datetime.datetime(2022, 1, 4, 0, 0), "cast('string_4615' as VARCHAR2(15))", "string_4615") def test_4616_NUMBER_to_DATE(self): "4616 - test unsupported data type changing from NUMBER to DATE" self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-00932", self.__test_type_change, "to_number('4616')", 4616, "to_date('05-JAN-2022')", datetime.datetime(2022, 1, 4, 0, 0)) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_4700_pool_params.py000066400000000000000000000052721434177474600223750ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 4700 - Module for testing pool parameters. """ import oracledb import test_env class TestCase(test_env.BaseTestCase): requires_connection = False def __test_writable_parameter(self, name, value): """ Tests that a writable parameter can be written to and the modified value read back successfully. """ params = oracledb.PoolParams() orig_value = getattr(params, name) copied_params = params.copy() args = {} args[name] = value params.set(**args) self.assertEqual(getattr(params, name), value) self.assertEqual(getattr(copied_params, name), orig_value) def test_4700_writable_params(self): "4700 - test writable parameters" self.__test_writable_parameter("min", 8) self.__test_writable_parameter("max", 12) self.__test_writable_parameter("increment", 2) self.__test_writable_parameter("connectiontype", oracledb.Connection) self.__test_writable_parameter("getmode", oracledb.POOL_GETMODE_NOWAIT) self.__test_writable_parameter("homogeneous", False) self.__test_writable_parameter("timeout", 25) self.__test_writable_parameter("wait_timeout", 45) self.__test_writable_parameter("max_lifetime_session", 65) self.__test_writable_parameter("session_callback", lambda c: None) self.__test_writable_parameter("max_sessions_per_shard", 5) self.__test_writable_parameter("soda_metadata_cache", True) self.__test_writable_parameter("ping_interval", 20) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_4800_timestamp_ltz_var.py000066400000000000000000000170011434177474600236170ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 4800 - Module for testing timestamp with local time zone variables """ import datetime import oracledb import test_env class TestCase(test_env.BaseTestCase): def setUp(self): super().setUp() self.raw_data = [] self.data_by_key = {} base_date = datetime.datetime(2022, 6, 2) self.cursor.execute("alter session set time_zone = 'UTC'") for i in range(1, 11): if i % 4 == 0: tz_hours = i elif i % 2 == 0: tz_hours = i + 0.5 else: tz_hours = -(i + 0.5) tz_offset = datetime.timedelta(hours=tz_hours) microseconds = int(str(i * 50).ljust(6, "0")) offset = datetime.timedelta(days=i, seconds=i * 2, microseconds=microseconds) col = base_date + tz_offset + offset if i % 2: tz_offset = datetime.timedelta(hours=6) microseconds = int(str(i * 125).ljust(6, "0")) offset = datetime.timedelta(days=i + 1, seconds=i * 3, microseconds=microseconds) nullable_col = base_date + offset else: nullable_col = None data_tuple = (i, col, nullable_col) self.raw_data.append(data_tuple) self.data_by_key[i] = data_tuple def test_4800_bind_timestamp(self): "4800 - test binding in a timestamp" self.cursor.setinputsizes(value=oracledb.DB_TYPE_TIMESTAMP_LTZ) self.cursor.execute(""" select * from TestTimestampLTZs where TimestampLTZCol = :value""", value=datetime.datetime(2022, 6, 6, 18, 30, 10, 250000)) self.assertEqual(self.cursor.fetchall(), [self.data_by_key[5]]) def test_4801_bind_null(self): "4801 - test binding in a null" self.cursor.setinputsizes(value=oracledb.DB_TYPE_TIMESTAMP_LTZ) self.cursor.execute(""" select * from TestTimestampLTZs where TimestampLTZCol = :value""", value=None) self.assertEqual(self.cursor.fetchall(), []) def test_4802_bind_out_set_input_sizes(self): "4802 - test binding out with set input sizes defined" bv = self.cursor.setinputsizes(value=oracledb.DB_TYPE_TIMESTAMP_LTZ) self.cursor.execute(""" begin :value := to_timestamp('20220603', 'YYYYMMDD'); end;""") self.assertEqual(bv["value"].getvalue(), datetime.datetime(2022, 6, 3)) def test_4803_bind_in_out_set_input_sizes(self): "4803 - test binding in/out with set input sizes defined" bv = self.cursor.setinputsizes(value=oracledb.DB_TYPE_TIMESTAMP_LTZ) self.cursor.execute(""" begin :value := :value + 5.25; end;""", value=datetime.datetime(2022, 5, 10, 12, 0, 0)) self.assertEqual(bv["value"].getvalue(), datetime.datetime(2022, 5, 15, 18, 0, 0)) def test_4804_bind_out_var(self): "4804 - test binding out with cursor.var() method" var = self.cursor.var(oracledb.DB_TYPE_TIMESTAMP_LTZ) self.cursor.execute(""" begin :value := to_date('20220601 15:38:12', 'YYYYMMDD HH24:MI:SS'); end;""", value=var) self.assertEqual(var.getvalue(), datetime.datetime(2022, 6, 1, 15, 38, 12)) def test_4805_bind_in_out_var_direct_set(self): "4805 - test binding in/out with cursor.var() method" var = self.cursor.var(oracledb.DB_TYPE_TIMESTAMP_LTZ) var.setvalue(0, datetime.datetime(2022, 5, 30, 6, 0, 0)) self.cursor.execute(""" begin :value := :value + 5.25; end;""", value = var) self.assertEqual(var.getvalue(), datetime.datetime(2022, 6, 4, 12, 0, 0)) def test_4806_cursor_description(self): "4806 - test cursor description is accurate" self.cursor.execute("select * from TestTimestampLTZs") expected_value = [ ('INTCOL', oracledb.DB_TYPE_NUMBER, 10, None, 9, 0, False), ('TIMESTAMPLTZCOL', oracledb.DB_TYPE_TIMESTAMP_LTZ, 23, None, 0, 6, False), ('NULLABLECOL', oracledb.DB_TYPE_TIMESTAMP_LTZ, 23, None, 0, 6, True) ] self.assertEqual(self.cursor.description, expected_value) def test_4807_fetchall(self): "4807 - test that fetching all of the data returns the correct results" self.cursor.execute("select * From TestTimestampLTZs order by IntCol") self.assertEqual(self.cursor.fetchall(), self.raw_data) self.assertEqual(self.cursor.fetchall(), []) def test_4808_fetchmany(self): "4808 - test that fetching data in chunks returns the correct results" self.cursor.execute("select * From TestTimestampLTZs order by IntCol") self.assertEqual(self.cursor.fetchmany(3), self.raw_data[0:3]) self.assertEqual(self.cursor.fetchmany(2), self.raw_data[3:5]) self.assertEqual(self.cursor.fetchmany(4), self.raw_data[5:9]) self.assertEqual(self.cursor.fetchmany(3), self.raw_data[9:]) self.assertEqual(self.cursor.fetchmany(3), []) def test_4809_fetchone(self): "4809 - test that fetching a single row returns the correct results" self.cursor.execute(""" select * from TestTimestampLTZs where IntCol in (3, 4) order by IntCol""") self.assertEqual(self.cursor.fetchone(), self.data_by_key[3]) self.assertEqual(self.cursor.fetchone(), self.data_by_key[4]) self.assertEqual(self.cursor.fetchone(), None) def test_4810_bind_timestamp_with_zero_fseconds(self): "4810 - test binding a timestamp with zero fractional seconds" self.cursor.setinputsizes(value=oracledb.DB_TYPE_TIMESTAMP_LTZ) self.cursor.execute(""" select * from TestTimestampLTZs where trunc(TimestampLTZCol) = :value""", value=datetime.datetime(2022, 6, 12)) self.assertEqual(self.cursor.fetchall(), [self.data_by_key[10]]) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_4900_timestamp_tz_var.py000066400000000000000000000162231434177474600234510ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 4900 - Module for testing timestamp with time zone variables """ import datetime import oracledb import test_env class TestCase(test_env.BaseTestCase): def setUp(self): super().setUp() self.raw_data = [] self.data_by_key = {} base_date = datetime.datetime(2022, 6, 3) for i in range(1, 11): microseconds = int(str(i * 50).ljust(6, "0")) offset = datetime.timedelta(days=i, seconds=i * 2, microseconds=microseconds) col = base_date + offset if i % 2: microseconds = int(str(i * 125).ljust(6, "0")) offset = datetime.timedelta(days=i + 1, seconds=i * 3, microseconds=microseconds) nullable_col = base_date + offset else: nullable_col = None data_tuple = (i, col, nullable_col) self.raw_data.append(data_tuple) self.data_by_key[i] = data_tuple def test_4900_bind_timestamp(self): "4900 - test binding in a timestamp" self.cursor.setinputsizes(value=oracledb.DB_TYPE_TIMESTAMP_TZ) self.cursor.execute(""" select * from TestTimestampTZs where TimestampTZCol = :value""", value=datetime.datetime(2022, 6, 7, 18, 30, 10, 250000)) self.assertEqual(self.cursor.fetchall(), [self.data_by_key[5]]) def test_4901_bind_null(self): "4901 - test binding in a null" self.cursor.setinputsizes(value=oracledb.DB_TYPE_TIMESTAMP_TZ) self.cursor.execute(""" select * from TestTimestampTZs where TimestampTZCol = :value""", value=None) self.assertEqual(self.cursor.fetchall(), []) def test_4902_bind_out_set_input_sizes(self): "4902 - test binding out with set input sizes defined" bv = self.cursor.setinputsizes(value=oracledb.DB_TYPE_TIMESTAMP_TZ) self.cursor.execute(""" begin :value := to_timestamp('20220603', 'YYYYMMDD'); end;""") self.assertEqual(bv["value"].getvalue(), datetime.datetime(2022, 6, 3)) def test_4903_bind_in_out_set_input_sizes(self): "4903 - test binding in/out with set input sizes defined" bv = self.cursor.setinputsizes(value=oracledb.DB_TYPE_TIMESTAMP_TZ) self.cursor.execute(""" begin :value := :value + to_dsinterval('5 06:00:00'); end;""", value=datetime.datetime(2022, 5, 25)) self.assertEqual(bv["value"].getvalue(), datetime.datetime(2022, 5, 30, 6, 0, 0)) def test_4904_bind_out_var(self): "4904 - test binding out with cursor.var() method" var = self.cursor.var(oracledb.DB_TYPE_TIMESTAMP_TZ) self.cursor.execute(""" begin :value := to_date('20021231 12:31:00', 'YYYYMMDD HH24:MI:SS'); end;""", value=var) self.assertEqual(var.getvalue(), datetime.datetime(2002, 12, 31, 12, 31, 0)) def test_4905_bind_in_out_var_direct_set(self): "4905 - test binding in/out with cursor.var() method" var = self.cursor.var(oracledb.DB_TYPE_TIMESTAMP_TZ) var.setvalue(0, datetime.datetime(2022, 6, 3, 6, 0, 0)) self.cursor.execute(""" begin :value := :value + to_dsinterval('5 06:00:00'); end;""", value = var) self.assertEqual(var.getvalue(), datetime.datetime(2022, 6, 8, 12, 0, 0)) def test_4906_cursor_description(self): "4906 - test cursor description is accurate" self.cursor.execute("select * from TestTimestampTZs") expected_value = [ ('INTCOL', oracledb.DB_TYPE_NUMBER, 10, None, 9, 0, False), ('TIMESTAMPTZCOL', oracledb.DB_TYPE_TIMESTAMP_TZ, 23, None, 0, 6, False), ('NULLABLECOL', oracledb.DB_TYPE_TIMESTAMP_TZ, 23, None, 0, 6, True) ] self.assertEqual(self.cursor.description, expected_value) def test_4907_fetchall(self): "4907 - test that fetching all of the data returns the correct results" self.cursor.execute("select * From TestTimestampTZs order by IntCol") self.assertEqual(self.cursor.fetchall(), self.raw_data) self.assertEqual(self.cursor.fetchall(), []) def test_4908_fetchmany(self): "4908 - test that fetching data in chunks returns the correct results" self.cursor.execute("select * From TestTimestampTZs order by IntCol") self.assertEqual(self.cursor.fetchmany(3), self.raw_data[0:3]) self.assertEqual(self.cursor.fetchmany(2), self.raw_data[3:5]) self.assertEqual(self.cursor.fetchmany(4), self.raw_data[5:9]) self.assertEqual(self.cursor.fetchmany(3), self.raw_data[9:]) self.assertEqual(self.cursor.fetchmany(3), []) def test_4909_fetchone(self): "4909 - test that fetching a single row returns the correct results" self.cursor.execute(""" select * from TestTimestampTZs where IntCol in (3, 4) order by IntCol""") self.assertEqual(self.cursor.fetchone(), self.data_by_key[3]) self.assertEqual(self.cursor.fetchone(), self.data_by_key[4]) self.assertEqual(self.cursor.fetchone(), None) def test_4910_bind_timestamp_with_zero_fseconds(self): "4910 - test binding a timestamp with zero fractional seconds" self.cursor.setinputsizes(value=oracledb.DB_TYPE_TIMESTAMP) self.cursor.execute(""" select * from TestTimestampTZs where trunc(TimestampTZCol) = :value""", value=datetime.datetime(2022, 6, 8)) self.assertEqual(self.cursor.fetchall(), [self.data_by_key[5]]) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_5000_externalauth.py000066400000000000000000000273011434177474600225540ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ """ 5000 - Module for testing external authentication """ import unittest import oracledb import test_env @unittest.skipIf(not test_env.get_external_user(), "external authentication not supported with this setup") class TestCase(test_env.BaseTestCase): require_connection = False def __verify_connection(self, connection, expected_user, expected_proxy_user=None): with connection.cursor() as cursor: cursor.execute(""" select sys_context('userenv', 'session_user'), sys_context('userenv', 'proxy_user') from dual""") actual_user, actual_proxy_user = cursor.fetchone() self.assertEqual(actual_user, expected_user.upper()) self.assertEqual(actual_proxy_user, expected_proxy_user and expected_proxy_user.upper()) def test_5000_pool_with_user_and_password_and_externalauth_enabled(self): """ 5000 - test error on creating a pool with user and password specified and externalauth enabled """ self.assertRaisesRegex(oracledb.DatabaseError, "^DPI-1032:", test_env.get_pool, min=1, max=2, increment=1, getmode=oracledb.POOL_GETMODE_WAIT, externalauth=True, homogeneous=False) def test_5001_pool_with_no_password_and_externalauth_enabled(self): """ 5001 - test error on creating a pool without password and with user specified and externalauth enabled """ self.assertRaisesRegex(oracledb.DatabaseError, "^DPI-1032:", oracledb.create_pool, user=test_env.get_main_user(), min=1, max=2, increment=1, getmode=oracledb.POOL_GETMODE_WAIT, externalauth=True, homogeneous=False) def test_5002_pool_with_no_user_and_externalauth_enabled(self): """ 5002 - test error on creating a pool without user and with password specified and externalauth enabled """ self.assertRaisesRegex(oracledb.DatabaseError, "^DPI-1032:", oracledb.create_pool, password=test_env.get_main_password(), min=1, max=2, increment=1, getmode=oracledb.POOL_GETMODE_WAIT, externalauth=True, homogeneous=False) def test_5003_pool_with_user_and_password_and_externalauth_off(self): """ 5003 - test creating a pool with user and password specified and externalauth set to False """ pool = test_env.get_pool(min=1, max=2, increment=1, getmode=oracledb.POOL_GETMODE_WAIT, externalauth=False, homogeneous=False) with pool.acquire() as connection: self.__verify_connection(connection, test_env.get_main_user()) def test_5004_user_and_password_with_externalauth_enabled(self): """ 5004 - test error when connecting with user and password specified and externalauth enabled """ self.assertRaisesRegex(oracledb.DatabaseError, "^DPI-1032:", oracledb.connect, user=test_env.get_main_user(), password=test_env.get_main_password(), dsn=test_env.get_connect_string(), externalauth=True) def test_5005_no_user_with_externalauth_enabled(self): """ 5005 - test error when connecting without username and with password specified and externalauth enabled """ self.assertRaisesRegex(oracledb.DatabaseError, "^DPI-1032:", oracledb.connect, password=test_env.get_main_password(), dsn=test_env.get_connect_string(), externalauth=True) # by default externalauth is False self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-01017:", oracledb.connect, password=test_env.get_main_password(), dsn=test_env.get_connect_string()) def test_5006_user_with_no_password_and_externalauth_enabled(self): """ 5006 - test error when connecting without password and with user specified and externalauth enabled """ self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-01017:", oracledb.connect, user="[invalid_user]", dsn=test_env.get_connect_string(), externalauth=True) # by default externalauth is False self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-01017:", oracledb.connect, user="[invalid_user]", dsn=test_env.get_connect_string()) def test_5007_external_authentication_with_invalid_proxy_user(self): "5007 - test external authentication with invalid proxy user" self.assertRaisesRegex(oracledb.DatabaseError, "^DPI-1069:", oracledb.connect, user=test_env.get_main_user(), dsn=test_env.get_connect_string(), externalauth=True) # by default externalauth is False self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4001:", oracledb.connect, user=test_env.get_main_user(), dsn=test_env.get_connect_string()) def test_5008_user_and_password_with_externalauth_off(self): """ 5008 - test creating a connection with user and password specified and externalauth set to False """ connection = oracledb.connect(user=test_env.get_main_user(), password=test_env.get_main_password(), dsn=test_env.get_connect_string(), externalauth=False) self.__verify_connection(connection, test_env.get_main_user()) def test_5009_external_authentication_with_externalauth_enabled(self): """ 5009 - test creating standalone connection with externalauth set to True explicitly """ connection = oracledb.connect(dsn=test_env.get_connect_string(), externalauth=True) self.__verify_connection(connection, test_env.get_external_user()) def test_5010_external_authentication_with_externalauth_not_set(self): """ 5010 - test creating standalone connection with no user and password specified and externalauth not set """ connection = oracledb.connect(dsn=test_env.get_connect_string()) self.__verify_connection(connection, test_env.get_external_user()) def test_5011_pool_with_external_authentication(self): "5011 - test creating a pool with external authentication" pool = oracledb.create_pool(dsn=test_env.get_connect_string(), min=1, max=2, increment=1, getmode=oracledb.POOL_GETMODE_WAIT, externalauth=True, homogeneous=False) self.assertEqual(pool.opened, 0) with pool.acquire() as connection: self.__verify_connection(connection, test_env.get_external_user()) def test_5012_pool_with_no_user_and_password_and_externalauth_not_set(self): """ 5012 - test creating a pool without user and password specified and externalauth not set """ pool = oracledb.create_pool(dsn=test_env.get_connect_string(), min=1, max=2, increment=1, getmode=oracledb.POOL_GETMODE_WAIT, homogeneous=False) self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-24415:", pool.acquire) def test_5013_pool_min_with_no_effect_under_external_authentication(self): "5013 - test pool min is always 0 under external authentication" pool = oracledb.create_pool(dsn=test_env.get_connect_string(), min=5, max=10, increment=3, getmode=oracledb.POOL_GETMODE_WAIT, externalauth=True, homogeneous=False) self.assertEqual(pool.opened, 0) def test_5014_pool_increment_with_no_effect_under_external_auth(self): "5014 - test pool increment is always 1 under external authentication" pool = oracledb.create_pool(dsn=test_env.get_connect_string(), min=5, max=10, increment=3, getmode=oracledb.POOL_GETMODE_WAIT, externalauth=True, homogeneous=False) conn1 = pool.acquire() self.assertEqual(pool.opened, 1) conn2 = pool.acquire() self.assertEqual(pool.opened, 2) def test_5015_external_authentication_with_proxy(self): "5015 - test external authentication with proxy" proxy_user = test_env.get_external_user() # proxy user schema_user = test_env.get_main_user() # schema user conn1 = oracledb.connect(user=f"[{schema_user}]", dsn=test_env.get_connect_string(), externalauth=True) self.__verify_connection(conn1, schema_user, proxy_user) conn2 = oracledb.connect(user=f"[{schema_user}]", dsn=test_env.get_connect_string()) self.__verify_connection(conn2, schema_user, proxy_user) def test_5016_pool_external_authentication_with_proxy(self): "5016 - test creating pool using external authentication with proxy" proxy_user = test_env.get_external_user() schema_user = test_env.get_main_user() pool = oracledb.create_pool(externalauth=True, homogeneous=False, dsn=test_env.get_connect_string(), min=1, max=2, increment=1, getmode=oracledb.POOL_GETMODE_WAIT) self.assertEqual(pool.opened, 0) connection = pool.acquire(user=f"[{schema_user}]") self.__verify_connection(connection, schema_user, proxy_user) if __name__ == "__main__": test_env.run_test_cases() python-oracledb-1.2.1/tests/test_env.py000066400000000000000000000361331434177474600201770ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # Sets the environment used by the python-oracledb test suite. Production # applications should consider using External Authentication to avoid hard # coded credentials. # # You can set values in environment variables to bypass having the test suite # request the information it requires. # # PYO_TEST_MAIN_USER: user used for most test cases # PYO_TEST_MAIN_PASSWORD: password of user used for most test cases # PYO_TEST_PROXY_USER: user for testing proxying # PYO_TEST_PROXY_PASSWORD: password of user for testing proxying # PYO_TEST_CONNECT_STRING: connect string for test suite # PYO_TEST_ADMIN_USER: administrative user for test suite # PYO_TEST_ADMIN_PASSWORD: administrative password for test suite # PYO_TEST_WALLET_LOCATION: location of wallet file (thin mode, mTLS) # PYO_TEST_WALLET_PASSWORD: password for wallet file (thin mode, mTLS) # PYO_TEST_DRIVER_MODE: python-oracledb mode (thick or thin) to use # PYO_TEST_EXTERNAL_USER: user for testing external authentication # # PYO_TEST_CONNECT_STRING can be set to an Easy Connect string, or a # Net Service Name from a tnsnames.ora file or external naming service, # or it can be the name of a local Oracle database instance. # # If oracledb is using Instant Client, then an Easy Connect string is generally # appropriate. The syntax is: # # [//]host_name[:port][/service_name][:server_type][/instance_name] # # Commonly just the host_name and service_name are needed # e.g. "localhost/orclpdb1" or "localhost/XEPDB1" # # If using a tnsnames.ora file, the file can be in a default # location such as $ORACLE_HOME/network/admin/tnsnames.ora or # /etc/tnsnames.ora. Alternatively set the TNS_ADMIN environment # variable and put the file in $TNS_ADMIN/tnsnames.ora. # # The administrative user for cloud databases is ADMIN and the administrative # user for on premises databases is SYSTEM. #------------------------------------------------------------------------------ import getpass import os import sys import unittest import oracledb # default values DEFAULT_MAIN_USER = "pythontest" DEFAULT_PROXY_USER = "pythontestproxy" DEFAULT_CONNECT_STRING = "localhost/orclpdb1" # dictionary containing all parameters; these are acquired as needed by the # methods below (which should be used instead of consulting this dictionary # directly) and then stored so that a value is not requested more than once PARAMETERS = {} def get_value(name, label, default_value=None, password=False): try: return PARAMETERS[name] except KeyError: pass env_name = "PYO_TEST_" + name value = os.environ.get(env_name) if value is None: if default_value is not None: label += " [%s]" % default_value label += ": " if password: value = getpass.getpass(label) else: value = input(label).strip() if not value: value = default_value PARAMETERS[name] = value return value def get_admin_connection(): admin_user = get_value("ADMIN_USER", "Administrative user", "admin") admin_password = get_value("ADMIN_PASSWORD", f"Password for {admin_user}", password=True) params = get_connect_params() if admin_user and admin_user.upper() == "SYS": params = params.copy() params.set(mode=oracledb.AUTH_MODE_SYSDBA) return oracledb.connect(dsn=get_connect_string(), params=params, user=admin_user, password=admin_password) def get_charset_ratios(): value = PARAMETERS.get("CS_RATIO") if value is None: connection = get_connection() cursor = connection.cursor() cursor.execute(""" select cast('X' as varchar2(1)), cast('Y' as nvarchar2(1)) from dual""") varchar_column_info, nvarchar_column_info = cursor.description value = (varchar_column_info[3], nvarchar_column_info[3]) PARAMETERS["CS_RATIO"] = value return value def get_client_version(): name = "CLIENT_VERSION" value = PARAMETERS.get(name) if value is None: if get_is_thin(): value = (21, 3) else: value = oracledb.clientversion()[:2] PARAMETERS[name] = value return value def get_connect_params(): name = "CONNECT_PARAMS" params = PARAMETERS.get(name) if params is None: wallet_location = get_wallet_location() params = oracledb.ConnectParams(user=get_main_user(), password=get_main_password(), config_dir=wallet_location, wallet_location=wallet_location, wallet_password=get_wallet_password()) PARAMETERS[name] = params return params def get_connection(dsn=None, **kwargs): if dsn is None: dsn = get_connect_string() return oracledb.connect(dsn=dsn, params=get_connect_params(), **kwargs) def get_connect_string(): return get_value("CONNECT_STRING", "Connect String", DEFAULT_CONNECT_STRING) def get_is_thin(): driver_mode = get_value("DRIVER_MODE", "Driver mode (thin|thick)", "thin") return driver_mode == "thin" def get_main_password(): return get_value("MAIN_PASSWORD", f"Password for {get_main_user()}", password=True) def get_main_user(): return get_value("MAIN_USER", "Main User Name", DEFAULT_MAIN_USER) def get_pool(**kwargs): return oracledb.create_pool(dsn=get_connect_string(), params=get_pool_params(), **kwargs) def get_pool_params(): name = "POOL_PARAMS" params = PARAMETERS.get(name) if params is None: wallet_location = get_wallet_location() params = oracledb.PoolParams(user=get_main_user(), password=get_main_password(), config_dir=wallet_location, wallet_location=wallet_location, wallet_password=get_wallet_password()) PARAMETERS[name] = params return params def get_proxy_password(): return get_value("PROXY_PASSWORD", f"Password for {get_proxy_user()}", password=True) def get_proxy_user(): return get_value("PROXY_USER", "Proxy User Name", DEFAULT_PROXY_USER) def get_sleep_proc_name(): server_version = get_server_version() return "dbms_session.sleep" if server_version[0] >= 18 \ else "dbms_lock.sleep" def get_server_version(): name = "SERVER_VERSION" value = PARAMETERS.get(name) if value is None: conn = get_connection() value = tuple(int(s) for s in conn.version.split("."))[:2] PARAMETERS[name] = value return value def get_wallet_location(): if get_is_thin(): return get_value("WALLET_LOCATION", "Wallet Location") def get_wallet_password(): if get_is_thin(): return get_value("WALLET_PASSWORD", "Wallet Password", password=True) def get_external_user(): if not get_is_thin(): return get_value("EXTERNAL_USER", "External User") def is_on_oracle_cloud(connection): server = get_server_version() if server < (18, 0): return False cursor = connection.cursor() cursor.execute(""" select sys_context('userenv', 'cloud_service') from dual""") service_name, = cursor.fetchone() return service_name is not None def run_sql_script(conn, script_name, **kwargs): statement_parts = [] cursor = conn.cursor() replace_values = [("&" + k + ".", v) for k, v in kwargs.items()] + \ [("&" + k, v) for k, v in kwargs.items()] script_dir = os.path.dirname(os.path.abspath(sys.argv[0])) file_name = os.path.join(script_dir, "sql", script_name + ".sql") for line in open(file_name): if line.strip() == "/": statement = "".join(statement_parts).strip() if statement: for search_value, replace_value in replace_values: statement = statement.replace(search_value, replace_value) try: cursor.execute(statement) except: print("Failed to execute SQL:", statement) raise statement_parts = [] else: statement_parts.append(line) cursor.execute(""" select name, type, line, position, text from dba_errors where owner = upper(:owner) order by name, type, line, position""", owner = get_main_user()) prev_name = prev_obj_type = None for name, obj_type, line_num, position, text in cursor: if name != prev_name or obj_type != prev_obj_type: print("%s (%s)" % (name, obj_type)) prev_name = name prev_obj_type = obj_type print(" %s/%s %s" % (line_num, position, text)) def run_test_cases(): unittest.main(testRunner=unittest.TextTestRunner(verbosity=2)) def skip_soda_tests(): if get_is_thin(): return True client = get_client_version() if client < (18, 3): return True server = get_server_version() if server < (18, 0): return True if server > (20, 1) and client < (20, 1): return True return False class FetchLobsContextManager: def __init__(self, desired_value): self.desired_value = desired_value def __enter__(self): self.original_value = oracledb.defaults.fetch_lobs oracledb.defaults.fetch_lobs = self.desired_value return self def __exit__(self, exc_type, exc_val, exc_tb): oracledb.defaults.fetch_lobs = self.original_value class SystemStatInfo: stat_name = None def __init__(self, connection): self.prev_value = 0 self.admin_conn = get_admin_connection() with connection.cursor() as cursor: cursor.execute("select sys_context('userenv', 'sid') from dual") self.sid, = cursor.fetchone() self.get_value() def get_value(self): with self.admin_conn.cursor() as cursor: cursor.execute(""" select ss.value from v$sesstat ss, v$statname sn where ss.sid = :sid and ss.statistic# = sn.statistic# and sn.name = :stat_name""", sid=self.sid, stat_name=self.stat_name) current_value, = cursor.fetchone() diff_value = current_value - self.prev_value self.prev_value = current_value return diff_value class RoundTripInfo(SystemStatInfo): stat_name = "SQL*Net roundtrips to/from client" class ParseCountInfo(SystemStatInfo): stat_name = "parse count (total)" class BaseTestCase(unittest.TestCase): requires_connection = True def assertParseCount(self, n): self.assertEqual(self.parse_count_info.get_value(), n) def assertRoundTrips(self, n): self.assertEqual(self.round_trip_info.get_value(), n) def get_and_clear_queue(self, queue_name, payload_type=None, message="not supported with this client/server " \ "combination"): if payload_type == "JSON": if get_client_version() < (21, 0) \ or get_server_version() < (21, 0): self.skipTest(message) elif isinstance(payload_type, str): payload_type = self.connection.gettype(payload_type) queue = self.connection.queue(queue_name, payload_type) queue.deqoptions.wait = oracledb.DEQ_NO_WAIT queue.deqoptions.deliverymode = oracledb.MSG_PERSISTENT_OR_BUFFERED queue.deqoptions.visibility = oracledb.DEQ_IMMEDIATE while queue.deqone(): pass return self.connection.queue(queue_name, payload_type) def get_db_object_as_plain_object(self, obj): if obj.type.iscollection: element_values = [] for value in obj.aslist(): if isinstance(value, oracledb.DbObject): value = self.get_db_object_as_plain_object(value) elif isinstance(value, oracledb.LOB): value = value.read() element_values.append(value) return element_values attr_values = [] for attribute in obj.type.attributes: value = getattr(obj, attribute.name) if isinstance(value, oracledb.DbObject): value = self.get_db_object_as_plain_object(value) elif isinstance(value, oracledb.LOB): value = value.read() attr_values.append(value) return tuple(attr_values) def get_soda_database(self, minclient=(18, 3), minserver=(18, 0), message="not supported with this client/server " \ "combination"): client = get_client_version() if client < minclient: self.skipTest(message) server = get_server_version() if server < minserver: self.skipTest(message) if server > (20, 1) and client < (20, 1): self.skipTest(message) return self.connection.getSodaDatabase() def is_on_oracle_cloud(self, connection=None): if connection is None: connection = self.connection return is_on_oracle_cloud(connection) def setUp(self): if self.requires_connection: self.connection = get_connection() self.cursor = self.connection.cursor() def setup_parse_count_checker(self): self.parse_count_info = ParseCountInfo(self.connection) def setup_round_trip_checker(self): self.round_trip_info = RoundTripInfo(self.connection) def tearDown(self): if self.requires_connection: self.connection.close() del self.cursor del self.connection # ensure that thick mode is enabled, if desired if not get_is_thin(): oracledb.init_oracle_client() python-oracledb-1.2.1/tox.ini000066400000000000000000000011211434177474600161340ustar00rootroot00000000000000[tox] envlist = py{36,37,38,39,310,311}-{thin,thick} [testenv] commands = {envpython} -m unittest discover -v -s tests passenv = PYO_TEST_MAIN_USER PYO_TEST_MAIN_PASSWORD PYO_TEST_PROXY_USER PYO_TEST_PROXY_PASSWORD PYO_TEST_CONNECT_STRING PYO_TEST_ADMIN_USER PYO_TEST_ADMIN_PASSWORD PYO_TEST_WALLET_LOCATION PYO_TEST_WALLET_PASSWORD PYO_TEST_EXTERNAL_USER DPI_DEBUG_LEVEL ORACLE_HOME [testenv:py{36,37,38,39,310,311}-thick] setenv = PYO_TEST_DRIVER_MODE=thick [testenv:py{36,37,38,39,310,311}-thin] setenv = PYO_TEST_DRIVER_MODE=thin python-oracledb-1.2.1/utils/000077500000000000000000000000001434177474600157665ustar00rootroot00000000000000python-oracledb-1.2.1/utils/Makefile000066400000000000000000000036471434177474600174400ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # Makefile to generate parameter files from templates #------------------------------------------------------------------------------ SOURCE_DIR = ../src/oracledb TEMPLATE_DIR = templates all: $(SOURCE_DIR)/connect_params.py $(SOURCE_DIR)/pool_params.py \ $(SOURCE_DIR)/connection.py $(SOURCE_DIR)/pool.py $(SOURCE_DIR)/connection.py: fields.cfg $(TEMPLATE_DIR)/connection.py python build_from_template.py connection $(SOURCE_DIR)/connect_params.py: fields.cfg $(TEMPLATE_DIR)/connect_params.py python build_from_template.py connect_params $(SOURCE_DIR)/pool.py: fields.cfg $(TEMPLATE_DIR)/pool.py python build_from_template.py pool $(SOURCE_DIR)/pool_params.py: fields.cfg $(TEMPLATE_DIR)/pool_params.py python build_from_template.py pool_params python-oracledb-1.2.1/utils/README.md000066400000000000000000000004311434177474600172430ustar00rootroot00000000000000This directory contains utility scripts for generating parameter files from templates using the configuration stored in fields.cfg. Generation is used instead of dynamic runtime equivalents in order to allow static analyzers (such as those used in Visual Studio Code) to benefit. python-oracledb-1.2.1/utils/build_from_template.py000066400000000000000000000213221434177474600223550ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # build_from_template.py # # Builds the parameter file from the template supplied on the command line. # The generated file is written to the src/oracledb directory. The following # template keys are recognized: # # #{{ args_help_with_defaults }} # is replaced by the arguments help string with field defaults # #{{ args_help_without_defaults }} # is replaced by the arguments help string without field defaults # #{{ args_with_defaults }} # is replaced by the arguments with field defaults included # #{{ args_without_defaults }} # is replaced by the arguments without field defaults (all set to None) # #{{ generated_notice }} # is replaced by a notice that the file is generated and should not be # modified directly # #{{ params_properties }} # is replaced by generated property getter and setter methods # #{{ params_repr_parts }} # is replaced by generated parts for the repr() function # # All of these could be accomplished by decorators, but doing so would # eliminate the usefulness of static analyzers such as those used within Visual # Studio Code. #------------------------------------------------------------------------------ import argparse import configparser import dataclasses import os import sys import textwrap TEXT_WIDTH = 79 @dataclasses.dataclass class Field: name: str = "" typ: str = "" default: str = "" hidden: bool = False pool_only: bool = False description: str = "" decorator: str = None # parse command line parser = argparse.ArgumentParser(description="build module from template") parser.add_argument("name", help="the name of the module to generate") args = parser.parse_args() # determine location of template and source and validate template base_dir = os.path.dirname(os.path.abspath(sys.argv[0])) target_dir = os.path.join(os.path.dirname(base_dir), "src", "oracledb") template_name = os.path.join(base_dir, "templates", f"{args.name}.py") config_name = os.path.join(base_dir, "fields.cfg") target_name = os.path.join(target_dir, f"{args.name}.py") if not os.path.exists(template_name): raise Exception(f"template {template_name} does not exist!") if not os.path.exists(config_name): raise Exception(f"configuration {config_name} does not exist!") code = open(template_name).read() pool_only = "pool" in args.name # acquire the fields from the configuration file fields = [] config = configparser.ConfigParser() config.read(config_name) for section in config.sections(): field = Field() field.name = section field.typ = config.get(section, "type") field.default = config.get(section, "default", fallback="None") field.hidden = config.getboolean(section, "hidden", fallback=False) field.pool_only = config.getboolean(section, "pool_only", fallback=False) field.description = config.get(section, "description", fallback="").strip() field.decorator = config.get(section, "decorator", fallback="") if not field.pool_only or pool_only: fields.append(field) def replace_tag(tag, content_generator): """ Replaces a template tag with content generated by a function. The content found before the tag is passed to the generator function. """ global code search_value = "#{{ " + tag + " }}" while True: pos = code.find(search_value) if pos < 0: break prev_line_pos = code[:pos].rfind("\n") indent = code[prev_line_pos + 1:pos] content = content_generator(indent) code = code[:pos] + content + code[pos + len(search_value):] def args_help_with_defaults_content(indent): """ Generates the content for the args_help_with_defaults template tag. """ raw_descriptions = [f"- {f.name}: {f.description} (default: {f.default})" \ for f in fields if f.description] descriptions = [textwrap.fill(d, initial_indent=indent, subsequent_indent=indent + " ", width=TEXT_WIDTH) \ for d in raw_descriptions] return "\n\n".join(descriptions).strip() def args_help_without_defaults_content(indent): """ Generates the content for the args_help_without_defaults template tag. """ raw_descriptions = [f"- {f.name}: {f.description}" \ for f in fields if f.description] descriptions = [textwrap.fill(d, initial_indent=indent, subsequent_indent=indent + " ", width=TEXT_WIDTH) \ for d in raw_descriptions] return "\n\n".join(descriptions).strip() def args_with_defaults_content(indent): """ Generates the content for the args_with_defaults template tag. """ args_joiner = ",\n" + indent args = [f"{f.name}: {f.typ}={f.default}" for f in fields] return args_joiner.join(args) def args_without_defaults_content(indent): """ Generates the content for the args_without_defaults template tag. """ args_joiner = ",\n" + indent args = [f"{f.name}: {f.typ}=None" for f in fields] return args_joiner.join(args) def generated_notice_content(indent): """ Generates the content for the generated_notice template tag. """ notice = """ *** NOTICE *** This file is generated from a template and should not be modified directly. See build_from_template.py in the utils subdirectory for more information.""" return textwrap.fill(textwrap.dedent(notice).strip(), subsequent_indent=indent, width=TEXT_WIDTH) def params_properties_content(indent): """ Generates the content for the params_properties template tag. """ functions = [] for field in sorted(fields, key=lambda f: f.name.upper()): if field.hidden: continue if field.pool_only != pool_only: continue description = f"{field.description[0].upper()}{field.description[1:]}." doc_string = textwrap.fill(description, initial_indent=" ", subsequent_indent=" ", width=TEXT_WIDTH - len(indent)) return_type = f"Union[list, {field.typ}]" if field.decorator \ else field.typ body_lines = [ f'@property', f'@{field.decorator}' if field.decorator else '', f'def {field.name}(self) -> {return_type}:', f' """' ] + doc_string.splitlines() + [ f' """', f' return self._impl.{field.name}' ] joiner = "\n" + indent functions.append(joiner.join(s for s in body_lines if s)) joiner = '\n\n' + indent return joiner.join(functions) def params_repr_parts_content(indent): """ Generates the content for the params_repr_parts template tag. """ parts = [f'f"{field.name}={{self.{field.name}!r}}, "' \ for field in fields if not field.hidden] parts[-1] = parts[-1][:-3] + '"' joiner = ' + \\\n' + indent return joiner.join(parts) # replace generated_notice template tag replace_tag("args_help_with_defaults", args_help_with_defaults_content) replace_tag("args_help_without_defaults", args_help_without_defaults_content) replace_tag("args_with_defaults", args_with_defaults_content) replace_tag("args_without_defaults", args_without_defaults_content) replace_tag("generated_notice", generated_notice_content) replace_tag("params_properties", params_properties_content) replace_tag("params_repr_parts", params_repr_parts_content) # write the final code to the target location open(target_name, "w").write(code) python-oracledb-1.2.1/utils/fields.cfg000066400000000000000000000274621434177474600177300ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # fields.cfg # # Contains the fields found on the ConnectParams and PoolParams classes. #------------------------------------------------------------------------------ # pool only parameters [min] type = int default: 1 pool_only: True description = the minimum number of connections the pool should contain [max] type = int default: 2 pool_only: True description = the maximum number of connections the pool should contain [increment] type = int default: 1 pool_only: True description = the number of connections that should be added to the pool whenever a new connection needs to be created [connectiontype] type = Type["oracledb.Connection"] pool_only: True description = the class of the connection that should be returned during calls to pool.acquire(). It must be Connection or a subclass of Connection [getmode] type = int default = oracledb.POOL_GETMODE_WAIT pool_only: True description = how pool.acquire() will behave. One of the constants oracledb.POOL_GETMODE_WAIT, oracledb.POOL_GETMODE_NOWAIT, oracledb.POOL_GETMODE_FORCEGET, or oracledb.POOL_GETMODE_TIMEDWAIT [homogeneous] type = bool default = True pool_only: True description = a boolean indicating whether the connections are homogeneous (same user) or heterogeneous (multiple users) [timeout] type = int default = 0 pool_only: True description = length of time (in seconds) that a connection may remain idle in the pool before it is terminated. If it is 0 then connections are never terminated [wait_timeout] type = int default = 0 pool_only: True description = length of time (in milliseconds) that a caller should wait when acquiring a connection from the pool with getmode set to oracledb.POOL_GETMODE_TIMEDWAIT [max_lifetime_session] type = int default = 0 pool_only: True description = length of time (in seconds) that connections can remain in the pool. If it is 0 then connections may remain in the pool indefinitely [session_callback] type = Callable pool_only: True description = a callable that is invoked when a connection is returned from the pool for the first time, or when the connection tag differs from the one requested [max_sessions_per_shard] type = int default = 0 pool_only: True description = the maximum number of connections that may be associated with a particular shard [soda_metadata_cache] type = bool default = False pool_only: True description = boolean indicating whether or not the SODA metadata cache should be enabled [ping_interval] type = int default = 60 pool_only: True description = length of time (in seconds) after which an unused connection in the pool will be a candidate for pinging when pool.acquire() is called. If the ping to the database indicates the connection is not alive a replacement connection will be returned by pool.acquire(). If ping_interval is a negative value the ping functionality will be disabled # common parameters [user] type = str description = the name of the user to connect to [proxy_user] type = str description = the name of the proxy user to connect to. If this value is not specified, it will be parsed out of user if user is in the form "user[proxy_user]" [password] type = str hidden = True description = the password for the user [newpassword] type = str hidden = True description = the new password for the user. The new password will take effect immediately upon a successful connection to the database [wallet_password] type = str hidden = True description = the password to use to decrypt the wallet, if it is encrypted. This value is only used in thin mode [access_token] type = Union[str, tuple, Callable] hidden = True description = expected to be a string or a 2-tuple or a callable. If it is a string, it specifies an Azure AD OAuth2 token used for Open Authorization (OAuth 2.0) token based authentication. If it is a 2-tuple, it specifies the token and private key strings used for Oracle Cloud Infrastructure (OCI) Identity and Access Management (IAM) token based authentication. If it is a callable, it returns either a string or a 2-tuple used for OAuth 2.0 or OCI IAM token based authentication and is useful when the pool needs to expand and create new connections but the current authentication token has expired [host] type = str decorator = _address_attr description = the name or IP address of the machine hosting the database or the database listener [port] type = int default = 1521 decorator = _address_attr description = the port number on which the database listener is listening [protocol] type = str default = "tcp" decorator = _address_attr description = one of the strings "tcp" or "tcps" indicating whether to use unencrypted network traffic or encrypted network traffic (TLS) [https_proxy] type = str decorator = _address_attr description = the name or IP address of a proxy host to use for tunneling secure connections [https_proxy_port] type = int default = 0 decorator = _address_attr description = the port on which to communicate with the proxy host [service_name] type = str decorator = _description_attr description = the service name of the database [sid] type = str decorator = _description_attr description = the system identifier (SID) of the database. Note using a service_name instead is recommended [server_type] type = str decorator = _description_attr description = the type of server connection that should be established. If specified, it should be one of "dedicated", "shared" or "pooled" [cclass] type = str decorator = _description_attr description = connection class to use for Database Resident Connection Pooling (DRCP) [purity] type = int default = oracledb.PURITY_DEFAULT decorator = _description_attr description = purity to use for Database Resident Connection Pooling (DRCP) [expire_time] type = int default = 0 decorator = _description_attr description = an integer indicating the number of minutes between the sending of keepalive probes. If this parameter is set to a value greater than zero it enables keepalive [retry_count] type = int default = 0 decorator = _description_attr description = the number of times that a connection attempt should be retried before the attempt is terminated [retry_delay] type = int default = 0 decorator = _description_attr description = the number of seconds to wait before making a new connection attempt [tcp_connect_timeout] type = float default = 60.0 decorator = _description_attr description = a float indicating the maximum number of seconds to wait for establishing a connection to the database host [ssl_server_dn_match] type = bool default = True decorator = _description_attr description = boolean indicating whether the server certificate distinguished name (DN) should be matched in addition to the regular certificate verification that is performed. Note that if the ssl_server_cert_dn parameter is not privided, host name matching is performed instead [ssl_server_cert_dn] type = str decorator = _description_attr description = the distinguished name (DN) which should be matched with the server. This value is ignored if the ssl_server_dn_match parameter is not set to the value True. If specified this value is used for any verfication. Otherwise the hostname will be used. [wallet_location] type = str decorator = _description_attr description = the directory where the wallet can be found. In thin mode this must be the directory containing the PEM-encoded wallet file ewallet.pem. In thick mode this must be the directory containing the file cwallet.sso [events] type = bool default = False description = boolean specifying whether events mode should be enabled. This value is only used in thick mode and is needed for continuous query notification and high availability event notifications [externalauth] type = bool default = False description = a boolean indicating whether to use external authentication [mode] type = int default = oracledb.AUTH_MODE_DEFAULT description = authorization mode to use. For example oracledb.AUTH_MODE_SYSDBA [disable_oob] type = bool default = False description = boolean indicating whether out-of-band breaks should be disabled. This value is only used in thin mode. It has no effect on Windows which does not support this functionality [stmtcachesize] type = int default = oracledb.defaults.stmtcachesize description = identifies the initial size of the statement cache [edition] type = str description = edition to use for the connection. This parameter cannot be used simultaneously with the cclass parameter [tag] type = str description = identifies the type of connection that should be returned from a pool. This value is only used in thick mode [matchanytag] type = bool default: False description = boolean specifying whether any tag can be used when acquiring a connection from the pool. This value is only used in thick mode. [config_dir] type = str default: oracledb.defaults.config_dir description = directory in which the optional tnsnames.ora configuration file is located. This value is only used in thin mode. For thick mode use the config_dir parameter of init_oracle_client() [appcontext] type = list description = application context used by the connection. It should be a list of 3-tuples (namespace, name, value) and each entry in the tuple should be a string. This value is only used in thick mode [shardingkey] type = list description = a list of strings, numbers, bytes or dates that identify the database shard to connect to. This value is only used in thick mode [supershardingkey] type = list description = a list of strings, numbers, bytes or dates that identify the database shard to connect to. This value is only used in thick mode [debug_jdwp] type = str description = a string with the format "host=;port=" that specifies the host and port of the PL/SQL debugger. This value is only used in thin mode. For thick mode set the ORA_DEBUG_JDWP environment variable [handle] type = int default = 0 hidden = True description = an integer representing a pointer to a valid service context handle. This value is only used in thick mode. It should be used with extreme caution [threaded] type = bool default = True hidden = True [encoding] type = str hidden = True [nencoding] type = str hidden = True [waitTimeout] type = int hidden = True pool_only = True [maxLifetimeSession] type = int hidden = True pool_only = True [maxSessionsPerShard] type = int hidden = True pool_only = True [sessionCallback] type = Callable hidden = True pool_only = True python-oracledb-1.2.1/utils/templates/000077500000000000000000000000001434177474600177645ustar00rootroot00000000000000python-oracledb-1.2.1/utils/templates/connect_params.py000066400000000000000000000100661434177474600233350ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2021, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # connect_params.py # # Contains the ConnectParams class used for managing the parameters required to # establish a connection to the database. # # #{{ generated_notice }} #------------------------------------------------------------------------------ import functools from typing import Union, Callable import oracledb from . import base_impl, constants, errors, utils class ConnectParams: """ Contains all parameters used for establishing a connection to the database. """ __module__ = oracledb.__name__ __slots__ = ["_impl"] _impl_class = base_impl.ConnectParamsImpl @utils.params_initer def __init__(self, *, #{{ args_with_defaults }} ): """ All parameters are optional. A brief description of each parameter follows: #{{ args_help_with_defaults }} """ pass def __repr__(self): return self.__class__.__qualname__ + "(" + \ #{{ params_repr_parts }} + \ ")" def _address_attr(f): """ Helper function used to get address level attributes. """ @functools.wraps(f) def wrapped(self): values = [getattr(a, f.__name__) \ for a in self._impl._get_addresses()] return values if len(values) > 1 else values[0] return wrapped def _description_attr(f): """ Helper function used to get description level attributes. """ @functools.wraps(f) def wrapped(self): values = [getattr(d, f.__name__) \ for d in self._impl.description_list.descriptions] return values if len(values) > 1 else values[0] return wrapped #{{ params_properties }} def copy(self) -> "ConnectParams": """ Creates a copy of the parameters and returns it. """ params = ConnectParams.__new__(ConnectParams) params._impl = self._impl.copy() return params def get_connect_string(self) -> str: """ Returns a connect string generated from the parameters. """ return self._impl.get_connect_string() def parse_connect_string(self, connect_string: str) -> None: """ Parses the connect string into its components and stores the parameters. The connect string could be an Easy Connect string, name-value pairs or a simple alias which is looked up in tnsnames.ora. Any parameters found in the connect string override any currently stored values. """ self._impl.parse_connect_string(connect_string) @utils.params_setter def set(self, *, #{{ args_without_defaults }} ): """ All parameters are optional. A brief description of each parameter follows: #{{ args_help_without_defaults }} """ pass python-oracledb-1.2.1/utils/templates/connection.py000066400000000000000000001204631434177474600225030ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # connection.py # # Contains the Connection class and the factory method connect() used for # establishing connections to the database. # # #{{ generated_notice }} #------------------------------------------------------------------------------ import collections import functools import oracledb from . import __name__ as MODULE_NAME from typing import Any, Callable, Type, Union from . import constants, driver_mode, errors, exceptions, utils from . import base_impl, thick_impl, thin_impl from . import pool as pool_module from .defaults import defaults from .connect_params import ConnectParams from .cursor import Cursor from .lob import LOB from .subscr import Subscription from .aq import Queue, MessageProperties from .soda import SodaDatabase from .dbobject import DbObjectType, DbObject from .base_impl import DB_TYPE_BLOB, DB_TYPE_CLOB, DB_TYPE_NCLOB, DbType # named tuple used for representing global transactions Xid = collections.namedtuple("Xid", ["format_id", "global_transaction_id", "branch_qualifier"]) class Connection: __module__ = MODULE_NAME def __init__(self, dsn: str=None, *, pool: "pool_module.ConnectionPool"=None, params: ConnectParams=None, **kwargs) -> None: """ Constructor for creating a connection to the database. The dsn parameter (data source name) can be a string in the format user/password@connect_string or can simply be the connect string (in which case authentication credentials such as the username and password need to be specified separately). See the documentation on connection strings for more information. The pool parameter is expected to be a pool object and the use of this parameter is the equivalent of calling acquire() on the pool. The params parameter is expected to be of type ConnectParams and contains connection parameters that will be used when establishing the connection. See the documentation on ConnectParams for more information. If this parameter is not specified, the additional keyword parameters will be used to create an instance of ConnectParams. If both the params parameter and additional keyword parameters are specified, the values in the keyword parameters have precedence. Note that if a dsn is also supplied, then in the python-oracledb Thin mode, the values of the parameters specified (if any) within the dsn will override the values passed as additional keyword parameters, which themselves override the values set in the params parameter object. """ # if this variable is not present, exceptions raised during # construction can result in cascading exceptions; the __repr__() # method depends on this variable being present, too, so make it # available first thing self._impl = None # determine if thin mode is being used with driver_mode.get_manager() as mode_mgr: thin = mode_mgr.thin # determine which connection parameters to use if params is None: params_impl = base_impl.ConnectParamsImpl() elif not isinstance(params, ConnectParams): errors._raise_err(errors.ERR_INVALID_CONNECT_PARAMS) else: params_impl = params._impl.copy() if kwargs: params_impl.set(kwargs) if dsn is not None: dsn = params_impl.parse_dsn(dsn, thin) if dsn is None: dsn = params_impl.get_connect_string() # see if connection is being acquired from a pool if pool is None: pool_impl = None elif not isinstance(pool, pool_module.ConnectionPool): message = "pool must be an instance of oracledb.ConnectionPool" raise TypeError(message) else: pool._verify_open() pool_impl = pool._impl # create thin or thick implementation object if thin: if pool is not None: impl = pool_impl.acquire(params_impl) else: impl = thin_impl.ThinConnImpl(dsn, params_impl) impl.connect(params_impl) else: impl = thick_impl.ThickConnImpl(dsn, params_impl) impl.connect(params_impl, pool_impl) self._impl = impl self._version = None # invoke callback, if applicable if impl.invoke_session_callback and pool is not None \ and pool.session_callback is not None \ and callable(pool.session_callback): pool.session_callback(self, params_impl.tag) impl.invoke_session_callback = False def __del__(self): if self._impl is not None: self._impl.close(in_del=True) self._impl = None def __enter__(self): self._verify_connected() return self def __exit__(self, exc_type, exc_value, exc_tb): self.close() def __repr__(self): typ = type(self) cls_name = f"{typ.__module__}.{typ.__qualname__}" if self._impl is None: return f"<{cls_name} disconnected>" elif self.username is None: return f"<{cls_name} to externally identified user>" return f"<{cls_name} to {self.username}@{self.dsn}>" def _get_oci_attr(self, handle_type: int, attr_num: int, attr_type: int) -> Any: """ Returns the value of the specified OCI attribute from the internal handle. This is only supported in python-oracledb thick mode and should only be used as directed by Oracle. """ self._verify_connected() return self._impl._get_oci_attr(handle_type, attr_num, attr_type) def _set_oci_attr(self, handle_type: int, attr_num: int, attr_type: int, value: Any) -> None: """ Sets the value of the specified OCI attribute on the internal handle. This is only supported in python-oracledb thick mode and should only be used as directed by Oracle. """ self._verify_connected() self._impl._set_oci_attr(handle_type, attr_num, attr_type, value) def _verify_connected(self) -> None: """ Verifies that the connection is connected to the database. If it is not, an exception is raised. """ if self._impl is None: errors._raise_err(errors.ERR_NOT_CONNECTED) def _verify_xid(self, xid: Xid) -> None: """ Verifies that the supplied xid is of the correct type. """ if not isinstance(xid, Xid): message = "expecting transaction id created with xid()" raise TypeError(message) @property def action(self) -> None: raise AttributeError("action is not readable") @action.setter def action(self, value: str) -> None: """ Specifies the action column in the v$session table. It is a string attribute and cannot be set to None -- use the empty string instead. """ self._verify_connected() self._impl.set_action(value) @property def autocommit(self) -> bool: """ Specifies whether autocommit mode is on or off. When autocommit mode is on, all statements are committed as soon as they have completed executing successfully. """ self._verify_connected() return self._impl.autocommit @autocommit.setter def autocommit(self, value: bool) -> None: self._verify_connected() self._impl.autocommit = value def begin(self, format_id: int=-1, transaction_id: str="", branch_id: str="") -> None: """ Deprecated. Use tpc_begin() instead. """ if format_id != -1: self.tpc_begin(self.xid(format_id, transaction_id, branch_id)) @property def call_timeout(self) -> int: """ Specifies the amount of time (in milliseconds) that a single round-trip to the database may take before a timeout will occur. A value of 0 means that no timeout will take place. """ self._verify_connected() return self._impl.get_call_timeout() @call_timeout.setter def call_timeout(self, value: int) -> None: self._verify_connected() self._impl.set_call_timeout(value) @property def callTimeout(self) -> int: """ Deprecated. Use property call_timeout instead. """ return self.call_timeout @callTimeout.setter def callTimeout(self, value: int) -> None: self._verify_connected() self._impl.set_call_timeout(value) def cancel(self) -> None: """ Break a long-running transaction. """ self._verify_connected() self._impl.cancel() def changepassword(self, old_password: str, new_password: str) -> None: """ Changes the password for the user to which the connection is connected. """ self._verify_connected() self._impl.change_password(old_password, new_password) @property def client_identifier(self) -> None: raise AttributeError("client_identifier is not readable") @client_identifier.setter def client_identifier(self, value: str) -> None: """ Specifies the client_identifier column in the v$session table. """ self._verify_connected() self._impl.set_client_identifier(value) @property def clientinfo(self) -> None: raise AttributeError("clientinfo is not readable") @clientinfo.setter def clientinfo(self, value: str) -> None: """ Specifies the client_info column in the v$session table. """ self._verify_connected() self._impl.set_client_info(value) def close(self) -> None: """ Closes the connection and makes it unusable for further operations. An Error exception will be raised if any operation is attempted with this connection after this method completes successfully. """ self._verify_connected() self._impl.close() self._impl = None def commit(self) -> None: """ Commits any pending transactions to the database. """ self._verify_connected() self._impl.commit() def createlob(self, lob_type: DbType) -> LOB: """ Create and return a new temporary LOB of the specified type. """ self._verify_connected() if lob_type not in (DB_TYPE_CLOB, DB_TYPE_NCLOB, DB_TYPE_BLOB): message = "parameter should be one of oracledb.DB_TYPE_CLOB, " \ "oracledb.DB_TYPE_BLOB or oracledb.DB_TYPE_NCLOB" raise TypeError(message) impl = self._impl.create_temp_lob_impl(lob_type) return LOB._from_impl(impl) @property def current_schema(self) -> str: """ Specifies the current schema for the session. Setting this value is the same as executing the SQL statement "ALTER SESSION SET CURRENT_SCHEMA". The attribute is set (and verified) on the next call that does a round trip to the server. The value is placed before unqualified database objects in SQL statements you then execute. """ self._verify_connected() return self._impl.get_current_schema() @current_schema.setter def current_schema(self, value: str) -> None: self._verify_connected() self._impl.set_current_schema(value) def cursor(self, scrollable: bool=False) -> Cursor: """ Returns a cursor associated with the connection. """ self._verify_connected() return Cursor(self, scrollable) @property def dbop(self) -> None: raise AttributeError("dbop is not readable") @dbop.setter def dbop(self, value: str) -> None: """ Specifies the database operation that is to be monitored. This can be viewed in the DBOP_NAME column of the V$SQL_MONITOR table. """ self._verify_connected() self._impl.set_dbop(value) @property def dsn(self) -> str: """ Specifies the connection string (TNS entry) of the database to which a connection has been established. """ self._verify_connected() return self._impl.dsn @property def econtext_id(self) -> None: raise AttributeError("econtext_id is not readable") @econtext_id.setter def econtext_id(self, value: str) -> None: """ Specifies the execution context id. This value can be found as ecid in the v$session table and econtext_id in the auditing tables. The maximum length is 64 bytes. """ self._verify_connected() self._impl.set_econtext_id(value) @property def edition(self) -> str: """ Specifies the session edition. """ self._verify_connected() return self._impl.get_edition() @property def encoding(self) -> str: """ Specifies the IANA character set name of the character set in use. This is always the value "UTF-8". """ return "UTF-8" @property def external_name(self) -> str: """ Specifies the external name that is used by the connection when logging distributed transactions. """ self._verify_connected() return self._impl.get_external_name() @external_name.setter def external_name(self, value: str) -> None: self._verify_connected() self._impl.set_external_name(value) def getSodaDatabase(self) -> SodaDatabase: """ Return a SODA database object for performing all operations on Simple Oracle Document Access (SODA). """ self._verify_connected() db_impl = self._impl.create_soda_database_impl(self) return SodaDatabase._from_impl(self, db_impl) def gettype(self, name: str) -> DbObjectType: """ Return a type object given its name. This can then be used to create objects which can be bound to cursors created by this connection. """ self._verify_connected() obj_type_impl = self._impl.get_type(self, name) return DbObjectType._from_impl(obj_type_impl) @property def handle(self) -> int: """ Returns the OCI service context handle for the connection. It is primarily provided to facilitate testing the creation of a connection using the OCI service context handle. This property is only relevant to python-oracledb's thick mode. """ self._verify_connected() return self._impl.get_handle() @property def inputtypehandler(self) -> Callable: """ Specifies a method called for each value that is bound to a statement executed on any cursor associated with this connection. The method signature is handler(cursor, value, arraysize) and the return value is expected to be a variable object or None in which case a default variable object will be created. If this attribute is None, the default behavior will take place for all values bound to statements. """ self._verify_connected() return self._impl.inputtypehandler @inputtypehandler.setter def inputtypehandler(self, value: Callable) -> None: self._verify_connected() self._impl.inputtypehandler = value @property def internal_name(self) -> str: """ Specifies the internal name that is used by the connection when logging distributed transactions. """ self._verify_connected() return self._impl.get_internal_name() @internal_name.setter def internal_name(self, value: str) -> None: self._verify_connected() self._impl.set_internal_name(value) def is_healthy(self) -> bool: """ Returns a boolean indicating the health status of a connection. Connections may become unusable in several cases, such as if the network socket is broken, if an Oracle error indicates the connection is unusable, or after receiving a planned down notification from the database. This function is best used before starting a new database request on an existing standalone connection. Pooled connections internally perform this check before returning a connection to the application. If this function returns False, the connection should be not be used by the application and a new connection should be established instead. This function performs a local check. To fully check a connection's health, use ping() which performs a round-trip to the database. """ return self._impl is not None and self._impl.get_is_healthy() @property def ltxid(self) -> bytes: """ Returns the logical transaction id for the connection. It is used within Oracle Transaction Guard as a means of ensuring that transactions are not duplicated. See the Oracle documentation and the provided sample for more information. """ self._verify_connected() return self._impl.get_ltxid() @property def maxBytesPerCharacter(self) -> int: """ Deprecated. Use the constant value 4 instead. """ return 4 @property def module(self) -> None: raise AttributeError("module is not readable") @module.setter def module(self, value: str) -> None: """ Specifies the module column in the v$session table. The maximum length for this string is 48 and if you exceed this length you will get ORA-24960. """ self._verify_connected() self._impl.set_module(value) def msgproperties(self, payload: Union[bytes, DbObject]=None, correlation: str=None, delay: int=None, exceptionq: str=None, expiration: int=None, priority: int=None, recipients: list=None) -> MessageProperties: """ Create and return a message properties object. If the parameters are not None, they act as a shortcut for setting each of the equivalently named properties. """ impl = self._impl.create_msg_props_impl() props = MessageProperties._from_impl(impl) if payload is not None: props.payload = payload if correlation is not None: props.correlation = correlation if delay is not None: props.delay = delay if exceptionq is not None: props.exceptionq = exceptionq if expiration is not None: props.expiration = expiration if priority is not None: props.priority = priority if recipients is not None: props.recipients = recipients return props @property def nencoding(self) -> str: """ Specifies the IANA character set name of the national character set in use. This is always the value "UTF-8". """ return "UTF-8" @property def outputtypehandler(self) -> Callable: """ Specifies a method called for each column that is going to be fetched from any cursor associated with this connection. The method signature is handler(cursor, name, defaultType, length, precision, scale) and the return value is expected to be a variable object or None in which case a default variable object will be created. If this attribute is None, the default behavior will take place for all columns fetched from cursors associated with this connection. """ self._verify_connected() return self._impl.outputtypehandler @outputtypehandler.setter def outputtypehandler(self, value: Callable) -> None: self._verify_connected() self._impl.outputtypehandler = value def ping(self) -> None: """ Pings the database to verify the connection is valid. """ self._verify_connected() self._impl.ping() def prepare(self) -> bool: """ Deprecated. Use tpc_prepare() instead. """ return self.tpc_prepare() def queue(self, name: str, payload_type: Union[DbObjectType, str]=None, *, payloadType: DbObjectType=None) -> Queue: """ Creates and returns a queue which is used to enqueue and dequeue messages in Advanced Queueing (AQ). The name parameter is expected to be a string identifying the queue in which messages are to be enqueued or dequeued. The payload_type parameter, if specified, is expected to be an object type that identifies the type of payload the queue expects. If the string "JSON" is specified, JSON data is enqueued and dequeued. If not specified, RAW data is enqueued and dequeued. """ self._verify_connected() payload_type_impl = None is_json = False if payloadType is not None: if payload_type is not None: errors._raise_err(errors.ERR_DUPLICATED_PARAMETER, deprecated_name="payloadType", new_name="payload_type") payload_type = payloadType if payload_type is not None: if payload_type == "JSON": is_json = True elif not isinstance(payload_type, DbObjectType): raise TypeError("expecting DbObjectType") else: payload_type_impl = payload_type._impl impl = self._impl.create_queue_impl() impl.initialize(self._impl, name, payload_type_impl, is_json) return Queue._from_impl(self, impl) def rollback(self) -> None: """ Rolls back any pending transactions. """ self._verify_connected() self._impl.rollback() def shutdown(self, mode: int=0) -> None: """ Shutdown the database. In order to do this the connection must be connected as SYSDBA or SYSOPER. Two calls must be made unless the mode specified is DBSHUTDOWN_ABORT. """ self._verify_connected() self._impl.shutdown(mode) def startup(self, force: bool=False, restrict: bool=False, pfile: str=None) -> None: """ Startup the database. This is equivalent to the SQL*Plus command “startup nomount”. The connection must be connected as SYSDBA or SYSOPER with the PRELIM_AUTH option specified for this to work. The pfile parameter, if specified, is expected to be a string identifying the location of the parameter file (PFILE) which will be used instead of the stored parameter file (SPFILE). """ self._verify_connected() self._impl.startup(force, restrict, pfile) @property def stmtcachesize(self) -> int: """ Specifies the size of the statement cache. This value can make a significant difference in performance (up to 100x) if you have a small number of statements that you execute repeatedly. """ self._verify_connected() return self._impl.get_stmt_cache_size() @stmtcachesize.setter def stmtcachesize(self, value: int) -> None: self._verify_connected() self._impl.set_stmt_cache_size(value) def subscribe(self, namespace: int=constants.SUBSCR_NAMESPACE_DBCHANGE, protocol: int=constants.SUBSCR_PROTO_CALLBACK, callback: Callable=None, timeout: int=0, operations: int=constants.OPCODE_ALLOPS, port: int=0, qos: int=constants.SUBSCR_QOS_DEFAULT, ip_address: str=None, grouping_class: int=constants.SUBSCR_GROUPING_CLASS_NONE, grouping_value: int=0, grouping_type: int=constants.SUBSCR_GROUPING_TYPE_SUMMARY, name: str=None, client_initiated: bool=False, *, ipAddress: str=None, groupingClass: int=constants.SUBSCR_GROUPING_CLASS_NONE, groupingValue: int=0, groupingType: int=constants.SUBSCR_GROUPING_TYPE_SUMMARY, clientInitiated: bool=False) -> Subscription: """ Return a new subscription object that receives notification for events that take place in the database that match the given parameters. The namespace parameter specifies the namespace the subscription uses. It can be one of SUBSCR_NAMESPACE_DBCHANGE or SUBSCR_NAMESPACE_AQ. The protocol parameter specifies the protocol to use when notifications are sent. Currently the only valid value is SUBSCR_PROTO_CALLBACK. The callback is expected to be a callable that accepts a single parameter. A message object is passed to this callback whenever a notification is received. The timeout value specifies that the subscription expires after the given time in seconds. The default value of 0 indicates that the subscription never expires. The operations parameter enables filtering of the messages that are sent (insert, update, delete). The default value will send notifications for all operations. This parameter is only used when the namespace is set to SUBSCR_NAMESPACE_DBCHANGE. The port parameter specifies the listening port for callback notifications from the database server. If not specified, an unused port will be selected by the Oracle Client libraries. The qos parameter specifies quality of service options. It should be one or more of the following flags, OR'ed together: SUBSCR_QOS_RELIABLE, SUBSCR_QOS_DEREG_NFY, SUBSCR_QOS_ROWIDS, SUBSCR_QOS_QUERY, SUBSCR_QOS_BEST_EFFORT. The ip_address parameter specifies the IP address (IPv4 or IPv6) in standard string notation to bind for callback notifications from the database server. If not specified, the client IP address will be determined by the Oracle Client libraries. The grouping_class parameter specifies what type of grouping of notifications should take place. Currently, if set, this value can only be set to the value SUBSCR_GROUPING_CLASS_TIME, which will group notifications by the number of seconds specified in the grouping_value parameter. The grouping_type parameter should be one of the values SUBSCR_GROUPING_TYPE_SUMMARY (the default) or SUBSCR_GROUPING_TYPE_LAST. The name parameter is used to identify the subscription and is specific to the selected namespace. If the namespace parameter is SUBSCR_NAMESPACE_DBCHANGE then the name is optional and can be any value. If the namespace parameter is SUBSCR_NAMESPACE_AQ, however, the name must be in the format '' for single consumer queues and ':' for multiple consumer queues, and identifies the queue that will be monitored for messages. The queue name may include the schema, if needed. The client_initiated parameter is used to determine if client initiated connections or server initiated connections (the default) will be established. Client initiated connections are only available in Oracle Client 19.4 and Oracle Database 19.4 and higher. """ self._verify_connected() if ipAddress is not None: if ip_address is not None: errors._raise_err(errors.ERR_DUPLICATED_PARAMETER, deprecated_name="ipAddress", new_name="ip_address") ip_address = ipAddress if groupingClass != constants.SUBSCR_GROUPING_CLASS_NONE: if grouping_class != constants.SUBSCR_GROUPING_CLASS_NONE: errors._raise_err(errors.ERR_DUPLICATED_PARAMETER, deprecated_name="groupingClass", new_name="grouping_class") grouping_class = groupingClass if groupingValue != 0: if grouping_value != 0: errors._raise_err(errors.ERR_DUPLICATED_PARAMETER, deprecated_name="groupingValue", new_name="grouping_value") grouping_value = groupingValue if groupingType != constants.SUBSCR_GROUPING_TYPE_SUMMARY: if grouping_type != constants.SUBSCR_GROUPING_TYPE_SUMMARY: errors._raise_err(errors.ERR_DUPLICATED_PARAMETER, deprecated_name="groupingType", new_name="grouping_type") grouping_type = groupingType if clientInitiated: if client_initiated: errors._raise_err(errors.ERR_DUPLICATED_PARAMETER, deprecated_name="clientInitiated", new_name="client_initiated") client_initiated = clientInitiated impl = self._impl.create_subscr_impl(self, callback, namespace, name, protocol, ip_address, port, timeout, operations, qos, grouping_class, grouping_value, grouping_type, client_initiated) subscr = Subscription._from_impl(impl) impl.subscribe(subscr, self._impl) return subscr @property def tag(self) -> str: """ This property initially contains the actual tag of the session that was acquired from a pool. If the connection was not acquired from a pool or no tagging parameters were specified (tag and matchanytag) when the connection was acquired from the pool, this value will be None. If the value is changed, it must be a string containing name=value pairs like “k1=v1;k2=v2”. If this value is not None when the connection is released back to the pool it will be used to retag the session. This value can be overridden in the call to SessionPool.release(). """ self._verify_connected() return self._impl.tag @tag.setter def tag(self, value: str) -> None: self._verify_connected() self._impl.tag = value @property def thin(self) -> bool: """ Returns a boolean indicating if the connection was established in python-oracledb's thin mode (True) or thick mode (False). """ self._verify_connected() return isinstance(self._impl, thin_impl.ThinConnImpl) @property def tnsentry(self) -> str: """ Deprecated. Use dsn property instead. """ return self.dsn def tpc_begin(self, xid: Xid, flags: int=constants.TPC_BEGIN_NEW, timeout: int=0) -> None: """ Begins a TPC (two-phase commit) transaction with the given transaction id. This method should be called outside of a transaction (i.e. nothing may have executed since the last commit() or rollback() was performed). """ self._verify_connected() self._verify_xid(xid) self._impl.tpc_begin(xid, flags, timeout) def tpc_commit(self, xid: Xid=None, one_phase: bool=False) -> None: """ Prepare the global transaction for commit. Return a boolean indicating if a transaction was actually prepared in order to avoid the error ORA-24756 (transaction does not exist). When called with no arguments, commits a transaction previously prepared with tpc_prepare(). If tpc_prepare() is not called, a single phase commit is performed. A transaction manager may choose to do this if only a single resource is participating in the global transaction. When called with a transaction id, the database commits the given transaction. This form should be called outside of a transaction and is intended for use in recovery. """ self._verify_connected() if xid is not None: self._verify_xid(xid) self._impl.tpc_commit(xid, one_phase) def tpc_end(self, xid: Xid=None, flags: int=constants.TPC_END_NORMAL) -> None: """ Ends (detaches from) a TPC (two-phase commit) transaction. """ self._verify_connected() if xid is not None: self._verify_xid(xid) self._impl.tpc_end(xid, flags) def tpc_forget(self, xid: Xid) -> None: """ Forgets a TPC (two-phase commit) transaction. """ self._verify_connected() self._verify_xid(xid) self._impl.tpc_forget(xid) def tpc_prepare(self, xid: Xid=None) -> bool: """ Prepares a global transaction for commit. After calling this function, no further activity should take place on this connection until either tpc_commit() or tpc_rollback() have been called. A boolean is returned indicating whether a commit is needed or not. If a commit is performed when one is not needed the error ORA-24756: transaction does not exist is raised. """ self._verify_connected() if xid is not None: self._verify_xid(xid) return self._impl.tpc_prepare(xid) def tpc_recover(self) -> list: """ Returns a list of pending transaction ids suitable for use with tpc_commit() or tpc_rollback(). This function requires select privilege on the view DBA_PENDING_TRANSACTIONS. """ with self.cursor() as cursor: cursor.rowfactory = Xid cursor.execute(""" select formatid, globalid, branchid from dba_pending_transactions""") return cursor.fetchall() def tpc_rollback(self, xid: Xid=None) -> None: """ When called with no arguments, rolls back the transaction previously started with tpc_begin(). When called with a transaction id, the database rolls back the given transaction. This form should be called outside of a transaction and is intended for use in recovery. """ self._verify_connected() if xid is not None: self._verify_xid(xid) self._impl.tpc_rollback(xid) def unsubscribe(self, subscr: Subscription) -> None: """ Unsubscribe from events in the database that were originally subscribed to using subscribe(). The connection used to unsubscribe should be the same one used to create the subscription, or should access the same database and be connected as the same user name. """ self._verify_connected() if not isinstance(subscr, Subscription): raise TypeError("expecting subscription") subscr._impl.unsubscribe(self._impl) @property def username(self) -> str: """ Returns the name of the user which established the connection to the database. """ self._verify_connected() return self._impl.username @property def version(self) -> str: """ Returns the version of the database to which the connection has been established. """ if self._version is None: self._verify_connected() self._version = self._impl.get_version() return self._version def xid(self, format_id: int, global_transaction_id: Union[bytes, str], branch_qualifier: Union[bytes, str]) -> Xid: """ Returns a global transaction identifier that can be used with the TPC (two-phase commit) functions. The format_id parameter should be a non-negative 32-bit integer. The global_transaction_id and branch_qualifier parameters should be bytes (or a string which will be UTF-8 encoded to bytes) of no more than 64 bytes. """ return Xid(format_id, global_transaction_id, branch_qualifier) def _connection_factory(f): """ Decorator which checks the validity of the supplied keyword parameters by calling the original function (which does nothing), then creates and returns an instance of the requested Connection class. The base Connection class constructor does not check the validity of the supplied keyword parameters. """ @functools.wraps(f) def connect(dsn: str=None, *, pool: "pool_module.ConnectionPool"=None, conn_class: Type[Connection]=Connection, params: ConnectParams=None, **kwargs) -> Connection: f(dsn=dsn, pool=pool, conn_class=conn_class, params=params, **kwargs) if not issubclass(conn_class, Connection): errors._raise_err(errors.ERR_INVALID_CONN_CLASS) return conn_class(dsn=dsn, pool=pool, params=params, **kwargs) return connect @_connection_factory def connect(dsn: str=None, *, pool: "pool_module.ConnectionPool"=None, conn_class: Type[Connection]=Connection, params: ConnectParams=None, #{{ args_with_defaults }} ) -> Connection: """ Factory function which creates a connection to the database and returns it. The dsn parameter (data source name) can be a string in the format user/password@connect_string or can simply be the connect string (in which case authentication credentials such as the username and password need to be specified separately). See the documentation on connection strings for more information. The pool parameter is expected to be a pool object and the use of this parameter is the equivalent of calling pool.acquire(). The conn_class parameter is expected to be Connection or a subclass of Connection. The params parameter is expected to be of type ConnectParams and contains connection parameters that will be used when establishing the connection. See the documentation on ConnectParams for more information. If this parameter is not specified, the additional keyword parameters will be used to create an instance of ConnectParams. If both the params parameter and additional keyword parameters are specified, the values in the keyword parameters have precedence. Note that if a dsn is also supplied, then in the python-oracledb Thin mode, the values of the parameters specified (if any) within the dsn will override the values passed as additional keyword parameters, which themselves override the values set in the params parameter object. The following parameters are all optional. A brief description of each parameter follows: #{{ args_help_with_defaults }} """ pass python-oracledb-1.2.1/utils/templates/pool.py000066400000000000000000000564641434177474600213260ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2020, 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # pool.py # # Contains the ConnectionPool class and the factory method create_pool() used # for creating connection pools. # # #{{ generated_notice }} #------------------------------------------------------------------------------ import functools from typing import Callable, Type, Union import oracledb from . import errors, exceptions, utils from . import base_impl, thick_impl, thin_impl from . import connection as connection_module from . import driver_mode from .connect_params import ConnectParams from .pool_params import PoolParams from .defaults import defaults class ConnectionPool: __module__ = oracledb.__name__ def __init__(self, dsn: str=None, *, params: PoolParams=None, **kwargs) -> None: """ Constructor for creating a connection pool. Connection pooling creates a pool of available connections to the database, allowing applications to acquire a connection very quickly. It is of primary use in a server where connections are requested in rapid succession and used for a short period of time, for example in a web server. The dsn parameter (data source name) can be a string in the format user/password@connect_string or can simply be the connect string (in which case authentication credentials such as the username and password need to be specified separately). See the documentation on connection strings for more information. The params parameter is expected to be of type PoolParams and contains parameters that are used to create the pool. See the documentation on PoolParams for more information. If this parameter is not specified, the additional keyword parameters will be used to create an instance of PoolParams. If both the params parameter and additional keyword parameters are specified, the values in the keyword parameters have precedence. Note that if a dsn is also supplied, then in the python-oracledb Thin mode, the values of the parameters specified (if any) within the dsn will override the values passed as additional keyword parameters, which themselves override the values set in the params parameter object. """ self._impl = None if params is None: params_impl = base_impl.PoolParamsImpl() elif not isinstance(params, PoolParams): errors._raise_err(errors.ERR_INVALID_POOL_PARAMS) else: params_impl = params._impl.copy() if kwargs: params_impl.set(kwargs) self._connection_type = \ params_impl.connectiontype or connection_module.Connection with driver_mode.get_manager() as mode_mgr: thin = mode_mgr.thin if dsn is not None: dsn = params_impl.parse_dsn(dsn, thin) if dsn is None: dsn = params_impl.get_connect_string() if thin: impl = thin_impl.ThinPoolImpl(dsn, params_impl) else: impl = thick_impl.ThickPoolImpl(dsn, params_impl) self._impl = impl self.session_callback = params_impl.session_callback def __del__(self): if self._impl is not None: self._impl.close(True) self._impl = None def _verify_open(self) -> None: """ Verifies that the pool is open and able to perform its work. """ if self._impl is None: errors._raise_err(errors.ERR_POOL_NOT_OPEN) def acquire(self, user: str=None, password: str=None, cclass: str=None, purity: int=oracledb.PURITY_DEFAULT, tag: str=None, matchanytag: bool=False, shardingkey: list=None, supershardingkey: list=None) -> "connection_module.Connection": """ Acquire a connection from the pool and return it. If the pool is homogeneous, the user and password parameters cannot be specified. If they are, an exception will be raised. The cclass parameter, if specified, should be a string corresponding to the connection class for database resident connection pooling (DRCP). The purity parameter is expected to be one of PURITY_DEFAULT, PURITY_NEW, or PURITY_SELF. The tag parameter, if specified, is expected to be a string with name=value pairs like “k1=v1;k2=v2” and will limit the connections that can be returned from a pool unless the matchanytag parameter is set to True. In that case connections with the specified tag will be preferred over others, but if no such connections are available a connection with a different tag may be returned instead. In any case, untagged connections will always be returned if no connections with the specified tag are available. Connections are tagged when they are released back to the pool. The shardingkey and supershardingkey parameters, if specified, are expected to be a sequence of values which will be used to identify the database shard to connect to. The key values can be strings, numbers, bytes or dates. """ self._verify_open() return self._connection_type(user=user, password=password, cclass=cclass, purity=purity, tag=tag, matchanytag=matchanytag, shardingkey=shardingkey, supershardingkey=supershardingkey, pool=self) @property def busy(self) -> int: """ Returns the number of connections that have been acquired from the pool and have not yet been returned to the pool. """ self._verify_open() return self._impl.get_busy_count() def close(self, force: bool=False) -> None: """ Close the pool now, rather than when the last reference to it is released, which makes it unusable for further work. If any connections have been acquired and not released back to the pool, this method will fail unless the force parameter is set to True. """ self._verify_open() self._impl.close(force) self._impl = None def drop(self, connection: "connection_module.Connection") -> None: """ Drop the connection from the pool, which is useful if the connection is no longer usable (such as when the database session is killed). """ self._verify_open() if not isinstance(connection, connection_module.Connection): message = "connection must be an instance of " \ "oracledb.Connection" raise TypeError(message) connection._verify_connected() self._impl.drop(connection._impl) connection._impl = None @property def dsn(self) -> str: """ Returns the connection string (TNS entry) of the database to which connections in the pool have been established. """ self._verify_open() return self._impl.dsn @property def getmode(self) -> int: self._verify_open() return self._impl.get_getmode() @getmode.setter def getmode(self, value: int) -> None: self._verify_open() self._impl.set_getmode(value) @property def homogeneous(self) -> bool: """ Returns a boolean indicating if the pool is homogeneous or not. If the pool is not homogeneous, different authentication can be used for each connection acquired from the pool. """ self._verify_open() return self._impl.homogeneous @property def increment(self) -> int: """ Returns the number of connections that will be created when additional connections need to be created to satisfy requests. """ self._verify_open() return self._impl.increment @property def max(self) -> int: """ Returns the maximum number of connections that the pool can control. """ self._verify_open() return self._impl.max @property def max_lifetime_session(self) -> int: """ Returns the maximum length of time (in seconds) that a pooled connection may exist. Connections that are in use will not be closed. They become candidates for termination only when they are released back to the pool and have existed for longer than max_lifetime_session seconds. Note that termination only occurs when the pool is accessed. A value of 0 means that there is no maximum length of time that a pooled connection may exist. This attribute is only available in Oracle Database 12.1. """ self._verify_open() return self._impl.get_max_lifetime_session() @max_lifetime_session.setter def max_lifetime_session(self, value: int) -> None: self._verify_open() self._impl.set_max_lifetime_session(value) @property def max_sessions_per_shard(self) -> int: """ Returns the number of sessions that can be created per shard in the pool. Setting this attribute greater than zero specifies the maximum number of sessions in the pool that can be used for any given shard in a sharded database. This lets connections in the pool be balanced across the shards. A value of zero will not set any maximum number of sessions for each shard. This attribute is only available in Oracle Client 18.3 and higher. """ self._verify_open() return self._impl.get_max_sessions_per_shard() @max_sessions_per_shard.setter def max_sessions_per_shard(self, value: int) -> None: self._verify_open() self._impl.set_max_sessions_per_shard(value) @property def min(self) -> int: """ Returns the minimum number of connections that the pool will control. These are created when the pool is first created. """ self._verify_open() return self._impl.min @property def name(self) -> str: """ Returns the name assigned to the pool by Oracle. This attribute is only relevant in python-oracledb thick mode. """ self._verify_open() return self._impl.name @property def opened(self) -> int: """ Returns the number of connections currently opened by the pool. """ self._verify_open() return self._impl.get_open_count() @property def ping_interval(self) -> int: """ Returns the pool ping interval in seconds. When a connection is acquired from the pool, a check is first made to see how long it has been since the connection was put into the pool. If this idle time exceeds ping_interval, then a round-trip ping to the database is performed. If the connection is unusable, it is discarded and a different connection is selected to be returned by SessionPool.acquire(). Setting ping_interval to a negative value disables pinging. Setting it to 0 forces a ping for every aquire() and is not recommended. """ self._verify_open() return self._impl.get_ping_interval() @ping_interval.setter def ping_interval(self, value: int) -> None: self._impl.set_ping_interval(value) def release(self, connection: "connection_module.Connection", tag: str=None) -> None: """ Release the connection back to the pool now, rather than whenever __del__ is called. The connection will be unusable from this point forward; an Error exception will be raised if any operation is attempted with the connection. Any cursors or LOBs created by the connection will also be marked unusable and an Error exception will be raised if any operation is attempted with them. Internally, references to the connection are held by cursor objects, LOB objects, etc. Once all of these references are released, the connection itself will be released back to the pool automatically. Either control references to these related objects carefully or explicitly release connections back to the pool in order to ensure sufficient resources are available. If the tag is not None, it is expected to be a string with name=value pairs like “k1=v1;k2=v2” and will override the value in the property Connection.tag. If either Connection.tag or the tag parameter are not None, the connection will be retagged when it is released back to the pool. """ self._verify_open() if not isinstance(connection, connection_module.Connection): message = "connection must be an instance of " \ "oracledb.Connection" raise TypeError(message) if tag is not None: connection.tag = tag connection.close() def reconfigure(self, min: int=None, max: int=None, increment: int=None, getmode: int=None, timeout: int=None, wait_timeout: int=None, max_lifetime_session: int=None, max_sessions_per_shard: int=None, soda_metadata_cache: bool=None, stmtcachesize: int=None, ping_interval: int=None) -> None: """ Reconfigures various parameters of a connection pool. The pool size can be altered with reconfigure() by passing values for min, max or increment. The getmode, timeout, wait_timeout, max_lifetime_session, max_sessions_per_shard, soda_metadata_cache, stmtcachesize and ping_interval can be set directly or by using reconfigure(). All parameters are optional. Unspecified parameters will leave those pool attributes unchanged. The parameters are processed in two stages. After any size change has been processed, reconfiguration on the other parameters is done sequentially. If an error such as an invalid value occurs when changing one attribute, then an exception will be generated but any already changed attributes will retain their new values. During reconfiguration of a pool's size, the behavior of acquire() depends on the getmode in effect when acquire() is called: * With mode POOL_GETMODE_FORCEGET, an acquire() call will wait until the pool has been reconfigured. * With mode POOL_GETMODE__TIMEDWAIT, an acquire() call will try to acquire a connection in the time specified by pool.wait_timeout and return an error if the time taken exceeds that value. * With mode POOL_GETMODE_WAIT, an acquire() call will wait until after the pool has been reconfigured and a connection is available. * With mode POOL_GETMODE_NOWAIT, if the number of busy connections is less than the pool size, acquire() will return a new connection after pool reconfiguration is complete. Closing connections with pool.release() or connection.close() will wait until any pool size reconfiguration is complete. Closing the connection pool with pool.close() will wait until reconfiguration is complete. """ if min is None: min = self.min if max is None: max = self.max if increment is None: increment = self.increment if self.min != min or self.max != max or self.increment != increment: self._impl.reconfigure(min, max, increment) if getmode is not None: self.getmode = getmode if timeout is not None: self.timeout = timeout if wait_timeout is not None: self.wait_timeout = wait_timeout if max_lifetime_session is not None: self.max_lifetime_session = max_lifetime_session if max_sessions_per_shard is not None: self.max_sessions_per_shard = max_sessions_per_shard if soda_metadata_cache is not None: self.soda_metadata_cache = soda_metadata_cache if stmtcachesize is not None: self.stmtcachesize = stmtcachesize if ping_interval is not None: self.ping_interval = ping_interval @property def soda_metadata_cache(self) -> bool: """ Specifies whether the SODA metadata cache is enabled or not. Enabling the cache significantly improves the performance of methods SodaDatabase.createCollection() (when not specifying a value for the metadata parameter) and SodaDatabase.openCollection(). Note that the cache can become out of date if changes to the metadata of cached collections are made externally. """ self._verify_open() return self._impl.get_soda_metadata_cache() @soda_metadata_cache.setter def soda_metadata_cache(self, value: bool) -> None: if not isinstance(value, bool): message = "soda_metadata_cache must be a boolean value." raise TypeError(message) self._verify_open() self._impl.set_soda_metadata_cache(value) @property def stmtcachesize(self) -> int: """ Specifies the size of the statement cache that will be used as the starting point for any connections that are created by the pool. Once a connection is created, that connection’s statement cache size can only be changed by setting the stmtcachesize attribute on the connection itself. """ self._verify_open() return self._impl.get_stmt_cache_size() @stmtcachesize.setter def stmtcachesize(self, value: int) -> None: self._verify_open() self._impl.set_stmt_cache_size(value) @property def thin(self) -> bool: """ Returns a boolean indicating if the pool was created in python-oracledb's thin mode (True) or thick mode (False). """ self._verify_open() return isinstance(self._impl, thin_impl.ThinPoolImpl) @property def timeout(self) -> int: """ Specifies the time (in seconds) after which idle connections will be terminated in order to maintain an optimum number of open connections. A value of 0 means that no idle connections are terminated. Note that in thick mode with older Oracle Client libraries termination only occurs when the pool is accessed. """ self._verify_open() return self._impl.get_timeout() @timeout.setter def timeout(self, value: int) -> None: self._verify_open() self._impl.set_timeout(value) @property def tnsentry(self) -> str: """ Deprecated. Use dsn instead. """ return self.dsn @property def username(self) -> str: """ Returns the name of the user which was used to create the pool. """ self._verify_open() return self._impl.username @property def wait_timeout(self) -> int: """ Specifies the time (in milliseconds) that the caller should wait for a connection to become available in the pool before returning with an error. This value is only used if the getmode parameter used to create the pool was POOL_GETMODE_TIMEDWAIT. """ self._verify_open() return self._impl.get_wait_timeout() @wait_timeout.setter def wait_timeout(self, value: int) -> None: self._verify_open() self._impl.set_wait_timeout(value) def _pool_factory(f): """ Decorator which checks the validity of the supplied keyword parameters by calling the original function (which does nothing), then creates and returns an instance of the requested ConnectionPool class. The base ConnectionPool class constructor does not check the validity of the supplied keyword parameters. """ @functools.wraps(f) def create_pool(dsn: str=None, *, pool_class: Type[ConnectionPool]=ConnectionPool, params: PoolParams=None, **kwargs) -> ConnectionPool: f(dsn=dsn, pool_class=pool_class, params=params, **kwargs) if not issubclass(pool_class, ConnectionPool): errors._raise_err(errors.ERR_INVALID_POOL_CLASS) return pool_class(dsn, params=params, **kwargs) return create_pool @_pool_factory def create_pool(dsn: str=None, *, pool_class: Type[ConnectionPool]=ConnectionPool, params: PoolParams=None, #{{ args_with_defaults }} ) -> ConnectionPool: """ Creates a connection pool with the supplied parameters and returns it. The dsn parameter (data source name) can be a string in the format user/password@connect_string or can simply be the connect string (in which case authentication credentials such as the username and password need to be specified separately). See the documentation on connection strings for more information. The pool_class parameter is expected to be ConnectionPool or a subclass of ConnectionPool. The params parameter is expected to be of type PoolParams and contains parameters that are used to create the pool. See the documentation on PoolParams for more information. If this parameter is not specified, the additional keyword parameters will be used to create an instance of PoolParams. If both the params parameter and additional keyword parameters are specified, the values in the keyword parameters have precedence. Note that if a dsn is also supplied, then in the python-oracledb Thin mode, the values of the parameters specified (if any) within the dsn will override the values passed as additional keyword parameters, which themselves override the values set in the params parameter object. The following parameters are all optional. A brief description of each parameter follows: #{{ args_help_with_defaults }} """ pass python-oracledb-1.2.1/utils/templates/pool_params.py000066400000000000000000000053641434177474600226620ustar00rootroot00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2022, Oracle and/or its affiliates. # # This software is dual-licensed to you under the Universal Permissive License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose # either license. # # If you elect to accept the software under the Apache License, Version 2.0, # the following applies: # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # pool_params.py # # Contains the PoolParams class used for managing the parameters required to # create a connection pool. # # #{{ generated_notice }} #------------------------------------------------------------------------------ from typing import Callable, Type, Union import oracledb from . import base_impl, constants, errors, utils from . import connection as connection_module from .connect_params import ConnectParams class PoolParams(ConnectParams): """ Contains all parameters used for creating a connection pool. """ __module__ = oracledb.__name__ __slots__ = ["_impl"] _impl_class = base_impl.PoolParamsImpl @utils.params_initer def __init__(self, *, #{{ args_with_defaults }} ): """ All parameters are optional. A brief description of each parameter follows: #{{ args_help_with_defaults }} """ pass def __repr__(self): return self.__class__.__qualname__ + "(" + \ #{{ params_repr_parts }} + \ ")" #{{ params_properties }} def copy(self) -> "PoolParams": """ Creates a copy of the parameters and returns it. """ params = PoolParams.__new__(PoolParams) params._impl = self._impl.copy() return params @utils.params_setter def set(self, *, #{{ args_without_defaults }} ): """ All parameters are optional. A brief description of each parameter follows: #{{ args_help_without_defaults }} """ pass