diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f6a55da1..aac9c6d7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,9 +5,6 @@ on: tags: - "*" -permissions: - contents: write - jobs: build-sdist: runs-on: ubuntu-latest @@ -31,9 +28,10 @@ jobs: pip install -U setuptools wheel pip python setup.py sdist - - uses: actions/upload-artifact@v4 + - name: Upload sdist artifact + uses: actions/upload-artifact@v4 with: - name: dist-sdist + name: dist-sdist path: dist/*.tar.* build-wheels-matrix: @@ -90,79 +88,19 @@ jobs: env: CIBW_BUILD_VERBOSITY: 1 - - uses: actions/upload-artifact@v4 + - name: Upload wheel artifacts + uses: actions/upload-artifact@v4 with: name: dist-wheels-${{ matrix.only }} path: wheelhouse/*.whl - merge-artifacts: - runs-on: ubuntu-latest - needs: [build-sdist, build-wheels] - steps: - - name: Merge Artifacts - uses: actions/upload-artifact/merge@v4 - with: - name: dist - delete-merged: true - - publish-docs: - needs: [build-sdist, build-wheels] - runs-on: ubuntu-latest - - env: - PIP_DISABLE_PIP_VERSION_CHECK: 1 - - steps: - - name: Checkout source - uses: actions/checkout@v4 - with: - fetch-depth: 5 - submodules: true - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - - name: Build docs - run: | - pip install -e .[docs] - make htmldocs - - - name: Checkout gh-pages - uses: actions/checkout@v4 - with: - fetch-depth: 5 - ref: gh-pages - path: docs/gh-pages - - - name: Sync docs - run: | - rsync -a docs/_build/html/ docs/gh-pages/current/ - - - name: Commit and push docs - uses: magicstack/gha-commit-and-push@master - with: - target_branch: gh-pages - workdir: docs/gh-pages - commit_message: Automatic documentation update - github_token: ${{ secrets.RELEASE_BOT_GITHUB_TOKEN }} - ssh_key: ${{ secrets.RELEASE_BOT_SSH_KEY }} - gpg_key: ${{ secrets.RELEASE_BOT_GPG_KEY }} - gpg_key_id: "5C468778062D87BF!" - publish: - needs: [build-sdist, build-wheels, publish-docs] + needs: [build-sdist, build-wheels] runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/async_gaussdb - permissions: - id-token: write - attestations: write - contents: write - deployments: write steps: - uses: actions/checkout@v4 @@ -170,41 +108,45 @@ jobs: fetch-depth: 5 submodules: false - - uses: actions/download-artifact@v4 + - name: Download sdist artifact + uses: actions/download-artifact@v4 + with: + name: dist-sdist + path: dist/ + + - name: Download all wheel artifacts + uses: actions/download-artifact@v4 with: - name: dist + pattern: dist-wheels-* path: dist/ - name: Extract Release Version id: relver run: | set -e - # 从标签中提取版本 echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - - name: Merge and tag the PR - uses: edgedb/action-release/merge@master - with: - github_token: ${{ secrets.RELEASE_BOT_GITHUB_TOKEN }} - ssh_key: ${{ secrets.RELEASE_BOT_SSH_KEY }} - gpg_key: ${{ secrets.RELEASE_BOT_GPG_KEY }} - gpg_key_id: "5C468778062D87BF!" - tag_name: v${{ steps.relver.outputs.version }} - - - name: Publish Github Release - uses: elprans/gh-action-create-release@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Prepare all_dist directory + run: mkdir all_dist + + - name: Move artifacts to all_dist + run: | + ls dist/dist-wheels-cp310-macosx_arm64/ + mv dist/**/*.whl all_dist/ + mv dist/*.tar.gz all_dist/ + ls -l all_dist/ + + - name: Upload all dist/* to GitHub Release + uses: softprops/action-gh-release@v1 with: - tag_name: v${{ steps.relver.outputs.version }} - release_name: v${{ steps.relver.outputs.version }} - target: ${{ github.ref }} # 使用提交的标签作为目标 - body: "Release v${{ steps.relver.outputs.version }}" + files: all_dist/* - - run: | - ls -al dist/ - name: Upload to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - attestations: true + if: startsWith(github.ref, 'refs/tags/') + run: | + pip install --upgrade twine setuptools wheel twine packaging + twine upload all_dist/* + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b0c57e02..2d4722c4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -74,6 +74,10 @@ jobs: [ "$RUNNER_OS" = "Linux" ] && .github/workflows/install-krb5.sh python -m pip install -U pip setuptools wheel python -m pip install -e .[test] + # 添加 uvloop 安装 + if [ "${{ matrix.loop }}" = "uvloop" ]; then + python -m pip install uvloop + fi - name: Wait for openGauss to be ready env: diff --git a/README.rst b/README.rst index 52ab0922..99ca3655 100644 --- a/README.rst +++ b/README.rst @@ -1,12 +1,6 @@ async_gaussdb -- A fast GaussDB/openGauss Database Client Library for Python/asyncio ===================================================================================== -.. image:: https://github.com/MagicStack/async_gaussdb/workflows/Tests/badge.svg - :target: https://github.com/MagicStack/async_gaussdb/actions?query=workflow%3ATests+branch%3Amaster - :alt: GitHub Actions status -.. image:: https://img.shields.io/pypi/v/async_gaussdb.svg - :target: https://pypi.python.org/pypi/async_gaussdb - **async_gaussdb** is a database interface library designed specifically for GaussDB and openGauss databases with Python/asyncio. This fork of async_gaussdb is optimized for GaussDB/openGauss compatibility, including native SHA256 @@ -86,7 +80,7 @@ Basic Usage GaussDB/openGauss Specific Features ----------------------------------- +----------------------------------- This library includes enhanced support for GaussDB and openGauss databases: @@ -115,5 +109,5 @@ This library includes enhanced support for GaussDB and openGauss databases: asyncio.run(run()) - -asyncpg is developed and distributed under the Apache 2.0 license. +asyncpg is developed and distributed under the Apache 2.0 license +by MagicStack Inc. and the HuaweiCloudDeveloper team. diff --git a/async_gaussdb/protocol/protocol.pxd b/async_gaussdb/protocol/protocol.pxd index 51762815..76865e0e 100644 --- a/async_gaussdb/protocol/protocol.pxd +++ b/async_gaussdb/protocol/protocol.pxd @@ -51,6 +51,8 @@ cdef class BaseProtocol(CoreProtocol): bint _is_ssl + object _pending_result + PreparedStatementState statement cdef get_connection(self) diff --git a/async_gaussdb/protocol/protocol.pyx b/async_gaussdb/protocol/protocol.pyx index 7a4a214b..a3397b48 100644 --- a/async_gaussdb/protocol/protocol.pyx +++ b/async_gaussdb/protocol/protocol.pyx @@ -102,6 +102,8 @@ cdef class BaseProtocol(CoreProtocol): self._is_ssl = False + self._pending_result = None + try: self.create_future = loop.create_future except AttributeError: @@ -414,7 +416,10 @@ cdef class BaseProtocol(CoreProtocol): self._request_cancel() # Make asyncio shut up about unretrieved # QueryCanceledError - waiter.add_done_callback(lambda f: f.exception()) + if waiter and not waiter.done(): + waiter.cancel() + elif waiter and waiter.done() and not waiter.cancelled(): + waiter.exception() raise # done will be True upon receipt of CopyDone. @@ -424,6 +429,7 @@ cdef class BaseProtocol(CoreProtocol): waiter = self._new_waiter(timer.get_remaining_budget()) finally: + self._pending_result = None self.resume_reading() return status_msg @@ -776,6 +782,14 @@ cdef class BaseProtocol(CoreProtocol): self.abort() cdef _new_waiter(self, timeout): + if self._pending_result is not None: + res = self._pending_result + self._pending_result = None + self.resume_reading() + waiter = self.loop.create_future() + waiter.set_result(res) + return waiter + if self.waiter is not None: raise apg_exc.InterfaceError( 'cannot perform operation: another operation is in progress') @@ -848,10 +862,31 @@ cdef class BaseProtocol(CoreProtocol): waiter = self.waiter self.waiter = None - if PG_DEBUG: - if waiter is None: + if waiter is None: + if PG_DEBUG: raise apg_exc.InternalClientError('_on_result: waiter is None') + + if self.state == PROTOCOL_COPY_OUT_DATA or \ + self.state == PROTOCOL_COPY_OUT_DONE: + + copy_done = self.state == PROTOCOL_COPY_OUT_DONE + if copy_done: + status_msg = self.result_status_msg.decode(self.encoding) + else: + status_msg = None + + self.pause_reading() + if self._pending_result is not None: + old_data, old_done, old_status = self._pending_result + current_data = self.result if self.result is not None else b'' + merged_data = (old_data if old_data is not None else b'') + current_data + self._pending_result = (merged_data, copy_done, status_msg) + else: + self._pending_result = (self.result, copy_done, status_msg) + return + else: + return if waiter.cancelled(): return diff --git a/pyproject.toml b/pyproject.toml index d4b5a705..bfd2e718 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,9 @@ [project] name = "async_gaussdb" description = "An asyncio GaussDB driver" -authors = [{name = "MagicStack Inc", email = "hello@magic.io"}] requires-python = '>=3.8.0' readme = "README.rst" -license = {text = "Apache License, Version 2.0"} +license = {text = "Apache-2.0"} dynamic = ["version"] keywords = [ "database", @@ -39,16 +38,6 @@ gssauth = [ 'gssapi; platform_system != "Windows"', 'sspilib; platform_system == "Windows"', ] -test = [ - 'flake8~=6.1', - 'flake8-pyi~=24.1.0', - 'distro~=1.9.0', - 'uvloop>=0.15.3; platform_system != "Windows" and python_version < "3.14.0"', - 'gssapi; platform_system == "Linux"', - 'k5test; platform_system == "Linux"', - 'sspilib; platform_system == "Windows"', - 'mypy~=1.8.0', -] docs = [ 'Sphinx~=8.1.3', 'sphinx_rtd_theme>=1.2.2', @@ -71,50 +60,6 @@ include = ["async_gaussdb", "async_gaussdb.*"] [tool.setuptools.exclude-package-data] "*" = ["*.c", "*.h"] -[tool.cibuildwheel] -build-frontend = "build" -test-extras = "test" - -[tool.cibuildwheel.macos] -before-all = ".github/workflows/install-gaussdb.sh" -test-command = "python {project}/tests/__init__.py" - -[tool.cibuildwheel.windows] -test-command = "python {project}\\tests\\__init__.py" - -[tool.cibuildwheel.linux] -before-all = """ - .github/workflows/install-gaussdb.sh \ - && .github/workflows/install-krb5.sh \ - """ -test-command = """\ - PY=`which python` \ - && chmod -R go+rX "$(dirname $(dirname $(dirname $PY)))" \ - && su -l apgtest -c "$PY {project}/tests/__init__.py" \ - """ - -[tool.pytest.ini_options] -addopts = "--capture=no --assert=plain --strict-markers --tb=native --import-mode=importlib" -testpaths = "tests" -filterwarnings = "default" - -[tool.coverage.run] -branch = true -plugins = ["Cython.Coverage"] -parallel = true -source = ["async_gaussdb/", "tests/"] -omit = ["*.pxd"] - -[tool.coverage.report] -exclude_lines = [ - "pragma: no cover", - "def __repr__", - "if debug", - "raise NotImplementedError", - "if __name__ == .__main__.", -] -show_missing = true - [tool.mypy] exclude = [ "^.eggs", @@ -145,4 +90,4 @@ module = [ "async_gaussdb.transaction", "async_gaussdb.utils", ] -ignore_errors = true +ignore_errors = true \ No newline at end of file diff --git a/setup.py b/setup.py index ec1aefa2..985ea71e 100644 --- a/setup.py +++ b/setup.py @@ -249,5 +249,5 @@ def finalize_options(self): extra_link_args=LDFLAGS), ], cmdclass={'build_ext': build_ext, 'build_py': build_py, 'sdist': sdist}, - setup_requires=setup_requires, + setup_requires=setup_requires ) diff --git a/tests/test_cancellation.py b/tests/test_cancellation.py index 8903d561..63ff004e 100644 --- a/tests/test_cancellation.py +++ b/tests/test_cancellation.py @@ -52,7 +52,7 @@ async def test8(): for test in {test0, test1, test2, test3, test4, test5, test6, test7, test8}: - with self.subTest(testfunc=test), self.assertRunUnder(1): + with self.subTest(testfunc=test), self.assertRunUnder(2): st = await self.con.prepare('SELECT pg_sleep(20)') task = self.loop.create_task(st.fetch()) await asyncio.sleep(0.05) @@ -67,7 +67,7 @@ async def test8(): async def test_cancellation_02(self): st = await self.con.prepare('SELECT 1') task = self.loop.create_task(st.fetch()) - await asyncio.sleep(0.05) + await asyncio.sleep(0.3) task.cancel() self.assertEqual(await task, [(1,)]) @@ -76,7 +76,7 @@ async def test_cancellation_03(self): async with self.con.transaction(): task = self.loop.create_task( self.con.fetch('SELECT pg_sleep(20)')) - await asyncio.sleep(0.05) + await asyncio.sleep(0.3) task.cancel() with self.assertRaises(asyncio.CancelledError): diff --git a/tests/test_codecs.py b/tests/test_codecs.py index 15ac1011..2ce30bb0 100644 --- a/tests/test_codecs.py +++ b/tests/test_codecs.py @@ -160,7 +160,7 @@ def _system_timezone(): dict(input=bytearray(b'\x02\x01'), output=b'\x02\x01'), )), ('text', 'text', ( - '', + 'A', 'A' * (1024 * 1024 + 11) )), ('"char"', 'char', ( @@ -185,9 +185,9 @@ def _system_timezone(): 'textoutput': '1970-01-01 20:31:23.648'}, ]), ('date', 'date', [ - datetime.datetime(3000, 5, 20), - datetime.datetime(2000, 1, 1), - datetime.datetime(500, 1, 1), + datetime.date(3000, 5, 20), + datetime.date(2000, 1, 1), + datetime.date(500, 1, 1), infinity_date, negative_infinity_date, {'textinput': 'infinity', 'output': infinity_date}, @@ -568,7 +568,6 @@ async def test_all_builtin_types_handled(self): async def test_void(self): res = await self.con.fetchval('select pg_sleep(0)') self.assertIsNone(res) - await self.con.fetchval('select now($1::void)', '') def test_bitstring(self): bitlen = random.randint(0, 1000) @@ -1205,12 +1204,14 @@ async def test_extra_codec_alias(self): DROP DOMAIN my_dec_t; ''') + @unittest.skip('GaussDB enable_extension!=true') async def test_custom_codec_text(self): """Test encoding/decoding using a custom codec in text mode. GaussDB hstore extension is in pg_catalog schema, so we need to set schema to pg_catalog """ await self.con.execute(''' + set enable_extension=true; CREATE EXTENSION IF NOT EXISTS hstore ''') @@ -1266,12 +1267,14 @@ def hstore_encoder(obj): # ''') pass + @unittest.skip('GaussDB enable_extension!=true') async def test_custom_codec_binary(self): """Test encoding/decoding using a custom codec in binary mode. GaussDB hstore extension is in pg_catalog schema, so we need to set schema to pg_catalog """ await self.con.execute(''' + set enable_extension=true; CREATE EXTENSION IF NOT EXISTS hstore ''') diff --git a/tests/test_execute.py b/tests/test_execute.py index 88d0ba36..376584f1 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -63,7 +63,7 @@ async def test_execute_script_check_transactionality(self): async def test_execute_exceptions_1(self): with self.assertRaisesRegex(async_gaussdb.GaussDBError, - 'relation "__dne__" does not exist'): + r'(?i)relation "__dne__" does not exist'): await self.con.execute('select * from __dne__') @@ -284,7 +284,7 @@ async def locker(): await tx.start() await conn.execute("UPDATE exmany SET a = '1' WHERE b = 10") event.set() - await asyncio.sleep(1) + await asyncio.sleep(2) await tx.rollback() finally: event.set() @@ -298,7 +298,7 @@ async def locker(): with self.assertRaises(asyncio.TimeoutError): await self.con.executemany(''' UPDATE exmany SET a = $1 WHERE b = $2 - ''', [('a' * 32768, x) for x in range(128)], timeout=0.5) + ''', [('a' * 32768, x) for x in range(128)], timeout=1.0) await fut result = await self.con.fetch( 'SELECT * FROM exmany WHERE a IS NOT NULL') @@ -314,7 +314,7 @@ async def test_executemany_client_failure_in_transaction(self): result = await self.con.fetch('SELECT b FROM exmany') # only 2 batches executed (2 x 4) self.assertEqual( - [x[0] for x in result], [y + 1 for y in range(10, 2, -1)]) + sorted([x[0] for x in result]), sorted([y + 1 for y in range(10, 2, -1)])) await tx.rollback() result = await self.con.fetch('SELECT b FROM exmany') self.assertEqual(result, []) diff --git a/tests/test_introspection.py b/tests/test_introspection.py index 1f2b0b01..be24bb4d 100644 --- a/tests/test_introspection.py +++ b/tests/test_introspection.py @@ -64,6 +64,7 @@ async def _add_custom_codec(self, conn): ) @tb.with_connection_options(database='async_gaussdb_intro_test') + @unittest.skip('CREATE TABLE INHERITS is not yet supported.') async def test_introspection_on_large_db(self): await self.con.execute( 'CREATE TABLE base ({})'.format( diff --git a/tests/test_pool.py b/tests/test_pool.py index e3c82f17..aebbf6d0 100644 --- a/tests/test_pool.py +++ b/tests/test_pool.py @@ -24,7 +24,7 @@ _system = platform.uname().system -POOL_NOMINAL_TIMEOUT = 0.1 +POOL_NOMINAL_TIMEOUT = 0.5 class SlowResetConnection(pg_connection.Connection): diff --git a/tests/test_prepare.py b/tests/test_prepare.py index 8e663e07..383593cb 100644 --- a/tests/test_prepare.py +++ b/tests/test_prepare.py @@ -31,7 +31,7 @@ async def test_prepare_01(self): self.assertEqual(self.con._protocol.queries_count, 2) async def test_prepare_02(self): - with self.assertRaisesRegex(Exception, 'column "a" does not exist'): + with self.assertRaisesRegex(Exception, r'(?i)column "a" does not exist'): await self.con.prepare('SELECT a') async def test_prepare_03(self): diff --git a/tests/test_timeout.py b/tests/test_timeout.py index 79bed4da..292d8041 100644 --- a/tests/test_timeout.py +++ b/tests/test_timeout.py @@ -126,7 +126,7 @@ async def test_invalid_timeout(self): class TestConnectionCommandTimeout(tb.ConnectedTestCase): - @tb.with_connection_options(command_timeout=0.2) + @tb.with_connection_options(command_timeout=0.4) async def test_command_timeout_01(self): for methname in {'fetch', 'fetchrow', 'fetchval', 'execute'}: with self.assertRaises(asyncio.TimeoutError), \