diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 28784749c4..ac3943ef57 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,6 @@ version: 2 updates: - - package-ecosystem: "pip" + - package-ecosystem: "uv" directory: "/docs" schedule: interval: "daily" diff --git a/.github/workflows/build-pre-release.yml b/.github/workflows/build-pre-release.yml index e1326b6aa5..f6473c1cc3 100644 --- a/.github/workflows/build-pre-release.yml +++ b/.github/workflows/build-pre-release.yml @@ -15,7 +15,7 @@ on: jobs: build-and-publish: - uses: ./.github/workflows/lib-build-and-push.yml + uses: ./.github/workflows/lib-build.yml with: python-version: ${{ inputs.python-version }} target: ${{ inputs.target }} diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index 4e73e811fa..a1a6c854c7 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -10,11 +10,12 @@ on: jobs: build-and-publish: name: "Build wheels" - uses: ./.github/workflows/lib-build-and-push.yml - with: - upload: false + uses: ./.github/workflows/lib-build.yml - # TODO: Remove when https://github.com/pypa/gh-action-pypi-publish/issues/166 is fixed and update build-and-publish.with.upload to ${{ endsWith(github.event.ref, 'scylla') }} + # Publishing is a separate job (not inside the reusable workflow) because PyPI Trusted Publishing + # requires the *caller* workflow path in the OIDC token. A reusable workflow would embed its own + # path instead, causing an `invalid-publisher` error on the PyPI side. + # See: https://github.com/pypa/gh-action-pypi-publish/issues/166 publish: name: "Publish wheels to PyPi" if: ${{ endsWith(github.event.ref, 'scylla') }} @@ -23,11 +24,11 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: dist merge-multiple: true - - uses: pypa/gh-action-pypi-publish@release/v1 + - uses: pypa/gh-action-pypi-publish@cef2210092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: skip-existing: true diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 3e1f1067d7..ebfe383047 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -18,6 +18,4 @@ jobs: test-wheels-build: name: "Test wheels building" if: "!contains(github.event.pull_request.labels.*.name, 'disable-test-build')" - uses: ./.github/workflows/lib-build-and-push.yml - with: - upload: false \ No newline at end of file + uses: ./.github/workflows/lib-build.yml \ No newline at end of file diff --git a/.github/workflows/call_jira_sync.yml b/.github/workflows/call_jira_sync.yml new file mode 100644 index 0000000000..0855246f48 --- /dev/null +++ b/.github/workflows/call_jira_sync.yml @@ -0,0 +1,18 @@ +name: Sync Jira Based on PR Events + +on: + pull_request_target: + types: [opened, edited, ready_for_review, review_requested, labeled, unlabeled, closed] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + jira-sync: + uses: scylladb/github-automation/.github/workflows/main_pr_events_jira_sync.yml@83115dc2553dbf968e73271e97fc7aac16b8145a # main 2026-05-20 + with: + caller_action: ${{ github.event.action }} + secrets: + caller_jira_auth: ${{ secrets.USER_AND_KEY_FOR_JIRA_AUTOMATION }} diff --git a/.github/workflows/docs-pages.yaml b/.github/workflows/docs-pages.yml similarity index 61% rename from .github/workflows/docs-pages.yaml rename to .github/workflows/docs-pages.yml index 31f8dc74c5..a413e3317e 100644 --- a/.github/workflows/docs-pages.yaml +++ b/.github/workflows/docs-pages.yml @@ -2,6 +2,9 @@ name: "Docs / Publish" # For more information, # see https://sphinx-theme.scylladb.com/stable/deployment/production.html#available-workflows +permissions: + contents: write + on: push: branches: @@ -9,28 +12,36 @@ on: - 'branch-**' paths: - "docs/**" + - ".github/workflows/docs-pages.yml" + - "cassandra/**" + - "pyproject.toml" + - "setup.py" + - "CHANGELOG.rst" workflow_dispatch: jobs: release: - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.repository.default_branch }} persist-credentials: false fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@v5 + + - name: Install uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: - python-version: '3.10' - - name: Set up env - run: make -C docs setupenv + working-directory: docs + enable-cache: true + - name: Build docs run: make -C docs multiversion + - name: Build redirects run: make -C docs redirects + - name: Deploy docs to GitHub Pages run: ./docs/_utils/deploy.sh env: diff --git a/.github/workflows/docs-pr.yaml b/.github/workflows/docs-pr.yaml deleted file mode 100644 index 28a74f2e58..0000000000 --- a/.github/workflows/docs-pr.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: "Docs / Build PR" -# For more information, -# see https://sphinx-theme.scylladb.com/stable/deployment/production.html#available-workflows - -on: - pull_request: - branches: - - master - - 'branch-**' - paths: - - "docs/**" - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-24.04 - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - persist-credentials: false - fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - name: Set up env - run: make -C docs setupenv - - name: Build docs - run: make -C docs test diff --git a/.github/workflows/docs-pr.yml b/.github/workflows/docs-pr.yml new file mode 100644 index 0000000000..1881c227ed --- /dev/null +++ b/.github/workflows/docs-pr.yml @@ -0,0 +1,46 @@ +name: "Docs / Build PR" +# For more information, +# see https://sphinx-theme.scylladb.com/stable/deployment/production.html#available-workflows + +permissions: + contents: read + +on: + push: + branches: + - master + paths: + - "docs/**" + - ".github/workflows/docs-pr.yml" + - "cassandra/**" + - "pyproject.toml" + - "setup.py" + - "CHANGELOG.rst" + pull_request: + paths: + - "docs/**" + - ".github/workflows/docs-pr.yml" + - "cassandra/**" + - "pyproject.toml" + - "setup.py" + - "CHANGELOG.rst" + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Install uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + working-directory: docs + enable-cache: true + + - name: Build docs + run: make -C docs test diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index bccbdc63cc..5e76d6bbb4 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -5,6 +5,18 @@ on: branches: - master - 'branch-**' + paths-ignore: + - docs/* + - examples/* + - scripts/* + - .gitignore + - '*.rst' + - '*.ini' + - LICENSE + - .github/dependabot.yml + - .github/pull_request_template.md + - "*.md" + - .github/workflows/docs-* pull_request: paths-ignore: - docs/* @@ -16,6 +28,8 @@ on: - LICENSE - .github/dependabot.yml - .github/pull_request_template.md + - "*.md" + - .github/workflows/docs-* workflow_dispatch: jobs: @@ -23,23 +37,29 @@ jobs: name: test ${{ matrix.event_loop_manager }} (${{ matrix.python-version }}) if: "!contains(github.event.pull_request.labels.*.name, 'disable-integration-tests')" runs-on: ubuntu-24.04 + env: + SCYLLA_VERSION: release:2026.1 strategy: fail-fast: false matrix: java-version: [8] - python-version: ["3.9", "3.11", "3.12", "3.13"] + python-version: ["3.11", "3.12", "3.13", "3.14", "3.14t"] event_loop_manager: ["libev", "asyncio", "asyncore"] exclude: - python-version: "3.12" event_loop_manager: "asyncore" - python-version: "3.13" event_loop_manager: "asyncore" + - python-version: "3.14" + event_loop_manager: "asyncore" + - python-version: "3.14t" + event_loop_manager: "asyncore" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up JDK ${{ matrix.java-version }} - uses: actions/setup-java@v4 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: java-version: ${{ matrix.java-version }} distribution: 'adopt' @@ -48,7 +68,7 @@ jobs: run: sudo apt-get install libev4 libev-dev - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: python-version: ${{ matrix.python-version }} @@ -57,17 +77,25 @@ jobs: - name: Build driver run: uv sync + - name: Cache Scylla download + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.ccm/repository + key: scylla-${{ env.SCYLLA_VERSION }}-${{ runner.os }} + # This is to get honest accounting of test time vs download time vs build time. # Not strictly necessary for running tests. - name: Download Scylla run: | - export SCYLLA_VERSION='release:6.2' uv run ccm create scylla-driver-temp -n 1 --scylla --version ${SCYLLA_VERSION} uv run ccm remove - name: Test with pytest + env: + EVENT_LOOP_MANAGER: ${{ matrix.event_loop_manager }} + PROTOCOL_VERSION: 4 run: | - export EVENT_LOOP_MANAGER=${{ matrix.event_loop_manager }} - export SCYLLA_VERSION='release:6.2' - export PROTOCOL_VERSION=4 + if [[ "${{ matrix.python-version }}" =~ t$ ]]; then + export PYTHON_GIL=0 + fi uv run pytest tests/integration/standard/ tests/integration/cqlengine/ diff --git a/.github/workflows/lib-build-and-push.yml b/.github/workflows/lib-build.yml similarity index 73% rename from .github/workflows/lib-build-and-push.yml rename to .github/workflows/lib-build.yml index b68ef4eba5..f6959ddfec 100644 --- a/.github/workflows/lib-build-and-push.yml +++ b/.github/workflows/lib-build.yml @@ -1,14 +1,8 @@ -name: Build and upload to PyPi +name: Build wheels on: workflow_call: inputs: - upload: - description: 'Upload to PyPI' - type: boolean - required: false - default: false - python-version: description: 'Python version to run on' type: string @@ -61,7 +55,7 @@ jobs: was_added=1 elif [[ "${target}" == "macos-x86" ]]; then [ -n "$was_added" ] && echo -n "," >> /tmp/matrix.json - echo -n '{"os":"macos-13", "target": "macos-x86"}' >> /tmp/matrix.json + echo -n '{"os":"macos-15-intel", "target": "macos-x86"}' >> /tmp/matrix.json was_added=1 elif [[ "${target}" == "macos-arm" ]]; then [ -n "$was_added" ] && echo -n "," >> /tmp/matrix.json @@ -83,11 +77,11 @@ jobs: include: ${{ fromJson(needs.prepare-matrix.outputs.matrix) }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Checkout tag ${{ inputs.target_tag }} if: inputs.target_tag != '' - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.target_tag }} @@ -98,28 +92,26 @@ jobs: echo "CIBW_TEST_COMMAND=true" >> $GITHUB_ENV; echo "CIBW_TEST_COMMAND_WINDOWS=(exit 0)" >> $GITHUB_ENV; echo "CIBW_TEST_SKIP=*" >> $GITHUB_ENV; - echo "CIBW_SKIP=cp2* cp36* pp36* cp37* pp37* cp38* pp38* *i686 *musllinux*" >> $GITHUB_ENV; - echo "CIBW_BUILD=cp3* pp3*" >> $GITHUB_ENV; echo "CIBW_BEFORE_TEST=true" >> $GITHUB_ENV; echo "CIBW_BEFORE_TEST_WINDOWS=(exit 0)" >> $GITHUB_ENV; - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: python-version: ${{ inputs.python-version }} - name: Install cibuildwheel run: | - uv tool install 'cibuildwheel==2.22.0' + uv tool install 'cibuildwheel==3.2.1' - name: Install OpenSSL for Windows if: runner.os == 'Windows' run: | - choco install openssl --version=3.5.1 -f -y --no-progress + choco install openssl.light --no-progress -y - name: Install Conan if: runner.os == 'Windows' - uses: turtlebrowser/get-conan@main + uses: turtlebrowser/get-conan@c171f295f3f507360ee018736a6608731aa2109d # v1.2 - name: Configure libev for Windows if: runner.os == 'Windows' @@ -127,7 +119,7 @@ jobs: conan profile detect conan install conanfile.py - - name: Install OpenSSL for MacOS + - name: Install libev for MacOS if: runner.os == 'MacOs' run: | brew install libev @@ -136,9 +128,9 @@ jobs: if: runner.os == 'MacOS' run: | ##### Set MACOSX_DEPLOYMENT_TARGET - if [ "${{ matrix.os }}" == "macos-13" ]; then - echo "MACOSX_DEPLOYMENT_TARGET=13.0" >> $GITHUB_ENV; - echo "Enforcing target deployment for 13.0" + if [ "${{ matrix.os }}" == "macos-15-intel" ]; then + echo "MACOSX_DEPLOYMENT_TARGET=15.0" >> $GITHUB_ENV; + echo "Enforcing target deployment for 15.0" elif [ "${{ matrix.os }}" == "macos-14" ]; then echo "MACOSX_DEPLOYMENT_TARGET=14.0" >> $GITHUB_ENV; echo "Enforcing target deployment for 14.0" @@ -148,14 +140,14 @@ jobs: if: matrix.target != 'linux-aarch64' shell: bash run: | - GITHUB_WORKFLOW_REF="scylladb/python-driver/.github/workflows/lib-build-and-push.yml@refs/heads/master" cibuildwheel --output-dir wheelhouse + cibuildwheel --output-dir wheelhouse - name: Build wheels for linux aarch64 if: matrix.target == 'linux-aarch64' run: | - GITHUB_WORKFLOW_REF="scylladb/python-driver/.github/workflows/lib-build-and-push.yml@refs/heads/master" CIBW_BUILD="cp3*" cibuildwheel --archs aarch64 --output-dir wheelhouse + CIBW_BUILD="cp3*" cibuildwheel --archs aarch64 --output-dir wheelhouse - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: wheels-${{ matrix.target }}-${{ matrix.os }} path: ./wheelhouse/*.whl @@ -164,34 +156,17 @@ jobs: name: Build source distribution runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: python-version: ${{ inputs.python-version }} - name: Build sdist run: uv build --sdist - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: source-dist path: dist/*.tar.gz - - upload_pypi: - if: inputs.upload - needs: [build-wheels, build-sdist] - runs-on: ubuntu-24.04 - permissions: - id-token: write - - steps: - - uses: actions/download-artifact@v4 - with: - path: dist - merge-multiple: true - - - uses: pypa/gh-action-pypi-publish@release/v1 - with: - skip-existing: true diff --git a/.github/workflows/publish-manually.yml b/.github/workflows/publish-manually.yml index d2dda897ed..5b9298fb7f 100644 --- a/.github/workflows/publish-manually.yml +++ b/.github/workflows/publish-manually.yml @@ -1,5 +1,8 @@ name: Build and upload to PyPi manually +permissions: + contents: read + on: workflow_dispatch: inputs: @@ -36,15 +39,17 @@ on: jobs: build-and-publish: name: "Build wheels" - uses: ./.github/workflows/lib-build-and-push.yml + uses: ./.github/workflows/lib-build.yml with: - upload: false python-version: ${{ inputs.python-version }} ignore_tests: ${{ inputs.ignore_tests }} target_tag: ${{ inputs.target_tag }} target: ${{ inputs.target }} - # TODO: Remove when https://github.com/pypa/gh-action-pypi-publish/issues/166 is fixed and update build-and-publish.with.upload to ${{ inputs.upload }} + # Publishing is a separate job (not inside the reusable workflow) because PyPI Trusted Publishing + # requires the *caller* workflow path in the OIDC token. A reusable workflow would embed its own + # path instead, causing an `invalid-publisher` error on the PyPI side. + # See: https://github.com/pypa/gh-action-pypi-publish/issues/166 publish: name: "Publish wheels to PyPi" needs: build-and-publish @@ -53,11 +58,11 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: dist merge-multiple: true - - uses: pypa/gh-action-pypi-publish@release/v1 + - uses: pypa/gh-action-pypi-publish@cef2210092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: skip-existing: true diff --git a/.gitignore b/.gitignore index b96d8702d6..881012f340 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,11 @@ tests/unit/cython/bytesio_testhelper.c #iPython *.ipynb +uv.lock +.venv/ + + + # Files from upstream that we don't need Jenkinsfile Jenkinsfile.bak @@ -64,3 +69,22 @@ docs/core_graph.rst docs/geo_types.rst docs/graph.rst docs/graph_fluent.rst + +# Personal list of items to do +TODO.md + +# Codex - AI assistant metadata +.codex/ +.codex-cache/ +.codex-config.json +.codex-settings.json +codex.log +AGENTS.md + +# Claude - AI assistant metadata +.anthropic/ +.claude/ +claude.log +claude_history.json +claude_config.json +CLAUDE.md \ No newline at end of file diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 80f8d52d7a..39a8aca069 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,151 @@ +3.29.10 +======= +May 10, 2026 + +Features +-------- +* Fast-path ``lookup_casstype()`` for simple type names +* Add ``Session.wait_for_schema_agreement`` + +Bug Fixes +--------- +* Fix CQL injection in ``Connection.set_keyspace_blocking`` and ``Connection.set_keyspace_async`` +* Fix libev shutdown crashes by correcting atexit registration +* Handle ``None`` ``control_connection_timeout`` in ``wait_for_schema_agreement`` +* Clean up failed heartbeat sends +* Fix ``ExponentialBackoffRetryPolicy.__init__`` super() call +* Correct ``clustering_key`` to ``clustering`` in column kind filter +* Fix inverted cooldown check in ``_get_shard_aware_endpoint`` + +Others +------ +* Deprecate ``ControlConnection.wait_for_schema_agreement`` +* Add timeout and in-flight observability to ``OperationTimedOut`` +* Drop per-query connection log + +3.29.9 +====== +March 18, 2026 + +Features +-------- +* Add Private Link support via client routes handler +* Add optional query_params parameter to QueryMessage + +Bug Fixes +--------- +* Fix segmentation fault in libev prepare_callback during shutdown +* Add null checks to io_callback and timer_callback in libev wrapper +* Fix RecursionError in execute_concurrent on synchronous errbacks +* Fix floating-point precision loss for timestamps far from epoch + +Others +------ +* Cache parsed tablet routing type in ResponseFuture +* Remove deprecated setup_requires in favor of PEP 517 build-system.requires +* Update dependency hatchling to v1.29.0 + +3.29.8 +====== +February 09, 2026 + +Features +-------- +* Add frozen parameter to collection columns with FULL index support +* Include original error in ConnectionShutdown messages + +Bug Fixes +--------- +* Fix IntStat comparison operators and metrics cleanup on shutdown +* Fix NumPy 2.0 compatibility in numpy_parser +* Fix race condition during host IP address update +* Fix infinite retry when single host fails with server error +* Don't mark node down when control connection fails to connect +* Call on_add before distance to properly initialize lbp +* Don't check if host is in initial contact points when setting default local_dc +* Pull version information from system.local when version info is not present +* Fix missing call to superclass ``__init__`` during object initialization + +Others +------ +* Remove scales dependency with self-contained metrics implementation +* Migrate from pytz to zoneinfo +* Remove Python 2 compatibility code +* Optimize write path in protocol.py to reduce copies +* TokenAwarePolicy: remove redundant check if a table is using tablets +* Don't create Host instances with random host_id +* Use endpoint instead of Host in _try_connect +* Remove support for protocols <3 from cython files +* Return empty query plan if there are no live hosts +* Replace asynctest with stdlib mock + +3.29.7 +====== +December 08, 2025 + +Bug Fixes +--------- +* Make compression=None a valid case (#610) + +3.29.6 +====== +November 27, 2025 + +* Rename connection_metadata to client_routes (#608) +* TokenAwarePolicy: enable shuffling by default (#478) +* Add support of LWT flag for BatchStatement (#606) +* Add support of CONNECTION_METADATA_CHANGE event (#601) +* Add LWT support (#584) +* Add support for Python 3.14 (#566) +* Fix dict handling in pool and metrics (#595) +* Remove serverless code (#590) +* tests: drop `sure` package (#592) +* compression: better handle configuration problems (#585) + +3.29.5 +====== +November 5, 2025 + +Bug Fixes +--------- +* Update TokenAwarePolicy.make_query_plan to schedule to replicas first (#548) +* Drop _tablets_routing_v1 flag from token-aware policy (#547) +* Fix dc aware and rack aware policies initialization (#546) +* Fix Cluster.metadata_request_timeout and default it from control_connection_timeout (#539) + +Others +------ +* Drop python 3.9 support (#564) + +3.29.4 +====== +August 16, 2025 + +Features +-------- +* Add Cluster.application_info to report application information to server (#486) +* Move to uv package manager (#496) + +Bug Fixes +--------- +* Fix deadlocks on evicting connection in HostConnectionPool and ConnectionPool (#499) +* Fix libevreactor crashing when connection added and closed right away (#508) + +Others +------ +* Remove outdated protocols support (v1 and v2) (#493, #525) +* Remove DSE integration tests (#502) +* Optimise shard port allocator (#506) +* Remove self.assert (#507) +* Minor performance improvement for make_token_replica_map (#513) +* Remove in-memory Scylla tables support (#518) +* Add optional dependencies for SNAPPY and LZ4 compressors (#517) +* Remove support for protocol versions not supported by Scylla (#492) +* Set monitor_reporting_enabled False by default (#523) + 3.29.3 ====== -Mart 11, 2025 +March 11, 2025 Bug Fixes --------- diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index c635fd8c1b..82bf21e52f 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -18,7 +18,7 @@ good bug reports. They will not be repeated in detail here, but in general, the Pull Requests ------------- If you're able to fix a bug yourself, you can `fork the repository `_ and submit a `Pull Request `_ with the fix. -Please include tests demonstrating the issue and fix. For examples of how to run the tests, consult the `dev README `_. +Please include tests demonstrating the issue and fix. For examples of how to run the tests, consult the further parts of this document. Design and Implementation Guidelines ------------------------------------ @@ -26,3 +26,110 @@ Design and Implementation Guidelines - This project follows `semantic versioning `_, so breaking API changes will only be introduced in major versions. - Legacy ``cqlengine`` has varying degrees of overreaching client-side validation. Going forward, we will avoid client validation where server feedback is adequate and not overly expensive. - When writing tests, try to achieve maximal coverage in unit tests (where it is faster to run across many runtimes). Integration tests are good for things where we need to test server interaction, or where it is important to test across different server versions (emulating in unit tests would not be effective). + +Dev setup +========= + +We recommend using `uv` tool for running tests, linters and basically everything else, +since it makes Python tooling ecosystem mostly usable. +To install it, see instructions at https://docs.astral.sh/uv/getting-started/installation/ +The rest of this document assumes you have `uv` installed. + +It is also strongly recommended to use C/C++-caching tool like ccache or sccache. +When modifying driver files, rebuilding Cython modules is often necessary. +Without caching, each such rebuild may take over a minute. Caching usually brings it +down to about 2-3 seconds. + +**Important:** After modifying any ``.py`` file under ``cassandra/`` that is +Cython-compiled (such as ``query.py``, ``protocol.py``, ``cluster.py``, etc.), +extensions must be rebuilt before running tests. If you always use ``uv run`` +(e.g. ``uv run pytest``), this is handled automatically via the ``cache-keys`` +configuration in ``pyproject.toml``. If you invoke ``pytest`` directly, you can +rebuild with:: + + uv sync --reinstall-package scylla-driver + +Without rebuilding, Python will load the stale compiled extension (``.so`` / ``.pyd``) +instead of your modified ``.py`` source, and your changes will not actually be tested. +The test suite will emit a warning if it detects this situation. + +Building the Docs +================= + +To build and preview the documentation for the ScyllaDB Python driver locally, you must first manually install `python-driver`. +This is necessary for autogenerating the reference documentation of the driver. +You can find detailed instructions on how to install the driver in the `Installation guide `_. + +After installing the driver, you can build the documentation: +- Make sure you have Python version compatible with docs. You can see supported version in ``docs/pyproject.toml`` - look for ``python`` in ``tool.poetry.dependencies`` section. +- Install poetry: ``pip install poetry`` +- To preview docs in your browser: ``make -C docs preview`` + +Tests +===== + +Running Unit Tests +------------------ +Unit tests can be run like so:: + + uv run pytest tests/unit + EVENT_LOOP_MANAGER=gevent uv run pytest tests/unit/io/test_geventreactor.py + EVENT_LOOP_MANAGER=eventlet uv run pytest tests/unit/io/test_eventletreactor.py + +You can run a specific test method like so:: + + uv run pytest tests/unit/test_connection.py::ConnectionTest::test_bad_protocol_version + +Running Integration Tests +------------------------- +In order to run integration tests, you must specify a version to run using either of: +* ``SCYLLA_VERSION`` e.g. ``release:2025.2`` +* ``CASSANDRA_VERSION`` +environment variable:: + + SCYLLA_VERSION="release:2025.2" uv run pytest tests/integration/standard tests/integration/cqlengine/ + +Or you can specify a scylla/cassandra directory (to test unreleased versions):: + + SCYLLA_VERSION=/path/to/scylla uv run pytest tests/integration/standard/ + +Specifying the usage of an already running Scylla cluster +------------------------------------------------------------ +The test will start the appropriate Scylla clusters when necessary but if you don't want this to happen because a Scylla cluster is already running the flag ``USE_CASS_EXTERNAL`` can be used, for example:: + + USE_CASS_EXTERNAL=1 SCYLLA_VERSION='release:5.1' uv run pytest tests/integration/standard + +Specify a Protocol Version for Tests +------------------------------------ +The protocol version defaults to: +- 4 for Scylla >= 3.0 and Scylla Enterprise > 2019. +- 3 for older versions of Scylla +- 5 for Cassandra >= 4.0, 4 for Cassandra >= 2.2, 3 for Cassandra >= 2.1, 2 for Cassandra >= 2.0 +You can overwrite it with the ``PROTOCOL_VERSION`` environment variable:: + + PROTOCOL_VERSION=3 SCYLLA_VERSION="release:5.1" uv run pytest tests/integration/standard tests/integration/cqlengine/ + +Seeing Test Logs in Real Time +----------------------------- +Sometimes it's useful to output logs for the tests as they run:: + + uv run pytest -s tests/unit/ + +Use tee to capture logs and see them on your terminal:: + + uv run pytest -s tests/unit/ 2>&1 | tee test.log + + +Running the Benchmarks +====================== +There needs to be a version of Scyll running locally so before running the benchmarks, if ccm is installed: + + uv run ccm create benchmark_cluster --scylla -v release:2025.2 -n 1 -s + +To run the benchmarks, pick one of the files under the ``benchmarks/`` dir and run it:: + + uv run benchmarks/future_batches.py + +There are a few options. Use ``--help`` to see them all:: + + uv run benchmarks/future_batches.py --help diff --git a/MAINTENANCE.md b/MAINTENANCE.md new file mode 100644 index 0000000000..8fc860ac4b --- /dev/null +++ b/MAINTENANCE.md @@ -0,0 +1,13 @@ +Releasing +========= +* Run the tests and ensure they all pass +* Update the version in ``cassandra/__init__.py`` +* Add the new version in ``docs/conf.py`` (variables: ``TAGS``, ``LATEST_VERSION``, ``DEPRECATED_VERSIONS``). + * For patch version releases (like ``3.26.8-scylla -> 3.26.9-scylla``) replace the old version with new one in ``TAGS`` and update ``LATEST_VERSION``. + * For minor version releases (like ``3.26.9-scylla -> 3.27.0-scylla``) add new version to ``TAGS``, update ``LATEST_VERSION`` and add previous minor version to ``DEPRECATED_VERSIONS``. +* Commit the version changes, e.g. ``git commit -m 'Release 3.26.9'`` +* Tag the release. For example: ``git tag -a 3.26.9-scylla -m 'Release 3.26.9'`` +* Push the tag and new ``master`` SIMULTANEOUSLY: ``git push --atomic origin master v6.0.21-scylla`` +* Now new version and its docs should be automatically published. Check `PyPI `_ and `docs `_ to make sure its there. +* If you didn't push branch and tag simultaneously (or doc publishing failed for other reason) then restart the relevant job from GitHub Actions UI. +* Publish a GitHub Release and a post on community forum. diff --git a/README-dev.rst b/README-dev.rst deleted file mode 100644 index f158226de0..0000000000 --- a/README-dev.rst +++ /dev/null @@ -1,102 +0,0 @@ -Releasing -========= -* Run the tests and ensure they all pass -* Update the version in ``cassandra/__init__.py`` -* Add the new version in ``docs/conf.py`` (variables: ``TAGS``, ``LATEST_VERSION``, ``DEPRECATED_VERSIONS``). - * For patch version releases (like ``3.26.8-scylla -> 3.26.9-scylla``) replace the old version with new one in ``TAGS`` and update ``LATEST_VERSION``. - * For minor version releases (like ``3.26.9-scylla -> 3.27.0-scylla``) add new version to ``TAGS``, update ``LATEST_VERSION`` and add previous minor version to ``DEPRECATED_VERSIONS``. -* Commit the version changes, e.g. ``git commit -m 'Release 3.26.9'`` -* Tag the release. For example: ``git tag -a 3.26.9-scylla -m 'Release 3.26.9'`` -* Push the tag and new ``master`` SIMULTANEOUSLY: ``git push --atomic origin master v6.0.21-scylla`` -* Now new version and its docs should be automatically published. Check `PyPI `_ and `docs `_ to make sure its there. -* If you didn't push branch and tag simultaneously (or doc publishing failed for other reason) then restart the relevant job from GitHub Actions UI. -* Publish a GitHub Release and a post on community forum. - -Building the Docs -================= - -To build and preview the documentation for the ScyllaDB Python driver locally, you must first manually install `python-driver`. -This is necessary for autogenerating the reference documentation of the driver. -You can find detailed instructions on how to install the driver in the `Installation guide `_. - -After installing the driver, you can build the documentation: -- Make sure you have Python version compatible with docs. You can see supported version in ``docs/pyproject.toml`` - look for ``python`` in ``tool.poetry.dependencies`` section. -- Install poetry: ``pip install poetry`` -- To preview docs in your browser: ``make -C docs preview`` - -Tooling -======= - -We recommend using `uv` tool for running tests, linters and basically everything else, -since it makes Python tooling ecosystem mostly usable. -To install it, see instructions at https://docs.astral.sh/uv/getting-started/installation/ -The rest of this document assumes you have `uv` installed. - -Tests -===== - -Running Unit Tests ------------------- -Unit tests can be run like so:: - - uv run pytest tests/unit - EVENT_LOOP_MANAGER=gevent uv run pytest tests/unit/io/test_geventreactor.py - EVENT_LOOP_MANAGER=eventlet uv run pytest tests/unit/io/test_eventletreactor.py - -You can run a specific test method like so:: - - uv run pytest tests/unit/test_connection.py::ConnectionTest::test_bad_protocol_version - -Running Integration Tests -------------------------- -In order to run integration tests, you must specify a version to run using either of: -* ``SCYLLA_VERSION`` e.g. ``release:5.1`` -* ``CASSANDRA_VERSION`` -environment variable:: - - SCYLLA_VERSION="release:5.1" uv run pytest tests/integration/standard tests/integration/cqlengine/ - -Or you can specify a scylla/cassandra directory (to test unreleased versions):: - - SCYLLA_VERSION=/path/to/scylla uv run pytest tests/integration/standard/ - -Specifying the usage of an already running Scylla cluster ------------------------------------------------------------- -The test will start the appropriate Scylla clusters when necessary but if you don't want this to happen because a Scylla cluster is already running the flag ``USE_CASS_EXTERNAL`` can be used, for example:: - - USE_CASS_EXTERNAL=1 SCYLLA_VERSION='release:5.1' uv run pytest tests/integration/standard - -Specify a Protocol Version for Tests ------------------------------------- -The protocol version defaults to: -- 4 for Scylla >= 3.0 and Scylla Enterprise > 2019. -- 3 for older versions of Scylla -- 5 for Cassandra >= 4.0, 4 for Cassandra >= 2.2, 3 for Cassandra >= 2.1, 2 for Cassandra >= 2.0 -You can overwrite it with the ``PROTOCOL_VERSION`` environment variable:: - - PROTOCOL_VERSION=3 SCYLLA_VERSION="release:5.1" uv run pytest tests/integration/standard tests/integration/cqlengine/ - -Seeing Test Logs in Real Time ------------------------------ -Sometimes it's useful to output logs for the tests as they run:: - - uv run pytest -s tests/unit/ - -Use tee to capture logs and see them on your terminal:: - - uv run pytest -s tests/unit/ 2>&1 | tee test.log - - -Running the Benchmarks -====================== -There needs to be a version of cassandra running locally so before running the benchmarks, if ccm is installed: - - uv run ccm create benchmark_cluster -v 3.0.1 -n 1 -s - -To run the benchmarks, pick one of the files under the ``benchmarks/`` dir and run it:: - - uv run benchmarks/future_batches.py - -There are a few options. Use ``--help`` to see them all:: - - uv run benchmarks/future_batches.py --help diff --git a/README.rst b/README.rst index f6a983a5b2..84ceb443a3 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,7 @@ Scylla Enterprise (2018.1.x+) using exclusively Cassandra's binary protocol and .. image:: https://github.com/scylladb/python-driver/actions/workflows/integration-tests.yml/badge.svg?branch=master :target: https://github.com/scylladb/python-driver/actions/workflows/integration-tests.yml?query=event%3Apush+branch%3Amaster -The driver supports Python versions 3.9-3.13. +The driver supports Python versions 3.10-3.14. .. **Note:** This driver does not support big-endian systems. diff --git a/benchmarks/base.py b/benchmarks/base.py index 2000b4069f..3922eefad5 100644 --- a/benchmarks/base.py +++ b/benchmarks/base.py @@ -21,7 +21,7 @@ from optparse import OptionParser import uuid -from greplin import scales +from cassandra.metrics import getStats dirname = os.path.dirname(os.path.abspath(__file__)) sys.path.append(dirname) @@ -97,7 +97,7 @@ def setup(options): try: session.execute(""" CREATE KEYSPACE %s - WITH replication = { 'class': 'SimpleStrategy', 'replication_factor': '2' } + WITH replication = { 'class': 'NetworkTopologyStrategy', 'replication_factor': '2' } """ % options.keyspace) log.debug("Setting keyspace...") @@ -192,7 +192,7 @@ def benchmark(thread_class): log.info("Total time: %0.2fs" % total) log.info("Average throughput: %0.2f/sec" % (options.num_ops / total)) if options.enable_metrics: - stats = scales.getStats()['cassandra'] + stats = getStats()['cassandra'] log.info("Connection errors: %d", stats['connection_errors']) log.info("Write timeouts: %d", stats['write_timeouts']) log.info("Read timeouts: %d", stats['read_timeouts']) diff --git a/benchmarks/callback_full_pipeline.py b/benchmarks/callback_full_pipeline.py index a4a4c33315..87eb999cfe 100644 --- a/benchmarks/callback_full_pipeline.py +++ b/benchmarks/callback_full_pipeline.py @@ -49,10 +49,7 @@ def insert_next(self, previous_result=sentinel): def run(self): self.start_profile() - if self.protocol_version >= 3: - concurrency = 1000 - else: - concurrency = 100 + concurrency = 1000 for _ in range(min(concurrency, self.num_queries)): self.insert_next() diff --git a/cassandra/__init__.py b/cassandra/__init__.py index dfded7d1a6..1286f20e9b 100644 --- a/cassandra/__init__.py +++ b/cassandra/__init__.py @@ -23,7 +23,7 @@ def emit(self, record): logging.getLogger('cassandra').addHandler(NullHandler()) -__version_info__ = (3, 29, 3) +__version_info__ = (3, 29, 10) __version__ = '.'.join(map(str, __version_info__)) @@ -135,16 +135,6 @@ class ProtocolVersion(object): """ Defines native protocol versions supported by this driver. """ - V1 = 1 - """ - v1, supported in Cassandra 1.2-->2.2 - """ - - V2 = 2 - """ - v2, supported in Cassandra 2.0-->2.2; - added support for lightweight transactions, batch operations, and automatic query paging. - """ V3 = 3 """ @@ -180,9 +170,9 @@ class ProtocolVersion(object): DSE private protocol v2, supported in DSE 6.0+ """ - SUPPORTED_VERSIONS = (DSE_V2, DSE_V1, V6, V5, V4, V3, V2, V1) + SUPPORTED_VERSIONS = (V5, V4, V3) """ - A tuple of all supported protocol versions + A tuple of all supported protocol versions for ScyllaDB, including future v5 version. """ BETA_VERSIONS = (V6,) @@ -233,14 +223,6 @@ def uses_error_code_map(cls, version): def uses_keyspace_flag(cls, version): return version >= cls.V5 and version != cls.DSE_V1 - @classmethod - def has_continuous_paging_support(cls, version): - return version >= cls.DSE_V1 - - @classmethod - def has_continuous_paging_next_pages(cls, version): - return version >= cls.DSE_V2 - @classmethod def has_checksumming_support(cls, version): return cls.V5 <= version < cls.DSE_V1 @@ -705,10 +687,29 @@ class OperationTimedOut(DriverException): The last :class:`~.Host` this operation was attempted against. """ - def __init__(self, errors=None, last_host=None): + timeout = None + """ + The timeout value (in seconds) that was in effect when the operation + timed out, or ``None`` if not applicable. + """ + + in_flight = None + """ + The number of in-flight requests on the connection at the time of + the timeout (includes orphaned requests), or ``None`` if not applicable. + """ + + def __init__(self, errors=None, last_host=None, timeout=None, in_flight=None): self.errors = errors self.last_host = last_host + self.timeout = timeout + self.in_flight = in_flight message = "errors=%s, last_host=%s" % (self.errors, self.last_host) + if self.timeout is not None: + message += " (timeout=%ss" % self.timeout + if self.in_flight is not None: + message += ", in_flight=%d" % self.in_flight + message += ")" Exception.__init__(self, message) diff --git a/cassandra/client_routes.py b/cassandra/client_routes.py new file mode 100644 index 0000000000..80b2477a6d --- /dev/null +++ b/cassandra/client_routes.py @@ -0,0 +1,451 @@ +# Copyright 2026 ScyllaDB, Inc. +# +# 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 +# +# http://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. + +""" +Client Routes support for Private Link and similar network configurations. + +This module implements support for dynamic address translation via the +system.client_routes table and CLIENT_ROUTES_CHANGE events. +""" + +from __future__ import absolute_import + +from dataclasses import dataclass +import enum +import logging +import socket +import threading +import uuid +from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Set, Tuple + +from cassandra import ConsistencyLevel +from cassandra.protocol import QueryMessage +from cassandra.query import dict_factory + +if TYPE_CHECKING: + from cassandra.connection import Connection + +log = logging.getLogger(__name__) + + +class ClientRoutesChangeType(enum.Enum): + """ + Types of CLIENT_ROUTES_CHANGE events. + + Currently the protocol defines only UPDATE_NODES. + New variants will be added here if the protocol is extended. + """ + UPDATE_NODES = "UPDATE_NODES" + + +@dataclass +class ClientRouteProxy: + """ + :param connection_id: String identifying the connection (required) + :param connection_addr_override:: Optional string address for initial connection + """ + + connection_id: str + connection_addr_override: Optional[str] = None + + def __post_init__(self): + if self.connection_id is None: + raise ValueError("connection_id is required") + +class ClientRoutesConfig: + """ + Configuration for client routes (Private Link support). + + :param proxies: List of :class:`ClientRouteProxy` objects + (REQUIRED, at least one) + :param advanced_shard_awareness: Whether to enable advanced shard awareness + (default: ``False``) + """ + + proxies: List[ClientRouteProxy] + advanced_shard_awareness: bool + + def __init__(self, proxies: List[ClientRouteProxy], advanced_shard_awareness: bool = False): + """ + :param proxies: List of ClientRouteProxy objects + :param advanced_shard_awareness: Enable advanced shard awareness (default False) + """ + if not proxies: + raise ValueError("At least one proxy must be specified") + + if not isinstance(proxies, (list, tuple)): + raise TypeError("proxies must be a list or tuple") + + for proxy in proxies: + if not isinstance(proxy, ClientRouteProxy): + raise TypeError("All proxies must be ClientRouteProxy instances") + + self.proxies = proxies + self.advanced_shard_awareness = advanced_shard_awareness + + def __repr__(self) -> str: + return (f"ClientRoutesConfig(proxies={self.proxies}, " + f"advanced_shard_awareness={self.advanced_shard_awareness})") + + +@dataclass(frozen=True) +class _Route: + connection_id: str + host_id: uuid.UUID + address: str # ipv4, ipv6 or DNS hostname from system.client_routes + port: int + +class _RouteStore: + """ + Thread-safe storage for routes. Reads are safe under CPython's GIL; + writes are serialized with a lock. + + This uses atomic pointer swaps for updates, allowing lock-free reads + while serializing writes. + """ + + _routes_by_host_id: Dict[uuid.UUID, _Route] + _lock: threading.Lock + + def __init__(self) -> None: + self._routes_by_host_id = {} + self._lock = threading.Lock() + + def get_by_host_id(self, host_id: uuid.UUID) -> Optional[_Route]: + """ + Get route for a host ID (lock-free read). + + :param host_id: UUID of the host + :return: _Route or None + """ + return self._routes_by_host_id.get(host_id) + + def get_all(self) -> List[_Route]: + """ + Get all routes as a list (lock-free read). + + :return: List of _Route + """ + return list(self._routes_by_host_id.values()) + + def _select_preferred_routes(self, new_routes: List[_Route]) -> List[_Route]: + """ + When multiple routes exist for the same host_id (different connection_ids), + prefer the connection_id already in use. Only migrate to a different + connection_id when the previously used one is no longer available. + + Must be called under self._lock. + """ + by_host: Dict[uuid.UUID, List[_Route]] = {} + for route in new_routes: + by_host.setdefault(route.host_id, []).append(route) + + selected = [] + for host_id, candidates in by_host.items(): + if len(candidates) == 1: + selected.append(candidates[0]) + continue + + existing = self._routes_by_host_id.get(host_id) + if existing: + preferred = [c for c in candidates if c.connection_id == existing.connection_id] + if preferred: + selected.append(preferred[0]) + continue + + selected.append(candidates[0]) + + return selected + + def update(self, routes: List[_Route]) -> None: + """ + Replace all routes atomically. + + :param routes: List of _Route objects + """ + with self._lock: + preferred = self._select_preferred_routes(routes) + self._routes_by_host_id = {route.host_id: route for route in preferred} + + def merge(self, new_routes: List[_Route], affected_host_ids: Set[uuid.UUID]) -> None: + """ + Merge new routes with existing ones atomically. + + Routes for affected_host_ids are replaced entirely: existing routes + for those hosts are dropped and replaced with whatever is in new_routes. + This handles deletions from system.client_routes (affected host present + but no new route for it). + + :param new_routes: List of _Route objects to merge + :param affected_host_ids: Set of host IDs affected by the change. + """ + with self._lock: + preferred = self._select_preferred_routes(new_routes) + new_by_host = {r.host_id: r for r in preferred} + + updated = {hid: r for hid, r in self._routes_by_host_id.items() + if hid not in affected_host_ids} + updated.update(new_by_host) + self._routes_by_host_id = updated + + +class _ClientRoutesHandler: + """ + Handles dynamic address translation for Private Link via system.client_routes. + + Lifecycle: + 1. Construction: Create with configuration + 2. Initialization: Read system.client_routes after control connection established + 3. Steady state: Listen for CLIENT_ROUTES_CHANGE events and update routes + 4. Translation: Translate addresses using Host ID lookup + """ + + config: 'ClientRoutesConfig' + ssl_enabled: bool + _routes: _RouteStore + _connection_ids: Set[str] + _proxy_addresses_override: Dict[str, str] + + def __init__(self, config: 'ClientRoutesConfig', ssl_enabled: bool = False): + """ + :param config: ClientRoutesConfig instance + :param ssl_enabled: Whether TLS is enabled (determines port selection) + """ + if not isinstance(config, ClientRoutesConfig): + raise TypeError("config must be a ClientRoutesConfig instance") + + self.config = config + self.ssl_enabled = ssl_enabled + self._routes = _RouteStore() + self._connection_ids = {dep.connection_id for dep in config.proxies} + # Precalculate proxy address mappings for efficient lookup + self._proxy_addresses_override = { + proxy.connection_id: proxy.connection_addr_override + for proxy in config.proxies + if proxy.connection_addr_override + } + + def initialize(self, connection: 'Connection', timeout: float) -> None: + """ + Load all routes from system.client_routes. + + Called once at startup and again whenever the control connection + is re-established. Reads all configured connection IDs and + replaces the in-memory route store atomically. + + Raises on failure so the caller can decide how to react (e.g. + abort startup or schedule a reconnect). + + :param connection: The Connection instance to execute queries on + :param timeout: Query timeout in seconds + """ + log.info("[client routes] Loading routes for %d proxies", len(self.config.proxies)) + + routes = self._query_all_routes_for_connections(connection, timeout, self._connection_ids) + self._routes.update(routes) + + def handle_client_routes_change(self, connection: 'Connection', timeout: float, + change_type: 'ClientRoutesChangeType', + connection_ids: Sequence[str], host_ids: Sequence[str]) -> None: + """ + Handle CLIENT_ROUTES_CHANGE event. + + Currently the protocol defines only :attr:`ClientRoutesChangeType.UPDATE_NODES`. + New variants will be added to the enum if the protocol is extended. + + :param connection: The Connection instance to execute queries on + :param timeout: Query timeout in seconds + :param change_type: A :class:`ClientRoutesChangeType` value + :param connection_ids: Affected connection ID strings; empty means all. + :param host_ids: Affected host ID strings; empty means all. + """ + + full_refresh = False + if not connection_ids or not host_ids: + log.warning( + "[client routes] CLIENT_ROUTES_CHANGE has no connection_ids or host_ids, doing full refresh") + full_refresh = True + elif len(connection_ids) != len(host_ids): + log.warning("[client routes] CLIENT_ROUTES_CHANGE has mismatched lengths (conn: %d, host: %d), doing full refresh", + len(connection_ids), len(host_ids)) + full_refresh = True + + if full_refresh: + routes = self._query_all_routes_for_connections(connection, timeout, self._connection_ids) + self._routes.update(routes) + return + + host_uuids = [uuid.UUID(hid) for hid in host_ids] + pairs = [(cid, hid) for cid, hid in zip(connection_ids, host_uuids) + if cid in self._connection_ids] + + if not pairs: + return + + routes = self._query_routes_for_change_event(connection, timeout, pairs) + self._routes.merge(routes, affected_host_ids=set(host_uuids)) + + def _query_all_routes_for_connections(self, connection: 'Connection', timeout: float, + connection_ids: Set[str]) -> List[_Route]: + """ + Query all routes for the given connection IDs (complete refresh). + + Used when control connection reconnects or as a fallback when + CLIENT_ROUTES_CHANGE event has malformed data. + + :param connection: Connection to execute query on + :param timeout: Query timeout in seconds + :param connection_ids: Set of connection ID strings + :return: List of _Route + """ + if not connection_ids: + return [] + + placeholders = ', '.join('?' for _ in connection_ids) + query = f"SELECT connection_id, host_id, address, port, tls_port FROM system.client_routes WHERE connection_id IN ({placeholders})" + params = [cid.encode('utf-8') for cid in connection_ids] + + log.debug("[client routes] Querying all routes for connection_ids=%s", connection_ids) + return self._execute_routes_query(connection, timeout, query, params) + + def _query_routes_for_change_event(self, connection: 'Connection', timeout: float, + route_pairs: List[Tuple[str, uuid.UUID]]) -> List[_Route]: + """ + Query specific routes affected by a CLIENT_ROUTES_CHANGE event. + + Takes a list of (connection_id, host_id) pairs that represent the exact + routes affected by an operation. This provides precise updates without + fetching unrelated routes. + + If the pairs list is empty or None, falls back to a complete refresh + of all routes for safety. + + :param connection: Connection to execute query on + :param timeout: Query timeout in seconds + :param route_pairs: List of (connection_id, host_id) tuples + :return: List of _Route + """ + unique_pairs = list(dict.fromkeys(route_pairs)) + + conn_ids = list(dict.fromkeys(cid for cid, _ in unique_pairs)) + host_ids = list(dict.fromkeys(hid for _, hid in unique_pairs)) + + log.debug("[client routes] Querying route pairs from CLIENT_ROUTES_CHANGE " + "(first 5 of %d): %s", len(unique_pairs), unique_pairs[:5]) + + conn_ph = ', '.join('?' for _ in conn_ids) + host_ph = ', '.join('?' for _ in host_ids) + query = ( + "SELECT connection_id, host_id, address, port, tls_port " + "FROM system.client_routes " + f"WHERE connection_id IN ({conn_ph}) AND host_id IN ({host_ph})" + ) + params: List = [cid.encode('utf-8') for cid in conn_ids] + params.extend(hid.bytes for hid in host_ids) + + return self._execute_routes_query(connection, timeout, query, params) + + def _execute_routes_query(self, connection: 'Connection', timeout: float, + query: str, params: List) -> List[_Route]: + """ + Execute a routes query and parse results. + + Common helper for both complete refresh and change event queries. + + :param connection: Connection to execute query on + :param timeout: Query timeout in seconds + :param query: CQL query string + :param params: Query parameters + :return: List of _Route + """ + log.debug("[client routes] Executing query: %s with %d parameters", query, len(params)) + + query_msg = QueryMessage(query=query, consistency_level=ConsistencyLevel.ONE, + query_params=params if params else None) + result = connection.wait_for_response( + query_msg, timeout=timeout + ) + + routes = [] + broken = 0 + rows = dict_factory(result.column_names, result.parsed_rows) + for row in rows: + try: + absent = [] + port = row['tls_port'] if self.ssl_enabled else row['port'] + connection_id = row['connection_id'] + host_id = row['host_id'] + address = row['address'] + + if not port: + absent.append("tls_port" if self.ssl_enabled else "port") + if not connection_id: + absent.append("connection_id") + if not host_id: + absent.append("host_id") + if not address: + absent.append("address") + + if absent: + log.error("[client routes] read a route %s, that has no values for the following fields: %s", row, ",".join(absent)) + broken += 1 + continue + + final_address = self._proxy_addresses_override.get(connection_id, address) + + routes.append(_Route( + connection_id=connection_id, + host_id=host_id, + address=final_address, + port=port, + )) + except Exception as e: + log.warning("[client routes] Failed to parse route row: %s", e) + broken += 1 + + if broken and not routes: + raise RuntimeError( + "[client routes] All %d route rows failed validation; " + "refusing to return empty result that would wipe the route store" % broken + ) + + return routes + + def resolve_host(self, host_id: uuid.UUID) -> Optional[Tuple[str, int]]: + """ + Resolve a host_id to an (address, port) pair. + + Looks up the current route and selects the appropriate port. + + :param host_id: Host UUID to resolve + :return: Tuple of (address, port) or None if no route mapping exists + """ + route = self._routes.get_by_host_id(host_id) + if route is None: + return None + + if not route.port: + raise ValueError("Mapping for host %s has no port" % host_id) + + try: + result = socket.getaddrinfo(route.address, route.port, + socket.AF_UNSPEC, socket.SOCK_STREAM) + if not result: + raise socket.gaierror("No addresses found for %s" % route.address) + resolved_ip = result[0][4][0] + return resolved_ip, route.port + except socket.gaierror as e: + log.warning('[client routes] Could not resolve hostname "%s" (host_id=%s): %s', + route.address, host_id, e) + raise diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 679293a52d..1181c6f686 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -20,16 +20,18 @@ import atexit import datetime +from enum import Enum from binascii import hexlify from collections import defaultdict from collections.abc import Mapping -from concurrent.futures import ThreadPoolExecutor, FIRST_COMPLETED, wait as wait_futures +from concurrent.futures import Future, ThreadPoolExecutor, FIRST_COMPLETED, wait as wait_futures from copy import copy from functools import partial, reduce, wraps from itertools import groupby, count, chain +import enum import json import logging -from typing import Optional +from typing import Any, Dict, Optional, Union, Tuple from warnings import warn from random import random import re @@ -48,10 +50,11 @@ SchemaTargetType, DriverException, ProtocolVersion, UnresolvableContactPoints, DependencyException) from cassandra.auth import _proxy_execute_key, PlainTextAuthProvider -from cassandra.connection import (ConnectionException, ConnectionShutdown, +from cassandra.client_routes import ClientRoutesChangeType, ClientRoutesConfig, _ClientRoutesHandler +from cassandra.connection import (ClientRoutesEndPointFactory, ConnectionException, ConnectionShutdown, ConnectionHeartbeat, ProtocolVersionUnsupported, EndPoint, DefaultEndPoint, DefaultEndPointFactory, - ContinuousPagingState, SniEndPointFactory, ConnectionBusy) + SniEndPointFactory, ConnectionBusy, locally_supported_compressions) from cassandra.cqltypes import UserType import cassandra.cqltypes as types from cassandra.encoder import Encoder @@ -75,7 +78,7 @@ NoSpeculativeExecutionPolicy, DefaultLoadBalancingPolicy, NeverRetryPolicy) from cassandra.pool import (Host, _ReconnectionHandler, _HostReconnectionHandler, - HostConnectionPool, HostConnection, + HostConnection, NoConnectionsAvailable) from cassandra.query import (SimpleStatement, PreparedStatement, BoundStatement, BatchStatement, bind_params, QueryTrace, TraceUnavailable, @@ -95,7 +98,6 @@ GraphSON3Serializer) from cassandra.datastax.graph.query import _request_timeout_key, _GraphSONContextRowFactory from cassandra.datastax import cloud as dscloud -from cassandra.scylla.cloud import CloudConfiguration from cassandra.application_info import ApplicationInfoBase try: @@ -191,16 +193,6 @@ def _connection_reduce_fn(val,import_fn): log = logging.getLogger(__name__) - -DEFAULT_MIN_REQUESTS = 5 -DEFAULT_MAX_REQUESTS = 100 - -DEFAULT_MIN_CONNECTIONS_PER_LOCAL_HOST = 2 -DEFAULT_MAX_CONNECTIONS_PER_LOCAL_HOST = 8 - -DEFAULT_MIN_CONNECTIONS_PER_REMOTE_HOST = 1 -DEFAULT_MAX_CONNECTIONS_PER_REMOTE_HOST = 2 - _GRAPH_PAGING_MIN_DSE_VERSION = Version('6.8.0') _NOT_SET = object() @@ -224,6 +216,14 @@ def __init__(self, message, errors): self.errors = errors +class SchemaAgreementScope(str, Enum): + """Scope selectors for :meth:`.Session.wait_for_schema_agreement`.""" + + RACK = 'rack' + DC = 'dc' + CLUSTER = 'cluster' + + def _future_completed(future): """ Helper for run_in_executor() """ exc = future.exception() @@ -515,8 +515,9 @@ def __init__(self, load_balancing_policy=None, retry_policy=None, class ProfileManager(object): - def __init__(self): + def __init__(self, pools_allowed: bool=True): self.profiles = dict() + self.pools_allowed = pools_allowed def _profiles_without_explicit_lbps(self): names = (profile_name for @@ -528,6 +529,8 @@ def _profiles_without_explicit_lbps(self): ) def distance(self, host): + if not self.pools_allowed: + return HostDistance.IGNORED distances = set(p.load_balancing_policy.distance(host) for p in self.profiles.values()) return HostDistance.LOCAL_RACK if HostDistance.LOCAL_RACK in distances else \ HostDistance.LOCAL if HostDistance.LOCAL in distances else \ @@ -543,10 +546,14 @@ def check_supported(self): p.load_balancing_policy.check_supported() def on_up(self, host): + if not self.pools_allowed: + return for p in self.profiles.values(): p.load_balancing_policy.on_up(host) def on_down(self, host): + if not self.pools_allowed: + return for p in self.profiles.values(): p.load_balancing_policy.on_down(host) @@ -620,6 +627,31 @@ class _ConfigMode(object): PROFILES = 2 +class ControlConnectionQueryFallback(enum.Enum): + """ + Controls how application queries use the control connection when node pools + are unavailable. + + ``Disabled`` requires a usable node pool for application queries. If the + driver cannot establish one during session startup, it raises + :class:`NoHostAvailable`. + + ``Fallback`` still attempts to create node pools, but allows application + queries to fall back to the control connection when no usable node pool is + available. Session startup is allowed to proceed even if the initial pool + attempts all fail. + + ``SkipPoolCreation`` disables node-pool creation for the session and uses + the control-connection fallback path for application queries. + + The fallback path is not used for requests targeted to an explicit host. + """ + + Disabled = "Disabled" + Fallback = "Fallback" + SkipPoolCreation = "SkipPoolCreation" + + class Cluster(object): """ The main class to use when interacting with a Cassandra cluster. @@ -672,7 +704,7 @@ class Cluster(object): server will be automatically used. """ - protocol_version = ProtocolVersion.DSE_V2 + protocol_version = ProtocolVersion.V5 """ The maximum version of the native protocol to use. @@ -680,7 +712,7 @@ class Cluster(object): If not set in the constructor, the driver will automatically downgrade version based on a negotiation with the server, but it is most efficient - to set this to the maximum supported by your version of Cassandra. + to set this to the maximum supported by your version of ScyllaDB. Setting this will also prevent conflicting versions negotiated if your cluster is upgraded. @@ -695,7 +727,7 @@ class Cluster(object): Used for testing new protocol features incrementally before the new version is complete. """ - compression = True + compression: Union[bool, str, None] = True """ Controls compression for communications between the driver and Cassandra. If left as the default of :const:`True`, either lz4 or snappy compression @@ -705,7 +737,7 @@ class Cluster(object): You may also set this to 'snappy' or 'lz4' to request that specific compression type. - Setting this to :const:`False` disables compression. + Setting this to :const:`False` or :const:`None` disables compression. """ _application_info: Optional[ApplicationInfoBase] = None @@ -731,9 +763,6 @@ def auth_provider(self): be an instance of a subclass of :class:`~cassandra.auth.AuthProvider`, such as :class:`~.PlainTextAuthProvider`. - When :attr:`~.Cluster.protocol_version` is 1, this should be - a function that accepts one argument, the IP address of a node, - and returns a dict of credentials for that node. When not using authentication, this should be left as :const:`None`. """ @@ -851,8 +880,8 @@ def default_retry_policy(self, policy): Using ssl_options without ssl_context is deprecated and will be removed in the next major release. - An optional dict which will be used as kwargs for ``ssl.SSLContext.wrap_socket`` - when new sockets are created. This should be used when client encryption is enabled + An optional dict which will be used as kwargs for ``ssl.SSLContext.wrap_socket`` + when new sockets are created. This should be used when client encryption is enabled in Cassandra. The following documentation only applies when ssl_options is used without ssl_context. @@ -943,6 +972,16 @@ def default_retry_policy(self, policy): If set to :const:`None`, there will be no timeout for these queries. """ + allow_control_connection_query_fallback: ControlConnectionQueryFallback = ControlConnectionQueryFallback.Disabled + """ + Controls whether application queries may fall back to the control connection. + + ``Disabled`` keeps the old behavior. + ``Fallback`` enables control-connection fallback when no usable node pools exist. + ``SkipPoolCreation`` skips node-pool creation and uses the control connection fallback path. + This fallback is still not used for requests targeted to an explicit host. + """ + idle_heartbeat_interval = 30 """ Interval, in seconds, on which to heartbeat idle connections. This helps @@ -1039,7 +1078,7 @@ def default_retry_policy(self, policy): documentation for :meth:`Session.timestamp_generator`. """ - monitor_reporting_enabled = True + monitor_reporting_enabled = False """ A boolean indicating if monitor reporting, which sends gathered data to Insights when running against DSE 6.8 and higher. @@ -1095,10 +1134,19 @@ def default_retry_policy(self, policy): used for columns in this cluster. """ - metadata_request_timeout = datetime.timedelta(seconds=2) + metadata_request_timeout: Optional[float] = None """ - Timeout for all queries used by driver it self. - Supported only by Scylla clusters. + Specifies a server-side timeout (in seconds) for all internal driver queries, + such as schema metadata lookups and cluster topology requests. + + The timeout is enforced by appending `USING TIMEOUT ` to queries + executed by the driver. + + - A value of `0` disables explicit timeout enforcement. In this case, + the driver does not add `USING TIMEOUT`, and the timeout is determined + by the server's defaults. + - Only supported when connected to Scylla clusters. + - If not explicitly set, defaults to the value of `control_connection_timeout`. """ @property @@ -1176,7 +1224,7 @@ def token_metadata_enabled(self, enabled): def __init__(self, contact_points=_NOT_SET, port=9042, - compression=True, + compression: Union[bool, str, None] = True, auth_provider=None, load_balancing_policy=None, reconnection_policy=None, @@ -1217,9 +1265,11 @@ def __init__(self, cloud=None, scylla_cloud=None, shard_aware_options=None, - metadata_request_timeout=None, + metadata_request_timeout: Optional[float] = None, column_encryption_policy=None, - application_info:Optional[ApplicationInfoBase]=None + application_info:Optional[ApplicationInfoBase]=None, + client_routes_config:Optional[ClientRoutesConfig]=None, + allow_control_connection_query_fallback:Optional[ControlConnectionQueryFallback]=ControlConnectionQueryFallback.Disabled ): """ ``executor_threads`` defines the number of threads in a pool for handling asynchronous tasks such as @@ -1237,27 +1287,15 @@ def __init__(self, if port < 1 or port > 65535: raise ValueError("Invalid port number (%s) (1-65535)" % port) + if not isinstance(allow_control_connection_query_fallback, ControlConnectionQueryFallback): + raise TypeError( + "allow_control_connection_query_fallback must be a ControlConnectionQueryFallback value") + if connection_class is not None: self.connection_class = connection_class if scylla_cloud is not None: - if contact_points is not _NOT_SET or ssl_context or ssl_options: - raise ValueError("contact_points, ssl_context, and ssl_options " - "cannot be specified with a scylla cloud configuration") - if shard_aware_options and not shard_aware_options.disable_shardaware_port: - raise ValueError("shard_aware_options.disable_shardaware_port=False " - "cannot be specified with a scylla cloud configuration") - uses_twisted = TwistedConnection and issubclass(self.connection_class, TwistedConnection) - uses_eventlet = EventletConnection and issubclass(self.connection_class, EventletConnection) - - scylla_cloud_config = CloudConfiguration.create(scylla_cloud, pyopenssl=uses_twisted or uses_eventlet, - endpoint_factory=endpoint_factory) - ssl_context = scylla_cloud_config.ssl_context - endpoint_factory = scylla_cloud_config.endpoint_factory - contact_points = scylla_cloud_config.contact_points - ssl_options = scylla_cloud_config.ssl_options - auth_provider = scylla_cloud_config.auth_provider - shard_aware_options = ShardAwareOptions(shard_aware_options, disable_shardaware_port=True) + raise NotImplementedError("scylla_cloud was removed and not supported anymore") if cloud is not None: self.cloud = cloud @@ -1300,11 +1338,69 @@ def __init__(self, if column_encryption_policy is not None: self.column_encryption_policy = column_encryption_policy + if client_routes_config is not None and endpoint_factory is not None: + raise ValueError("client_routes_config and endpoint_factory are mutually exclusive") + + self._client_routes_handler = None + if client_routes_config is not None: + if not isinstance(client_routes_config, ClientRoutesConfig): + raise TypeError("client_routes_config must be a ClientRoutesConfig instance") + + # SSL hostname verification is incompatible with client routes: + # connections go through NLB proxies whose addresses won't match + # server certificates. + _check_hostname_enabled = False + if ssl_context is not None and ssl_context.check_hostname: + _check_hostname_enabled = True + if ssl_options is not None and ssl_options.get('check_hostname', False): + _check_hostname_enabled = True + if _check_hostname_enabled: + raise ValueError( + "SSL hostname verification (check_hostname=True) is currently incompatible " + "with client_routes_config. When using client routes, connections " + "go through NLB proxies whose addresses won't match server " + "certificates. Disable hostname verification by setting " + "ssl_context.check_hostname = False." + ) + + ssl_enabled = ssl_context is not None or ssl_options is not None + self._client_routes_handler = _ClientRoutesHandler(client_routes_config, ssl_enabled=ssl_enabled) + + if contact_points is _NOT_SET or not self._contact_points_explicit: + seed_addrs = [dep.connection_addr_override for dep in client_routes_config.proxies + if dep.connection_addr_override] + if seed_addrs: + self.contact_points = seed_addrs + self._contact_points_explicit = True + log.info("[client routes] Using %d deployment connection addresses as contact points", + len(seed_addrs)) + + if self._client_routes_handler is not None: + endpoint_factory = ClientRoutesEndPointFactory(self._client_routes_handler, self.port) self.endpoint_factory = endpoint_factory or DefaultEndPointFactory(port=self.port) self.endpoint_factory.configure(self) self._resolve_hostnames() + if isinstance(compression, bool) or compression is None: + compression = bool(compression) + if compression and not locally_supported_compressions: + log.error( + "Compression is enabled, but no compression libraries are available. " + "Disabling compression, consider installing one of the Python packages: lz4 and/or python-snappy." + ) + compression = False + elif isinstance(compression, str): + if not locally_supported_compressions.get(compression): + raise ValueError( + "Compression '%s' was requested, but it is not available. " + "Consider installing the corresponding Python package." % compression + ) + else: + raise TypeError( + "The 'compression' option must be either a string (e.g., 'lz4' or 'snappy') " + "or a boolean (True to enable any available compression, False to disable it)." + ) self.compression = compression if protocol_version is not _NOT_SET: @@ -1315,8 +1411,6 @@ def __init__(self, self.no_compact = no_compact self.auth_provider = auth_provider - if metadata_request_timeout is not None: - self.metadata_request_timeout = metadata_request_timeout if load_balancing_policy is not None: if isinstance(load_balancing_policy, type): @@ -1358,7 +1452,8 @@ def __init__(self, else: self.timestamp_generator = MonotonicTimestampGenerator() - self.profile_manager = ProfileManager() + self.profile_manager = ProfileManager( + pools_allowed=allow_control_connection_query_fallback != ControlConnectionQueryFallback.SkipPoolCreation) self.profile_manager.profiles[EXEC_PROFILE_DEFAULT] = ExecutionProfile( self.load_balancing_policy, self.default_retry_policy, @@ -1427,6 +1522,8 @@ def __init__(self, self.cql_version = cql_version self.max_schema_agreement_wait = max_schema_agreement_wait self.control_connection_timeout = control_connection_timeout + self.allow_control_connection_query_fallback = allow_control_connection_query_fallback + self.metadata_request_timeout = self.control_connection_timeout if metadata_request_timeout is None else metadata_request_timeout self.idle_heartbeat_interval = idle_heartbeat_interval self.idle_heartbeat_timeout = idle_heartbeat_timeout self.schema_event_refresh_window = schema_event_refresh_window @@ -1439,6 +1536,10 @@ def __init__(self, self.monitor_reporting_interval = monitor_reporting_interval self.shard_aware_options = ShardAwareOptions(opts=shard_aware_options) + if (client_routes_config is not None + and not client_routes_config.advanced_shard_awareness): + self.shard_aware_options.disable_shardaware_port = True + self._listeners = set() self._listener_lock = Lock() @@ -1452,30 +1553,6 @@ def __init__(self, self._user_types = defaultdict(dict) - self._min_requests_per_connection = { - HostDistance.LOCAL_RACK: DEFAULT_MIN_REQUESTS, - HostDistance.LOCAL: DEFAULT_MIN_REQUESTS, - HostDistance.REMOTE: DEFAULT_MIN_REQUESTS - } - - self._max_requests_per_connection = { - HostDistance.LOCAL_RACK: DEFAULT_MAX_REQUESTS, - HostDistance.LOCAL: DEFAULT_MAX_REQUESTS, - HostDistance.REMOTE: DEFAULT_MAX_REQUESTS - } - - self._core_connections_per_host = { - HostDistance.LOCAL_RACK: DEFAULT_MIN_CONNECTIONS_PER_LOCAL_HOST, - HostDistance.LOCAL: DEFAULT_MIN_CONNECTIONS_PER_LOCAL_HOST, - HostDistance.REMOTE: DEFAULT_MIN_CONNECTIONS_PER_REMOTE_HOST - } - - self._max_connections_per_host = { - HostDistance.LOCAL_RACK: DEFAULT_MAX_CONNECTIONS_PER_LOCAL_HOST, - HostDistance.LOCAL: DEFAULT_MAX_CONNECTIONS_PER_LOCAL_HOST, - HostDistance.REMOTE: DEFAULT_MAX_CONNECTIONS_PER_REMOTE_HOST - } - self.executor = self._create_thread_pool_executor(max_workers=executor_threads) self.scheduler = _Scheduler(self.executor) @@ -1664,116 +1741,8 @@ def add_execution_profile(self, name, profile, pool_wait_timeout=5): futures.update(session.update_created_pools()) _, not_done = wait_futures(futures, pool_wait_timeout) if not_done: - raise OperationTimedOut("Failed to create all new connection pools in the %ss timeout.") - - def get_min_requests_per_connection(self, host_distance): - return self._min_requests_per_connection[host_distance] - - def set_min_requests_per_connection(self, host_distance, min_requests): - """ - Sets a threshold for concurrent requests per connection, below which - connections will be considered for disposal (down to core connections; - see :meth:`~Cluster.set_core_connections_per_host`). - - Pertains to connection pool management in protocol versions {1,2}. - """ - if self.protocol_version >= 3: - raise UnsupportedOperation( - "Cluster.set_min_requests_per_connection() only has an effect " - "when using protocol_version 1 or 2.") - if min_requests < 0 or min_requests > 126 or \ - min_requests >= self._max_requests_per_connection[host_distance]: - raise ValueError("min_requests must be 0-126 and less than the max_requests for this host_distance (%d)" % - (self._min_requests_per_connection[host_distance],)) - self._min_requests_per_connection[host_distance] = min_requests - - def get_max_requests_per_connection(self, host_distance): - return self._max_requests_per_connection[host_distance] - - def set_max_requests_per_connection(self, host_distance, max_requests): - """ - Sets a threshold for concurrent requests per connection, above which new - connections will be created to a host (up to max connections; - see :meth:`~Cluster.set_max_connections_per_host`). - - Pertains to connection pool management in protocol versions {1,2}. - """ - if self.protocol_version >= 3: - raise UnsupportedOperation( - "Cluster.set_max_requests_per_connection() only has an effect " - "when using protocol_version 1 or 2.") - if max_requests < 1 or max_requests > 127 or \ - max_requests <= self._min_requests_per_connection[host_distance]: - raise ValueError("max_requests must be 1-127 and greater than the min_requests for this host_distance (%d)" % - (self._min_requests_per_connection[host_distance],)) - self._max_requests_per_connection[host_distance] = max_requests - - def get_core_connections_per_host(self, host_distance): - """ - Gets the minimum number of connections per Session that will be opened - for each host with :class:`~.HostDistance` equal to `host_distance`. - The default is 2 for :attr:`~HostDistance.LOCAL` and 1 for - :attr:`~HostDistance.REMOTE`. - - This property is ignored if :attr:`~.Cluster.protocol_version` is - 3 or higher. - """ - return self._core_connections_per_host[host_distance] - - def set_core_connections_per_host(self, host_distance, core_connections): - """ - Sets the minimum number of connections per Session that will be opened - for each host with :class:`~.HostDistance` equal to `host_distance`. - The default is 2 for :attr:`~HostDistance.LOCAL` and 1 for - :attr:`~HostDistance.REMOTE`. - - Protocol version 1 and 2 are limited in the number of concurrent - requests they can send per connection. The driver implements connection - pooling to support higher levels of concurrency. - - If :attr:`~.Cluster.protocol_version` is set to 3 or higher, this - is not supported (there is always one connection per host, unless - the host is remote and :attr:`connect_to_remote_hosts` is :const:`False`) - and using this will result in an :exc:`~.UnsupportedOperation`. - """ - if self.protocol_version >= 3: - raise UnsupportedOperation( - "Cluster.set_core_connections_per_host() only has an effect " - "when using protocol_version 1 or 2.") - old = self._core_connections_per_host[host_distance] - self._core_connections_per_host[host_distance] = core_connections - if old < core_connections: - self._ensure_core_connections() - - def get_max_connections_per_host(self, host_distance): - """ - Gets the maximum number of connections per Session that will be opened - for each host with :class:`~.HostDistance` equal to `host_distance`. - The default is 8 for :attr:`~HostDistance.LOCAL` and 2 for - :attr:`~HostDistance.REMOTE`. - - This property is ignored if :attr:`~.Cluster.protocol_version` is - 3 or higher. - """ - return self._max_connections_per_host[host_distance] - - def set_max_connections_per_host(self, host_distance, max_connections): - """ - Sets the maximum number of connections per Session that will be opened - for each host with :class:`~.HostDistance` equal to `host_distance`. - The default is 2 for :attr:`~HostDistance.LOCAL` and 1 for - :attr:`~HostDistance.REMOTE`. - - If :attr:`~.Cluster.protocol_version` is set to 3 or higher, this - is not supported (there is always one connection per host, unless - the host is remote and :attr:`connect_to_remote_hosts` is :const:`False`) - and using this will result in an :exc:`~.UnsupportedOperation`. - """ - if self.protocol_version >= 3: - raise UnsupportedOperation( - "Cluster.set_max_connections_per_host() only has an effect " - "when using protocol_version 1 or 2.") - self._max_connections_per_host[host_distance] = max_connections + raise OperationTimedOut("Failed to create all new connection pools in the %ss timeout." % pool_wait_timeout, + timeout=pool_wait_timeout) def connection_factory(self, endpoint, host_conn = None, *args, **kwargs): """ @@ -1818,14 +1787,7 @@ def protocol_downgrade(self, host_endpoint, previous_version): "http://datastax.github.io/python-driver/api/cassandra/cluster.html#cassandra.cluster.Cluster.protocol_version", self.protocol_version, new_version, host_endpoint) self.protocol_version = new_version - def _add_resolved_hosts(self): - for endpoint in self.endpoints_resolved: - host, new = self.add_host(endpoint, signal=False) - if new: - host.set_up() - for listener in self.listeners: - listener.on_add(host) - + def _populate_hosts(self): self.profile_manager.populate( weakref.proxy(self), self.metadata.all_hosts()) self.load_balancing_policy.populate( @@ -1852,17 +1814,10 @@ def connect(self, keyspace=None, wait_for_all_pools=False): self.contact_points, self.protocol_version) self.connection_class.initialize_reactor() _register_cluster_shutdown(self) - - self._add_resolved_hosts() try: self.control_connection.connect() - - # we set all contact points up for connecting, but we won't infer state after this - for endpoint in self.endpoints_resolved: - h = self.metadata.get_host(endpoint) - if h and self.profile_manager.distance(h) == HostDistance.IGNORED: - h.is_up = None + self._populate_hosts() log.debug("Control connection created") except Exception: @@ -1871,14 +1826,6 @@ def connect(self, keyspace=None, wait_for_all_pools=False): self.shutdown() raise - # Update the information about tablet support after connection handshake. - self.load_balancing_policy._tablets_routing_v1 = self.control_connection._tablets_routing_v1 - child_policy = self.load_balancing_policy.child_policy if hasattr(self.load_balancing_policy, 'child_policy') else None - while child_policy is not None: - if hasattr(child_policy, '_tablet_routing_v1'): - child_policy._tablet_routing_v1 = self.control_connection._tablets_routing_v1 - child_policy = child_policy.child_policy if hasattr(child_policy, 'child_policy') else None - self.profile_manager.check_supported() # todo: rename this method if self.idle_heartbeat_interval: @@ -1918,7 +1865,8 @@ def get_all_pools(self): return pools def is_shard_aware(self): - return bool(self.get_all_pools()[0].host.sharding_info) + pools = self.get_all_pools() + return bool(pools and pools[0].host.sharding_info) def shard_aware_stats(self): if self.is_shard_aware(): @@ -1952,6 +1900,9 @@ def shutdown(self): self.executor.shutdown() + if self.metrics_enabled and self.metrics: + self.metrics.shutdown() + _discard_cluster_shutdown(self) def __enter__(self): @@ -2020,7 +1971,7 @@ def on_up(self, host): """ Intended for internal use only. """ - if self.is_shutdown: + if self.is_shutdown or self.allow_control_connection_query_fallback == ControlConnectionQueryFallback.SkipPoolCreation: return log.debug("Waiting to acquire lock for handling up status of node %s", host) @@ -2128,7 +2079,7 @@ def on_down(self, host, is_host_addition, expect_host_to_be_down=False): """ Intended for internal use only. """ - if self.is_shutdown: + if self.is_shutdown or self.allow_control_connection_query_fallback == ControlConnectionQueryFallback.SkipPoolCreation: return with host.lock: @@ -2159,14 +2110,14 @@ def on_add(self, host, refresh_nodes=True): log.debug("Handling new host %r and notifying listeners", host) + self.profile_manager.on_add(host) + self.control_connection.on_add(host, refresh_nodes) + distance = self.profile_manager.distance(host) if distance != HostDistance.IGNORED: self._prepare_all_queries(host) log.debug("Done preparing queries for new host %r", host) - self.profile_manager.on_add(host) - self.control_connection.on_add(host, refresh_nodes) - if distance == HostDistance.IGNORED: log.debug("Not adding connection pool for new host %r because the " "load balancing policy has marked it as IGNORED", host) @@ -2733,23 +2684,26 @@ def __init__(self, cluster, hosts, keyspace=None): # create connection pools in parallel self._initial_connect_futures = set() - for host in hosts: - future = self.add_or_renew_pool(host, is_host_addition=False) - if future: - self._initial_connect_futures.add(future) - - futures = wait_futures(self._initial_connect_futures, return_when=FIRST_COMPLETED) - while futures.not_done and not any(f.result() for f in futures.done): - futures = wait_futures(futures.not_done, return_when=FIRST_COMPLETED) - - if not any(f.result() for f in self._initial_connect_futures): - msg = "Unable to connect to any servers" - if self.keyspace: - msg += " using keyspace '%s'" % self.keyspace - raise NoHostAvailable(msg, [h.address for h in hosts]) + fallback_mode = self.cluster.allow_control_connection_query_fallback + if fallback_mode is not ControlConnectionQueryFallback.SkipPoolCreation: + for host in hosts: + future = self.add_or_renew_pool(host, is_host_addition=False) + if future: + self._initial_connect_futures.add(future) + + futures = wait_futures(self._initial_connect_futures, return_when=FIRST_COMPLETED) + while futures.not_done and not any(f.result() for f in futures.done): + futures = wait_futures(futures.not_done, return_when=FIRST_COMPLETED) + + # Only Disabled requires an initial pool to come up. + if not any(f.result() for f in self._initial_connect_futures) and \ + fallback_mode is ControlConnectionQueryFallback.Disabled: + msg = "Unable to connect to any servers" + if self.keyspace: + msg += " using keyspace '%s'" % self.keyspace + raise NoHostAvailable(msg, [h.address for h in hosts]) self.session_id = uuid.uuid4() - self._graph_paging_available = self._check_graph_paging_available() if self.cluster.column_encryption_policy is not None: try: @@ -2946,26 +2900,10 @@ def execute_graph_async(self, query, parameters=None, trace=False, execution_pro def _maybe_set_graph_paging(self, execution_profile): graph_paging = execution_profile.continuous_paging_options if execution_profile.continuous_paging_options is _NOT_SET: - graph_paging = ContinuousPagingOptions() if self._graph_paging_available else None + graph_paging = None execution_profile.continuous_paging_options = graph_paging - def _check_graph_paging_available(self): - """Verify if we can enable graph paging. This executed only once when the session is created.""" - - if not ProtocolVersion.has_continuous_paging_next_pages(self._protocol_version): - return False - - for host in self.cluster.metadata.all_hosts(): - if host.dse_version is None: - return False - - version = Version(host.dse_version) - if version < _GRAPH_PAGING_MIN_DSE_VERSION: - return False - - return True - def _resolve_execution_profile_options(self, execution_profile): """ Determine the GraphSON protocol and row factory for a graph query. This is useful @@ -3101,25 +3039,15 @@ def _create_response_future(self, query, parameters, trace, custom_payload, spec_exec_policy = execution_profile.speculative_execution_policy fetch_size = query.fetch_size - if fetch_size is FETCH_SIZE_UNSET and self._protocol_version >= 2: + if fetch_size is FETCH_SIZE_UNSET: fetch_size = self.default_fetch_size - elif self._protocol_version == 1: - fetch_size = None start_time = time.time() - if self._protocol_version >= 3 and self.use_client_timestamp: + if self.use_client_timestamp: timestamp = self.cluster.timestamp_generator() else: timestamp = None - supports_continuous_paging_state = ( - ProtocolVersion.has_continuous_paging_next_pages(self._protocol_version) - ) - if continuous_paging_options and supports_continuous_paging_state: - continuous_paging_state = ContinuousPagingState(continuous_paging_options.max_queue_size) - else: - continuous_paging_state = None - if isinstance(query, SimpleStatement): query_string = query.query_string statement_keyspace = query.keyspace if ProtocolVersion.uses_keyspace_flag(self._protocol_version) else None @@ -3163,7 +3091,7 @@ def _create_response_future(self, query, parameters, trace, custom_payload, self, message, query, timeout, metrics=self._metrics, prepared_statement=prepared_statement, retry_policy=retry_policy, row_factory=row_factory, load_balancer=load_balancing_policy, start_time=start_time, speculative_execution_plan=spec_exec_plan, - continuous_paging_state=continuous_paging_state, host=host) + continuous_paging_state=None, host=host) def get_execution_profile(self, name): """ @@ -3281,7 +3209,7 @@ def prepare(self, query, custom_payload=None, keyspace=None): prepared_keyspace = keyspace if keyspace else None prepared_statement = PreparedStatement.from_message( response.query_id, response.bind_metadata, response.pk_indexes, self.cluster.metadata, query, prepared_keyspace, - self._protocol_version, response.column_metadata, response.result_metadata_id, self.cluster.column_encryption_policy) + self._protocol_version, response.column_metadata, response.result_metadata_id, response.is_lwt, self.cluster.column_encryption_policy) prepared_statement.custom_payload = future.custom_payload self.cluster.add_prepared(response.query_id, prepared_statement) @@ -3372,17 +3300,16 @@ def add_or_renew_pool(self, host, is_host_addition): """ For internal use only. """ + if self.cluster.allow_control_connection_query_fallback is ControlConnectionQueryFallback.SkipPoolCreation: + return None + distance = self._profile_manager.distance(host) if distance == HostDistance.IGNORED: return None def run_add_or_renew_pool(): try: - if self._protocol_version >= 3: - new_pool = HostConnection(host, distance, self) - else: - # TODO remove host pool again ??? - new_pool = HostConnectionPool(host, distance, self) + new_pool = HostConnection(host, distance, self) except AuthenticationFailed as auth_exc: conn_exc = ConnectionException(str(auth_exc), endpoint=host) self.cluster.signal_connection_failure(host, conn_exc, is_host_addition) @@ -3446,6 +3373,9 @@ def update_created_pools(self): For internal use only. """ + if self.cluster.allow_control_connection_query_fallback is ControlConnectionQueryFallback.SkipPoolCreation: + return set() + futures = set() for host in self.cluster.metadata.all_hosts(): distance = self._profile_manager.distance(host) @@ -3514,6 +3444,185 @@ def pool_finished_setting_keyspace(pool, host_errors): for pool in tuple(self._pools.values()): pool._set_keyspace_for_all_conns(keyspace, pool_finished_setting_keyspace) + def wait_for_schema_agreement(self, wait_time: Optional[float] = None, + scope: SchemaAgreementScope = SchemaAgreementScope.CLUSTER) -> bool: + """ + Wait for connected hosts in the selected scope to report the same + schema version from ``system.local``. + + By default, the timeout for this operation is governed by + :attr:`~.Cluster.max_schema_agreement_wait` and + :attr:`~.Cluster.control_connection_timeout`. + + Passing ``wait_time`` here overrides + :attr:`~.Cluster.max_schema_agreement_wait`. If provided, ``wait_time`` + must be greater than 0. + + ``scope`` determines which connected hosts participate in the check. + Pass :attr:`SchemaAgreementScope.RACK`, :attr:`SchemaAgreementScope.DC`, + or :attr:`SchemaAgreementScope.CLUSTER`. + The default is :attr:`SchemaAgreementScope.CLUSTER`. ``RACK`` narrows + the check to connected hosts in the local rack only. ``DC`` checks + connected hosts in the local datacenter. ``CLUSTER`` queries every + connected host across all datacenters. + + :param wait_time: Override for + :attr:`~.Cluster.max_schema_agreement_wait`, should be positive + number. + :param scope: Restricts the check to connected hosts in the local rack, + local datacenter, or whole connected cluster. + :returns: ``True`` when the selected connected hosts agree on schema, + otherwise ``False``. + :raises ValueError: If ``wait_time`` is provided and is not greater + than 0. + :raises ValueError: If ``scope`` is not one of the schema agreement + scope values. + """ + + if wait_time is not None and wait_time <= 0: + raise ValueError("wait_time must be greater than 0") + + total_timeout = wait_time if wait_time is not None else self.cluster.max_schema_agreement_wait + if total_timeout <= 0: + raise ValueError("total_timeout must be greater than 0") + + deadline = time.time() + total_timeout + schema_mismatches = None + scope_label = 'local rack' if scope is SchemaAgreementScope.RACK else ( + 'local datacenter' if scope is SchemaAgreementScope.DC else 'cluster') + + while time.time() < deadline: + schema_mismatches = self._get_schema_mismatches_for_scope(deadline, scope) + if schema_mismatches is None: + return True + + log.debug("[session] Connected hosts in the %s still disagree on schema, trying again", scope_label) + remaining = deadline - time.time() + if remaining > 0: + time.sleep(min(0.2, remaining)) + + log.warning("[session] Connected hosts in the %s are reporting a schema disagreement: %s", + scope_label, schema_mismatches) + return False + + def _get_schema_mismatches_for_scope(self, deadline: float, + scope: SchemaAgreementScope) -> Optional[Dict[Any, Any]]: + hosts = self._get_schema_agreement_hosts(scope) + mismatches = defaultdict(list) + errors = {} + scope_label = 'local rack' if scope is SchemaAgreementScope.RACK else ( + 'local datacenter' if scope is SchemaAgreementScope.DC else 'cluster') + + if not hosts: + errors[scope.value] = ConnectionException( + "No connected hosts available in the %s" % (scope_label,) + ) + return {'unavailable': errors} + + metadata_request_timeout = self.cluster.control_connection._metadata_request_timeout + query = maybe_add_timeout_to_query(ControlConnection._SELECT_SCHEMA_LOCAL, metadata_request_timeout) + + schema_version_futures = [] + for host in hosts: + try: + schema_version_future = self._query_local_schema_version(host, query, deadline) + except Exception as exc: + errors[host.endpoint] = exc + continue + + schema_version_futures.append((host, schema_version_future)) + + if schema_version_futures: + # Start all host queries first, then wait for the whole batch. + remaining = max(0.0, deadline - time.time()) + if remaining > 0: + wait_futures([future for _, future in schema_version_futures], timeout=remaining) + + for host, future in schema_version_futures: + if future.done(): + try: + rows = future.result() + except Exception as exc: + errors[host.endpoint] = exc + continue + + row = rows.one() + schema_version = getattr(row, "schema_version", None) if row is not None else None + mismatches[schema_version].append(host.endpoint) + else: + errors[host.endpoint] = OperationTimedOut(last_host=host, timeout=max(0.0, deadline - time.time())) + + if len(mismatches) == 1 and None not in mismatches and not errors: + log.debug("[session] Connected hosts in the %s agree on schema", scope_label) + return None + + if errors: + mismatches['unavailable'] = errors + return dict(mismatches) + + def _get_schema_agreement_hosts(self, scope: SchemaAgreementScope) -> Tuple[Host, ...]: + if scope is SchemaAgreementScope.RACK: + allowed_distances = (HostDistance.LOCAL_RACK,) + elif scope is SchemaAgreementScope.DC: + allowed_distances = (HostDistance.LOCAL_RACK, HostDistance.LOCAL) + else: + allowed_distances = (HostDistance.LOCAL_RACK, HostDistance.LOCAL, HostDistance.REMOTE) + + return tuple( + host for host, pool in tuple(self._pools.items()) + if host.is_up + and not pool.is_shutdown + and self._profile_manager.distance(host) in allowed_distances) + + def _query_local_schema_version(self, host: Host, query: str, deadline: float) -> Future: + remaining = max(0.0, deadline - time.time()) + try: + response_future = self.execute_async( + query, + timeout=self._schema_agreement_query_timeout(remaining), + host=host, + ) + except OperationTimedOut as timeout: + log.debug("[session] Timed out waiting for schema version from %s: %s", host, timeout) + raise + except Exception as exc: + log.debug("[session] Error querying schema version from %s: %s", host, exc) + raise + + # execute_async returns cassandra.cluster.ResponseFuture, which does not have bulk waiting logic for it. + # That is why _query_local_schema_version returns concurrent.futures.Future + # so that schema agreement logic could use concurrent.futures.wait_futures to wait on them. + # schema_version_future is an adapter between cassandra.cluster.ResponseFuture and concurrent.futures.Future + # to make things work + schema_version_future = Future() + + def _set_result(result, result_future=schema_version_future, response_future=response_future): + if result_future.done(): + return + try: + result_future.set_result(ResultSet(response_future, result)) + except Exception as exc: + result_future.set_exception(exc) + + def _set_exception(exc, result_future=schema_version_future): + if result_future.done(): + return + result_future.set_exception(exc) + + try: + response_future.add_callbacks(_set_result, _set_exception) + except Exception as exc: + log.debug("[session] Error registering schema version callback from %s: %s", host, exc) + raise + + return schema_version_future + + def _schema_agreement_query_timeout(self, remaining: float) -> float: + control_timeout = self.cluster.control_connection._timeout + if control_timeout is None: + return max(0.0, remaining) + return max(0.0, min(control_timeout, remaining)) + def user_type_registered(self, keyspace, user_type, klass): """ Called by the parent Cluster instance when the user registers a new @@ -3620,9 +3729,9 @@ class ControlConnection(object): Internal """ - _SELECT_PEERS = "SELECT * FROM system.peers" + _SELECT_PEERS = "SELECT peer, data_center, host_id, rack, release_version, rpc_address, schema_version, tokens FROM system.peers" _SELECT_PEERS_NO_TOKENS_TEMPLATE = "SELECT host_id, peer, data_center, rack, rpc_address, {nt_col_name}, release_version, schema_version FROM system.peers" - _SELECT_LOCAL = "SELECT * FROM system.local WHERE key='local'" + _SELECT_LOCAL = "SELECT broadcast_address, cluster_name, data_center, host_id, listen_address, partitioner, rack, release_version, rpc_address, schema_version, tokens FROM system.local WHERE key='local'" _SELECT_LOCAL_NO_TOKENS = "SELECT host_id, cluster_name, data_center, rack, partitioner, release_version, schema_version, rpc_address FROM system.local WHERE key='local'" # Used only when token_metadata_enabled is set to False _SELECT_LOCAL_NO_TOKENS_RPC_ADDRESS = "SELECT rpc_address FROM system.local WHERE key='local'" @@ -3708,28 +3817,22 @@ def _set_new_connection(self, conn): if old: log.debug("[control connection] Closing old connection %r, replacing with %r", old, conn) old.close() - - def _connect_host_in_lbp(self): + + def _try_connect_to_hosts(self): errors = {} - lbp = ( - self._cluster.load_balancing_policy - if self._cluster._config_mode == _ConfigMode.LEGACY else - self._cluster._default_load_balancing_policy - ) - for host in lbp.make_query_plan(): + lbp = self._cluster.load_balancing_policy \ + if self._cluster._config_mode == _ConfigMode.LEGACY else self._cluster._default_load_balancing_policy + + for endpoint in chain((host.endpoint for host in lbp.make_query_plan()), self._cluster.endpoints_resolved): try: - return (self._try_connect(host), None) - except ConnectionException as exc: - errors[str(host.endpoint)] = exc - log.warning("[control connection] Error connecting to %s:", host, exc_info=True) - self._cluster.signal_connection_failure(host, exc, is_host_addition=False) + return (self._try_connect(endpoint), None) except Exception as exc: - errors[str(host.endpoint)] = exc - log.warning("[control connection] Error connecting to %s:", host, exc_info=True) + errors[str(endpoint)] = exc + log.warning("[control connection] Error connecting to %s:", endpoint, exc_info=True) if self._is_shutdown: raise DriverException("[control connection] Reconnection in progress during shutdown") - + return (None, errors) def _reconnect_internal(self): @@ -3741,43 +3844,43 @@ def _reconnect_internal(self): to the exception that was raised when an attempt was made to open a connection to that host. """ - (conn, _) = self._connect_host_in_lbp() + (conn, _) = self._try_connect_to_hosts() if conn is not None: return conn # Try to re-resolve hostnames as a fallback when all hosts are unreachable self._cluster._resolve_hostnames() - self._cluster._add_resolved_hosts() + self._cluster._populate_hosts() - (conn, errors) = self._connect_host_in_lbp() + (conn, errors) = self._try_connect_to_hosts() if conn is not None: return conn - + raise NoHostAvailable("Unable to connect to any servers", errors) - def _try_connect(self, host): + def _try_connect(self, endpoint): """ Creates a new Connection, registers for pushed events, and refreshes node/token and schema metadata. """ - log.debug("[control connection] Opening new connection to %s", host) + log.debug("[control connection] Opening new connection to %s", endpoint) while True: try: - connection = self._cluster.connection_factory(host.endpoint, is_control_connection=True) + connection = self._cluster.connection_factory(endpoint, is_control_connection=True) if self._is_shutdown: connection.close() raise DriverException("Reconnecting during shutdown") break except ProtocolVersionUnsupported as e: - self._cluster.protocol_downgrade(host.endpoint, e.startup_version) + self._cluster.protocol_downgrade(endpoint, e.startup_version) except ProtocolException as e: # protocol v5 is out of beta in C* >=4.0-beta5 and is now the default driver # protocol version. If the protocol version was not explicitly specified, # and that the server raises a beta protocol error, we should downgrade. if not self._cluster._protocol_version_explicit and e.is_beta_protocol_error: - self._cluster.protocol_downgrade(host.endpoint, self._cluster.protocol_version) + self._cluster.protocol_downgrade(endpoint, self._cluster.protocol_version) else: raise @@ -3790,9 +3893,11 @@ def _try_connect(self, host): if connection.features.sharding_info is not None: self._uses_peers_v2 = False - # Cassandra does not support "USING TIMEOUT" - self._metadata_request_timeout = None if connection.features.sharding_info is None \ - else datetime.timedelta(seconds=self._cluster.control_connection_timeout) + # Only ScyllaDB supports "USING TIMEOUT" + # Sharding information signals it is ScyllaDB + self._metadata_request_timeout = None if connection.features.sharding_info is None or not self._cluster.metadata_request_timeout \ + else datetime.timedelta(seconds=self._cluster.metadata_request_timeout) + self._tablets_routing_v1 = connection.features.tablets_routing_v1 # use weak references in both directions @@ -3801,11 +3906,21 @@ def _try_connect(self, host): # this object (after a dereferencing a weakref) self_weakref = weakref.ref(self, partial(_clear_watcher, weakref.proxy(connection))) try: - connection.register_watchers({ + watchers = { "TOPOLOGY_CHANGE": partial(_watch_callback, self_weakref, '_handle_topology_change'), "STATUS_CHANGE": partial(_watch_callback, self_weakref, '_handle_status_change'), "SCHEMA_CHANGE": partial(_watch_callback, self_weakref, '_handle_schema_change') - }, register_timeout=self._timeout) + } + + if self._cluster._client_routes_handler is not None: + watchers["CLIENT_ROUTES_CHANGE"] = partial(_watch_callback, self_weakref, '_handle_client_routes_change') + + connection.register_watchers(watchers, register_timeout=self._timeout) + + if self._cluster._client_routes_handler is not None: + self._cluster._client_routes_handler.initialize( + connection, + self._timeout) sel_peers = self._get_peers_query(self.PeersQueryType.PEERS, connection) sel_local = self._SELECT_LOCAL if self._token_meta_enabled else self._SELECT_LOCAL_NO_TOKENS @@ -3920,7 +4035,7 @@ def _refresh_schema(self, connection, preloaded_results=None, schema_agreement_w if self._cluster.is_shutdown: return False - agreed = self.wait_for_schema_agreement(connection, + agreed = self._wait_for_schema_agreement(connection=connection, preloaded_results=preloaded_results, wait_time=schema_agreement_wait) @@ -3990,67 +4105,10 @@ def _refresh_node_list_and_token_map(self, connection, preloaded_results=None, self._cluster.metadata.cluster_name = cluster_name partitioner = local_row.get("partitioner") - tokens = local_row.get("tokens") - - host = self._cluster.metadata.get_host(connection.original_endpoint) - if host: - datacenter = local_row.get("data_center") - rack = local_row.get("rack") - self._update_location_info(host, datacenter, rack) - - # support the use case of connecting only with public address - if isinstance(self._cluster.endpoint_factory, SniEndPointFactory): - new_endpoint = self._cluster.endpoint_factory.create(local_row) - - if new_endpoint.address: - host.endpoint = new_endpoint - - host.host_id = local_row.get("host_id") - - found_host_ids.add(host.host_id) - found_endpoints.add(host.endpoint) - - host.listen_address = local_row.get("listen_address") - host.listen_port = local_row.get("listen_port") - host.broadcast_address = _NodeInfo.get_broadcast_address(local_row) - host.broadcast_port = _NodeInfo.get_broadcast_port(local_row) - - host.broadcast_rpc_address = _NodeInfo.get_broadcast_rpc_address(local_row) - host.broadcast_rpc_port = _NodeInfo.get_broadcast_rpc_port(local_row) - if host.broadcast_rpc_address is None: - if self._token_meta_enabled: - # local rpc_address is not available, use the connection endpoint - host.broadcast_rpc_address = connection.endpoint.address - host.broadcast_rpc_port = connection.endpoint.port - else: - # local rpc_address has not been queried yet, try to fetch it - # separately, which might fail because C* < 2.1.6 doesn't have rpc_address - # in system.local. See CASSANDRA-9436. - local_rpc_address_query = QueryMessage( - query=maybe_add_timeout_to_query(self._SELECT_LOCAL_NO_TOKENS_RPC_ADDRESS, self._metadata_request_timeout), - consistency_level=ConsistencyLevel.ONE) - success, local_rpc_address_result = connection.wait_for_response( - local_rpc_address_query, timeout=self._timeout, fail_on_error=False) - if success: - row = dict_factory( - local_rpc_address_result.column_names, - local_rpc_address_result.parsed_rows) - host.broadcast_rpc_address = _NodeInfo.get_broadcast_rpc_address(row[0]) - host.broadcast_rpc_port = _NodeInfo.get_broadcast_rpc_port(row[0]) - else: - host.broadcast_rpc_address = connection.endpoint.address - host.broadcast_rpc_port = connection.endpoint.port + tokens = local_row.get("tokens", None) - host.release_version = local_row.get("release_version") - host.dse_version = local_row.get("dse_version") - host.dse_workload = local_row.get("workload") - host.dse_workloads = local_row.get("workloads") + peers_result.insert(0, local_row) - if partitioner and tokens: - token_map[host] = tokens - - self._cluster.metadata.update_host(host, old_endpoint=connection.endpoint) - connection.original_endpoint = connection.endpoint = host.endpoint # Check metadata.partitioner to see if we haven't built anything yet. If # every node in the cluster was in the contact points, we won't discover # any new nodes, so we need this additional check. (See PYTHON-90) @@ -4080,14 +4138,16 @@ def _refresh_node_list_and_token_map(self, connection, preloaded_results=None, host = self._cluster.metadata.get_host_by_host_id(host_id) if host and host.endpoint != endpoint: log.debug("[control connection] Updating host ip from %s to %s for (%s)", host.endpoint, endpoint, host_id) - old_endpoint = host.endpoint - host.endpoint = endpoint - self._cluster.metadata.update_host(host, old_endpoint) reconnector = host.get_and_set_reconnection_handler(None) if reconnector: reconnector.cancel() self._cluster.on_down(host, is_host_addition=False, expect_host_to_be_down=True) + old_endpoint = host.endpoint + host.endpoint = endpoint + self._cluster.metadata.update_host(host, old_endpoint) + self._cluster.on_up(host) + if host is None: log.debug("[control connection] Found new host to connect to: %s", endpoint) host, _ = self._cluster.add_host(endpoint, datacenter=datacenter, rack=rack, signal=True, refresh_nodes=False, host_id=host_id) @@ -4223,6 +4283,44 @@ def _handle_status_change(self, event): # this will be run by the scheduler self._cluster.on_down(host, is_host_addition=False) + def _handle_client_routes_change(self, event: Dict[str, Any]) -> None: + """ + Handle CLIENT_ROUTES_CHANGE event from the server. + + This event indicates that the system.client_routes table has been updated + and we need to refresh our route mappings. + """ + if self._cluster._client_routes_handler is None: + log.warning("[control connection] Received CLIENT_ROUTES_CHANGE but no handler configured") + return + + raw_change_type = event.get("change_type") + try: + change_type = ClientRoutesChangeType(raw_change_type) + except ValueError: + log.warning("[control connection] Unknown CLIENT_ROUTES_CHANGE type: %s", raw_change_type) + return + + connection_ids = tuple(event.get("connection_ids", [])) + host_ids = tuple(event.get("host_ids", [])) + + self._cluster.scheduler.schedule_unique( + 0, + self._handle_client_routes_refresh, + self._connection, self._timeout, change_type, connection_ids, host_ids + ) + + def _handle_client_routes_refresh(self, connection, timeout, + change_type, connection_ids, host_ids): + try: + self._cluster._client_routes_handler.handle_client_routes_change( + connection, timeout, change_type, connection_ids, host_ids) + except ReferenceError: + pass # our weak reference to the Cluster is no good + except Exception: + log.debug("[control connection] Error handling CLIENT_ROUTES_CHANGE", exc_info=True) + self._signal_error() + def _handle_schema_change(self, event): if self._schema_event_refresh_window < 0: return @@ -4230,7 +4328,30 @@ def _handle_schema_change(self, event): self._cluster.scheduler.schedule_unique(delay, self.refresh_schema, **event) def wait_for_schema_agreement(self, connection=None, preloaded_results=None, wait_time=None): + """ + Wait for schema agreement from the control connection's metadata view. + + This method is intended for internal metadata refresh flows. External + callers should use :meth:`.Session.wait_for_schema_agreement` instead. + + The control connection observes schema agreement from its own + perspective, which may include hosts the session is not using, and it + may fail when the control connection itself is transiently unhealthy. + That can produce false positives or failures that do not reflect + whether a session can safely proceed. + .. deprecated:: 3.30.0 + Use :meth:`.Session.wait_for_schema_agreement` instead. + """ + warn("ControlConnection.wait_for_schema_agreement is deprecated and will be removed in 4.0. " + "Use Session.wait_for_schema_agreement instead. " + "This method is for internal metadata refresh use only.", + DeprecationWarning, stacklevel=2) + return self._wait_for_schema_agreement(connection=connection, + preloaded_results=preloaded_results, + wait_time=wait_time) + + def _wait_for_schema_agreement(self, connection=None, preloaded_results=None, wait_time=None): total_timeout = wait_time if wait_time is not None else self._cluster.max_schema_agreement_wait if total_timeout <= 0: return True @@ -4268,7 +4389,8 @@ def wait_for_schema_agreement(self, connection=None, preloaded_results=None, wai local_query = QueryMessage(query=maybe_add_timeout_to_query(self._SELECT_SCHEMA_LOCAL, self._metadata_request_timeout), consistency_level=cl) try: - timeout = min(self._timeout, total_timeout - elapsed) + remaining = total_timeout - elapsed + timeout = min(self._timeout, remaining) if self._timeout is not None else remaining peers_result, local_result = connection.wait_for_responses( peers_query, local_query, timeout=timeout) except OperationTimedOut as timeout: @@ -4349,8 +4471,9 @@ def _get_peers_query(self, peers_query_type, connection=None): query_template = (self._SELECT_SCHEMA_PEERS_TEMPLATE if peers_query_type == self.PeersQueryType.PEERS_SCHEMA else self._SELECT_PEERS_NO_TOKENS_TEMPLATE) - host_release_version = self._cluster.metadata.get_host(connection.original_endpoint).release_version - host_dse_version = self._cluster.metadata.get_host(connection.original_endpoint).dse_version + original_endpoint_host = self._cluster.metadata.get_host(connection.original_endpoint) + host_release_version = None if original_endpoint_host is None else original_endpoint_host.release_version + host_dse_version = None if original_endpoint_host is None else original_endpoint_host.dse_version uses_native_address_query = ( host_dse_version and Version(host_dse_version) >= self._MINIMUM_NATIVE_ADDRESS_DSE_VERSION) @@ -4586,9 +4709,10 @@ class ResponseFuture(object): _timer = None _protocol_handler = ProtocolHandler _spec_execution_plan = NoSpeculativeExecutionPlan() - _continuous_paging_options = None _continuous_paging_session = None _host = None + _control_connection_query_attempted = False + _TABLET_ROUTING_CTYPE = None _warned_timeout = False @@ -4608,6 +4732,7 @@ def __init__(self, session, message, query, timeout, metrics=None, prepared_stat self._callback_lock = Lock() self._start_time = start_time or time.time() self._host = host + self._control_connection_query_attempted = False self._spec_execution_plan = speculative_execution_plan or self._spec_execution_plan self._make_query_plan() self._event = Event() @@ -4654,6 +4779,7 @@ def _on_timeout(self, _attempts=0): ) return + conn_in_flight = None if self._connection is not None: try: self._connection._requests.pop(self._req_id) @@ -4664,9 +4790,14 @@ def _on_timeout(self, _attempts=0): except KeyError: key = "Connection defunct by heartbeat" errors = {key: "Client request timeout. See Session.execute[_async](timeout)"} - self._set_final_exception(OperationTimedOut(errors, self._current_host)) + self._set_final_exception(OperationTimedOut(errors, self._current_host, + timeout=self.timeout, + in_flight=self._connection.in_flight)) return + # Capture connection stats before pool.return_connection() can alter state + conn_in_flight = self._connection.in_flight + pool = self.session._pools.get(self._current_host) if pool and not pool.is_shutdown: # Do not return the stream ID to the pool yet. We cannot reuse it @@ -4680,18 +4811,31 @@ def _on_timeout(self, _attempts=0): self._connection.orphaned_threshold_reached = True pool.return_connection(self._connection, stream_was_orphaned=True) + elif self._connection.is_control_connection: + with self._connection.lock: + self._connection.orphaned_request_ids.add(self._req_id) + if len(self._connection.orphaned_request_ids) >= self._connection.orphaned_threshold: + self._connection.orphaned_threshold_reached = True errors = self._errors if not errors: if self.is_schema_agreed: - key = str(self._current_host.endpoint) if self._current_host else 'no host queried before timeout' + if self._current_host is None: + key = 'no host queried before timeout' + elif self._connection is not None and self._connection.is_control_connection: + control_host = self.session.cluster.get_control_connection_host() + key = str(control_host.endpoint) if control_host is not None else str(self._connection.endpoint) + else: + key = str(self._current_host.endpoint) errors = {key: "Client request timeout. See Session.execute[_async](timeout)"} else: connection = self.session.cluster.control_connection._connection host = str(connection.endpoint) if connection else 'unknown' errors = {host: "Request timed out while waiting for schema agreement. See Session.execute[_async](timeout) and Cluster.max_schema_agreement_wait."} - self._set_final_exception(OperationTimedOut(errors, self._current_host)) + self._set_final_exception(OperationTimedOut(errors, self._current_host, + timeout=self.timeout, + in_flight=conn_in_flight)) def _on_speculative_execute(self): self._timer = None @@ -4720,7 +4864,7 @@ def _make_query_plan(self): # or to the explicit host target if set if self._host: # returning a single value effectively disables retries - self.query_plan = [self._host] + self.query_plan = iter([self._host]) else: # convert the list/generator/etc to an iterator so that subsequent # calls to send_request (which retries may do) will resume where @@ -4740,14 +4884,110 @@ def send_request(self, error_no_hosts=True): self._on_timeout() return True if error_no_hosts: + if self._fallback_to_control_connection(): + req_id = self._query_control_connection() + if req_id is not None: + self._req_id = req_id + return True + self._set_final_exception(NoHostAvailable( "Unable to complete the operation against any hosts", self._errors)) return False + def _has_usable_node_pool(self): + try: + pools = tuple(self.session._pools.values()) + except (AttributeError, TypeError): + return False + + return any(pool and not pool.is_shutdown for pool in pools) + + def _fallback_to_control_connection(self): + fallback_mode = self.session.cluster.allow_control_connection_query_fallback + if fallback_mode is ControlConnectionQueryFallback.Disabled: + return False + if self._host or self._control_connection_query_attempted: + return False + if fallback_mode is ControlConnectionQueryFallback.SkipPoolCreation: + return True + return not self._has_usable_node_pool() + + def _borrow_control_connection(self, connection): + with connection.lock: + if connection.in_flight >= connection.max_request_id: + raise NoConnectionsAvailable("All request IDs are currently in use") + connection.in_flight += 1 + return connection.get_request_id() + + def _release_control_connection_request(self, connection, request_id): + with connection.lock: + connection.in_flight -= 1 + connection.request_ids.append(request_id) + connection._requests.pop(request_id, None) + + def _handle_control_connection_response(self, connection, cb, response): + with connection.lock: + connection.in_flight -= 1 + cb(response) + + def _query_control_connection(self, message=None, cb=None, connection=None, host=None): + self._control_connection_query_attempted = True + + if message is None: + message = self.message + + if connection is None: + control_connection = self.session.cluster.control_connection + connection = control_connection._connection if control_connection else None + if not connection: + self._errors['control connection'] = ConnectionException("Control connection is not connected") + return None + + if host is None: + host = self.session.cluster.get_control_connection_host() or connection.endpoint + self._current_host = host + + request_id = None + request_sent = False + try: + request_id = self._borrow_control_connection(connection) + self._connection = connection + result_meta = self.prepared_statement.result_metadata if self.prepared_statement else [] + if cb is None: + cb = partial(self._set_result, host, connection, None) + cb = partial(self._handle_control_connection_response, connection, cb) + + log.debug("No usable node pools; falling back to control connection for host %s", host) + self.request_encoded_size = connection.send_msg(message, request_id, cb=cb, + encoder=self._protocol_handler.encode_message, + decoder=self._protocol_handler.decode_message, + result_metadata=result_meta) + request_sent = True + self.attempted_hosts.append(host) + return request_id + except NoConnectionsAvailable as exc: + log.debug("Control connection is at capacity") + self._errors[host] = exc + except ConnectionBusy as exc: + log.debug("Control connection is busy") + self._errors[host] = exc + except Exception as exc: + log.debug("Error querying control connection", exc_info=True) + self._errors[host] = exc + if self._metrics is not None: + self._metrics.on_connection_error() + finally: + if request_id is not None and not request_sent: + self._release_control_connection_request(connection, request_id) + + return None + def _query(self, host, message=None, cb=None): if message is None: message = self.message + self._control_connection_query_attempted = False + pool = self.session._pools.get(host) if not pool: self._errors[host] = ConnectionException("Host has been marked down or removed") @@ -4858,12 +5098,17 @@ def start_fetching_next_page(self): self._event.clear() self._final_result = _NOT_SET self._final_exception = None + self._control_connection_query_attempted = False self._start_timer() self.send_request() def _reprepare(self, prepare_message, host, connection, pool): cb = partial(self.session.submit, self._execute_after_prepare, host, connection, pool) - request_id = self._query(host, prepare_message, cb=cb) + if pool is None and connection is not None and connection.is_control_connection: + request_id = self._query_control_connection(prepare_message, cb=cb, + connection=connection, host=host) + else: + request_id = self._query(host, prepare_message, cb=cb) if request_id is None: # try to submit the original prepared statement on some other host self.send_request() @@ -4886,7 +5131,10 @@ def _set_result(self, host, connection, pool, response): if self._custom_payload and self.session.cluster.control_connection._tablets_routing_v1 and 'tablets-routing-v1' in self._custom_payload: protocol = self.session.cluster.protocol_version info = self._custom_payload.get('tablets-routing-v1') - ctype = types.lookup_casstype('TupleType(LongType, LongType, ListType(TupleType(UUIDType, Int32Type)))') + ctype = ResponseFuture._TABLET_ROUTING_CTYPE + if ctype is None: + ctype = types.lookup_casstype('TupleType(LongType, LongType, ListType(TupleType(UUIDType, Int32Type)))') + ResponseFuture._TABLET_ROUTING_CTYPE = ctype tablet_routing_info = ctype.from_binary(info, protocol) first_token = tablet_routing_info[0] last_token = tablet_routing_info[1] @@ -4899,6 +5147,8 @@ def _set_result(self, host, connection, pool, response): if isinstance(response, ResultMessage): if response.kind == RESULT_KIND_SET_KEYSPACE: session = getattr(self, 'session', None) + if connection is not None: + connection.keyspace = response.new_keyspace # since we're running on the event loop thread, we need to # use a non-blocking method for setting the keyspace on # all connections in this session, otherwise the event @@ -5075,10 +5325,13 @@ def _execute_after_prepare(self, host, connection, pool, response): new_metadata_id = response.result_metadata_id if new_metadata_id is not None: self.prepared_statement.result_metadata_id = new_metadata_id - + # use self._query to re-use the same host and # at the same time properly borrow the connection - request_id = self._query(host) + if pool is None and connection is not None and connection.is_control_connection: + request_id = self._query_control_connection(connection=connection, host=host) + else: + request_id = self._query(host) if request_id is None: # this host errored out, move on to the next self.send_request() @@ -5191,6 +5444,11 @@ def _retry_task(self, reuse_connection, host): # to retry the operation return + if self._control_connection_query_attempted: + self._control_connection_query_attempted = False + self.send_request() + return + if reuse_connection and self._query(host) is not None: return diff --git a/cassandra/concurrent.py b/cassandra/concurrent.py index fb8f26e1cc..b96d0b12d4 100644 --- a/cassandra/concurrent.py +++ b/cassandra/concurrent.py @@ -33,13 +33,7 @@ def execute_concurrent(session, statements_and_parameters, concurrency=100, rais ``parameters`` item must be a sequence or :const:`None`. The `concurrency` parameter controls how many statements will be executed - concurrently. When :attr:`.Cluster.protocol_version` is set to 1 or 2, - it is recommended that this be kept below 100 times the number of - core connections per host times the number of connected hosts (see - :meth:`.Cluster.set_core_connections_per_host`). If that amount is exceeded, - the event loop thread may attempt to block on new connection creation, - substantially impacting throughput. If :attr:`~.Cluster.protocol_version` - is 3 or higher, you can safely experiment with higher levels of concurrency. + concurrently. If `raise_on_first_error` is left as :const:`True`, execution will stop after the first failed statement and the corresponding exception will be @@ -98,8 +92,6 @@ def execute_concurrent(session, statements_and_parameters, concurrency=100, rais class _ConcurrentExecutor(object): - max_error_recursion = 100 - def __init__(self, session, statements_and_params, execution_profile): self.session = session self._enum_statements = enumerate(iter(statements_and_params)) @@ -109,7 +101,7 @@ def __init__(self, session, statements_and_params, execution_profile): self._results_queue = [] self._current = 0 self._exec_count = 0 - self._exec_depth = 0 + self._executing = False def execute(self, concurrency, fail_fast): self._fail_fast = fail_fast @@ -133,22 +125,34 @@ def _execute_next(self): pass def _execute(self, idx, statement, params): - self._exec_depth += 1 + # When execute_async completes synchronously (e.g. immediate timeout), + # the errback fires inline: _on_error -> _put_result -> _execute_next + # -> _execute. Without protection this recurses once per remaining + # statement and blows the stack. + # + # ``_executing`` marks that we are already inside this method higher up + # the call stack. When a synchronous callback re-enters, we just stash + # the pending work in ``_pending_executions`` and let the outermost + # invocation drain it in a loop -- no recursion. + if self._executing: + self._pending_executions.append((idx, statement, params)) + return + + self._executing = True + self._pending_executions = [(idx, statement, params)] try: - future = self.session.execute_async(statement, params, timeout=None, execution_profile=self._execution_profile) - args = (future, idx) - future.add_callbacks( - callback=self._on_success, callback_args=args, - errback=self._on_error, errback_args=args) - except Exception as exc: - # If we're not failing fast and all executions are raising, there is a chance of recursing - # here as subsequent requests are attempted. If we hit this threshold, schedule this result/retry - # and let the event loop thread return. - if self._exec_depth < self.max_error_recursion: - self._put_result(exc, idx, False) - else: - self.session.submit(self._put_result, exc, idx, False) - self._exec_depth -= 1 + while self._pending_executions: + p_idx, p_statement, p_params = self._pending_executions.pop(0) + try: + future = self.session.execute_async(p_statement, p_params, timeout=None, execution_profile=self._execution_profile) + args = (future, p_idx) + future.add_callbacks( + callback=self._on_success, callback_args=args, + errback=self._on_error, errback_args=args) + except Exception as exc: + self._put_result(exc, p_idx, False) + finally: + self._executing = False def _on_success(self, result, future, idx): future.clear_callbacks() diff --git a/cassandra/connection.py b/cassandra/connection.py index e1646cafc1..f07160e385 100644 --- a/cassandra/connection.py +++ b/cassandra/connection.py @@ -25,12 +25,14 @@ from threading import Thread, Event, RLock, Condition import time import ssl +import uuid import weakref import random import itertools -from typing import Optional +from typing import Any, Dict, Optional, Tuple, Union from cassandra.application_info import ApplicationInfoBase +from cassandra.client_routes import _ClientRoutesHandler from cassandra.protocol_features import ProtocolFeatures if 'gevent.monkey' in sys.modules: @@ -64,6 +66,7 @@ try: import lz4 except ImportError: + log.debug("lz4 package could not be imported. LZ4 Compression will not be available") pass else: # The compress and decompress functions we need were moved from the lz4 to @@ -102,6 +105,7 @@ def lz4_decompress(byts): try: import snappy except ImportError: + log.debug("snappy package could not be imported. Snappy Compression will not be available") pass else: # work around apparently buggy snappy decompress @@ -123,7 +127,6 @@ def decompress(byts): DEFAULT_LOCAL_PORT_LOW = 49152 DEFAULT_LOCAL_PORT_HIGH = 65535 -frame_header_v1_v2 = struct.Struct('>BbBi') frame_header_v3 = struct.Struct('>BhBi') @@ -229,7 +232,7 @@ class DefaultEndPointFactory(EndPointFactory): port = None """ If no port is discovered in the row, this is the default port - used for endpoint creation. + used for endpoint creation. """ def __init__(self, port=None): @@ -327,6 +330,50 @@ def create_from_sni(self, sni): return SniEndPoint(self._proxy_address, sni, self._port) +class ClientRoutesEndPointFactory(EndPointFactory): + """ + EndPointFactory for Client Routes (Private Link) support. + + Creates ClientRoutesEndPoint instances that defer both address translation + (host_id -> hostname lookup) and DNS resolution until connection time. + This ensures immediate reaction to infrastructure changes. + """ + + client_routes_handler: _ClientRoutesHandler + default_port: int + + def __init__(self, client_routes_handler: _ClientRoutesHandler, default_port: int = None) -> None: + """ + :param client_routes_handler: _ClientRoutesHandler instance to lookup routes + :param default_port: Default port if none found in row + """ + self.client_routes_handler = client_routes_handler + self.default_port = default_port + + def create(self, row: Dict[str, Any]) -> 'ClientRoutesEndPoint': + """ + Create a ClientRoutesEndPoint from a system.peers row. + + Stores only the host_id and handler reference. Both translation + (route lookup) and DNS resolution happen later in resolve(). + """ + from cassandra.metadata import _NodeInfo + host_id = row.get("host_id") + + if host_id is None: + raise ValueError("No host_id to create ClientRoutesEndPoint") + + addr = _NodeInfo.get_broadcast_rpc_address(row) + port = _NodeInfo.get_broadcast_rpc_port(row) or _NodeInfo.get_broadcast_port(row) or self.default_port + + return ClientRoutesEndPoint( + host_id=host_id, + handler=self.client_routes_handler, + original_address=addr, + original_port=port, + ) + + @total_ordering class UnixSocketEndPoint(EndPoint): """ @@ -368,6 +415,76 @@ def __repr__(self): return "<%s: %s>" % (self.__class__.__name__, self._unix_socket_path) +@total_ordering +class ClientRoutesEndPoint(EndPoint): + """ + Client Routes (Private Link) EndPoint implementation. + + Defers both address translation (route lookup) and DNS resolution + until resolve() is called at connection time. This ensures immediate + reaction to infrastructure changes and CLIENT_ROUTES_CHANGE events. + """ + + _host_id: uuid.UUID + _handler: _ClientRoutesHandler + _original_address: str + _original_port: int + + def __init__(self, host_id: uuid.UUID, handler: _ClientRoutesHandler, original_address: str, original_port: int = None) -> None: + """ + :param host_id: Host UUID for route lookup + :param handler: _ClientRoutesHandler instance + :param original_address: Original address from system.peers (for identification) + :param original_port: Original port if route doesn't specify one + """ + self._host_id = host_id + self._handler = handler + self._original_address = original_address + self._original_port = original_port + + @property + def address(self) -> str: + """Returns the original address (updated by resolve()).""" + return self._original_address + + @property + def port(self) -> Optional[int]: + return self._original_port + + @property + def host_id(self) -> uuid.UUID: + return self._host_id + + def resolve(self) -> Tuple[str, int]: + """ + Resolve endpoint by delegating to the handler. + Falls back to original address/port if no route mapping is available. + """ + result = self._handler.resolve_host(self._host_id) + if result is None: + return self._original_address, self._original_port + return result + + def __eq__(self, other): + return (isinstance(other, ClientRoutesEndPoint) and + self._host_id == other._host_id and + self._original_address == other._original_address) + + def __hash__(self): + return hash((self._host_id, self._original_address)) + + def __lt__(self, other): + return ((self._host_id, self._original_address) < + (other._host_id, other._original_address)) + + def __str__(self): + return str("%s (host_id=%s)" % (self._original_address, self._host_id)) + + def __repr__(self): + return "<%s: host_id=%s, original_addr=%s>" % ( + self.__class__.__name__, self._host_id, self._original_address) + + class _Frame(object): def __init__(self, version, flags, stream, opcode, body_offset, end_pos): self.version = version @@ -444,33 +561,6 @@ class ProtocolError(Exception): class CrcMismatchException(ConnectionException): pass - -class ContinuousPagingState(object): - """ - A class for specifying continuous paging state, only supported starting with DSE_V2. - """ - - num_pages_requested = None - """ - How many pages we have already requested - """ - - num_pages_received = None - """ - How many pages we have already received - """ - - max_queue_size = None - """ - The max queue size chosen by the user via the options - """ - - def __init__(self, max_queue_size): - self.num_pages_requested = max_queue_size # the initial query requests max_queue_size - self.num_pages_received = 0 - self.max_queue_size = max_queue_size - - class ContinuousPagingSession(object): def __init__(self, stream_id, decoder, row_factory, connection, state): self.stream_id = stream_id @@ -668,15 +758,29 @@ def reset_cql_frame_buffer(self): self.reset_io_buffer() -class ShardawarePortGenerator: - @classmethod - def generate(cls, shard_id, total_shards): - start = random.randrange(DEFAULT_LOCAL_PORT_LOW, DEFAULT_LOCAL_PORT_HIGH) - available_ports = itertools.chain(range(start, DEFAULT_LOCAL_PORT_HIGH), range(DEFAULT_LOCAL_PORT_LOW, start)) +class ShardAwarePortGenerator: + def __init__(self, start_port: int, end_port: int): + self.start_port = start_port + self.end_port = end_port + + @staticmethod + def _align(value: int, total_shards: int): + shift = value % total_shards + if shift == 0: + return value + return value + total_shards - shift + + def generate(self, shard_id: int, total_shards: int): + start = self._align(random.randrange(self.start_port, self.end_port), total_shards) + shard_id + beginning = self._align(self.start_port, total_shards) + shard_id + available_ports = itertools.chain(range(start, self.end_port, total_shards), + range(beginning, start, total_shards)) for port in available_ports: - if port % total_shards == shard_id: - yield port + yield port + + +DefaultShardAwarePortGenerator = ShardAwarePortGenerator(DEFAULT_LOCAL_PORT_LOW, DEFAULT_LOCAL_PORT_HIGH) class Connection(object): @@ -691,7 +795,7 @@ class Connection(object): protocol_version = ProtocolVersion.MAX_SUPPORTED keyspace = None - compression = True + compression: Union[bool, str] = True _compression_type = None compressor = None decompressor = None @@ -772,7 +876,7 @@ def _iobuf(self): return self._io_buffer.io_buffer def __init__(self, host='127.0.0.1', port=9042, authenticator=None, - ssl_options=None, sockopts=None, compression=True, + ssl_options=None, sockopts=None, compression: Union[bool, str] = True, cql_version=None, protocol_version=ProtocolVersion.MAX_SUPPORTED, is_control_connection=False, user_type_map=None, connect_timeout=None, allow_beta_protocol_version=False, no_compact=False, ssl_context=None, owning_pool=None, shard_id=None, total_shards=None, @@ -817,17 +921,12 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None, if not self.ssl_context and self.ssl_options: self.ssl_context = self._build_ssl_context_from_options() - if protocol_version >= 3: - self.max_request_id = min(self.max_in_flight - 1, (2 ** 15) - 1) - # Don't fill the deque with 2**15 items right away. Start with some and add - # more if needed. - initial_size = min(300, self.max_in_flight) - self.request_ids = deque(range(initial_size)) - self.highest_request_id = initial_size - 1 - else: - self.max_request_id = min(self.max_in_flight, (2 ** 7) - 1) - self.request_ids = deque(range(self.max_request_id + 1)) - self.highest_request_id = self.max_request_id + self.max_request_id = min(self.max_in_flight - 1, (2 ** 15) - 1) + # Don't fill the deque with 2**15 items right away. Start with some and add + # more if needed. + initial_size = min(300, self.max_in_flight) + self.request_ids = deque(range(initial_size)) + self.highest_request_id = initial_size - 1 self.lock = RLock() self.connected_event = Event() @@ -885,7 +984,8 @@ def factory(cls, endpoint, timeout, host_conn = None, *args, **kwargs): raise conn.last_error elif not conn.connected_event.is_set(): conn.close() - raise OperationTimedOut("Timed out creating connection (%s seconds)" % timeout) + raise OperationTimedOut("Timed out creating connection (%s seconds)" % timeout, + timeout=timeout) else: return conn @@ -934,7 +1034,7 @@ def _wrap_socket_from_context(self): def _initiate_connection(self, sockaddr): if self.features.shard_id is not None: - for port in ShardawarePortGenerator.generate(self.features.shard_id, self.total_shards): + for port in DefaultShardAwarePortGenerator.generate(self.features.shard_id, self.total_shards): try: self._socket.bind(('', port)) break @@ -1104,9 +1204,15 @@ def handle_pushed(self, response): def send_msg(self, msg, request_id, cb, encoder=ProtocolHandler.encode_message, decoder=ProtocolHandler.decode_message, result_metadata=None): if self.is_defunct: - raise ConnectionShutdown("Connection to %s is defunct" % self.endpoint) + msg = "Connection to %s is defunct" % self.endpoint + if self.last_error: + msg += ": %s" % (self.last_error,) + raise ConnectionShutdown(msg) elif self.is_closed: - raise ConnectionShutdown("Connection to %s is closed" % self.endpoint) + msg = "Connection to %s is closed" % self.endpoint + if self.last_error: + msg += ": %s" % (self.last_error,) + raise ConnectionShutdown(msg) elif not self._socket_writable: raise ConnectionBusy("Connection %s is overloaded" % self.endpoint) @@ -1137,8 +1243,12 @@ def wait_for_responses(self, *msgs, **kwargs): failed, the corresponding Exception will be raised. """ if self.is_closed or self.is_defunct: - raise ConnectionShutdown("Connection %s is already closed" % (self, )) + msg = "Connection %s is already closed" % (self,) + if self.last_error: + msg += ": %s" % (self.last_error,) + raise ConnectionShutdown(msg) timeout = kwargs.get('timeout') + original_timeout = timeout # preserve for exception reporting fail_on_error = kwargs.get('fail_on_error', True) waiter = ResponseWaiter(self, len(msgs), fail_on_error) @@ -1163,7 +1273,8 @@ def wait_for_responses(self, *msgs, **kwargs): if timeout is not None: timeout -= 0.01 if timeout <= 0.0: - raise OperationTimedOut() + raise OperationTimedOut(timeout=original_timeout, + in_flight=self.in_flight) time.sleep(0.01) try: @@ -1205,11 +1316,10 @@ def _read_frame_header(self): version = buf[0] & PROTOCOL_VERSION_MASK if version not in ProtocolVersion.SUPPORTED_VERSIONS: raise ProtocolError("This version of the driver does not support protocol version %d" % version) - frame_header = frame_header_v3 if version >= 3 else frame_header_v1_v2 # this frame header struct is everything after the version byte - header_size = frame_header.size + 1 + header_size = frame_header_v3.size + 1 if pos >= header_size: - flags, stream, op, body_len = frame_header.unpack_from(buf, 1) + flags, stream, op, body_len = frame_header_v3.unpack_from(buf, 1) if body_len < 0: raise ProtocolError("Received negative body length: %r" % body_len) self._current_frame = _Frame(version, flags, stream, op, header_size, body_len + header_size) @@ -1401,10 +1511,11 @@ def _handle_options_response(self, options_response): overlap = (set(locally_supported_compressions.keys()) & set(remote_supported_compressions)) if len(overlap) == 0: - log.debug("No available compression types supported on both ends." - " locally supported: %r. remotely supported: %r", - locally_supported_compressions.keys(), - remote_supported_compressions) + if locally_supported_compressions: + log.error("No available compression types supported on both ends." + " locally supported: %r. remotely supported: %r", + locally_supported_compressions.keys(), + remote_supported_compressions) else: compression_type = None if isinstance(self.compression, str): @@ -1550,7 +1661,8 @@ def set_keyspace_blocking(self, keyspace): if not keyspace or keyspace == self.keyspace: return - query = QueryMessage(query='USE "%s"' % (keyspace,), + from cassandra.metadata import escape_name + query = QueryMessage(query='USE %s' % (escape_name(keyspace),), consistency_level=ConsistencyLevel.ONE) try: result = self.wait_for_response(query) @@ -1604,7 +1716,8 @@ def set_keyspace_async(self, keyspace, callback): callback(self, None) return - query = QueryMessage(query='USE "%s"' % (keyspace,), + from cassandra.metadata import escape_name + query = QueryMessage(query='USE %s' % (escape_name(keyspace),), consistency_level=ConsistencyLevel.ONE) def process_result(result): @@ -1686,7 +1799,8 @@ def deliver(self, timeout=None): if self.error: raise self.error elif not self.event.is_set(): - raise OperationTimedOut() + raise OperationTimedOut(timeout=timeout, + in_flight=self.connection.in_flight) else: return self.responses @@ -1702,7 +1816,19 @@ def __init__(self, connection, owner): with connection.lock: if connection.in_flight < connection.max_request_id: connection.in_flight += 1 - connection.send_msg(OptionsMessage(), connection.get_request_id(), self._options_callback) + request_id = connection.get_request_id() + try: + connection.send_msg(OptionsMessage(), request_id, self._options_callback) + except Exception as exc: + if connection.is_control_connection: + connection.in_flight -= 1 + # send_msg() registers the callback before writing to the socket, + # so a write failure must unwind that registration here. + connection._requests.pop(request_id, None) + if request_id not in connection.request_ids: + connection.request_ids.append(request_id) + self._exception = exc + self._event.set() else: self._exception = Exception("Failed to send heartbeat because connection 'in_flight' exceeds threshold") self._event.set() @@ -1713,7 +1839,10 @@ def wait(self, timeout): if self._exception: raise self._exception else: - raise OperationTimedOut("Connection heartbeat timeout after %s seconds" % (timeout,), self.connection.endpoint) + raise OperationTimedOut("Connection heartbeat timeout after %s seconds" % (timeout,), + self.connection.endpoint, + timeout=timeout, + in_flight=self.connection.in_flight) def _options_callback(self, response): if isinstance(response, SupportedMessage): diff --git a/cassandra/cqlengine/columns.py b/cassandra/cqlengine/columns.py index 3d85587524..509b606ccc 100644 --- a/cassandra/cqlengine/columns.py +++ b/cassandra/cqlengine/columns.py @@ -837,7 +837,30 @@ def to_database(self, value): class BaseContainerColumn(BaseCollectionColumn): - pass + """ + Base class for container columns (Set, List, Map). + + Supports optional freezing for immutable collections. + """ + + frozen = False + """ + bool flag, indicates this collection should be frozen (immutable). + Frozen collections use FULL indexes instead of VALUES indexes. + """ + + def __init__(self, types, frozen=False, **kwargs): + """ + :param types: a sequence of sub types in this collection + :param frozen: if True, the collection will be frozen (immutable) + """ + self.frozen = frozen + super(BaseContainerColumn, self).__init__(types, **kwargs) + + def _apply_frozen(self): + """Apply frozen wrapper to db_type if frozen=True.""" + if self.frozen: + self._freeze_db_type() class Set(BaseContainerColumn): @@ -849,18 +872,21 @@ class Set(BaseContainerColumn): _python_type_hashable = False - def __init__(self, value_type, strict=True, default=set, **kwargs): + def __init__(self, value_type, strict=True, default=set, frozen=False, **kwargs): """ :param value_type: a column class indicating the types of the value :param strict: sets whether non set values will be coerced to set type on validation, or raise a validation error, defaults to True + :param frozen: if True, the collection will be frozen (immutable) and + use FULL indexes instead of VALUES indexes """ self.strict = strict - super(Set, self).__init__((value_type,), default=default, **kwargs) + super(Set, self).__init__((value_type,), frozen=frozen, default=default, **kwargs) self.value_col = self.types[0] if not self.value_col._python_type_hashable: raise ValidationError("Cannot create a Set with unhashable value type (see PYTHON-494)") self.db_type = 'set<{0}>'.format(self.value_col.db_type) + self._apply_frozen() def validate(self, value): val = super(Set, self).validate(value) @@ -899,13 +925,16 @@ class List(BaseContainerColumn): _python_type_hashable = False - def __init__(self, value_type, default=list, **kwargs): + def __init__(self, value_type, default=list, frozen=False, **kwargs): """ :param value_type: a column class indicating the types of the value + :param frozen: if True, the collection will be frozen (immutable) and + use FULL indexes instead of VALUES indexes """ - super(List, self).__init__((value_type,), default=default, **kwargs) + super(List, self).__init__((value_type,), frozen=frozen, default=default, **kwargs) self.value_col = self.types[0] self.db_type = 'list<{0}>'.format(self.value_col.db_type) + self._apply_frozen() def validate(self, value): val = super(List, self).validate(value) @@ -937,12 +966,14 @@ class Map(BaseContainerColumn): _python_type_hashable = False - def __init__(self, key_type, value_type, default=dict, **kwargs): + def __init__(self, key_type, value_type, default=dict, frozen=False, **kwargs): """ :param key_type: a column class indicating the types of the key :param value_type: a column class indicating the types of the value + :param frozen: if True, the collection will be frozen (immutable) and + use FULL indexes instead of VALUES indexes """ - super(Map, self).__init__((key_type, value_type), default=default, **kwargs) + super(Map, self).__init__((key_type, value_type), frozen=frozen, default=default, **kwargs) self.key_col = self.types[0] self.value_col = self.types[1] @@ -950,6 +981,7 @@ def __init__(self, key_type, value_type, default=dict, **kwargs): raise ValidationError("Cannot create a Map with unhashable key type (see PYTHON-494)") self.db_type = 'map<{0}, {1}>'.format(self.key_col.db_type, self.value_col.db_type) + self._apply_frozen() def validate(self, value): val = super(Map, self).validate(value) diff --git a/cassandra/cqlengine/management.py b/cassandra/cqlengine/management.py index 4ac4192a80..684bc50b8a 100644 --- a/cassandra/cqlengine/management.py +++ b/cassandra/cqlengine/management.py @@ -56,7 +56,7 @@ def _get_context(keyspaces, connections): def create_keyspace_simple(name, replication_factor, durable_writes=True, connections=None): """ - Creates a keyspace with SimpleStrategy for replica placement + Creates a keyspace with NetworkTopologyStrategy for replica placement If the keyspace already exists, it will not be modified. @@ -66,11 +66,11 @@ def create_keyspace_simple(name, replication_factor, durable_writes=True, connec *There are plans to guard schema-modifying functions with an environment-driven conditional.* :param str name: name of keyspace to create - :param int replication_factor: keyspace replication factor, used with :attr:`~.SimpleStrategy` + :param int replication_factor: keyspace replication factor, used with :attr:`~.NetworkTopologyStrategy` :param bool durable_writes: Write log is bypassed if set to False :param list connections: List of connection names """ - _create_keyspace(name, durable_writes, 'SimpleStrategy', + _create_keyspace(name, durable_writes, 'NetworkTopologyStrategy', {'replication_factor': replication_factor}, connections=connections) @@ -282,7 +282,11 @@ def _sync_table(model, connection=None): qs = ['CREATE INDEX'] qs += ['ON {0}'.format(cf_name)] - qs += ['("{0}")'.format(column.db_field_name)] + # Use FULL index for frozen collections, VALUES index (implicit) for non-frozen + if isinstance(column, columns.BaseContainerColumn) and column.frozen: + qs += ['(FULL("{0}"))'.format(column.db_field_name)] + else: + qs += ['("{0}")'.format(column.db_field_name)] qs = ' '.join(qs) execute(qs, connection=connection) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index d1580f00ff..547a13c979 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -249,6 +249,8 @@ def lookup_casstype(casstype): """ if isinstance(casstype, (CassandraType, CassandraTypeType)): return casstype + if '(' not in casstype: + return lookup_casstype_simple(casstype) try: return parse_casstype_args(casstype) except (ValueError, AssertionError, IndexError) as e: @@ -636,24 +638,24 @@ def interpret_datestring(val): except ValueError: continue # scale seconds to millis for the raw value - return (calendar.timegm(tval) + offset) * 1e3 + return (calendar.timegm(tval) + offset) * 1000 else: raise ValueError("can't interpret %r as a date" % (val,)) @staticmethod def deserialize(byts, protocol_version): - timestamp = int64_unpack(byts) / 1000.0 - return util.datetime_from_timestamp(timestamp) + timestamp_ms = int64_unpack(byts) + return util.datetime_from_ms_timestamp(timestamp_ms) @staticmethod def serialize(v, protocol_version): try: # v is datetime timestamp_seconds = calendar.timegm(v.utctimetuple()) - timestamp = timestamp_seconds * 1e3 + getattr(v, 'microsecond', 0) / 1e3 + timestamp = timestamp_seconds * 1000 + getattr(v, 'microsecond', 0) // 1000 except AttributeError: try: - timestamp = calendar.timegm(v.timetuple()) * 1e3 + timestamp = calendar.timegm(v.timetuple()) * 1000 except AttributeError: # Ints and floats are valid timestamps too if type(v) not in _number_types: @@ -812,18 +814,13 @@ class _SimpleParameterizedType(_ParameterizedType): @classmethod def deserialize_safe(cls, byts, protocol_version): subtype, = cls.subtypes - if protocol_version >= 3: - unpack = int32_unpack - length = 4 - else: - unpack = uint16_unpack - length = 2 - numelements = unpack(byts[:length]) + length = 4 + numelements = int32_unpack(byts[:length]) p = length result = [] inner_proto = max(3, protocol_version) for _ in range(numelements): - itemlen = unpack(byts[p:p + length]) + itemlen = int32_unpack(byts[p:p + length]) p += length if itemlen < 0: result.append(None) @@ -839,16 +836,15 @@ def serialize_safe(cls, items, protocol_version): raise TypeError("Received a string for a type that expects a sequence") subtype, = cls.subtypes - pack = int32_pack if protocol_version >= 3 else uint16_pack buf = io.BytesIO() - buf.write(pack(len(items))) + buf.write(int32_pack(len(items))) inner_proto = max(3, protocol_version) for item in items: if item is None: - buf.write(pack(-1)) + buf.write(int32_pack(-1)) else: itembytes = subtype.to_binary(item, inner_proto) - buf.write(pack(len(itembytes))) + buf.write(int32_pack(len(itembytes))) buf.write(itembytes) return buf.getvalue() @@ -872,18 +868,13 @@ class MapType(_ParameterizedType): @classmethod def deserialize_safe(cls, byts, protocol_version): key_type, value_type = cls.subtypes - if protocol_version >= 3: - unpack = int32_unpack - length = 4 - else: - unpack = uint16_unpack - length = 2 - numelements = unpack(byts[:length]) + length = 4 + numelements = int32_unpack(byts[:length]) p = length themap = util.OrderedMapSerializedKey(key_type, protocol_version) inner_proto = max(3, protocol_version) for _ in range(numelements): - key_len = unpack(byts[p:p + length]) + key_len = int32_unpack(byts[p:p + length]) p += length if key_len < 0: keybytes = None @@ -893,7 +884,7 @@ def deserialize_safe(cls, byts, protocol_version): p += key_len key = key_type.from_binary(keybytes, inner_proto) - val_len = unpack(byts[p:p + length]) + val_len = int32_unpack(byts[p:p + length]) p += length if val_len < 0: val = None @@ -908,9 +899,8 @@ def deserialize_safe(cls, byts, protocol_version): @classmethod def serialize_safe(cls, themap, protocol_version): key_type, value_type = cls.subtypes - pack = int32_pack if protocol_version >= 3 else uint16_pack buf = io.BytesIO() - buf.write(pack(len(themap))) + buf.write(int32_pack(len(themap))) try: items = themap.items() except AttributeError: @@ -919,16 +909,16 @@ def serialize_safe(cls, themap, protocol_version): for key, val in items: if key is not None: keybytes = key_type.to_binary(key, inner_proto) - buf.write(pack(len(keybytes))) + buf.write(int32_pack(len(keybytes))) buf.write(keybytes) else: - buf.write(pack(-1)) + buf.write(int32_pack(-1)) if val is not None: valbytes = value_type.to_binary(val, inner_proto) - buf.write(pack(len(valbytes))) + buf.write(int32_pack(len(valbytes))) buf.write(valbytes) else: - buf.write(pack(-1)) + buf.write(int32_pack(-1)) return buf.getvalue() diff --git a/cassandra/cython_utils.pxd b/cassandra/cython_utils.pxd index 4a1e71dba5..7469657b04 100644 --- a/cassandra/cython_utils.pxd +++ b/cassandra/cython_utils.pxd @@ -1,2 +1,3 @@ from libc.stdint cimport int64_t cdef datetime_from_timestamp(double timestamp) +cdef datetime_from_ms_timestamp(int64_t timestamp_ms) diff --git a/cassandra/cython_utils.pyx b/cassandra/cython_utils.pyx index 7539f33f31..f3421063da 100644 --- a/cassandra/cython_utils.pyx +++ b/cassandra/cython_utils.pyx @@ -60,3 +60,22 @@ cdef datetime_from_timestamp(double timestamp): microseconds += tmp return DATETIME_EPOC + timedelta_new(days, seconds, microseconds) + + +cdef datetime_from_ms_timestamp(int64_t timestamp_ms): + """ + Creates a datetime from a timestamp in milliseconds using integer + arithmetic to preserve precision for large values. + """ + cdef int64_t total_seconds = timestamp_ms // 1000 + cdef int microseconds = ((timestamp_ms % 1000) * 1000) + # For negative timestamps, ensure microseconds is non-negative + if microseconds < 0: + total_seconds -= 1 + microseconds += 1000000 + cdef int days = (total_seconds // 86400) + cdef int seconds = (total_seconds % 86400) + if seconds < 0: + days -= 1 + seconds += 86400 + return DATETIME_EPOC + timedelta_new(days, seconds, microseconds) diff --git a/cassandra/deserializers.pyx b/cassandra/deserializers.pyx index 7c256674b0..98e8676bbc 100644 --- a/cassandra/deserializers.pyx +++ b/cassandra/deserializers.pyx @@ -17,7 +17,7 @@ from libc.stdint cimport int32_t, uint16_t include 'cython_marshal.pyx' from cassandra.buffer cimport Buffer, to_bytes, slice_buffer -from cassandra.cython_utils cimport datetime_from_timestamp +from cassandra.cython_utils cimport datetime_from_timestamp, datetime_from_ms_timestamp from cython.view cimport array as cython_array from cassandra.tuple cimport tuple_new, tuple_set @@ -135,8 +135,8 @@ cdef class DesCounterColumnType(DesLongType): cdef class DesDateType(Deserializer): cdef deserialize(self, Buffer *buf, int protocol_version): - cdef double timestamp = unpack_num[int64_t](buf) / 1000.0 - return datetime_from_timestamp(timestamp) + cdef int64_t timestamp_ms = unpack_num[int64_t](buf) + return datetime_from_ms_timestamp(timestamp_ms) cdef class TimestampType(DesDateType): @@ -208,15 +208,9 @@ cdef class _DesSingleParamType(_DesParameterizedType): cdef class DesListType(_DesSingleParamType): cdef deserialize(self, Buffer *buf, int protocol_version): - cdef uint16_t v2_and_below = 2 - cdef int32_t v3_and_above = 3 - if protocol_version >= 3: - result = _deserialize_list_or_set[int32_t]( - v3_and_above, buf, protocol_version, self.deserializer) - else: - result = _deserialize_list_or_set[uint16_t]( - v2_and_below, buf, protocol_version, self.deserializer) + result = _deserialize_list_or_set( + buf, protocol_version, self.deserializer) return result @@ -225,60 +219,49 @@ cdef class DesSetType(DesListType): return util.sortedset(DesListType.deserialize(self, buf, protocol_version)) -ctypedef fused itemlen_t: - uint16_t # protocol <= v2 - int32_t # protocol >= v3 - -cdef list _deserialize_list_or_set(itemlen_t dummy_version, - Buffer *buf, int protocol_version, +cdef list _deserialize_list_or_set(Buffer *buf, int protocol_version, Deserializer deserializer): """ Deserialize a list or set. - - The 'dummy' parameter is needed to make fused types work, so that - we can specialize on the protocol version. """ cdef Buffer itemlen_buf cdef Buffer elem_buf - cdef itemlen_t numelements + cdef int32_t numelements cdef int offset cdef list result = [] - _unpack_len[itemlen_t](buf, 0, &numelements) - offset = sizeof(itemlen_t) + _unpack_len(buf, 0, &numelements) + offset = sizeof(int32_t) protocol_version = max(3, protocol_version) for _ in range(numelements): - subelem[itemlen_t](buf, &elem_buf, &offset, dummy_version) + subelem(buf, &elem_buf, &offset) result.append(from_binary(deserializer, &elem_buf, protocol_version)) return result cdef inline int subelem( - Buffer *buf, Buffer *elem_buf, int* offset, itemlen_t dummy) except -1: + Buffer *buf, Buffer *elem_buf, int* offset) except -1: """ Read the next element from the buffer: first read the size (in bytes) of the element, then fill elem_buf with a newly sliced buffer of this size (and the right offset). """ - cdef itemlen_t elemlen + cdef int32_t elemlen - _unpack_len[itemlen_t](buf, offset[0], &elemlen) - offset[0] += sizeof(itemlen_t) + _unpack_len(buf, offset[0], &elemlen) + offset[0] += sizeof(int32_t) slice_buffer(buf, elem_buf, offset[0], elemlen) offset[0] += elemlen return 0 -cdef int _unpack_len(Buffer *buf, int offset, itemlen_t *output) except -1: +cdef int _unpack_len(Buffer *buf, int offset, int32_t *output) except -1: cdef Buffer itemlen_buf - slice_buffer(buf, &itemlen_buf, offset, sizeof(itemlen_t)) + slice_buffer(buf, &itemlen_buf, offset, sizeof(int32_t)) - if itemlen_t is uint16_t: - output[0] = unpack_num[uint16_t](&itemlen_buf) - else: - output[0] = unpack_num[int32_t](&itemlen_buf) + output[0] = unpack_num[int32_t](&itemlen_buf) return 0 @@ -295,42 +278,33 @@ cdef class DesMapType(_DesParameterizedType): self.val_deserializer = self.deserializers[1] cdef deserialize(self, Buffer *buf, int protocol_version): - cdef uint16_t v2_and_below = 0 - cdef int32_t v3_and_above = 0 key_type, val_type = self.cqltype.subtypes - if protocol_version >= 3: - result = _deserialize_map[int32_t]( - v3_and_above, buf, protocol_version, - self.key_deserializer, self.val_deserializer, - key_type, val_type) - else: - result = _deserialize_map[uint16_t]( - v2_and_below, buf, protocol_version, - self.key_deserializer, self.val_deserializer, - key_type, val_type) + result = _deserialize_map( + buf, protocol_version, + self.key_deserializer, self.val_deserializer, + key_type, val_type) return result -cdef _deserialize_map(itemlen_t dummy_version, - Buffer *buf, int protocol_version, +cdef _deserialize_map(Buffer *buf, int protocol_version, Deserializer key_deserializer, Deserializer val_deserializer, key_type, val_type): cdef Buffer key_buf, val_buf cdef Buffer itemlen_buf - cdef itemlen_t numelements + cdef int32_t numelements cdef int offset cdef list result = [] - _unpack_len[itemlen_t](buf, 0, &numelements) - offset = sizeof(itemlen_t) + _unpack_len(buf, 0, &numelements) + offset = sizeof(int32_t) themap = util.OrderedMapSerializedKey(key_type, protocol_version) protocol_version = max(3, protocol_version) for _ in range(numelements): - subelem[itemlen_t](buf, &key_buf, &offset, dummy_version) - subelem[itemlen_t](buf, &val_buf, &offset, numelements) + subelem(buf, &key_buf, &offset) + subelem(buf, &val_buf, &offset) key = from_binary(key_deserializer, &key_buf, protocol_version) val = from_binary(val_deserializer, &val_buf, protocol_version) themap._insert_unchecked(key, to_bytes(&key_buf), val) diff --git a/cassandra/encoder.py b/cassandra/encoder.py index e834550fd3..d803c087ba 100644 --- a/cassandra/encoder.py +++ b/cassandra/encoder.py @@ -142,7 +142,7 @@ def cql_encode_datetime(self, val): with millisecond precision. """ timestamp = calendar.timegm(val.utctimetuple()) - return str(int(timestamp * 1e3 + getattr(val, 'microsecond', 0) / 1e3)) + return str(timestamp * 1000 + getattr(val, 'microsecond', 0) // 1000) def cql_encode_date(self, val): """ diff --git a/cassandra/io/asyncioreactor.py b/cassandra/io/asyncioreactor.py index 41b744602d..452667c8eb 100644 --- a/cassandra/io/asyncioreactor.py +++ b/cassandra/io/asyncioreactor.py @@ -23,8 +23,8 @@ asyncio.run_coroutine_threadsafe except AttributeError: raise ImportError( - 'Cannot use asyncioreactor without access to ' - 'asyncio.run_coroutine_threadsafe (added in 3.4.6 and 3.5.1)' + "Cannot use asyncioreactor without access to " + "asyncio.run_coroutine_threadsafe (added in 3.4.6 and 3.5.1)" ) @@ -38,12 +38,12 @@ class AsyncioTimer(object): @property def end(self): - raise NotImplementedError('{} is not compatible with TimerManager and ' - 'does not implement .end()') + raise NotImplementedError( + "{} is not compatible with TimerManager and does not implement .end()" + ) def __init__(self, timeout, callback, loop): - delayed = self._call_delayed_coro(timeout=timeout, - callback=callback) + delayed = self._call_delayed_coro(timeout=timeout, callback=callback) self._handle = asyncio.run_coroutine_threadsafe(delayed, loop=loop) @staticmethod @@ -63,17 +63,61 @@ def cancel(self): def finish(self): # connection.Timer method not implemented here because we can't inspect # the Handle returned from call_later - raise NotImplementedError('{} is not compatible with TimerManager and ' - 'does not implement .finish()') + raise NotImplementedError( + "{} is not compatible with TimerManager and does not implement .finish()" + ) + + +class _AsyncioProtocol(asyncio.Protocol): + """ + Protocol adapter for asyncio SSL connections. Bridges asyncio's + transport/protocol API back to AsyncioConnection's buffer processing. + """ + + def __init__(self, connection, loop_args=None): + self._connection = connection + self.transport = None + self.write_ready = asyncio.Event(**(loop_args or {})) + self.write_ready.set() + + def connection_made(self, transport): + self.transport = transport + + def data_received(self, data): + conn = self._connection + conn._iobuf.write(data) + if conn._iobuf.tell(): + conn.process_io_buffer() + + def pause_writing(self): + self.write_ready.clear() + + def resume_writing(self): + self.write_ready.set() + + def connection_lost(self, exc): + # Unblock any paused writer so shutdown does not hang + self.write_ready.set() + conn = self._connection + if exc: + log.debug("Connection %s lost: %s", conn, exc) + conn.defunct(exc) + else: + log.debug("Connection %s closed by server", conn) + conn.close() + + def eof_received(self): + return False class AsyncioConnection(Connection): """ - An experimental implementation of :class:`.Connection` that uses the - ``asyncio`` module in the Python standard library for its event loop. + An implementation of :class:`.Connection` that uses the ``asyncio`` + module in the Python standard library for its event loop. - Note that it requires ``asyncio`` features that were only introduced in the - 3.4 line in 3.4.6, and in the 3.5 line in 3.5.1. + Supports SSL connections via asyncio's native TLS transport, which + avoids the incompatibility between ``ssl.SSLSocket`` and asyncio's + low-level socket methods (``sock_sendall``, ``sock_recv``). """ _loop = None @@ -88,26 +132,109 @@ class AsyncioConnection(Connection): def __init__(self, *args, **kwargs): Connection.__init__(self, *args, **kwargs) self._background_tasks = set() + self._transport = None + self._using_ssl = bool(self.ssl_context) self._connect_socket() self._socket.setblocking(0) loop_args = dict() if sys.version_info[0] == 3 and sys.version_info[1] < 10: - loop_args['loop'] = self._loop + loop_args["loop"] = self._loop + self._protocol = _AsyncioProtocol(self, loop_args) if self._using_ssl else None + self._ssl_ready = asyncio.Event(**loop_args) if self._using_ssl else None self._write_queue = asyncio.Queue(**loop_args) self._write_queue_lock = asyncio.Lock(**loop_args) # see initialize_reactor -- loop is running in a separate thread, so we # have to use a threadsafe call - self._read_watcher = asyncio.run_coroutine_threadsafe( - self.handle_read(), loop=self._loop - ) + if self._using_ssl: + # For SSL: set up asyncio transport/protocol, then start writer + self._read_watcher = asyncio.run_coroutine_threadsafe( + self._setup_ssl_and_run(), loop=self._loop + ) + else: + # For non-SSL: use low-level sock_sendall/sock_recv as before + self._read_watcher = asyncio.run_coroutine_threadsafe( + self.handle_read(), loop=self._loop + ) self._write_watcher = asyncio.run_coroutine_threadsafe( self.handle_write(), loop=self._loop ) self._send_options_message() + def _connect_socket(self): + """ + Override base class to skip SSL wrapping of the socket. + For SSL connections, the plain TCP socket is connected here, and TLS + is set up later via asyncio's native SSL transport in _setup_ssl_and_run(). + """ + sockerr = None + addresses = self._get_socket_addresses() + for af, socktype, proto, _, sockaddr in addresses: + try: + self._socket = self._socket_impl.socket(af, socktype, proto) + # Do NOT wrap with ssl_context here -- asyncio will handle TLS + self._socket.settimeout(self.connect_timeout) + self._initiate_connection(sockaddr) + self._socket.settimeout(None) + + local_addr = self._socket.getsockname() + log.debug("Connection %s: '%s' -> '%s'", id(self), local_addr, sockaddr) + sockerr = None + break + except socket.error as err: + if self._socket: + self._socket.close() + self._socket = None + sockerr = err + + if sockerr: + raise socket.error( + sockerr.errno, + "Tried connecting to %s. Last error: %s" + % ([a[4] for a in addresses], sockerr.strerror or sockerr), + ) + + if self.sockopts: + for args in self.sockopts: + self._socket.setsockopt(*args) + + async def _setup_ssl_and_run(self): + """ + Upgrade the plain TCP connection to TLS using asyncio's native SSL + transport, then continuously read data via the protocol callbacks. + """ + try: + ssl_context = self.ssl_context + server_hostname = None + if self.ssl_options: + server_hostname = self.ssl_options.get("server_hostname", None) + if server_hostname is None: + # asyncio's create_connection requires server_hostname when + # ssl= is set. Use endpoint address for SNI/verification when + # check_hostname is enabled; otherwise pass "" to suppress SNI. + server_hostname = ( + self.endpoint.address if ssl_context.check_hostname else "" + ) + + transport, protocol = await self._loop.create_connection( + lambda: self._protocol, + sock=self._socket, + ssl=ssl_context, + server_hostname=server_hostname, + ) + self._transport = transport + + if self._check_hostname: + self._validate_hostname() + self._ssl_ready.set() + except Exception as exc: + log.debug("SSL setup failed for %s: %s", self, exc) + self.defunct(exc) + # Unblock handle_write so it can observe the defunct state and exit + self._ssl_ready.set() + return @classmethod def initialize_reactor(cls): @@ -126,8 +253,9 @@ def initialize_reactor(cls): cls._loop = asyncio.new_event_loop() # daemonize so the loop will be shut down on interpreter # shutdown - cls._loop_thread = Thread(target=cls._loop.run_forever, - daemon=True, name="asyncio_thread") + cls._loop_thread = Thread( + target=cls._loop.run_forever, daemon=True, name="asyncio_thread" + ) cls._loop_thread.start() @classmethod @@ -142,9 +270,7 @@ def close(self): # close from the loop thread to avoid races when removing file # descriptors - asyncio.run_coroutine_threadsafe( - self._close(), loop=self._loop - ) + asyncio.run_coroutine_threadsafe(self._close(), loop=self._loop) async def _close(self): log.debug("Closing connection (%s) to %s" % (id(self), self.endpoint)) @@ -152,7 +278,10 @@ async def _close(self): self._write_watcher.cancel() if self._read_watcher: self._read_watcher.cancel() - if self._socket: + if self._transport: + self._transport.close() + self._transport = None + elif self._socket: self._loop.remove_writer(self._socket.fileno()) self._loop.remove_reader(self._socket.fileno()) self._socket.close() @@ -160,8 +289,10 @@ async def _close(self): log.debug("Closed socket to %s" % (self.endpoint,)) if not self.is_defunct: - self.error_all_requests( - ConnectionShutdown("Connection to %s was closed" % self.endpoint)) + msg = "Connection to %s was closed" % self.endpoint + if self.last_error: + msg += ": %s" % (self.last_error,) + self.error_all_requests(ConnectionShutdown(msg)) # don't leave in-progress operations hanging self.connected_event.set() @@ -170,15 +301,12 @@ def push(self, data): if len(data) > buff_size: chunks = [] for i in range(0, len(data), buff_size): - chunks.append(data[i:i + buff_size]) + chunks.append(data[i : i + buff_size]) else: chunks = [data] if self._loop_thread != threading.current_thread(): - asyncio.run_coroutine_threadsafe( - self._push_msg(chunks), - loop=self._loop - ) + asyncio.run_coroutine_threadsafe(self._push_msg(chunks), loop=self._loop) else: # avoid races/hangs by just scheduling this, not using threadsafe task = self._loop.create_task(self._push_msg(chunks)) @@ -192,13 +320,25 @@ async def _push_msg(self, chunks): for chunk in chunks: self._write_queue.put_nowait(chunk) - async def handle_write(self): + # For SSL connections, wait until the TLS handshake completes + if self._ssl_ready: + await self._ssl_ready.wait() + if self.is_defunct: + return while True: try: next_msg = await self._write_queue.get() if next_msg: - await self._loop.sock_sendall(self._socket, next_msg) + if self._transport: + # SSL: use asyncio transport (handles TLS transparently) + await self._protocol.write_ready.wait() + if self.is_closed or self.is_defunct or not self._transport: + return + self._transport.write(next_msg) + else: + # Non-SSL: use low-level socket API + await self._loop.sock_sendall(self._socket, next_msg) except socket.error as err: log.debug("Exception in send for %s: %s", self, err) self.defunct(err) @@ -221,8 +361,7 @@ async def handle_read(self): await asyncio.sleep(0) continue except socket.error as err: - log.debug("Exception during socket recv for %s: %s", - self, err) + log.debug("Exception during socket recv for %s: %s", self, err) self.defunct(err) return # leave the read loop except asyncio.CancelledError: diff --git a/cassandra/io/asyncorereactor.py b/cassandra/io/asyncorereactor.py index 2c75e7139d..02466ad0d2 100644 --- a/cassandra/io/asyncorereactor.py +++ b/cassandra/io/asyncorereactor.py @@ -385,12 +385,14 @@ def close(self): log.debug("Closed socket to %s", self.endpoint) if not self.is_defunct: - self.error_all_requests( - ConnectionShutdown("Connection to %s was closed" % self.endpoint)) + msg = "Connection to %s was closed" % self.endpoint + if self.last_error: + msg += ": %s" % (self.last_error,) + self.error_all_requests(ConnectionShutdown(msg)) #This happens when the connection is shutdown while waiting for the ReadyMessage if not self.connected_event.is_set(): - self.last_error = ConnectionShutdown("Connection to %s was closed" % self.endpoint) + self.last_error = ConnectionShutdown(msg) # don't leave in-progress operations hanging self.connected_event.set() diff --git a/cassandra/io/eventletreactor.py b/cassandra/io/eventletreactor.py index 42874036d5..234a4a574c 100644 --- a/cassandra/io/eventletreactor.py +++ b/cassandra/io/eventletreactor.py @@ -145,8 +145,10 @@ def close(self): log.debug("Closed socket to %s" % (self.endpoint,)) if not self.is_defunct: - self.error_all_requests( - ConnectionShutdown("Connection to %s was closed" % self.endpoint)) + msg = "Connection to %s was closed" % self.endpoint + if self.last_error: + msg += ": %s" % (self.last_error,) + self.error_all_requests(ConnectionShutdown(msg)) # don't leave in-progress operations hanging self.connected_event.set() diff --git a/cassandra/io/geventreactor.py b/cassandra/io/geventreactor.py index 4f1f158aa7..7516fdd6df 100644 --- a/cassandra/io/geventreactor.py +++ b/cassandra/io/geventreactor.py @@ -95,8 +95,10 @@ def close(self): log.debug("Closed socket to %s" % (self.endpoint,)) if not self.is_defunct: - self.error_all_requests( - ConnectionShutdown("Connection to %s was closed" % self.endpoint)) + msg = "Connection to %s was closed" % self.endpoint + if self.last_error: + msg += ": %s" % (self.last_error,) + self.error_all_requests(ConnectionShutdown(msg)) # don't leave in-progress operations hanging self.connected_event.set() diff --git a/cassandra/io/libevreactor.py b/cassandra/io/libevreactor.py index 29039653f4..3da809931f 100644 --- a/cassandra/io/libevreactor.py +++ b/cassandra/io/libevreactor.py @@ -13,7 +13,6 @@ # limitations under the License. import atexit from collections import deque -from functools import partial import logging import os import socket @@ -116,6 +115,10 @@ def _cleanup(self): if not self._thread: return + # Stop the prepare watcher first to prevent race conditions + if self._preparer: + self._preparer.stop() + for conn in self._live_conns | self._new_conns | self._closed_conns: conn.close() for watcher in (conn._write_watcher, conn._read_watcher): @@ -125,8 +128,9 @@ def _cleanup(self): self.notify() # wake the timer watcher # PYTHON-752 Thread might have just been created and not started + # Use longer timeout to allow proper cleanup with self._lock_thread: - self._thread.join(timeout=1.0) + self._thread.join(timeout=5.0) if self._thread.is_alive(): log.warning( @@ -165,6 +169,10 @@ def connection_created(self, conn): def connection_destroyed(self, conn): with self._conn_set_lock: + new_conns = self._new_conns.copy() + new_conns.discard(conn) + self._new_conns = new_conns + new_live_conns = self._live_conns.copy() new_live_conns.discard(conn) self._live_conns = new_live_conns @@ -194,7 +202,8 @@ def _loop_will_run(self, prepare): self._new_conns = set() for conn in to_start: - conn._read_watcher.start() + if conn._read_watcher: + conn._read_watcher.start() changed = True @@ -222,8 +231,20 @@ def _loop_will_run(self, prepare): self._notifier.send() +def _atexit_cleanup(): + """Cleanup function called by atexit that uses the current _global_loop value. + + This wrapper ensures that cleanup receives the actual LibevLoop instance + instead of None, which was the value of _global_loop when the module was + imported. + """ + global _global_loop + if _global_loop is not None: + _cleanup(_global_loop) + + _global_loop = None -atexit.register(partial(_cleanup, _global_loop)) +atexit.register(_atexit_cleanup) class LibevConnection(Connection): @@ -292,8 +313,10 @@ def close(self): # don't leave in-progress operations hanging if not self.is_defunct: - self.error_all_requests( - ConnectionShutdown("Connection to %s was closed" % self.endpoint)) + msg = "Connection to %s was closed" % self.endpoint + if self.last_error: + msg += ": %s" % (self.last_error,) + self.error_all_requests(ConnectionShutdown(msg)) self.connected_event.set() def handle_write(self, watcher, revents, errno=None): diff --git a/cassandra/io/libevwrapper.c b/cassandra/io/libevwrapper.c index f32504fa34..fc25f9ceba 100644 --- a/cassandra/io/libevwrapper.c +++ b/cassandra/io/libevwrapper.c @@ -118,7 +118,13 @@ IO_dealloc(libevwrapper_IO *self) { static void io_callback(struct ev_loop *loop, ev_io *watcher, int revents) { libevwrapper_IO *self = watcher->data; PyObject *result; - PyGILState_STATE gstate = PyGILState_Ensure(); + PyGILState_STATE gstate; + + if (!self || !self->callback) { + return; // Skip callback if object is being destroyed + } + + gstate = PyGILState_Ensure(); if (revents & EV_ERROR && errno) { result = PyObject_CallFunction(self->callback, "Obi", self, revents, errno); } else { @@ -354,6 +360,10 @@ static void prepare_callback(struct ev_loop *loop, ev_prepare *watcher, int reve PyObject *result = NULL; PyGILState_STATE gstate; + if (!self || !self->callback) { + return; // Skip callback if object is being destroyed + } + gstate = PyGILState_Ensure(); result = PyObject_CallFunction(self->callback, "O", self); if (!result) { @@ -473,6 +483,10 @@ static void timer_callback(struct ev_loop *loop, ev_timer *watcher, int revents) PyObject *result = NULL; PyGILState_STATE gstate; + if (!self || !self->callback) { + return; // Skip callback if object is being destroyed + } + gstate = PyGILState_Ensure(); result = PyObject_CallFunction(self->callback, NULL); if (!result) { diff --git a/cassandra/io/twistedreactor.py b/cassandra/io/twistedreactor.py index e4605a7446..446200bf63 100644 --- a/cassandra/io/twistedreactor.py +++ b/cassandra/io/twistedreactor.py @@ -283,8 +283,10 @@ def close(self): log.debug("Closed socket to %s", self.endpoint) if not self.is_defunct: - self.error_all_requests( - ConnectionShutdown("Connection to %s was closed" % self.endpoint)) + msg = "Connection to %s was closed" % self.endpoint + if self.last_error: + msg += ": %s" % (self.last_error,) + self.error_all_requests(ConnectionShutdown(msg)) # don't leave in-progress operations hanging self.connected_event.set() diff --git a/tests/integration/cqlengine/advanced/__init__.py b/cassandra/lwt_info.py similarity index 60% rename from tests/integration/cqlengine/advanced/__init__.py rename to cassandra/lwt_info.py index 386372eb4a..d64c08bbcf 100644 --- a/tests/integration/cqlengine/advanced/__init__.py +++ b/cassandra/lwt_info.py @@ -1,4 +1,4 @@ -# Copyright DataStax, Inc. +# Copyright 2020 ScyllaDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,3 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +class _LwtInfo: + """ + Holds LWT-related information parsed from the server's supported features. + """ + + def __init__(self, lwt_meta_bit_mask): + self.lwt_meta_bit_mask = lwt_meta_bit_mask + + def get_lwt_flag(self, flags): + return (flags & self.lwt_meta_bit_mask) == self.lwt_meta_bit_mask diff --git a/cassandra/marshal.py b/cassandra/marshal.py index a527a9e1d7..413e1831d4 100644 --- a/cassandra/marshal.py +++ b/cassandra/marshal.py @@ -33,11 +33,6 @@ def _make_packer(format_string): float_pack, float_unpack = _make_packer('>f') double_pack, double_unpack = _make_packer('>d') -# Special case for cassandra header -header_struct = struct.Struct('>BBbB') -header_pack = header_struct.pack -header_unpack = header_struct.unpack - # in protocol version 3 and higher, the stream ID is two bytes v3_header_struct = struct.Struct('>BBhB') v3_header_pack = v3_header_struct.pack diff --git a/cassandra/metadata.py b/cassandra/metadata.py index 30bcf81654..43399b7152 100644 --- a/cassandra/metadata.py +++ b/cassandra/metadata.py @@ -139,8 +139,9 @@ def export_schema_as_string(self): def refresh(self, connection, timeout, target_type=None, change_type=None, fetch_size=None, metadata_request_timeout=None, **kwargs): - server_version = self.get_host(connection.original_endpoint).release_version - dse_version = self.get_host(connection.original_endpoint).dse_version + host = self.get_host(connection.original_endpoint) + server_version = host.release_version if host else None + dse_version = host.dse_version if host else None parser = get_schema_parser(connection, server_version, dse_version, timeout, metadata_request_timeout, fetch_size) if not target_type: @@ -153,12 +154,7 @@ def refresh(self, connection, timeout, target_type=None, change_type=None, fetch meta = parse_method(self.keyspaces, **kwargs) if meta: update_method = getattr(self, '_update_' + tt_lower) - if tt_lower == 'keyspace' and connection.protocol_version < 3: - # we didn't have 'type' target in legacy protocol versions, so we need to query those too - user_types = parser.get_types_map(self.keyspaces, **kwargs) - self._update_keyspace(meta, user_types) - else: - update_method(meta) + update_method(meta) else: drop_method = getattr(self, '_drop_' + tt_lower) drop_method(**kwargs) @@ -574,10 +570,11 @@ def __init__(self, options_map): def make_token_replica_map(self, token_to_host_owner, ring): replica_map = {} - for i in range(len(ring)): + ring_len = len(ring) + for i in range(ring_len): j, hosts = 0, list() - while len(hosts) < self.replication_factor and j < len(ring): - token = ring[(i + j) % len(ring)] + while len(hosts) < self.replication_factor and j < ring_len: + token = ring[(i + j) % ring_len] host = token_to_host_owner[token] if host not in hosts: hosts.append(host) @@ -634,10 +631,14 @@ def make_token_replica_map(self, token_to_host_owner, ring): hosts_per_dc = defaultdict(set) for i, token in enumerate(ring): host = token_to_host_owner[token] - dc_to_token_offset[host.datacenter].append(i) - if host.datacenter and host.rack: - dc_racks[host.datacenter].add(host.rack) - hosts_per_dc[host.datacenter].add(host) + host_dc = host.datacenter + if host_dc in dc_rf_map: + # if the host is in a DC that has a replication factor, add it + # to the list of token offsets for that DC + dc_to_token_offset[host_dc].append(i) + if host.rack: + dc_racks[host_dc].add(host.rack) + hosts_per_dc[host_dc].add(host) # A map of DCs to an index into the dc_to_token_offset value for that dc. # This is how we keep track of advancing around the ring for each DC. @@ -649,8 +650,6 @@ def make_token_replica_map(self, token_to_host_owner, ring): # go through each DC and find the replicas in that DC for dc in dc_to_token_offset.keys(): - if dc not in dc_rf_map: - continue # advance our per-DC index until we're up to at least the # current token in the ring @@ -662,34 +661,34 @@ def make_token_replica_map(self, token_to_host_owner, ring): dc_to_current_index[dc] = index replicas_remaining = dc_rf_map[dc] - replicas_this_dc = 0 + num_replicas_this_dc = 0 skipped_hosts = [] racks_placed = set() - racks_this_dc = dc_racks[dc] - hosts_this_dc = len(hosts_per_dc[dc]) + num_racks_this_dc = len(dc_racks[dc]) + num_hosts_this_dc = len(hosts_per_dc[dc]) for token_offset_index in range(index, index+num_tokens): - if token_offset_index >= len(token_offsets): - token_offset_index = token_offset_index - len(token_offsets) + if replicas_remaining == 0 or num_replicas_this_dc == num_hosts_this_dc: + break + + if token_offset_index >= num_tokens: + token_offset_index = token_offset_index - num_tokens token_offset = token_offsets[token_offset_index] host = token_to_host_owner[ring[token_offset]] - if replicas_remaining == 0 or replicas_this_dc == hosts_this_dc: - break - if host in replicas: continue - if host.rack in racks_placed and len(racks_placed) < len(racks_this_dc): + if host.rack in racks_placed and len(racks_placed) < num_racks_this_dc: skipped_hosts.append(host) continue replicas.append(host) - replicas_this_dc += 1 + num_replicas_this_dc += 1 replicas_remaining -= 1 racks_placed.add(host.rack) - if len(racks_placed) == len(racks_this_dc): + if len(racks_placed) == num_racks_this_dc: for host in skipped_hosts: if replicas_remaining == 0: break @@ -1894,7 +1893,7 @@ def hash_fn(cls, key): def __init__(self, token): """ `token` is an int or string representing the token. """ - self.value = int(token) + super().__init__(int(token)) class MD5Token(HashToken): @@ -2077,7 +2076,6 @@ def __init__(self, connection, timeout, fetch_size, metadata_request_timeout): self.types_result = [] self.functions_result = [] self.aggregates_result = [] - self.scylla_result = [] self.keyspace_table_rows = defaultdict(list) self.keyspace_table_col_rows = defaultdict(lambda: defaultdict(list)) @@ -2085,7 +2083,6 @@ def __init__(self, connection, timeout, fetch_size, metadata_request_timeout): self.keyspace_func_rows = defaultdict(list) self.keyspace_agg_rows = defaultdict(list) self.keyspace_table_trigger_rows = defaultdict(lambda: defaultdict(list)) - self.keyspace_scylla_rows = defaultdict(lambda: defaultdict(list)) def get_all_keyspaces(self): self._query_all() @@ -2531,23 +2528,9 @@ def _query_all(self): self._aggregate_results() def _aggregate_results(self): - m = self.keyspace_scylla_rows - for row in self.scylla_result: - ksname = row["keyspace_name"] - cfname = row[self._table_name_col] - m[ksname][cfname].append(row) - m = self.keyspace_table_rows for row in self.tables_result: ksname = row["keyspace_name"] - cfname = row[self._table_name_col] - # in_memory property is stored in scylla private table - # add it to table properties if enabled - try: - if self.keyspace_scylla_rows[ksname][cfname][0]["in_memory"] == True: - row["in_memory"] = True - except (IndexError, KeyError): - pass m[ksname].append(row) m = self.keyspace_table_col_rows @@ -2593,7 +2576,10 @@ class SchemaParserV3(SchemaParserV22): _SELECT_FUNCTIONS = "SELECT * FROM system_schema.functions" _SELECT_AGGREGATES = "SELECT * FROM system_schema.aggregates" _SELECT_VIEWS = "SELECT * FROM system_schema.views" - _SELECT_SCYLLA = "SELECT * FROM system_schema.scylla_tables" + + def _is_not_scylla(self): + """Check if NOT connected to ScyllaDB by checking for shard awareness.""" + return getattr(getattr(self.connection, 'features', None), 'shard_id', None) is None _table_name_col = 'table_name' @@ -2645,40 +2631,44 @@ def get_table(self, keyspaces, keyspace, table): indexes_query = QueryMessage( query=maybe_add_timeout_to_query(self._SELECT_INDEXES + where_clause, self.metadata_request_timeout), consistency_level=cl, fetch_size=fetch_size) - triggers_query = QueryMessage( - query=maybe_add_timeout_to_query(self._SELECT_TRIGGERS + where_clause, self.metadata_request_timeout), - consistency_level=cl, fetch_size=fetch_size) - scylla_query = QueryMessage( - query=maybe_add_timeout_to_query(self._SELECT_SCYLLA + where_clause, self.metadata_request_timeout), - consistency_level=cl, fetch_size=fetch_size) + + # ScyllaDB doesn't have triggers, skip the query + if self._is_not_scylla(): + triggers_query = QueryMessage( + query=maybe_add_timeout_to_query(self._SELECT_TRIGGERS + where_clause, self.metadata_request_timeout), + consistency_level=cl, fetch_size=fetch_size) # in protocol v4 we don't know if this event is a view or a table, so we look for both where_clause = bind_params(" WHERE keyspace_name = %s AND view_name = %s", (keyspace, table), _encoder) view_query = QueryMessage( query=maybe_add_timeout_to_query(self._SELECT_VIEWS + where_clause, self.metadata_request_timeout), consistency_level=cl, fetch_size=fetch_size) - ((cf_success, cf_result), (col_success, col_result), - (indexes_sucess, indexes_result), (triggers_success, triggers_result), - (view_success, view_result), - (scylla_success, scylla_result)) = ( - self.connection.wait_for_responses( - cf_query, col_query, indexes_query, triggers_query, - view_query, scylla_query, timeout=self.timeout, fail_on_error=False) - ) + + if self._is_not_scylla(): + ((cf_success, cf_result), (col_success, col_result), + (indexes_sucess, indexes_result), (triggers_success, triggers_result), + (view_success, view_result)) = ( + self.connection.wait_for_responses( + cf_query, col_query, indexes_query, triggers_query, + view_query, timeout=self.timeout, fail_on_error=False) + ) + else: + ((cf_success, cf_result), (col_success, col_result), + (indexes_sucess, indexes_result), + (view_success, view_result)) = ( + self.connection.wait_for_responses( + cf_query, col_query, indexes_query, + view_query, timeout=self.timeout, fail_on_error=False) + ) + table_result = self._handle_results(cf_success, cf_result, query_msg=cf_query) col_result = self._handle_results(col_success, col_result, query_msg=col_query) if table_result: indexes_result = self._handle_results(indexes_sucess, indexes_result, query_msg=indexes_query) - triggers_result = self._handle_results(triggers_success, triggers_result, query_msg=triggers_query) - # in_memory property is stored in scylla private table - # add it to table properties if enabled - scylla_result = self._handle_results(scylla_success, scylla_result, expected_failures=(InvalidRequest,), - query_msg=scylla_query) - try: - if scylla_result[0]["in_memory"] == True: - table_result[0]["in_memory"] = True - except (IndexError, KeyError): - pass + if self._is_not_scylla(): + triggers_result = self._handle_results(triggers_success, triggers_result, query_msg=triggers_query) + else: + triggers_result = None return self._build_table_metadata(table_result[0], col_result, triggers_result, indexes_result) view_result = self._handle_results(view_success, view_result, query_msg=view_query) @@ -2727,9 +2717,10 @@ def _build_table_metadata(self, row, col_rows=None, trigger_rows=None, index_row self._build_table_columns(table_meta, col_rows, compact_static, is_dense, virtual) - for trigger_row in trigger_rows: - trigger_meta = self._build_trigger_metadata(table_meta, trigger_row) - table_meta.triggers[trigger_meta.name] = trigger_meta + if self._is_not_scylla(): + for trigger_row in trigger_rows: + trigger_meta = self._build_trigger_metadata(table_meta, trigger_row) + table_meta.triggers[trigger_meta.name] = trigger_meta for index_row in index_rows: index_meta = self._build_index_metadata(table_meta, index_row) @@ -2772,7 +2763,7 @@ def _build_table_columns(self, meta, col_rows, compact_static=False, is_dense=Fa meta.clustering_key.append(meta.columns[r.get('column_name')]) for col_row in (r for r in col_rows - if r.get('kind', None) not in ('partition_key', 'clustering_key')): + if r.get('kind', None) not in ('partition_key', 'clustering')): column_meta = self._build_column_metadata(meta, col_row) if is_dense and column_meta.cql_type == types.cql_empty_type: continue @@ -2824,6 +2815,7 @@ def _build_trigger_metadata(table_metadata, row): trigger_meta = TriggerMetadata(table_metadata, name, options) return trigger_meta + def _query_all(self): cl = ConsistencyLevel.ONE fetch_size = self.fetch_size @@ -2840,39 +2832,45 @@ def _query_all(self): fetch_size=fetch_size, consistency_level=cl), QueryMessage(query=maybe_add_timeout_to_query(self._SELECT_AGGREGATES, self.metadata_request_timeout), fetch_size=fetch_size, consistency_level=cl), - QueryMessage(query=maybe_add_timeout_to_query(self._SELECT_TRIGGERS, self.metadata_request_timeout), - fetch_size=fetch_size, consistency_level=cl), QueryMessage(query=maybe_add_timeout_to_query(self._SELECT_INDEXES, self.metadata_request_timeout), fetch_size=fetch_size, consistency_level=cl), QueryMessage(query=maybe_add_timeout_to_query(self._SELECT_VIEWS, self.metadata_request_timeout), fetch_size=fetch_size, consistency_level=cl), - QueryMessage(query=maybe_add_timeout_to_query(self._SELECT_SCYLLA, self.metadata_request_timeout), - fetch_size=fetch_size, consistency_level=cl), ] + # ScyllaDB doesn't have triggers, skip the query + if self._is_not_scylla(): + queries.append(QueryMessage(query=maybe_add_timeout_to_query(self._SELECT_TRIGGERS, self.metadata_request_timeout), + fetch_size=fetch_size, consistency_level=cl)) + + responses = self.connection.wait_for_responses(*queries, timeout=self.timeout, fail_on_error=False) + + # Unpack common responses (always present) ((ks_success, ks_result), (table_success, table_result), (col_success, col_result), (types_success, types_result), (functions_success, functions_result), (aggregates_success, aggregates_result), - (triggers_success, triggers_result), (indexes_success, indexes_result), - (views_success, views_result), - (scylla_success, scylla_result)) = self.connection.wait_for_responses( - *queries, timeout=self.timeout, fail_on_error=False - ) + (views_success, views_result)) = responses[:8] + + # Unpack triggers response if present (Cassandra/DSE only) + if self._is_not_scylla(): + (triggers_success, triggers_result) = responses[8] self.keyspaces_result = self._handle_results(ks_success, ks_result, query_msg=queries[0]) self.tables_result = self._handle_results(table_success, table_result, query_msg=queries[1]) self.columns_result = self._handle_results(col_success, col_result, query_msg=queries[2]) - self.triggers_result = self._handle_results(triggers_success, triggers_result, query_msg=queries[6]) self.types_result = self._handle_results(types_success, types_result, query_msg=queries[3]) self.functions_result = self._handle_results(functions_success, functions_result, query_msg=queries[4]) self.aggregates_result = self._handle_results(aggregates_success, aggregates_result, query_msg=queries[5]) - self.indexes_result = self._handle_results(indexes_success, indexes_result, query_msg=queries[7]) - self.views_result = self._handle_results(views_success, views_result, query_msg=queries[8]) - self.scylla_result = self._handle_results(scylla_success, scylla_result, expected_failures=(InvalidRequest,), query_msg=queries[9]) + self.indexes_result = self._handle_results(indexes_success, indexes_result, query_msg=queries[6]) + self.views_result = self._handle_results(views_success, views_result, query_msg=queries[7]) + if self._is_not_scylla(): + self.triggers_result = self._handle_results(triggers_success, triggers_result, query_msg=queries[8]) + else: + self.triggers_result = [] self._aggregate_results() @@ -2950,8 +2948,6 @@ def _query_all(self): fetch_size=fetch_size, consistency_level=cl), QueryMessage(query=maybe_add_timeout_to_query(self._SELECT_AGGREGATES, self.metadata_request_timeout), fetch_size=fetch_size, consistency_level=cl), - QueryMessage(query=maybe_add_timeout_to_query(self._SELECT_TRIGGERS, self.metadata_request_timeout), - fetch_size=fetch_size, consistency_level=cl), QueryMessage(query=maybe_add_timeout_to_query(self._SELECT_INDEXES, self.metadata_request_timeout), fetch_size=fetch_size, consistency_level=cl), QueryMessage(query=maybe_add_timeout_to_query(self._SELECT_VIEWS, self.metadata_request_timeout), @@ -2965,8 +2961,15 @@ def _query_all(self): fetch_size=fetch_size, consistency_level=cl), ] + # ScyllaDB doesn't have triggers, skip the query + if self._is_not_scylla(): + queries.append(QueryMessage(query=maybe_add_timeout_to_query(self._SELECT_TRIGGERS, self.metadata_request_timeout), + fetch_size=fetch_size, consistency_level=cl)) + responses = self.connection.wait_for_responses( *queries, timeout=self.timeout, fail_on_error=False) + + # Unpack common responses (always present) ( # copied from V3 (ks_success, ks_result), @@ -2975,39 +2978,45 @@ def _query_all(self): (types_success, types_result), (functions_success, functions_result), (aggregates_success, aggregates_result), - (triggers_success, triggers_result), (indexes_success, indexes_result), (views_success, views_result), # V4-only responses (virtual_ks_success, virtual_ks_result), (virtual_table_success, virtual_table_result), - (virtual_column_success, virtual_column_result) - ) = responses + (virtual_column_success, virtual_column_result), + ) = responses[:11] + + # Unpack triggers response if present (Cassandra/DSE only) + if self._is_not_scylla(): + (triggers_success, triggers_result) = responses[11] # copied from V3 self.keyspaces_result = self._handle_results(ks_success, ks_result, query_msg=queries[0]) self.tables_result = self._handle_results(table_success, table_result, query_msg=queries[1]) self.columns_result = self._handle_results(col_success, col_result, query_msg=queries[2]) - self.triggers_result = self._handle_results(triggers_success, triggers_result, query_msg=queries[6]) self.types_result = self._handle_results(types_success, types_result, query_msg=queries[3]) self.functions_result = self._handle_results(functions_success, functions_result, query_msg=queries[4]) self.aggregates_result = self._handle_results(aggregates_success, aggregates_result, query_msg=queries[5]) - self.indexes_result = self._handle_results(indexes_success, indexes_result, query_msg=queries[7]) - self.views_result = self._handle_results(views_success, views_result, query_msg=queries[8]) + self.indexes_result = self._handle_results(indexes_success, indexes_result, query_msg=queries[6]) + self.views_result = self._handle_results(views_success, views_result, query_msg=queries[7]) + if self._is_not_scylla(): + self.triggers_result = self._handle_results(triggers_success, triggers_result, query_msg=queries[11]) + else: + self.triggers_result = [] # V4-only results # These tables don't exist in some DSE versions reporting 4.X so we can # ignore them if we got an error self.virtual_keyspaces_result = self._handle_results( virtual_ks_success, virtual_ks_result, - expected_failures=(InvalidRequest,), query_msg=queries[9] + expected_failures=(InvalidRequest,), query_msg=queries[8] ) self.virtual_tables_result = self._handle_results( virtual_table_success, virtual_table_result, - expected_failures=(InvalidRequest,), query_msg=queries[10] + expected_failures=(InvalidRequest,), query_msg=queries[9] ) self.virtual_columns_result = self._handle_results( virtual_column_success, virtual_column_result, - expected_failures=(InvalidRequest,), query_msg=queries[11] + expected_failures=(InvalidRequest,), query_msg=queries[10] ) self._aggregate_results() @@ -3445,8 +3454,27 @@ def __init__( self.to_clustering_columns = to_clustering_columns +def get_column_from_system_local(connection, column_name: str, timeout, metadata_request_timeout) -> str: + success, local_result = connection.wait_for_response( + QueryMessage( + query=maybe_add_timeout_to_query( + "SELECT " + column_name + " FROM system.local WHERE key='local'", + metadata_request_timeout), + consistency_level=ConsistencyLevel.ONE) + , timeout=timeout, fail_on_error=False) + if not success or not local_result.parsed_rows: + return "" + local_rows = dict_factory(local_result.column_names, local_result.parsed_rows) + local_row = local_rows[0] + return local_row.get(column_name) + + def get_schema_parser(connection, server_version, dse_version, timeout, metadata_request_timeout, fetch_size=None): - version = Version(server_version) + if server_version is None and dse_version is None: + server_version = get_column_from_system_local(connection, "release_version", timeout, metadata_request_timeout) + dse_version = get_column_from_system_local(connection, "dse_version", timeout, metadata_request_timeout) + + version = Version(server_version or "0") if dse_version: v = Version(dse_version) if v >= Version('6.8.0'): @@ -3497,7 +3525,7 @@ def group_keys_by_replica(session, keyspace, table, keys): :class:`~.NO_VALID_REPLICA` Example usage:: - + >>> result = group_keys_by_replica( ... session, "system", "peers", ... (("127.0.0.1", ), ("127.0.0.2", ))) diff --git a/cassandra/metrics.py b/cassandra/metrics.py index 223b0c7c6e..7ff44107af 100644 --- a/cassandra/metrics.py +++ b/cassandra/metrics.py @@ -12,19 +12,326 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +Driver metrics collection module. + +This module provides metrics collection functionality without external dependencies. +It was originally based on the `scales` library but now uses a self-contained +implementation. +""" + from itertools import chain import logging - -try: - from greplin import scales -except ImportError: - raise ImportError( - "The scales library is required for metrics support: " - "https://pypi.org/project/scales/") +import math +import random +import threading log = logging.getLogger(__name__) +# Global stats registry +_stats_registry = {} +_registry_lock = threading.Lock() + + +def getStats(): + """ + Returns a copy of all registered stats. + """ + with _registry_lock: + return {name: stats._get_stats_dict() for name, stats in _stats_registry.items()} + + +class IntStat: + """ + A thread-safe integer counter statistic. + """ + __slots__ = ('name', '_value', '_lock') + + def __init__(self, name): + self.name = name + self._value = 0 + self._lock = threading.Lock() + + def __iadd__(self, other): + with self._lock: + self._value += other + return self + + def __int__(self): + return self._value + + def __eq__(self, other): + if isinstance(other, IntStat): + return self._value == other._value + return self._value == other + + def __ne__(self, other): + return not self.__eq__(other) + + def __lt__(self, other): + if isinstance(other, IntStat): + return self._value < other._value + return self._value < other + + def __le__(self, other): + if isinstance(other, IntStat): + return self._value <= other._value + return self._value <= other + + def __gt__(self, other): + if isinstance(other, IntStat): + return self._value > other._value + return self._value > other + + def __ge__(self, other): + if isinstance(other, IntStat): + return self._value >= other._value + return self._value >= other + + def __hash__(self): + return hash(self._value) + + def __repr__(self): + return f"IntStat({self.name}={self._value})" + + @property + def value(self): + return self._value + + +class Stat: + """ + A gauge statistic that evaluates a callable on access. + """ + __slots__ = ('name', '_func') + + def __init__(self, name, func): + self.name = name + self._func = func + + @property + def value(self): + return self._func() + + def __repr__(self): + return f"Stat({self.name}={self.value})" + + +class PmfStat: + """ + A probability mass function statistic that tracks timing/size distributions. + + Computes count, min, max, mean, stddev, median and various percentiles. + Uses reservoir sampling to limit memory usage for large sample counts. + """ + __slots__ = ('name', '_values', '_lock', '_count', '_min', '_max', '_sum', '_sum_sq') + + # Maximum number of values to retain for percentile calculations + _MAX_SAMPLES = 10000 + + def __init__(self, name): + self.name = name + self._values = [] + self._lock = threading.Lock() + self._count = 0 + self._min = float('inf') + self._max = float('-inf') + self._sum = 0.0 + self._sum_sq = 0.0 + + def addValue(self, value): + """Record a new value.""" + with self._lock: + self._count += 1 + self._sum += value + self._sum_sq += value * value + + if value < self._min: + self._min = value + if value > self._max: + self._max = value + + # Reservoir sampling for percentiles + if len(self._values) < self._MAX_SAMPLES: + self._values.append(value) + else: + # Replace random element with decreasing probability + idx = random.randint(0, self._count - 1) + if idx < self._MAX_SAMPLES: + self._values[idx] = value + + def _percentile(self, sorted_values, p): + """Calculate the p-th percentile from sorted values.""" + if not sorted_values: + return 0.0 + k = (len(sorted_values) - 1) * p / 100.0 + f = math.floor(k) + c = math.ceil(k) + if f == c: + return sorted_values[int(k)] + return sorted_values[int(f)] * (c - k) + sorted_values[int(c)] * (k - f) + + def _get_stats(self): + """Calculate all statistics.""" + with self._lock: + count = self._count + if count == 0: + return { + 'count': 0, + 'min': 0.0, + 'max': 0.0, + 'mean': 0.0, + 'stddev': 0.0, + 'median': 0.0, + '75percentile': 0.0, + '95percentile': 0.0, + '98percentile': 0.0, + '99percentile': 0.0, + '999percentile': 0.0, + } + + mean = self._sum / count + + # Calculate stddev using Welford's algorithm values + variance = (self._sum_sq / count) - (mean * mean) + stddev = math.sqrt(max(0, variance)) # max to handle floating point errors + + sorted_values = sorted(self._values) + + return { + 'count': count, + 'min': self._min, + 'max': self._max, + 'mean': mean, + 'stddev': stddev, + 'median': self._percentile(sorted_values, 50), + '75percentile': self._percentile(sorted_values, 75), + '95percentile': self._percentile(sorted_values, 95), + '98percentile': self._percentile(sorted_values, 98), + '99percentile': self._percentile(sorted_values, 99), + '999percentile': self._percentile(sorted_values, 99.9), + } + + def __getitem__(self, key): + return self._get_stats()[key] + + def __iter__(self): + return iter(self._get_stats()) + + def keys(self): + return self._get_stats().keys() + + def items(self): + return self._get_stats().items() + + def values(self): + return self._get_stats().values() + + def __repr__(self): + return f"PmfStat({self.name}, count={self._count})" + + +class StatsCollection: + """ + A named collection of statistics. + """ + __slots__ = ('_name', '_stats', '_int_stats', '_pmf_stats', '_gauge_stats') + + def __init__(self, name, *stats): + self._name = name + self._stats = {} + self._int_stats = {} + self._pmf_stats = {} + self._gauge_stats = {} + + for stat in stats: + self._stats[stat.name] = stat + if isinstance(stat, IntStat): + self._int_stats[stat.name] = stat + elif isinstance(stat, PmfStat): + self._pmf_stats[stat.name] = stat + elif isinstance(stat, Stat): + self._gauge_stats[stat.name] = stat + + def __getattr__(self, name): + if name.startswith('_'): + raise AttributeError(name) + try: + stats = object.__getattribute__(self, '_stats') + if name in stats: + return stats[name] + except AttributeError: + pass + raise AttributeError(f"No stat named '{name}'") + + def __setattr__(self, name, value): + if name.startswith('_'): + object.__setattr__(self, name, value) + return + # Allow rebinding stats (e.g., for augmented assignment like stats.errors += 1) + try: + stats = object.__getattribute__(self, '_stats') + if name in stats: + # For augmented assignment, value should be the same IntStat/PmfStat object + # Just verify and allow the rebind + return + except AttributeError: + pass + raise AttributeError(f"Cannot set attribute '{name}' on StatsCollection") + + def _get_stats_dict(self): + """Return dictionary representation of all stats.""" + result = {} + for name, stat in self._int_stats.items(): + result[name] = stat.value + for name, stat in self._pmf_stats.items(): + result[name] = stat._get_stats() + for name, stat in self._gauge_stats.items(): + result[name] = stat.value + return result + + +def collection(name, *stats): + """ + Create a named collection of statistics and register it globally. + """ + coll = StatsCollection(name, *stats) + with _registry_lock: + _stats_registry[name] = coll + return coll + + +def init(obj, path): + """ + Initialize class-level stats on an instance and register in the global registry. + + This allows class-level PmfStat/IntStat descriptors to be used per-instance. + """ + # Get class-level stats and create instance copies + cls = obj.__class__ + instance_stats = {} + + for attr_name in dir(cls): + attr = getattr(cls, attr_name, None) + if isinstance(attr, (PmfStat, IntStat)): + # Create a new instance of the stat for this object + if isinstance(attr, PmfStat): + new_stat = PmfStat(attr.name) + else: + new_stat = IntStat(attr.name) + instance_stats[attr_name] = new_stat + # Set on instance to shadow class attribute + object.__setattr__(obj, attr_name, new_stat) + + # Register under the given path (remove leading /) + reg_name = path.lstrip('/') + if instance_stats: + stats_coll = StatsCollection(reg_name, *instance_stats.values()) + with _registry_lock: + _stats_registry[reg_name] = stats_coll + + class Metrics(object): """ A collection of timers and counters for various performance metrics. @@ -34,7 +341,7 @@ class Metrics(object): request_timer = None """ - A :class:`greplin.scales.PmfStat` timer for requests. This is a dict-like + A :class:`~cassandra.metrics.PmfStat` timer for requests. This is a dict-like object with the following keys: * count - number of requests that have been timed @@ -52,64 +359,64 @@ class Metrics(object): connection_errors = None """ - A :class:`greplin.scales.IntStat` count of the number of times that a + A :class:`~cassandra.metrics.IntStat` count of the number of times that a request to a Cassandra node has failed due to a connection problem. """ write_timeouts = None """ - A :class:`greplin.scales.IntStat` count of write requests that resulted + A :class:`~cassandra.metrics.IntStat` count of write requests that resulted in a timeout. """ read_timeouts = None """ - A :class:`greplin.scales.IntStat` count of read requests that resulted + A :class:`~cassandra.metrics.IntStat` count of read requests that resulted in a timeout. """ unavailables = None """ - A :class:`greplin.scales.IntStat` count of write or read requests that + A :class:`~cassandra.metrics.IntStat` count of write or read requests that failed due to an insufficient number of replicas being alive to meet the requested :class:`.ConsistencyLevel`. """ other_errors = None """ - A :class:`greplin.scales.IntStat` count of all other request failures, + A :class:`~cassandra.metrics.IntStat` count of all other request failures, including failures caused by invalid requests, bootstrapping nodes, overloaded nodes, etc. """ retries = None """ - A :class:`greplin.scales.IntStat` count of the number of times a + A :class:`~cassandra.metrics.IntStat` count of the number of times a request was retried based on the :class:`.RetryPolicy` decision. """ ignores = None """ - A :class:`greplin.scales.IntStat` count of the number of times a + A :class:`~cassandra.metrics.IntStat` count of the number of times a failed request was ignored based on the :class:`.RetryPolicy` decision. """ known_hosts = None """ - A :class:`greplin.scales.IntStat` count of the number of nodes in + A :class:`~cassandra.metrics.IntStat` count of the number of nodes in the cluster that the driver is aware of, regardless of whether any connections are opened to those nodes. """ connected_to = None """ - A :class:`greplin.scales.IntStat` count of the number of nodes that + A :class:`~cassandra.metrics.IntStat` count of the number of nodes that the driver currently has at least one connection open to. """ open_connections = None """ - A :class:`greplin.scales.IntStat` count of the number connections + A :class:`~cassandra.metrics.IntStat` count of the number connections the driver currently has open. """ @@ -120,28 +427,29 @@ def __init__(self, cluster_proxy): self.stats_name = 'cassandra-{0}'.format(str(self._stats_counter)) Metrics._stats_counter += 1 - self.stats = scales.collection(self.stats_name, - scales.PmfStat('request_timer'), - scales.IntStat('connection_errors'), - scales.IntStat('write_timeouts'), - scales.IntStat('read_timeouts'), - scales.IntStat('unavailables'), - scales.IntStat('other_errors'), - scales.IntStat('retries'), - scales.IntStat('ignores'), + self.stats = collection(self.stats_name, + PmfStat('request_timer'), + IntStat('connection_errors'), + IntStat('write_timeouts'), + IntStat('read_timeouts'), + IntStat('unavailables'), + IntStat('other_errors'), + IntStat('retries'), + IntStat('ignores'), # gauges - scales.Stat('known_hosts', + Stat('known_hosts', lambda: len(cluster_proxy.metadata.all_hosts())), - scales.Stat('connected_to', - lambda: len(set(chain.from_iterable(s._pools.keys() for s in cluster_proxy.sessions)))), - scales.Stat('open_connections', - lambda: sum(sum(p.open_count for p in s._pools.values()) for s in cluster_proxy.sessions))) + Stat('connected_to', + lambda: len(set(chain.from_iterable(list(s._pools.keys()) for s in cluster_proxy.sessions)))), + Stat('open_connections', + lambda: sum(sum(p.open_count for p in list(s._pools.values())) for s in cluster_proxy.sessions))) # TODO, to be removed in 4.0 # /cassandra contains the metrics of the first cluster registered - if 'cassandra' not in scales._Stats.stats: - scales._Stats.stats['cassandra'] = scales._Stats.stats[self.stats_name] + with _registry_lock: + if 'cassandra' not in _stats_registry: + _stats_registry['cassandra'] = _stats_registry[self.stats_name] self.request_timer = self.stats.request_timer self.connection_errors = self.stats.connection_errors @@ -180,22 +488,35 @@ def get_stats(self): """ Returns the metrics for the registered cluster instance. """ - return scales.getStats()[self.stats_name] + return getStats()[self.stats_name] def set_stats_name(self, stats_name): """ Set the metrics stats name. - The stats_name is a string used to access the metris through scales: scales.getStats()[] + The stats_name is a string used to access the metrics through getStats(): getStats()[] Default is 'cassandra-'. """ if self.stats_name == stats_name: return - if stats_name in scales._Stats.stats: - raise ValueError('"{0}" already exists in stats.'.format(stats_name)) + with _registry_lock: + if stats_name in _stats_registry: + raise ValueError('"{0}" already exists in stats.'.format(stats_name)) - stats = scales._Stats.stats[self.stats_name] - del scales._Stats.stats[self.stats_name] - self.stats_name = stats_name - scales._Stats.stats[self.stats_name] = stats + stats = _stats_registry[self.stats_name] + del _stats_registry[self.stats_name] + self.stats_name = stats_name + _stats_registry[self.stats_name] = stats + + def shutdown(self): + """ + Remove this metrics instance from the global registry. + Called when the cluster is shutdown to prevent stale references. + """ + with _registry_lock: + if self.stats_name in _stats_registry: + del _stats_registry[self.stats_name] + # Also clean up the legacy 'cassandra' entry if it points to our stats + if _stats_registry.get('cassandra') is self.stats: + del _stats_registry['cassandra'] diff --git a/cassandra/numpy_parser.pyx b/cassandra/numpy_parser.pyx index 030c2c65c7..0ad34f66e2 100644 --- a/cassandra/numpy_parser.pyx +++ b/cassandra/numpy_parser.pyx @@ -181,5 +181,6 @@ def make_native_byteorder(arr): # accordingly (e.g. from '>i8' to ' time.time()) or \ self._session.cluster.shard_aware_options.disable_shardaware_port: return None @@ -756,23 +751,26 @@ def _open_connection_to_missing_shard(self, shard_id): ) old_conn = None with self._lock: - if self.is_shutdown: - conn.close() - return - if conn.features.shard_id in self._connections.keys(): - # Move the current connection to the trash and use the new one from now on - old_conn = self._connections[conn.features.shard_id] - log.debug( - "Replacing overloaded connection (%s) with (%s) for shard %i for host %s", - id(old_conn), - id(conn), - conn.features.shard_id, - self.host - ) - if self._keyspace: - conn.set_keyspace_blocking(self._keyspace) + is_shutdown = self.is_shutdown + if not is_shutdown: + if conn.features.shard_id in self._connections: + # Move the current connection to the trash and use the new one from now on + old_conn = self._connections[conn.features.shard_id] + log.debug( + "Replacing overloaded connection (%s) with (%s) for shard %i for host %s", + id(old_conn), + id(conn), + conn.features.shard_id, + self.host + ) + if self._keyspace: + conn.set_keyspace_blocking(self._keyspace) + self._connections[conn.features.shard_id] = conn + + if is_shutdown: + conn.close() + return - self._connections[conn.features.shard_id] = conn if old_conn is not None: remaining = old_conn.in_flight - len(old_conn.orphaned_request_ids) if remaining == 0: @@ -792,14 +790,15 @@ def _open_connection_to_missing_shard(self, shard_id): remaining, ) with self._lock: - if self.is_shutdown: - old_conn.close() - else: + is_shutdown = self.is_shutdown + if not is_shutdown: self._trash.add(old_conn) + if is_shutdown: + conn.close() num_missing_or_needing_replacement = self.num_missing_or_needing_replacement log.debug( "Connected to %s/%i shards on host %s (%i missing or needs replacement)", - len(self._connections.keys()), + len(self._connections), self.host.sharding_info.shards_count, self.host, num_missing_or_needing_replacement @@ -811,7 +810,7 @@ def _open_connection_to_missing_shard(self, shard_id): len(self._excess_connections) ) self._close_excess_connections() - elif self.host.sharding_info.shards_count == len(self._connections.keys()) and self.num_missing_or_needing_replacement == 0: + elif self.host.sharding_info.shards_count == len(self._connections) and self.num_missing_or_needing_replacement == 0: log.debug( "All shards are already covered, closing newly opened excess connection %s for host %s", id(self), @@ -912,7 +911,7 @@ def get_state(self): @property def num_missing_or_needing_replacement(self): return self.host.sharding_info.shards_count \ - - sum(1 for c in self._connections.values() if not c.orphaned_threshold_reached) + - sum(1 for c in list(self._connections.values()) if not c.orphaned_threshold_reached) @property def open_count(self): @@ -923,362 +922,3 @@ def _excess_connection_limit(self): return self.host.sharding_info.shards_count * self.max_excess_connections_per_shard_multiplier -_MAX_SIMULTANEOUS_CREATION = 1 -_MIN_TRASH_INTERVAL = 10 - - -class HostConnectionPool(object): - """ - Used to pool connections to a host for v1 and v2 native protocol. - """ - - host = None - host_distance = None - - is_shutdown = False - open_count = 0 - _scheduled_for_creation = 0 - _next_trash_allowed_at = 0 - _keyspace = None - - def __init__(self, host, host_distance, session): - self.host = host - self.host_distance = host_distance - - self._session = weakref.proxy(session) - self._lock = RLock() - self._conn_available_condition = Condition() - - log.debug("Initializing new connection pool for host %s", self.host) - core_conns = session.cluster.get_core_connections_per_host(host_distance) - self._connections = [session.cluster.connection_factory(host.endpoint, on_orphaned_stream_released=self.on_orphaned_stream_released) - for i in range(core_conns)] - - self._keyspace = session.keyspace - if self._keyspace: - for conn in self._connections: - conn.set_keyspace_blocking(self._keyspace) - - self._trash = set() - self._next_trash_allowed_at = time.time() - self.open_count = core_conns - log.debug("Finished initializing new connection pool for host %s", self.host) - - def borrow_connection(self, timeout, routing_key=None): - if self.is_shutdown: - raise ConnectionException( - "Pool for %s is shutdown" % (self.host,), self.host) - - conns = self._connections - if not conns: - # handled specially just for simpler code - log.debug("Detected empty pool, opening core conns to %s", self.host) - core_conns = self._session.cluster.get_core_connections_per_host(self.host_distance) - with self._lock: - # we check the length of self._connections again - # along with self._scheduled_for_creation while holding the lock - # in case multiple threads hit this condition at the same time - to_create = core_conns - (len(self._connections) + self._scheduled_for_creation) - for i in range(to_create): - self._scheduled_for_creation += 1 - self._session.submit(self._create_new_connection) - - # in_flight is incremented by wait_for_conn - conn = self._wait_for_conn(timeout) - return conn - else: - # note: it would be nice to push changes to these config settings - # to pools instead of doing a new lookup on every - # borrow_connection() call - max_reqs = self._session.cluster.get_max_requests_per_connection(self.host_distance) - max_conns = self._session.cluster.get_max_connections_per_host(self.host_distance) - - least_busy = min(conns, key=lambda c: c.in_flight) - request_id = None - # to avoid another thread closing this connection while - # trashing it (through the return_connection process), hold - # the connection lock from this point until we've incremented - # its in_flight count - need_to_wait = False - with least_busy.lock: - if least_busy.in_flight < least_busy.max_request_id: - least_busy.in_flight += 1 - request_id = least_busy.get_request_id() - else: - # once we release the lock, wait for another connection - need_to_wait = True - - if need_to_wait: - # wait_for_conn will increment in_flight on the conn - least_busy, request_id = self._wait_for_conn(timeout) - - # if we have too many requests on this connection but we still - # have space to open a new connection against this host, go ahead - # and schedule the creation of a new connection - if least_busy.in_flight >= max_reqs and len(self._connections) < max_conns: - self._maybe_spawn_new_connection() - - return least_busy, request_id - - def _maybe_spawn_new_connection(self): - with self._lock: - if self._scheduled_for_creation >= _MAX_SIMULTANEOUS_CREATION: - return - if self.open_count >= self._session.cluster.get_max_connections_per_host(self.host_distance): - return - self._scheduled_for_creation += 1 - - log.debug("Submitting task for creation of new Connection to %s", self.host) - self._session.submit(self._create_new_connection) - - def _create_new_connection(self): - try: - self._add_conn_if_under_max() - except (ConnectionException, socket.error) as exc: - log.warning("Failed to create new connection to %s: %s", self.host, exc) - except Exception: - log.exception("Unexpectedly failed to create new connection") - finally: - with self._lock: - self._scheduled_for_creation -= 1 - - def _add_conn_if_under_max(self): - max_conns = self._session.cluster.get_max_connections_per_host(self.host_distance) - with self._lock: - if self.is_shutdown: - return True - - if self.open_count >= max_conns: - return True - - self.open_count += 1 - - log.debug("Going to open new connection to host %s", self.host) - try: - conn = self._session.cluster.connection_factory(self.host.endpoint, on_orphaned_stream_released=self.on_orphaned_stream_released) - if self._keyspace: - conn.set_keyspace_blocking(self._session.keyspace) - self._next_trash_allowed_at = time.time() + _MIN_TRASH_INTERVAL - with self._lock: - new_connections = self._connections[:] + [conn] - self._connections = new_connections - log.debug("Added new connection (%s) to pool for host %s, signaling availablility", - id(conn), self.host) - self._signal_available_conn() - return True - except (ConnectionException, socket.error) as exc: - log.warning("Failed to add new connection to pool for host %s: %s", self.host, exc) - with self._lock: - self.open_count -= 1 - if self._session.cluster.signal_connection_failure(self.host, exc, is_host_addition=False): - self.shutdown() - return False - except AuthenticationFailed: - with self._lock: - self.open_count -= 1 - return False - - def _await_available_conn(self, timeout): - with self._conn_available_condition: - self._conn_available_condition.wait(timeout) - - def _signal_available_conn(self): - with self._conn_available_condition: - self._conn_available_condition.notify() - - def _signal_all_available_conn(self): - with self._conn_available_condition: - self._conn_available_condition.notify_all() - - def _wait_for_conn(self, timeout): - start = time.time() - remaining = timeout - - while remaining > 0: - # wait on our condition for the possibility that a connection - # is useable - self._await_available_conn(remaining) - - # self.shutdown() may trigger the above Condition - if self.is_shutdown: - raise ConnectionException("Pool is shutdown") - - conns = self._connections - if conns: - least_busy = min(conns, key=lambda c: c.in_flight) - with least_busy.lock: - if least_busy.in_flight < least_busy.max_request_id: - least_busy.in_flight += 1 - return least_busy, least_busy.get_request_id() - - remaining = timeout - (time.time() - start) - - raise NoConnectionsAvailable() - - def return_connection(self, connection, stream_was_orphaned=False): - with connection.lock: - if not stream_was_orphaned: - connection.in_flight -= 1 - in_flight = connection.in_flight - - if connection.is_defunct or connection.is_closed: - if not connection.signaled_error: - log.debug("Defunct or closed connection (%s) returned to pool, potentially " - "marking host %s as down", id(connection), self.host) - is_down = self._session.cluster.signal_connection_failure( - self.host, connection.last_error, is_host_addition=False) - connection.signaled_error = True - if is_down: - self.shutdown() - else: - self._replace(connection) - else: - if connection in self._trash: - with connection.lock: - if connection.in_flight == 0: - with self._lock: - if connection in self._trash: - self._trash.remove(connection) - log.debug("Closing trashed connection (%s) to %s", id(connection), self.host) - connection.close() - return - - core_conns = self._session.cluster.get_core_connections_per_host(self.host_distance) - min_reqs = self._session.cluster.get_min_requests_per_connection(self.host_distance) - # we can use in_flight here without holding the connection lock - # because the fact that in_flight dipped below the min at some - # point is enough to start the trashing procedure - if len(self._connections) > core_conns and in_flight <= min_reqs and \ - time.time() >= self._next_trash_allowed_at: - self._maybe_trash_connection(connection) - else: - self._signal_available_conn() - - def on_orphaned_stream_released(self): - """ - Called when a response for an orphaned stream (timed out on the client - side) was received. - """ - self._signal_available_conn() - - def _maybe_trash_connection(self, connection): - core_conns = self._session.cluster.get_core_connections_per_host(self.host_distance) - did_trash = False - with self._lock: - if connection not in self._connections: - return - - if self.open_count > core_conns: - did_trash = True - self.open_count -= 1 - new_connections = self._connections[:] - new_connections.remove(connection) - self._connections = new_connections - - with connection.lock: - if connection.in_flight == 0: - log.debug("Skipping trash and closing unused connection (%s) to %s", id(connection), self.host) - connection.close() - - # skip adding it to the trash if we're already closing it - return - - self._trash.add(connection) - - if did_trash: - self._next_trash_allowed_at = time.time() + _MIN_TRASH_INTERVAL - log.debug("Trashed connection (%s) to %s", id(connection), self.host) - - def _replace(self, connection): - should_replace = False - with self._lock: - if connection in self._connections: - new_connections = self._connections[:] - new_connections.remove(connection) - self._connections = new_connections - self.open_count -= 1 - should_replace = True - - if should_replace: - log.debug("Replacing connection (%s) to %s", id(connection), self.host) - connection.close() - self._session.submit(self._retrying_replace) - else: - log.debug("Closing connection (%s) to %s", id(connection), self.host) - connection.close() - - def _retrying_replace(self): - replaced = False - try: - replaced = self._add_conn_if_under_max() - except Exception: - log.exception("Failed replacing connection to %s", self.host) - if not replaced: - log.debug("Failed replacing connection to %s. Retrying.", self.host) - self._session.submit(self._retrying_replace) - - def shutdown(self): - with self._lock: - if self.is_shutdown: - return - else: - self.is_shutdown = True - - self._signal_all_available_conn() - - connections_to_close = [] - with self._lock: - connections_to_close.extend(self._connections) - self.open_count -= len(self._connections) - self._connections.clear() - connections_to_close.extend(self._trash) - self._trash.clear() - - for conn in connections_to_close: - conn.close() - - def ensure_core_connections(self): - if self.is_shutdown: - return - - core_conns = self._session.cluster.get_core_connections_per_host(self.host_distance) - with self._lock: - to_create = core_conns - (len(self._connections) + self._scheduled_for_creation) - for i in range(to_create): - self._scheduled_for_creation += 1 - self._session.submit(self._create_new_connection) - - def _set_keyspace_for_all_conns(self, keyspace, callback): - """ - Asynchronously sets the keyspace for all connections. When all - connections have been set, `callback` will be called with two - arguments: this pool, and a list of any errors that occurred. - """ - remaining_callbacks = set(self._connections) - errors = [] - - if not remaining_callbacks: - callback(self, errors) - return - - def connection_finished_setting_keyspace(conn, error): - self.return_connection(conn) - remaining_callbacks.remove(conn) - if error: - errors.append(error) - - if not remaining_callbacks: - callback(self, errors) - - self._keyspace = keyspace - for conn in self._connections: - conn.set_keyspace_async(keyspace, connection_finished_setting_keyspace) - - def get_connections(self): - return self._connections - - def get_state(self): - in_flights = [c.in_flight for c in self._connections] - orphan_requests = [c.orphaned_request_ids for c in self._connections] - return {'shutdown': self.is_shutdown, 'open_count': self.open_count, \ - 'in_flights': in_flights, 'orphan_requests': orphan_requests} diff --git a/cassandra/protocol.py b/cassandra/protocol.py index 29ae404048..4628c7ee0e 100644 --- a/cassandra/protocol.py +++ b/cassandra/protocol.py @@ -36,7 +36,7 @@ TupleType, lookup_casstype, SimpleDateType, TimeType, ByteType, ShortType, DurationType) from cassandra.marshal import (int32_pack, int32_unpack, uint16_pack, uint16_unpack, - uint8_pack, int8_unpack, uint64_pack, header_pack, + uint8_pack, int8_unpack, uint64_pack, v3_header_pack, uint32_pack, uint32_le_unpack, uint32_le_pack) from cassandra.policies import ColDesc from cassandra import WriteType @@ -553,7 +553,6 @@ def __init__(self, query_params, consistency_level, self.paging_state = paging_state self.timestamp = timestamp self.skip_meta = skip_meta - self.continuous_paging_options = continuous_paging_options self.keyspace = keyspace def _write_query_params(self, f, protocol_version): @@ -563,41 +562,17 @@ def _write_query_params(self, f, protocol_version): flags |= _VALUES_FLAG # also v2+, but we're only setting params internally right now if self.serial_consistency_level: - if protocol_version >= 2: - flags |= _WITH_SERIAL_CONSISTENCY_FLAG - else: - raise UnsupportedOperation( - "Serial consistency levels require the use of protocol version " - "2 or higher. Consider setting Cluster.protocol_version to 2 " - "to support serial consistency levels.") + flags |= _WITH_SERIAL_CONSISTENCY_FLAG if self.fetch_size: - if protocol_version >= 2: - flags |= _PAGE_SIZE_FLAG - else: - raise UnsupportedOperation( - "Automatic query paging may only be used with protocol version " - "2 or higher. Consider setting Cluster.protocol_version to 2.") + flags |= _PAGE_SIZE_FLAG if self.paging_state: - if protocol_version >= 2: - flags |= _WITH_PAGING_STATE_FLAG - else: - raise UnsupportedOperation( - "Automatic query paging may only be used with protocol version " - "2 or higher. Consider setting Cluster.protocol_version to 2.") + flags |= _WITH_PAGING_STATE_FLAG if self.timestamp is not None: flags |= _PROTOCOL_TIMESTAMP_FLAG - if self.continuous_paging_options: - if ProtocolVersion.has_continuous_paging_support(protocol_version): - flags |= _PAGING_OPTIONS_FLAG - else: - raise UnsupportedOperation( - "Continuous paging may only be used with protocol version " - "ProtocolVersion.DSE_V1 or higher. Consider setting Cluster.protocol_version to ProtocolVersion.DSE_V1.") - if self.keyspace is not None: if ProtocolVersion.uses_keyspace_flag(protocol_version): flags |= _WITH_KEYSPACE_FLAG @@ -625,14 +600,10 @@ def _write_query_params(self, f, protocol_version): write_long(f, self.timestamp) if self.keyspace is not None: write_string(f, self.keyspace) - if self.continuous_paging_options: - self._write_paging_options(f, self.continuous_paging_options, protocol_version) def _write_paging_options(self, f, paging_options, protocol_version): write_int(f, paging_options.max_pages) write_int(f, paging_options.max_pages_per_second) - if ProtocolVersion.has_continuous_paging_next_pages(protocol_version): - write_int(f, paging_options.max_queue_size) class QueryMessage(_QueryMessage): @@ -640,9 +611,10 @@ class QueryMessage(_QueryMessage): name = 'QUERY' def __init__(self, query, consistency_level, serial_consistency_level=None, - fetch_size=None, paging_state=None, timestamp=None, continuous_paging_options=None, keyspace=None): + fetch_size=None, paging_state=None, timestamp=None, continuous_paging_options=None, keyspace=None, + query_params=None): self.query = query - super(QueryMessage, self).__init__(None, consistency_level, serial_consistency_level, fetch_size, + super(QueryMessage, self).__init__(query_params, consistency_level, serial_consistency_level, fetch_size, paging_state, timestamp, False, continuous_paging_options, keyspace) def send_body(self, f, protocol_version): @@ -664,22 +636,7 @@ def __init__(self, query_id, query_params, consistency_level, paging_state, timestamp, skip_meta, continuous_paging_options) def _write_query_params(self, f, protocol_version): - if protocol_version == 1: - if self.serial_consistency_level: - raise UnsupportedOperation( - "Serial consistency levels require the use of protocol version " - "2 or higher. Consider setting Cluster.protocol_version to 2 " - "to support serial consistency levels.") - if self.fetch_size or self.paging_state: - raise UnsupportedOperation( - "Automatic query paging may only be used with protocol version " - "2 or higher. Consider setting Cluster.protocol_version to 2.") - write_short(f, len(self.query_params)) - for param in self.query_params: - write_value(f, param) - write_consistency_level(f, self.consistency_level) - else: - super(ExecuteMessage, self)._write_query_params(f, protocol_version) + super(ExecuteMessage, self)._write_query_params(f, protocol_version) def send_body(self, f, protocol_version): write_string(f, self.query_id) @@ -730,11 +687,12 @@ class ResultMessage(_MessageType): bind_metadata = None pk_indexes = None schema_change_event = None + is_lwt = False def __init__(self, kind): self.kind = kind - def recv(self, f, protocol_version, user_type_map, result_metadata, column_encryption_policy): + def recv(self, f, protocol_version, protocol_features, user_type_map, result_metadata, column_encryption_policy): if self.kind == RESULT_KIND_VOID: return elif self.kind == RESULT_KIND_ROWS: @@ -742,7 +700,7 @@ def recv(self, f, protocol_version, user_type_map, result_metadata, column_encry elif self.kind == RESULT_KIND_SET_KEYSPACE: self.new_keyspace = read_string(f) elif self.kind == RESULT_KIND_PREPARED: - self.recv_results_prepared(f, protocol_version, user_type_map) + self.recv_results_prepared(f, protocol_version, protocol_features, user_type_map) elif self.kind == RESULT_KIND_SCHEMA_CHANGE: self.recv_results_schema_change(f, protocol_version) else: @@ -752,7 +710,7 @@ def recv(self, f, protocol_version, user_type_map, result_metadata, column_encry def recv_body(cls, f, protocol_version, protocol_features, user_type_map, result_metadata, column_encryption_policy): kind = read_int(f) msg = cls(kind) - msg.recv(f, protocol_version, user_type_map, result_metadata, column_encryption_policy) + msg.recv(f, protocol_version, protocol_features, user_type_map, result_metadata, column_encryption_policy) return msg def recv_results_rows(self, f, protocol_version, user_type_map, result_metadata, column_encryption_policy): @@ -785,13 +743,13 @@ def decode_row(row): col_md[3].cql_parameterized_type(), str(e))) - def recv_results_prepared(self, f, protocol_version, user_type_map): + def recv_results_prepared(self, f, protocol_version, protocol_features, user_type_map): self.query_id = read_binary_string(f) if ProtocolVersion.uses_prepared_metadata(protocol_version): self.result_metadata_id = read_binary_string(f) else: self.result_metadata_id = None - self.recv_prepared_metadata(f, protocol_version, user_type_map) + self.recv_prepared_metadata(f, protocol_version, protocol_features, user_type_map) def recv_results_metadata(self, f, user_type_map): flags = read_int(f) @@ -829,8 +787,9 @@ def recv_results_metadata(self, f, user_type_map): self.column_metadata = column_metadata - def recv_prepared_metadata(self, f, protocol_version, user_type_map): + def recv_prepared_metadata(self, f, protocol_version, protocol_features, user_type_map): flags = read_int(f) + self.is_lwt = protocol_features.lwt_info.get_lwt_flag(flags) if protocol_features.lwt_info is not None else False colcount = read_int(f) pk_indexes = None if protocol_version >= 4: @@ -853,8 +812,7 @@ def recv_prepared_metadata(self, f, protocol_version, user_type_map): coltype = self.read_type(f, user_type_map) bind_metadata.append(ColumnMetadata(colksname, colcfname, colname, coltype)) - if protocol_version >= 2: - self.recv_results_metadata(f, user_type_map) + self.recv_results_metadata(f, user_type_map) self.bind_metadata = bind_metadata self.pk_indexes = pk_indexes @@ -969,39 +927,38 @@ def send_body(self, f, protocol_version): write_value(f, param) write_consistency_level(f, self.consistency_level) - if protocol_version >= 3: - flags = 0 - if self.serial_consistency_level: - flags |= _WITH_SERIAL_CONSISTENCY_FLAG - if self.timestamp is not None: - flags |= _PROTOCOL_TIMESTAMP_FLAG - if self.keyspace: - if ProtocolVersion.uses_keyspace_flag(protocol_version): - flags |= _WITH_KEYSPACE_FLAG - else: - raise UnsupportedOperation( - "Keyspaces may only be set on queries with protocol version " - "5 or higher. Consider setting Cluster.protocol_version to 5.") - - if ProtocolVersion.uses_int_query_flags(protocol_version): - write_int(f, flags) + flags = 0 + if self.serial_consistency_level: + flags |= _WITH_SERIAL_CONSISTENCY_FLAG + if self.timestamp is not None: + flags |= _PROTOCOL_TIMESTAMP_FLAG + if self.keyspace: + if ProtocolVersion.uses_keyspace_flag(protocol_version): + flags |= _WITH_KEYSPACE_FLAG else: - write_byte(f, flags) + raise UnsupportedOperation( + "Keyspaces may only be set on queries with protocol version " + "5 or higher. Consider setting Cluster.protocol_version to 5.") + if ProtocolVersion.uses_int_query_flags(protocol_version): + write_int(f, flags) + else: + write_byte(f, flags) - if self.serial_consistency_level: - write_consistency_level(f, self.serial_consistency_level) - if self.timestamp is not None: - write_long(f, self.timestamp) + if self.serial_consistency_level: + write_consistency_level(f, self.serial_consistency_level) + if self.timestamp is not None: + write_long(f, self.timestamp) - if ProtocolVersion.uses_keyspace_flag(protocol_version): - if self.keyspace is not None: - write_string(f, self.keyspace) + if ProtocolVersion.uses_keyspace_flag(protocol_version): + if self.keyspace is not None: + write_string(f, self.keyspace) known_event_types = frozenset(( 'TOPOLOGY_CHANGE', 'STATUS_CHANGE', - 'SCHEMA_CHANGE' + 'SCHEMA_CHANGE', + 'CLIENT_ROUTES_CHANGE' )) @@ -1032,6 +989,14 @@ def recv_body(cls, f, protocol_version, *args): return cls(event_type=event_type, event_args=read_method(f, protocol_version)) raise NotSupportedError('Unknown event type %r' % event_type) + @classmethod + def recv_client_routes_change(cls, f, protocol_version): + # "UPDATE_NODES" + change_type = read_string(f) + connection_ids = read_stringlist(f) + host_ids = read_stringlist(f) + return dict(change_type=change_type, connection_ids=connection_ids, host_ids=host_ids) + @classmethod def recv_topology_change(cls, f, protocol_version): # "NEW_NODE" or "REMOVED_NODE" @@ -1050,25 +1015,17 @@ def recv_status_change(cls, f, protocol_version): def recv_schema_change(cls, f, protocol_version): # "CREATED", "DROPPED", or "UPDATED" change_type = read_string(f) - if protocol_version >= 3: - target = read_string(f) - keyspace = read_string(f) - event = {'target_type': target, 'change_type': change_type, 'keyspace': keyspace} - if target != SchemaTargetType.KEYSPACE: - target_name = read_string(f) - if target == SchemaTargetType.FUNCTION: - event['function'] = UserFunctionDescriptor(target_name, [read_string(f) for _ in range(read_short(f))]) - elif target == SchemaTargetType.AGGREGATE: - event['aggregate'] = UserAggregateDescriptor(target_name, [read_string(f) for _ in range(read_short(f))]) - else: - event[target.lower()] = target_name - else: - keyspace = read_string(f) - table = read_string(f) - if table: - event = {'target_type': SchemaTargetType.TABLE, 'change_type': change_type, 'keyspace': keyspace, 'table': table} + target = read_string(f) + keyspace = read_string(f) + event = {'target_type': target, 'change_type': change_type, 'keyspace': keyspace} + if target != SchemaTargetType.KEYSPACE: + target_name = read_string(f) + if target == SchemaTargetType.FUNCTION: + event['function'] = UserFunctionDescriptor(target_name, [read_string(f) for _ in range(read_short(f))]) + elif target == SchemaTargetType.AGGREGATE: + event['aggregate'] = UserAggregateDescriptor(target_name, [read_string(f) for _ in range(read_short(f))]) else: - event = {'target_type': SchemaTargetType.KEYSPACE, 'change_type': change_type, 'keyspace': keyspace} + event[target.lower()] = target_name return event @@ -1092,12 +1049,9 @@ def send_body(self, f, protocol_version): if self.op_type == ReviseRequestMessage.RevisionType.PAGING_BACKPRESSURE: if self.next_pages <= 0: raise UnsupportedOperation("Continuous paging backpressure requires next_pages > 0") - elif not ProtocolVersion.has_continuous_paging_next_pages(protocol_version): - raise UnsupportedOperation( - "Continuous paging backpressure may only be used with protocol version " - "ProtocolVersion.DSE_V2 or higher. Consider setting Cluster.protocol_version to ProtocolVersion.DSE_V2.") else: - write_int(f, self.next_pages) + raise UnsupportedOperation( + "Continuous paging backpressure is not supported.") class _ProtocolHandler(object): @@ -1132,20 +1086,10 @@ def encode_message(cls, msg, stream_id, protocol_version, compressor, allow_beta :param compressor: optional compression function to be used on the body """ flags = 0 - body = io.BytesIO() if msg.custom_payload: if protocol_version < 4: raise UnsupportedOperation("Custom key/value payloads can only be used with protocol version 4 or higher") flags |= CUSTOM_PAYLOAD_FLAG - write_bytesmap(body, msg.custom_payload) - msg.send_body(body, protocol_version) - body = body.getvalue() - - # With checksumming, the compression is done at the segment frame encoding - if (not ProtocolVersion.has_checksumming_support(protocol_version) - and compressor and len(body) > 0): - body = compressor(body) - flags |= COMPRESSED_FLAG if msg.tracing: flags |= TRACING_FLAG @@ -1154,9 +1098,31 @@ def encode_message(cls, msg, stream_id, protocol_version, compressor, allow_beta flags |= USE_BETA_FLAG buff = io.BytesIO() - cls._write_header(buff, protocol_version, flags, stream_id, msg.opcode, len(body)) - buff.write(body) + buff.seek(9) + + # With checksumming, the compression is done at the segment frame encoding + if (compressor and not ProtocolVersion.has_checksumming_support(protocol_version)): + body = io.BytesIO() + if msg.custom_payload: + write_bytesmap(body, msg.custom_payload) + msg.send_body(body, protocol_version) + body = body.getvalue() + + if len(body) > 0: + body = compressor(body) + flags |= COMPRESSED_FLAG + + buff.write(body) + length = len(body) + else: + if msg.custom_payload: + write_bytesmap(buff, msg.custom_payload) + msg.send_body(buff, protocol_version) + + length = buff.tell() - 9 + buff.seek(0) + cls._write_header(buff, protocol_version, flags, stream_id, msg.opcode, length) return buff.getvalue() @staticmethod @@ -1164,8 +1130,7 @@ def _write_header(f, version, flags, stream_id, opcode, length): """ Write a CQL protocol frame header. """ - pack = v3_header_pack if version >= 3 else header_pack - f.write(pack(version, flags, stream_id, opcode)) + f.write(v3_header_pack(version, flags, stream_id, opcode)) write_int(f, length) @classmethod diff --git a/cassandra/protocol_features.py b/cassandra/protocol_features.py index 4eb7019f84..877998be7d 100644 --- a/cassandra/protocol_features.py +++ b/cassandra/protocol_features.py @@ -1,10 +1,13 @@ import logging from cassandra.shard_info import _ShardingInfo +from cassandra.lwt_info import _LwtInfo log = logging.getLogger(__name__) +LWT_ADD_METADATA_MARK = "SCYLLA_LWT_ADD_METADATA_MARK" +LWT_OPTIMIZATION_META_BIT_MASK = "LWT_OPTIMIZATION_META_BIT_MASK" RATE_LIMIT_ERROR_EXTENSION = "SCYLLA_RATE_LIMIT_ERROR" TABLETS_ROUTING_V1 = "TABLETS_ROUTING_V1" @@ -13,19 +16,22 @@ class ProtocolFeatures(object): shard_id = 0 sharding_info = None tablets_routing_v1 = False + lwt_info = None - def __init__(self, rate_limit_error=None, shard_id=0, sharding_info=None, tablets_routing_v1=False): + def __init__(self, rate_limit_error=None, shard_id=0, sharding_info=None, tablets_routing_v1=False, lwt_info=None): self.rate_limit_error = rate_limit_error self.shard_id = shard_id self.sharding_info = sharding_info self.tablets_routing_v1 = tablets_routing_v1 + self.lwt_info = lwt_info @staticmethod def parse_from_supported(supported): rate_limit_error = ProtocolFeatures.maybe_parse_rate_limit_error(supported) shard_id, sharding_info = ProtocolFeatures.parse_sharding_info(supported) tablets_routing_v1 = ProtocolFeatures.parse_tablets_info(supported) - return ProtocolFeatures(rate_limit_error, shard_id, sharding_info, tablets_routing_v1) + lwt_info = ProtocolFeatures.parse_lwt_info(supported) + return ProtocolFeatures(rate_limit_error, shard_id, sharding_info, tablets_routing_v1, lwt_info) @staticmethod def maybe_parse_rate_limit_error(supported): @@ -49,6 +55,8 @@ def add_startup_options(self, options): options[RATE_LIMIT_ERROR_EXTENSION] = "" if self.tablets_routing_v1: options[TABLETS_ROUTING_V1] = "" + if self.lwt_info is not None: + options[LWT_ADD_METADATA_MARK] = str(self.lwt_info.lwt_meta_bit_mask) @staticmethod def parse_sharding_info(options): @@ -72,3 +80,18 @@ def parse_sharding_info(options): @staticmethod def parse_tablets_info(options): return TABLETS_ROUTING_V1 in options + + @staticmethod + def parse_lwt_info(options): + value_list = options.get(LWT_ADD_METADATA_MARK, [None]) + for value in value_list: + if value is None or not value.startswith(LWT_OPTIMIZATION_META_BIT_MASK + "="): + continue + try: + lwt_meta_bit_mask = int(value[len(LWT_OPTIMIZATION_META_BIT_MASK + "="):]) + return _LwtInfo(lwt_meta_bit_mask) + except Exception as e: + log.exception(f"Error while parsing {LWT_ADD_METADATA_MARK}: {e}") + return None + + return None diff --git a/cassandra/query.py b/cassandra/query.py index f3922849ab..6c6878fdb4 100644 --- a/cassandra/query.py +++ b/cassandra/query.py @@ -345,6 +345,9 @@ def _set_serial_consistency_level(self, serial_consistency_level): def _del_serial_consistency_level(self): self._serial_consistency_level = None + def is_lwt(self): + return False + serial_consistency_level = property( _get_serial_consistency_level, _set_serial_consistency_level, @@ -454,10 +457,11 @@ class PreparedStatement(object): routing_key_indexes = None _routing_key_index_set = None serial_consistency_level = None # TODO never used? + _is_lwt = False def __init__(self, column_metadata, query_id, routing_key_indexes, query, keyspace, protocol_version, result_metadata, result_metadata_id, - column_encryption_policy=None): + is_lwt=False, column_encryption_policy=None): self.column_metadata = column_metadata self.query_id = query_id self.routing_key_indexes = routing_key_indexes @@ -468,15 +472,16 @@ def __init__(self, column_metadata, query_id, routing_key_indexes, query, self.result_metadata_id = result_metadata_id self.column_encryption_policy = column_encryption_policy self.is_idempotent = False + self._is_lwt = is_lwt @classmethod def from_message(cls, query_id, column_metadata, pk_indexes, cluster_metadata, query, prepared_keyspace, protocol_version, result_metadata, - result_metadata_id, column_encryption_policy=None): + result_metadata_id, is_lwt, column_encryption_policy=None): if not column_metadata: return PreparedStatement(column_metadata, query_id, None, query, prepared_keyspace, protocol_version, result_metadata, - result_metadata_id, column_encryption_policy) + result_metadata_id, is_lwt, column_encryption_policy) if pk_indexes: routing_key_indexes = pk_indexes @@ -502,7 +507,7 @@ def from_message(cls, query_id, column_metadata, pk_indexes, cluster_metadata, return PreparedStatement(column_metadata, query_id, routing_key_indexes, query, prepared_keyspace, protocol_version, result_metadata, - result_metadata_id, column_encryption_policy) + result_metadata_id, is_lwt, column_encryption_policy) def bind(self, values): """ @@ -517,6 +522,9 @@ def is_routing_key_index(self, i): self._routing_key_index_set = set(self.routing_key_indexes) if self.routing_key_indexes else set() return i in self._routing_key_index_set + def is_lwt(self): + return self._is_lwt + def __str__(self): consistency = ConsistencyLevel.value_to_name.get(self.consistency_level, 'Not Set') return (u'' % @@ -682,6 +690,9 @@ def routing_key(self): return self._routing_key + def is_lwt(self): + return self.prepared_statement.is_lwt() + def __str__(self): consistency = ConsistencyLevel.value_to_name.get(self.consistency_level, 'Not Set') return (u'' % @@ -750,6 +761,7 @@ class BatchStatement(Statement): _statements_and_parameters = None _session = None + _is_lwt = False def __init__(self, batch_type=BatchType.LOGGED, retry_policy=None, consistency_level=None, serial_consistency_level=None, @@ -834,6 +846,8 @@ def add(self, statement, parameters=None): query_id = statement.query_id bound_statement = statement.bind(() if parameters is None else parameters) self._update_state(bound_statement) + if statement.is_lwt(): + self._is_lwt = True self._add_statement_and_params(True, query_id, bound_statement.values) elif isinstance(statement, BoundStatement): if parameters: @@ -841,6 +855,8 @@ def add(self, statement, parameters=None): "Parameters cannot be passed with a BoundStatement " "to BatchStatement.add()") self._update_state(statement) + if statement.is_lwt(): + self._is_lwt = True self._add_statement_and_params(True, statement.prepared_statement.query_id, statement.values) else: # it must be a SimpleStatement @@ -849,6 +865,8 @@ def add(self, statement, parameters=None): encoder = Encoder() if self._session is None else self._session.encoder query_string = bind_params(query_string, parameters, encoder) self._update_state(statement) + if statement.is_lwt(): + self._is_lwt = True self._add_statement_and_params(False, query_string, ()) return self @@ -882,6 +900,9 @@ def _update_state(self, statement): self._maybe_set_routing_attributes(statement) self._update_custom_payload(statement) + def is_lwt(self): + return self._is_lwt + def __len__(self): return len(self._statements_and_parameters) diff --git a/cassandra/scylla/cloud.py b/cassandra/scylla/cloud.py deleted file mode 100644 index c3298b199a..0000000000 --- a/cassandra/scylla/cloud.py +++ /dev/null @@ -1,139 +0,0 @@ -# Copyright ScyllaDB, Inc. -# -# 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 -# -# http://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 sys -import ssl -import tempfile -import base64 -from ssl import SSLContext -from contextlib import contextmanager -from itertools import islice - -import yaml - -from cassandra.connection import SniEndPointFactory -from cassandra.auth import AuthProvider, PlainTextAuthProvider - - -@contextmanager -def file_or_memory(path=None, data=None): - # since we can't read keys/cert from memory yet - # see https://github.com/python/cpython/pull/2449 which isn't accepted and PEP-543 that was withdrawn - # so we use temporary file to load the key - if data: - with tempfile.NamedTemporaryFile(mode="wb") as f: - d = base64.b64decode(data) - f.write(d) - if not d.endswith(b"\n"): - f.write(b"\n") - - f.flush() - yield f.name - - if path: - yield path - - -def nth(iterable, n, default=None): - "Returns the nth item or a default value" - return next(islice(iterable, n, None), default) - - -class CloudConfiguration: - endpoint_factory: SniEndPointFactory - contact_points: list - auth_provider: AuthProvider = None - ssl_options: dict - ssl_context: SSLContext - skip_tls_verify: bool - - def __init__(self, configuration_file, pyopenssl=False, endpoint_factory=None): - cloud_config = yaml.safe_load(open(configuration_file)) - - self.current_context = cloud_config['contexts'][cloud_config['currentContext']] - self.data_centers = cloud_config['datacenters'] - self.current_data_center = self.data_centers[self.current_context['datacenterName']] - self.auth_info = cloud_config['authInfos'][self.current_context['authInfoName']] - self.ssl_options = {} - self.skip_tls_verify = self.current_data_center.get('insecureSkipTlsVerify', False) - self.ssl_context = self.create_pyopenssl_context() if pyopenssl else self.create_ssl_context() - - proxy_address, port, node_domain = self.get_server(self.current_data_center) - - if not endpoint_factory: - endpoint_factory = SniEndPointFactory(proxy_address, port=int(port), node_domain=node_domain) - else: - assert isinstance(endpoint_factory, SniEndPointFactory) - self.endpoint_factory = endpoint_factory - - username, password = self.auth_info.get('username'), self.auth_info.get('password') - if username and password: - self.auth_provider = PlainTextAuthProvider(username, password) - - @property - def contact_points(self): - _contact_points = [] - for data_center in self.data_centers.values(): - _, _, node_domain = self.get_server(data_center) - _contact_points.append(self.endpoint_factory.create_from_sni(node_domain)) - return _contact_points - - def get_server(self, data_center): - address = data_center.get('server') - address = address.split(":") - port = nth(address, 1, default=9142) - address = nth(address, 0) - node_domain = data_center.get('nodeDomain') - assert address and port and node_domain, "server or nodeDomain are missing" - return address, port, node_domain - - def create_ssl_context(self): - ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT) - ssl_context.verify_mode = ssl.CERT_NONE if self.skip_tls_verify else ssl.CERT_REQUIRED - for data_center in self.data_centers.values(): - with file_or_memory(path=data_center.get('certificateAuthorityPath'), - data=data_center.get('certificateAuthorityData')) as cafile: - ssl_context.load_verify_locations(cadata=open(cafile).read()) - with file_or_memory(path=self.auth_info.get('clientCertificatePath'), - data=self.auth_info.get('clientCertificateData')) as certfile, \ - file_or_memory(path=self.auth_info.get('clientKeyPath'), data=self.auth_info.get('clientKeyData')) as keyfile: - ssl_context.load_cert_chain(keyfile=keyfile, - certfile=certfile) - - return ssl_context - - def create_pyopenssl_context(self): - try: - from OpenSSL import SSL - except ImportError as e: - raise ImportError( - "PyOpenSSL must be installed to connect to scylla-cloud with the Eventlet or Twisted event loops") \ - .with_traceback(e.__traceback__) - ssl_context = SSL.Context(SSL.TLS_METHOD) - ssl_context.set_verify(SSL.VERIFY_PEER, callback=lambda _1, _2, _3, _4, ok: True if self.skip_tls_verify else ok) - for data_center in self.data_centers.values(): - with file_or_memory(path=data_center.get('certificateAuthorityPath'), - data=data_center.get('certificateAuthorityData')) as cafile: - ssl_context.load_verify_locations(cafile) - with file_or_memory(path=self.auth_info.get('clientCertificatePath'), - data=self.auth_info.get('clientCertificateData')) as certfile, \ - file_or_memory(path=self.auth_info.get('clientKeyPath'), data=self.auth_info.get('clientKeyData')) as keyfile: - ssl_context.use_privatekey_file(keyfile) - ssl_context.use_certificate_file(certfile) - - return ssl_context - - @classmethod - def create(cls, configuration_file, pyopenssl=False, endpoint_factory=None): - return cls(configuration_file, pyopenssl=pyopenssl, endpoint_factory=endpoint_factory) diff --git a/cassandra/tablets.py b/cassandra/tablets.py index 457ee93ca4..96e61a50c2 100644 --- a/cassandra/tablets.py +++ b/cassandra/tablets.py @@ -1,7 +1,13 @@ +from bisect import bisect_left +from operator import attrgetter from threading import Lock from typing import Optional from uuid import UUID +# C-accelerated attrgetter avoids per-call lambda allocation overhead +_get_first_token = attrgetter("first_token") +_get_last_token = attrgetter("last_token") + class Tablet(object): """ @@ -49,12 +55,15 @@ def __init__(self, tablets): self._tablets = tablets self._lock = Lock() + def table_has_tablets(self, keyspace, table) -> bool: + return bool(self._tablets.get((keyspace, table), [])) + def get_tablet_for_key(self, keyspace, table, t): tablet = self._tablets.get((keyspace, table), []) if not tablet: return None - id = bisect_left(tablet, t.value, key=lambda tablet: tablet.last_token) + id = bisect_left(tablet, t.value, key=_get_last_token) if id < len(tablet) and t.value > tablet[id].first_token: return tablet[id] return None @@ -91,12 +100,12 @@ def add_tablet(self, keyspace, table, tablet): tablets_for_table = self._tablets.setdefault((keyspace, table), []) # find first overlapping range - start = bisect_left(tablets_for_table, tablet.first_token, key=lambda t: t.first_token) + start = bisect_left(tablets_for_table, tablet.first_token, key=_get_first_token) if start > 0 and tablets_for_table[start - 1].last_token > tablet.first_token: start = start - 1 # find last overlapping range - end = bisect_left(tablets_for_table, tablet.last_token, key=lambda t: t.last_token) + end = bisect_left(tablets_for_table, tablet.last_token, key=_get_last_token) if end < len(tablets_for_table) and tablets_for_table[end].first_token >= tablet.last_token: end = end - 1 @@ -105,39 +114,3 @@ def add_tablet(self, keyspace, table, tablet): tablets_for_table.insert(start, tablet) - -# bisect.bisect_left implementation from Python 3.11, needed untill support for -# Python < 3.10 is dropped, it is needed to use `key` to extract last_token from -# Tablet list - better solution performance-wise than materialize list of last_tokens -def bisect_left(a, x, lo=0, hi=None, *, key=None): - """Return the index where to insert item x in list a, assuming a is sorted. - - The return value i is such that all e in a[:i] have e < x, and all e in - a[i:] have e >= x. So if x already appears in the list, a.insert(i, x) will - insert just before the leftmost x already there. - - Optional args lo (default 0) and hi (default len(a)) bound the - slice of a to be searched. - """ - - if lo < 0: - raise ValueError('lo must be non-negative') - if hi is None: - hi = len(a) - # Note, the comparison uses "<" to match the - # __lt__() logic in list.sort() and in heapq. - if key is None: - while lo < hi: - mid = (lo + hi) // 2 - if a[mid] < x: - lo = mid + 1 - else: - hi = mid - return - while lo < hi: - mid = (lo + hi) // 2 - if key(a[mid]) < x: - lo = mid + 1 - else: - hi = mid - return lo diff --git a/cassandra/util.py b/cassandra/util.py index 12886d05ab..593c264033 100644 --- a/cassandra/util.py +++ b/cassandra/util.py @@ -62,6 +62,16 @@ def datetime_from_timestamp(timestamp): return dt +def datetime_from_ms_timestamp(timestamp_ms): + """ + Creates a timezone-agnostic datetime from a timestamp in milliseconds, + using integer arithmetic to preserve precision for large values. + + :param timestamp_ms: a unix timestamp, in milliseconds (integer) + """ + return DATETIME_EPOC + datetime.timedelta(milliseconds=timestamp_ms) + + def utc_datetime_from_ms_timestamp(timestamp): """ Creates a UTC datetime from a timestamp in milliseconds. See diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000000..733bc65597 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +# Track uv.lock for reproducible docs builds +!uv.lock diff --git a/docs/Makefile b/docs/Makefile index b1c54c8199..09512be470 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,8 +1,9 @@ # Global variables # You can set these variables from the command line. -POETRY = poetry +SHELL = bash +UV = uv SPHINXOPTS = -j auto -SPHINXBUILD = $(POETRY) run sphinx-build +SPHINXBUILD = $(UV) run --frozen sphinx-build PAPER = BUILDDIR = _build SOURCEDIR = . @@ -17,18 +18,13 @@ TESTSPHINXOPTS = $(ALLSPHINXOPTS) -W --keep-going all: dirhtml # Setup commands -.PHONY: setupenv -setupenv: - pip install -q poetry - sudo apt-get install gcc python3-dev libev4 libev-dev - -.PHONY: setup -setup: - $(POETRY) install +#.PHONY: setupenv +#setupenv: +# uv pip install -r <(uv pip compile pyproject.toml) .PHONY: update update: - $(POETRY) update + $(UV) lock --upgrade # Clean commands .PHONY: pristine @@ -41,58 +37,58 @@ clean: # Generate output commands .PHONY: dirhtml -dirhtml: setup +dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml -singlehtml: setup +singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: epub -epub: setup +epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: epub3 -epub3: setup +epub3: $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 @echo @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." .PHONY: multiversion -multiversion: setup - $(POETRY) run sphinx-multiversion $(SOURCEDIR) $(BUILDDIR)/dirhtml +multiversion: + $(UV) run --frozen sphinx-multiversion $(SOURCEDIR) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: redirects -redirects: setup - $(POETRY) run redirects-cli fromfile --yaml-file _utils/redirects.yaml --output-dir $(BUILDDIR)/dirhtml +redirects: + $(UV) run --frozen redirects-cli fromfile --yaml-file _utils/redirects.yaml --output-dir $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." # Preview commands .PHONY: preview -preview: setup - $(POETRY) run sphinx-autobuild -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml --port 5500 +preview: + $(UV) run --frozen sphinx-autobuild -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml --port 5500 .PHONY: multiversionpreview multiversionpreview: multiversion - $(POETRY) run python -m http.server 5500 --directory $(BUILDDIR)/dirhtml + $(UV) run --frozen python -m http.server 5500 --directory $(BUILDDIR)/dirhtml # Test commands .PHONY: test -test: setup +test: $(SPHINXBUILD) -b dirhtml $(TESTSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: linkcheck -linkcheck: setup +linkcheck: $(SPHINXBUILD) -b linkcheck $(SOURCEDIR) $(BUILDDIR)/linkcheck diff --git a/docs/api/cassandra.rst b/docs/api/cassandra.rst index d46aae56cb..53789b9582 100644 --- a/docs/api/cassandra.rst +++ b/docs/api/cassandra.rst @@ -1,5 +1,7 @@ -:mod:`cassandra` - Exceptions and Enums -======================================= +cassandra +========= + +Exceptions and Enums .. module:: cassandra diff --git a/docs/api/cassandra/auth.rst b/docs/api/cassandra/auth.rst index 58c964cf89..91bb4e9139 100644 --- a/docs/api/cassandra/auth.rst +++ b/docs/api/cassandra/auth.rst @@ -1,5 +1,7 @@ -``cassandra.auth`` - Authentication -=================================== +cassandra.auth +============== + +Authentication .. module:: cassandra.auth diff --git a/docs/api/cassandra/cluster.rst b/docs/api/cassandra/cluster.rst index a9a9d378a4..44b7b63f67 100644 --- a/docs/api/cassandra/cluster.rst +++ b/docs/api/cassandra/cluster.rst @@ -1,5 +1,7 @@ -``cassandra.cluster`` - Clusters and Sessions -============================================= +cassandra.cluster +================= + +Clusters and Sessions .. module:: cassandra.cluster @@ -46,6 +48,8 @@ .. autoattribute:: control_connection_timeout + .. autoattribute:: allow_control_connection_query_fallback + .. autoattribute:: idle_heartbeat_interval .. autoattribute:: idle_heartbeat_timeout @@ -86,22 +90,6 @@ .. automethod:: add_execution_profile - .. automethod:: set_max_requests_per_connection - - .. automethod:: get_max_requests_per_connection - - .. automethod:: set_min_requests_per_connection - - .. automethod:: get_min_requests_per_connection - - .. automethod:: get_core_connections_per_host - - .. automethod:: set_core_connections_per_host - - .. automethod:: get_max_connections_per_host - - .. automethod:: set_max_connections_per_host - .. automethod:: get_control_connection_host .. automethod:: refresh_schema_metadata @@ -120,6 +108,9 @@ .. automethod:: set_meta_refresh_enabled +.. autoclass:: ControlConnectionQueryFallback + :members: + .. autoclass:: ExecutionProfile (load_balancing_policy=, retry_policy=None, consistency_level=ConsistencyLevel.LOCAL_ONE, serial_consistency_level=None, request_timeout=10.0, row_factory=, speculative_execution_policy=None) :members: :exclude-members: consistency_level @@ -183,6 +174,8 @@ .. automethod:: set_keyspace(keyspace) + .. automethod:: wait_for_schema_agreement + .. automethod:: get_execution_profile .. automethod:: execution_profile_clone_update diff --git a/docs/api/cassandra/concurrent.rst b/docs/api/cassandra/concurrent.rst index f4bab6f048..8f403a3e3c 100644 --- a/docs/api/cassandra/concurrent.rst +++ b/docs/api/cassandra/concurrent.rst @@ -1,5 +1,7 @@ -``cassandra.concurrent`` - Utilities for Concurrent Statement Execution -======================================================================= +cassandra.concurrent +==================== + +Utilities for Concurrent Statement Execution .. module:: cassandra.concurrent diff --git a/docs/api/cassandra/connection.rst b/docs/api/cassandra/connection.rst index 32cca590c0..f9ec4eef61 100644 --- a/docs/api/cassandra/connection.rst +++ b/docs/api/cassandra/connection.rst @@ -1,5 +1,7 @@ -``cassandra.connection`` - Low Level Connection Info -==================================================== +cassandra.connection +==================== + +Low Level Connection Info .. module:: cassandra.connection diff --git a/docs/api/cassandra/cqlengine/columns.rst b/docs/api/cassandra/cqlengine/columns.rst index d44be8adb8..35a47f0ef4 100644 --- a/docs/api/cassandra/cqlengine/columns.rst +++ b/docs/api/cassandra/cqlengine/columns.rst @@ -1,5 +1,7 @@ -``cassandra.cqlengine.columns`` - Column types for object mapping models -======================================================================== +cassandra.cqlengine.columns +=========================== + +Column types for object mapping models .. module:: cassandra.cqlengine.columns diff --git a/docs/api/cassandra/cqlengine/connection.rst b/docs/api/cassandra/cqlengine/connection.rst index 0f584fcca2..6270b75c4e 100644 --- a/docs/api/cassandra/cqlengine/connection.rst +++ b/docs/api/cassandra/cqlengine/connection.rst @@ -1,5 +1,7 @@ -``cassandra.cqlengine.connection`` - Connection management for cqlengine -======================================================================== +cassandra.cqlengine.connection +============================== + +Connection management for cqlengine .. module:: cassandra.cqlengine.connection diff --git a/docs/api/cassandra/cqlengine/management.rst b/docs/api/cassandra/cqlengine/management.rst index fb483abc81..62709019da 100644 --- a/docs/api/cassandra/cqlengine/management.rst +++ b/docs/api/cassandra/cqlengine/management.rst @@ -1,5 +1,7 @@ -``cassandra.cqlengine.management`` - Schema management for cqlengine -======================================================================== +cassandra.cqlengine.management +============================== + +Schema management for cqlengine .. module:: cassandra.cqlengine.management diff --git a/docs/api/cassandra/cqlengine/models.rst b/docs/api/cassandra/cqlengine/models.rst index 44a015a9f4..9905926c4e 100644 --- a/docs/api/cassandra/cqlengine/models.rst +++ b/docs/api/cassandra/cqlengine/models.rst @@ -1,5 +1,7 @@ -``cassandra.cqlengine.models`` - Table models for object mapping -================================================================ +cassandra.cqlengine.models +========================== + +Table models for object mapping .. module:: cassandra.cqlengine.models diff --git a/docs/api/cassandra/cqlengine/query.rst b/docs/api/cassandra/cqlengine/query.rst index ce8f764b6b..0d8c52164f 100644 --- a/docs/api/cassandra/cqlengine/query.rst +++ b/docs/api/cassandra/cqlengine/query.rst @@ -1,5 +1,7 @@ -``cassandra.cqlengine.query`` - Query and filter model objects -================================================================= +cassandra.cqlengine.query +========================= + +Query and filter model objects .. module:: cassandra.cqlengine.query diff --git a/docs/api/cassandra/cqlengine/usertype.rst b/docs/api/cassandra/cqlengine/usertype.rst index ebed187da9..219de8c300 100644 --- a/docs/api/cassandra/cqlengine/usertype.rst +++ b/docs/api/cassandra/cqlengine/usertype.rst @@ -1,5 +1,7 @@ -``cassandra.cqlengine.usertype`` - Model classes for User Defined Types -======================================================================= +cassandra.cqlengine.usertype +============================ + +Model classes for User Defined Types .. module:: cassandra.cqlengine.usertype diff --git a/docs/api/cassandra/decoder.rst b/docs/api/cassandra/decoder.rst index e213cc6d74..6341664cb3 100644 --- a/docs/api/cassandra/decoder.rst +++ b/docs/api/cassandra/decoder.rst @@ -1,5 +1,7 @@ -``cassandra.decoder`` - Data Return Formats -=========================================== +cassandra.decoder +================= + +Data Return Formats .. module:: cassandra.decoder diff --git a/docs/api/cassandra/encoder.rst b/docs/api/cassandra/encoder.rst index de3b180510..8919c87ddd 100644 --- a/docs/api/cassandra/encoder.rst +++ b/docs/api/cassandra/encoder.rst @@ -1,5 +1,7 @@ -``cassandra.encoder`` - Encoders for non-prepared Statements -============================================================ +cassandra.encoder +================= + +Encoders for non-prepared Statements .. module:: cassandra.encoder diff --git a/docs/api/cassandra/io/asyncioreactor.rst b/docs/api/cassandra/io/asyncioreactor.rst index 38ae63ca7f..a7509ed6a8 100644 --- a/docs/api/cassandra/io/asyncioreactor.rst +++ b/docs/api/cassandra/io/asyncioreactor.rst @@ -1,5 +1,7 @@ -``cassandra.io.asyncioreactor`` - ``asyncio`` Event Loop -===================================================================== +cassandra.io.asyncioreactor +=========================== + +``asyncio`` Event Loop .. module:: cassandra.io.asyncioreactor diff --git a/docs/api/cassandra/io/asyncorereactor.rst b/docs/api/cassandra/io/asyncorereactor.rst index ade7887e70..661fd9c1ec 100644 --- a/docs/api/cassandra/io/asyncorereactor.rst +++ b/docs/api/cassandra/io/asyncorereactor.rst @@ -1,5 +1,7 @@ -``cassandra.io.asyncorereactor`` - ``asyncore`` Event Loop -========================================================== +cassandra.io.asyncorereactor +============================ + +``asyncore`` Event Loop .. module:: cassandra.io.asyncorereactor diff --git a/docs/api/cassandra/io/eventletreactor.rst b/docs/api/cassandra/io/eventletreactor.rst index 1ba742c7e9..2e71153b70 100644 --- a/docs/api/cassandra/io/eventletreactor.rst +++ b/docs/api/cassandra/io/eventletreactor.rst @@ -1,5 +1,7 @@ -``cassandra.io.eventletreactor`` - ``eventlet``-compatible Connection -===================================================================== +cassandra.io.eventletreactor +============================ + +``eventlet``-compatible Connection .. module:: cassandra.io.eventletreactor diff --git a/docs/api/cassandra/io/geventreactor.rst b/docs/api/cassandra/io/geventreactor.rst index 603affe140..a4b0235c6a 100644 --- a/docs/api/cassandra/io/geventreactor.rst +++ b/docs/api/cassandra/io/geventreactor.rst @@ -1,5 +1,7 @@ -``cassandra.io.geventreactor`` - ``gevent``-compatible Event Loop -================================================================= +cassandra.io.geventreactor +========================== + +``gevent``-compatible Event Loop .. module:: cassandra.io.geventreactor diff --git a/docs/api/cassandra/io/libevreactor.rst b/docs/api/cassandra/io/libevreactor.rst index 5b7288edf2..2269d0822a 100644 --- a/docs/api/cassandra/io/libevreactor.rst +++ b/docs/api/cassandra/io/libevreactor.rst @@ -1,5 +1,7 @@ -``cassandra.io.libevreactor`` - ``libev`` Event Loop -==================================================== +cassandra.io.libevreactor +========================= + +``libev`` Event Loop .. module:: cassandra.io.libevreactor diff --git a/docs/api/cassandra/io/twistedreactor.rst b/docs/api/cassandra/io/twistedreactor.rst index 24e93bd432..cc6944c9fd 100644 --- a/docs/api/cassandra/io/twistedreactor.rst +++ b/docs/api/cassandra/io/twistedreactor.rst @@ -1,5 +1,7 @@ -``cassandra.io.twistedreactor`` - Twisted Event Loop -==================================================== +cassandra.io.twistedreactor +=========================== + +Twisted Event Loop .. module:: cassandra.io.twistedreactor diff --git a/docs/api/cassandra/metadata.rst b/docs/api/cassandra/metadata.rst index 7c1280bcf7..25526f61ec 100644 --- a/docs/api/cassandra/metadata.rst +++ b/docs/api/cassandra/metadata.rst @@ -1,5 +1,7 @@ -``cassandra.metadata`` - Schema and Ring Topology -================================================= +cassandra.metadata +================== + +Schema and Ring Topology .. module:: cassandra.metadata diff --git a/docs/api/cassandra/metrics.rst b/docs/api/cassandra/metrics.rst index 0df7f8b5b9..d2ee997bca 100644 --- a/docs/api/cassandra/metrics.rst +++ b/docs/api/cassandra/metrics.rst @@ -1,5 +1,7 @@ -``cassandra.metrics`` - Performance Metrics -=========================================== +cassandra.metrics +================= + +Performance Metrics .. module:: cassandra.metrics diff --git a/docs/api/cassandra/policies.rst b/docs/api/cassandra/policies.rst index ea3b19d796..84d5575a40 100644 --- a/docs/api/cassandra/policies.rst +++ b/docs/api/cassandra/policies.rst @@ -1,5 +1,7 @@ -``cassandra.policies`` - Load balancing and Failure Handling Policies -===================================================================== +cassandra.policies +================== + +Load balancing and Failure Handling Policies .. module:: cassandra.policies diff --git a/docs/api/cassandra/pool.rst b/docs/api/cassandra/pool.rst index b14d30e19c..f6a59ce58a 100644 --- a/docs/api/cassandra/pool.rst +++ b/docs/api/cassandra/pool.rst @@ -1,5 +1,7 @@ -``cassandra.pool`` - Hosts and Connection Pools -=============================================== +cassandra.pool +============== + +Hosts and Connection Pools .. automodule:: cassandra.pool diff --git a/docs/api/cassandra/protocol.rst b/docs/api/cassandra/protocol.rst index f615ab1a70..8b8f303574 100644 --- a/docs/api/cassandra/protocol.rst +++ b/docs/api/cassandra/protocol.rst @@ -1,5 +1,7 @@ -``cassandra.protocol`` - Protocol Features -===================================================================== +cassandra.protocol +================== + +Protocol Features .. module:: cassandra.protocol @@ -14,7 +16,7 @@ holding custom key/value pairs. By default these are ignored by the server. They can be useful for servers implementing a custom QueryHandler. -See :meth:`.Session.execute`, ::meth:`.Session.execute_async`, :attr:`.ResponseFuture.custom_payload`. +See :meth:`.Session.execute`, :meth:`.Session.execute_async`, :attr:`.ResponseFuture.custom_payload`. .. autoclass:: _ProtocolHandler @@ -51,5 +53,5 @@ These protocol handlers comprise different parsers, and return results as descri - LazyProtocolHandler: near drop-in replacement for the above, except that it returns an iterator over rows, lazily decoded into the default row format (this is more efficient since all decoded results are not materialized at once) -- NumpyProtocolHander: deserializes results directly into NumPy arrays. This facilitates efficient integration with +- NumpyProtocolHandler: deserializes results directly into NumPy arrays. This facilitates efficient integration with analysis toolkits such as Pandas. diff --git a/docs/api/cassandra/query.rst b/docs/api/cassandra/query.rst index fcd79739b9..aa3a8c1035 100644 --- a/docs/api/cassandra/query.rst +++ b/docs/api/cassandra/query.rst @@ -1,5 +1,7 @@ -``cassandra.query`` - Prepared Statements, Batch Statements, Tracing, and Row Factories -======================================================================================= +cassandra.query +=============== + +Prepared Statements, Batch Statements, Tracing, and Row Factories .. module:: cassandra.query diff --git a/docs/api/cassandra/timestamps.rst b/docs/api/cassandra/timestamps.rst index 00d25b06d9..4335784de3 100644 --- a/docs/api/cassandra/timestamps.rst +++ b/docs/api/cassandra/timestamps.rst @@ -1,5 +1,7 @@ -``cassandra.timestamps`` - Timestamp Generation -=============================================== +cassandra.timestamps +==================== + +Timestamp Generation .. module:: cassandra.timestamps diff --git a/docs/api/cassandra/util.rst b/docs/api/cassandra/util.rst index 848d4d5fc2..ace39f86dd 100644 --- a/docs/api/cassandra/util.rst +++ b/docs/api/cassandra/util.rst @@ -1,5 +1,7 @@ -``cassandra.util`` - Utilities -=================================== +cassandra.util +============== + +Utilities .. automodule:: cassandra.util :members: diff --git a/docs/api/index.rst b/docs/api/index.rst index cf792283d0..cecbea5e75 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -4,7 +4,7 @@ API Documentation Core Driver ----------- .. toctree:: - :maxdepth: 2 + :maxdepth: 1 cassandra cassandra/cluster diff --git a/docs/conf.py b/docs/conf.py index e505986661..34ef31ccae 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,14 +10,34 @@ # -- Global variables # Build documentation for the following tags and branches -TAGS = ['3.21.0-scylla', '3.22.3-scylla', '3.24.8-scylla', '3.25.4-scylla', '3.25.11-scylla', '3.26.9-scylla', '3.28.0-scylla', '3.28.1-scylla', '3.28.2-scylla'] +TAGS = [ + '3.21.0-scylla', + '3.22.3-scylla', + '3.24.8-scylla', + '3.25.4-scylla', + '3.25.11-scylla', + '3.26.9-scylla', + '3.28.0-scylla', + '3.28.1-scylla', + '3.28.2-scylla', + '3.29.0-scylla', + '3.29.1-scylla', + '3.29.2-scylla', + '3.29.3-scylla', + '3.29.4-scylla', + '3.29.5-scylla', + '3.29.6-scylla', + '3.29.7-scylla', + '3.29.8-scylla', + '3.29.10-scylla', +] BRANCHES = ['master'] # Set the latest version. -LATEST_VERSION = '3.28.2-scylla' +LATEST_VERSION = '3.29.10-scylla' # Set which versions are not released yet. UNSTABLE_VERSIONS = ['master'] # Set which versions are deprecated -DEPRECATED_VERSIONS = ['3.21.0-scylla', '3.22.3-scylla', '3.24.8-scylla', '3.25.4-scylla', '3.25.11-scylla', '3.26.9-scylla', '3.28.1-scylla'] +DEPRECATED_VERSIONS = ['3.21.0-scylla', '3.22.3-scylla', '3.24.8-scylla', '3.25.4-scylla', '3.25.11-scylla', '3.26.9-scylla', '3.28.1-scylla', '3.29.1-scylla'] # -- General configuration @@ -32,7 +52,7 @@ 'sphinx_sitemap', 'sphinx_scylladb_theme', 'sphinx_multiversion', # optional - 'recommonmark', # optional + 'myst_parser', # optional ] # Add any paths that contain templates here, relative to this directory. @@ -65,7 +85,16 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = [ + "_build", + "Thumbs.db", + ".DS_Store", + ".venv", + ".venv/**", + "**/site-packages/**", + "**/*.dist-info/**", + "**/licenses/**", +] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' @@ -134,3 +163,9 @@ # Dictionary of values to pass into the template engine’s context for all pages html_context = {'html_baseurl': html_baseurl} +autodoc_mock_imports = [ + # Asyncore has been removed from python 3.12, we need to mock it until `cassandra/io/asyncorereactor.py` is dropped + "asyncore", + # Since driver is not built, binary modules also not built, so we need to mock them + "cassandra.io.libevwrapper" +] diff --git a/docs/dates-and-times.rst b/docs/dates-and-times.rst index 7a89f77437..369ff15027 100644 --- a/docs/dates-and-times.rst +++ b/docs/dates-and-times.rst @@ -8,10 +8,10 @@ timestamps (Cassandra DateType) ------------------------------- Timestamps in Cassandra are timezone-naive timestamps encoded as millseconds since UNIX epoch. Clients working with -timestamps in this database usually find it easiest to reason about them if they are always assumed to be UTC. To quote the -pytz documentation, "The preferred way of dealing with times is to always work in UTC, converting to localtime only when -generating output to be read by humans." The driver adheres to this tenant, and assumes UTC is always in the database. The -driver attempts to make this correct on the way in, and assumes no timezone on the way out. +timestamps in this database usually find it easiest to reason about them if they are always assumed to be UTC. The +preferred way of dealing with times is to always work in UTC, converting to localtime only when generating output to +be read by humans. The driver adheres to this tenet, and assumes UTC is always in the database. The driver attempts +to make this correct on the way in, and assumes no timezone on the way out. Write Path ~~~~~~~~~~ @@ -45,12 +45,15 @@ saving time, and the defacto package for handling this is a third-party package and not make decisions for the integrator). The decision for how to handle timezones is left to the application. For the most part it is straightforward to apply -localization to the ``datetime``\s returned by queries. One prevalent method is to use pytz for localization:: +localization to the ``datetime``\s returned by queries. Use the standard library ``zoneinfo`` module (available since +Python 3.9) for localization:: - import pytz - user_tz = pytz.timezone('US/Central') + from datetime import timezone + from zoneinfo import ZoneInfo + + user_tz = ZoneInfo('US/Central') timestamp_naive = row.ts - timestamp_utc = pytz.utc.localize(timestamp_naive) + timestamp_utc = timestamp_naive.replace(tzinfo=timezone.utc) timestamp_presented = timestamp_utc.astimezone(user_tz) This is the most robust approach (likely refactored into a function). If it is deemed too cumbersome to apply for all call diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 76685c5fdf..cb837a7098 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -5,15 +5,15 @@ First, make sure you have the driver properly :doc:`installed `. Connecting to a Cluster ----------------------- -Before we can start executing any queries against a Cassandra cluster we need to setup +Before we can start executing any queries against a Scylla cluster we need to setup an instance of :class:`~.Cluster`. As the name suggests, you will typically have one -instance of :class:`~.Cluster` for each Cassandra cluster you want to interact +instance of :class:`~.Cluster` for each Scylla cluster you want to interact with. -First, make sure you have the Cassandra driver properly :doc:`installed `. +First, make sure you have the Scylla driver properly :doc:`installed `. -Connecting to Cassandra -+++++++++++++++++++++++ +Connecting to Scylla +++++++++++++++++++++ The simplest way to create a :class:`~.Cluster` is like this: .. code-block:: python @@ -22,7 +22,7 @@ The simplest way to create a :class:`~.Cluster` is like this: cluster = Cluster() -This will attempt to connection to a Cassandra instance on your +This will attempt to connect to a Scylla instance on your local machine (127.0.0.1). You can also specify a list of IP addresses for nodes in your cluster: @@ -121,7 +121,7 @@ way to execute a query is to use :meth:`~.Session.execute()`: for user_row in rows: print(user_row.name, user_row.age, user_row.email) -This will transparently pick a Cassandra node to execute the query against +This will transparently pick a Scylla node to execute the query against and handle any retries that are necessary if the operation fails. By default, each row in the result set will be a @@ -160,10 +160,10 @@ frequently run queries. Prepared Statements ------------------- -Prepared statements are queries that are parsed by Cassandra and then saved +Prepared statements are queries that are parsed by Scylla and then saved for later use. When the driver uses a prepared statement, it only needs to send the values of parameters to bind. This lowers network traffic -and CPU utilization within Cassandra because Cassandra does not have to +and CPU utilization within Scylla because Scylla does not have to re-parse the query each time. To prepare a query, use :meth:`.Session.prepare()`: diff --git a/docs/index.rst b/docs/index.rst index f4abf44320..cd137917d9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,7 +4,7 @@ A Python client driver for `Scylla `_. This driver works exclusively with the Cassandra Query Language v3 (CQL3) and Cassandra's native protocol. -The driver supports Python 3.9-3.13. +The driver supports Python 3.10-3.14. This driver is open source under the `Apache v2 License `_. @@ -41,9 +41,6 @@ Contents :doc:`security` An overview of the security features of the driver -:doc:`upgrading` - A guide to upgrading versions of the driver - :doc:`user-defined-types` Working with Scylla's user-defined types (UDT) @@ -66,7 +63,6 @@ Contents installation getting-started scylla-specific - upgrading execution-profiles performance query-paging diff --git a/docs/installation.rst b/docs/installation.rst index b57ad37f96..6a4b38ea80 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -3,7 +3,7 @@ Installation Supported Platforms ------------------- -Python versions 3.9-3.13 are supported. Both CPython (the standard Python +Python versions 3.10-3.14 are supported. Both CPython (the standard Python implementation) and `PyPy `_ are supported and tested. Linux, OSX, and Windows are supported. @@ -26,7 +26,7 @@ To check if the installation was successful, you can run:: python -c 'import cassandra; print(cassandra.__version__)' -It should print something like "3.29.3". +It should print something like "3.29.10". (*Optional*) Compression Support -------------------------------- @@ -47,15 +47,6 @@ For snappy support:: (If using a Debian Linux derivative such as Ubuntu, it may be easier to just run ``apt-get install python-snappy``.) -(*Optional*) Metrics Support ----------------------------- -The driver has built-in support for capturing :attr:`.Cluster.metrics` about -the queries you run. However, the ``scales`` library is required to -support this:: - - pip install scales - - Speeding Up Installation ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -71,9 +62,6 @@ threads used to build the driver and any C extensions: .. code-block:: bash - $ # installing from source - $ CASS_DRIVER_BUILD_CONCURRENCY=8 python setup.py install - $ # installing from pip $ CASS_DRIVER_BUILD_CONCURRENCY=8 pip install scylla-driver Note that by default (when CASS_DRIVER_BUILD_CONCURRENCY is not specified), concurrency will be equal to the number of @@ -113,11 +101,11 @@ Manual Installation You can always install the driver directly from a source checkout or tarball. When installing manually, ensure the python dependencies are already installed. You can find the list of dependencies in -`requirements.txt `_. +`pyproject.toml `_. Once the dependencies are installed, simply run:: - python setup.py install + pip install . (*Optional*) Non-python Dependencies @@ -131,9 +119,9 @@ for token-aware routing with the ``Murmur3Partitioner``, `libev `_ event loop integration, and Cython optimized extensions. -When installing manually through setup.py, you can disable both with -the ``--no-extensions`` option, or selectively disable them with -with ``--no-murmur3``, ``--no-libev``, or ``--no-cython``. +Extensions can be selectively disabled using environment variables: +``CASS_DRIVER_NO_EXTENSIONS=1`` (disable all), ``CASS_DRIVER_NO_CYTHON=1``, +or ``CASS_DRIVER_NO_LIBEV=1``. To compile the extensions, ensure that GCC and the Python headers are available. @@ -158,31 +146,25 @@ This is not a hard requirement, but is engaged by default to build extensions of pure Python implementation. This is a costly build phase, especially in clean environments where the Cython compiler must be built -This build phase can be avoided using the build switch, or an environment variable:: +This build phase can be avoided using an environment variable:: - python setup.py install --no-cython + CASS_DRIVER_NO_CYTHON=1 pip install scylla-driver -Alternatively, an environment variable can be used to switch this option regardless of +Alternatively, the environment variable can be used to switch this option regardless of context:: CASS_DRIVER_NO_CYTHON=1 - or, to disable all extensions: CASS_DRIVER_NO_EXTENSIONS=1 -This method is required when using pip, which provides no other way of injecting user options in a single command:: - - CASS_DRIVER_NO_CYTHON=1 pip install scylla-driver - CASS_DRIVER_NO_CYTHON=1 sudo -E pip install ~/python-driver - -The environment variable is the preferred option because it spans all invocations of setup.py, and will +These environment variables are the preferred option, and will prevent Cython from being materialized as a setup requirement. -If your sudo configuration does not allow SETENV, you must push the option flag down via pip. However, pip -applies these options to all dependencies (which break on the custom flag). Therefore, you must first install -dependencies, then use install-option:: +If your sudo configuration does not allow SETENV, you must first install +dependencies, then install the driver:: sudo pip install futures - sudo pip install --install-option="--no-cython" + sudo CASS_DRIVER_NO_CYTHON=1 pip install scylla-driver Supported Event Loops @@ -208,13 +190,13 @@ through `Homebrew `_. For example, on Mac OS X:: $ brew install libev -The libev extension can now be built for Windows as of Python driver version 3.29.3. You can +The libev extension can now be built for Windows as of Python driver version 3.29.10. You can install libev using any Windows package manager. For example, to install using `vcpkg `_: $ vcpkg install libev If successful, you should be able to build and install the extension -(just using ``setup.py build`` or ``setup.py install``) and then use +(just using ``pip install .``) and then use the libev event loop by doing the following: .. code-block:: python @@ -228,5 +210,4 @@ the libev event loop by doing the following: (*Optional*) Configuring SSL ----------------------------- -Andrew Mussey has published a thorough guide on -`Using SSL with the DataStax Python driver `_. +See the :ref:`security` section for details on configuring SSL. diff --git a/docs/poetry.lock b/docs/poetry.lock deleted file mode 100644 index b1d2ed96e3..0000000000 --- a/docs/poetry.lock +++ /dev/null @@ -1,1908 +0,0 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. - -[[package]] -name = "aenum" -version = "2.2.6" -description = "Advanced Enumerations (compatible with Python's stdlib Enum), NamedTuples, and NamedConstants" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "aenum-2.2.6-py2-none-any.whl", hash = "sha256:aaebe735508d9cbc72cd6adfb59660a5e676dfbeb6fb24fb090041e7ddb8d3b3"}, - {file = "aenum-2.2.6-py3-none-any.whl", hash = "sha256:f9d20f7302ce3dc3639b3f75c3b3e146f3b22409a6b4513c1f0bd6dbdfcbd8c1"}, - {file = "aenum-2.2.6.tar.gz", hash = "sha256:260225470b49429f5893a195a8b99c73a8d182be42bf90c37c93e7b20e44eaae"}, -] - -[[package]] -name = "alabaster" -version = "0.7.16" -description = "A light, configurable Sphinx theme" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, - {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, -] - -[[package]] -name = "anyio" -version = "4.9.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, - {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" -typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} - -[package.extras] -doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] -trio = ["trio (>=0.26.1)"] - -[[package]] -name = "babel" -version = "2.17.0" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, - {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, -] - -[package.extras] -dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] - -[[package]] -name = "beartype" -version = "0.21.0" -description = "Unbearably fast near-real-time hybrid runtime-static type-checking in pure Python." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "beartype-0.21.0-py3-none-any.whl", hash = "sha256:b6a1bd56c72f31b0a496a36cc55df6e2f475db166ad07fa4acc7e74f4c7f34c0"}, - {file = "beartype-0.21.0.tar.gz", hash = "sha256:f9a5078f5ce87261c2d22851d19b050b64f6a805439e8793aecf01ce660d3244"}, -] - -[package.extras] -dev = ["autoapi (>=0.9.0)", "click", "coverage (>=5.5)", "equinox ; sys_platform == \"linux\"", "jax[cpu] ; sys_platform == \"linux\"", "jaxtyping ; sys_platform == \"linux\"", "langchain", "mypy (>=0.800) ; platform_python_implementation != \"PyPy\"", "nuitka (>=1.2.6) ; sys_platform == \"linux\"", "numba ; python_version < \"3.13.0\"", "numpy ; sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "pandera", "pydata-sphinx-theme (<=0.7.2)", "pygments", "pyright (>=1.1.370)", "pytest (>=4.0.0)", "rich-click", "sphinx", "sphinx (>=4.2.0,<6.0.0)", "sphinxext-opengraph (>=0.7.5)", "sqlalchemy", "tox (>=3.20.1)", "typing-extensions (>=3.10.0.0)", "xarray"] -doc-rtd = ["autoapi (>=0.9.0)", "pydata-sphinx-theme (<=0.7.2)", "sphinx (>=4.2.0,<6.0.0)", "sphinxext-opengraph (>=0.7.5)"] -test = ["click", "coverage (>=5.5)", "equinox ; sys_platform == \"linux\"", "jax[cpu] ; sys_platform == \"linux\"", "jaxtyping ; sys_platform == \"linux\"", "langchain", "mypy (>=0.800) ; platform_python_implementation != \"PyPy\"", "nuitka (>=1.2.6) ; sys_platform == \"linux\"", "numba ; python_version < \"3.13.0\"", "numpy ; sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "pandera", "pygments", "pyright (>=1.1.370)", "pytest (>=4.0.0)", "rich-click", "sphinx", "sqlalchemy", "tox (>=3.20.1)", "typing-extensions (>=3.10.0.0)", "xarray"] -test-tox = ["click", "equinox ; sys_platform == \"linux\"", "jax[cpu] ; sys_platform == \"linux\"", "jaxtyping ; sys_platform == \"linux\"", "langchain", "mypy (>=0.800) ; platform_python_implementation != \"PyPy\"", "nuitka (>=1.2.6) ; sys_platform == \"linux\"", "numba ; python_version < \"3.13.0\"", "numpy ; sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "pandera", "pygments", "pyright (>=1.1.370)", "pytest (>=4.0.0)", "rich-click", "sphinx", "sqlalchemy", "typing-extensions (>=3.10.0.0)", "xarray"] -test-tox-coverage = ["coverage (>=5.5)"] - -[[package]] -name = "beautifulsoup4" -version = "4.13.4" -description = "Screen-scraping library" -optional = false -python-versions = ">=3.7.0" -groups = ["main"] -files = [ - {file = "beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b"}, - {file = "beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195"}, -] - -[package.dependencies] -soupsieve = ">1.2" -typing-extensions = ">=4.0.0" - -[package.extras] -cchardet = ["cchardet"] -chardet = ["chardet"] -charset-normalizer = ["charset-normalizer"] -html5lib = ["html5lib"] -lxml = ["lxml"] - -[[package]] -name = "certifi" -version = "2025.6.15" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057"}, - {file = "certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b"}, -] - -[[package]] -name = "cffi" -version = "1.17.1" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.8" -groups = ["main"] -markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\"" -files = [ - {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, - {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, - {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, - {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, - {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, - {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, - {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, - {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, - {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, - {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, - {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, - {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, - {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, - {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, - {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, - {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, -] - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "charset-normalizer" -version = "3.4.2" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, - {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, - {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, -] - -[[package]] -name = "click" -version = "8.2.1" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, - {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main"] -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "commonmark" -version = "0.9.1" -description = "Python parser for the CommonMark Markdown spec" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, - {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, -] - -[package.extras] -test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] - -[[package]] -name = "dnspython" -version = "2.7.0" -description = "DNS toolkit" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"}, - {file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"}, -] - -[package.extras] -dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.16.0)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "quart-trio (>=0.11.0)", "sphinx (>=7.2.0)", "sphinx-rtd-theme (>=2.0.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] -dnssec = ["cryptography (>=43)"] -doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] -doq = ["aioquic (>=1.0.0)"] -idna = ["idna (>=3.7)"] -trio = ["trio (>=0.23)"] -wmi = ["wmi (>=1.5.1)"] - -[[package]] -name = "docutils" -version = "0.21.2" -description = "Docutils -- Python Documentation Utilities" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, - {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, -] - -[[package]] -name = "eventlet" -version = "0.33.3" -description = "Highly concurrent networking library" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "eventlet-0.33.3-py2.py3-none-any.whl", hash = "sha256:e43b9ae05ba4bb477a10307699c9aff7ff86121b2640f9184d29059f5a687df8"}, - {file = "eventlet-0.33.3.tar.gz", hash = "sha256:722803e7eadff295347539da363d68ae155b8b26ae6a634474d0a920be73cfda"}, -] - -[package.dependencies] -dnspython = ">=1.15.0" -greenlet = ">=0.3" -six = ">=1.10.0" - -[[package]] -name = "exceptiongroup" -version = "1.3.0" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["main"] -markers = "python_version == \"3.10\"" -files = [ - {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, - {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "futures" -version = "2.2.0" -description = "Backport of the concurrent.futures package from Python 3.2" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "futures-2.2.0-py2.py3-none-any.whl", hash = "sha256:9fd22b354a4c4755ad8c7d161d93f5026aca4cfe999bd2e53168f14765c02cd6"}, - {file = "futures-2.2.0.tar.gz", hash = "sha256:151c057173474a3a40f897165951c0e33ad04f37de65b6de547ddef107fd0ed3"}, -] - -[[package]] -name = "geomet" -version = "1.1.0" -description = "Pure Python conversion library for common geospatial data formats" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "geomet-1.1.0-py3-none-any.whl", hash = "sha256:4372fe4e286a34acc6f2e9308284850bd8c4aa5bc12065e2abbd4995900db12f"}, - {file = "geomet-1.1.0.tar.gz", hash = "sha256:51e92231a0ef6aaa63ac20c443377ba78a303fd2ecd179dc3567de79f3c11605"}, -] - -[package.dependencies] -click = "*" - -[[package]] -name = "gevent" -version = "23.9.1" -description = "Coroutine-based network library" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "gevent-23.9.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:a3c5e9b1f766a7a64833334a18539a362fb563f6c4682f9634dea72cbe24f771"}, - {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b101086f109168b23fa3586fccd1133494bdb97f86920a24dc0b23984dc30b69"}, - {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36a549d632c14684bcbbd3014a6ce2666c5f2a500f34d58d32df6c9ea38b6535"}, - {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:272cffdf535978d59c38ed837916dfd2b5d193be1e9e5dcc60a5f4d5025dd98a"}, - {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcb8612787a7f4626aa881ff15ff25439561a429f5b303048f0fca8a1c781c39"}, - {file = "gevent-23.9.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d57737860bfc332b9b5aa438963986afe90f49645f6e053140cfa0fa1bdae1ae"}, - {file = "gevent-23.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5f3c781c84794926d853d6fb58554dc0dcc800ba25c41d42f6959c344b4db5a6"}, - {file = "gevent-23.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:dbb22a9bbd6a13e925815ce70b940d1578dbe5d4013f20d23e8a11eddf8d14a7"}, - {file = "gevent-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:707904027d7130ff3e59ea387dddceedb133cc742b00b3ffe696d567147a9c9e"}, - {file = "gevent-23.9.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:45792c45d60f6ce3d19651d7fde0bc13e01b56bb4db60d3f32ab7d9ec467374c"}, - {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e24c2af9638d6c989caffc691a039d7c7022a31c0363da367c0d32ceb4a0648"}, - {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e1ead6863e596a8cc2a03e26a7a0981f84b6b3e956101135ff6d02df4d9a6b07"}, - {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65883ac026731ac112184680d1f0f1e39fa6f4389fd1fc0bf46cc1388e2599f9"}, - {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7af500da05363e66f122896012acb6e101a552682f2352b618e541c941a011"}, - {file = "gevent-23.9.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c3e5d2fa532e4d3450595244de8ccf51f5721a05088813c1abd93ad274fe15e7"}, - {file = "gevent-23.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c84d34256c243b0a53d4335ef0bc76c735873986d478c53073861a92566a8d71"}, - {file = "gevent-23.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ada07076b380918829250201df1d016bdafb3acf352f35e5693b59dceee8dd2e"}, - {file = "gevent-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:921dda1c0b84e3d3b1778efa362d61ed29e2b215b90f81d498eb4d8eafcd0b7a"}, - {file = "gevent-23.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ed7a048d3e526a5c1d55c44cb3bc06cfdc1947d06d45006cc4cf60dedc628904"}, - {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c1abc6f25f475adc33e5fc2dbcc26a732608ac5375d0d306228738a9ae14d3b"}, - {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4368f341a5f51611411ec3fc62426f52ac3d6d42eaee9ed0f9eebe715c80184e"}, - {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:52b4abf28e837f1865a9bdeef58ff6afd07d1d888b70b6804557e7908032e599"}, - {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52e9f12cd1cda96603ce6b113d934f1aafb873e2c13182cf8e86d2c5c41982ea"}, - {file = "gevent-23.9.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:de350fde10efa87ea60d742901e1053eb2127ebd8b59a7d3b90597eb4e586599"}, - {file = "gevent-23.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fde6402c5432b835fbb7698f1c7f2809c8d6b2bd9d047ac1f5a7c1d5aa569303"}, - {file = "gevent-23.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:dd6c32ab977ecf7c7b8c2611ed95fa4aaebd69b74bf08f4b4960ad516861517d"}, - {file = "gevent-23.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:455e5ee8103f722b503fa45dedb04f3ffdec978c1524647f8ba72b4f08490af1"}, - {file = "gevent-23.9.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:7ccf0fd378257cb77d91c116e15c99e533374a8153632c48a3ecae7f7f4f09fe"}, - {file = "gevent-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d163d59f1be5a4c4efcdd13c2177baaf24aadf721fdf2e1af9ee54a998d160f5"}, - {file = "gevent-23.9.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7532c17bc6c1cbac265e751b95000961715adef35a25d2b0b1813aa7263fb397"}, - {file = "gevent-23.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:78eebaf5e73ff91d34df48f4e35581ab4c84e22dd5338ef32714264063c57507"}, - {file = "gevent-23.9.1-cp38-cp38-win32.whl", hash = "sha256:f632487c87866094546a74eefbca2c74c1d03638b715b6feb12e80120960185a"}, - {file = "gevent-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:62d121344f7465e3739989ad6b91f53a6ca9110518231553fe5846dbe1b4518f"}, - {file = "gevent-23.9.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:bf456bd6b992eb0e1e869e2fd0caf817f0253e55ca7977fd0e72d0336a8c1c6a"}, - {file = "gevent-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43daf68496c03a35287b8b617f9f91e0e7c0d042aebcc060cadc3f049aadd653"}, - {file = "gevent-23.9.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7c28e38dcde327c217fdafb9d5d17d3e772f636f35df15ffae2d933a5587addd"}, - {file = "gevent-23.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fae8d5b5b8fa2a8f63b39f5447168b02db10c888a3e387ed7af2bd1b8612e543"}, - {file = "gevent-23.9.1-cp39-cp39-win32.whl", hash = "sha256:2c7b5c9912378e5f5ccf180d1fdb1e83f42b71823483066eddbe10ef1a2fcaa2"}, - {file = "gevent-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:a2898b7048771917d85a1d548fd378e8a7b2ca963db8e17c6d90c76b495e0e2b"}, - {file = "gevent-23.9.1.tar.gz", hash = "sha256:72c002235390d46f94938a96920d8856d4ffd9ddf62a303a0d7c118894097e34"}, -] - -[package.dependencies] -cffi = {version = ">=1.12.2", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} -greenlet = [ - {version = ">=2.0.0", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""}, - {version = ">=3.0rc3", markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\""}, -] -"zope.event" = "*" -"zope.interface" = "*" - -[package.extras] -dnspython = ["dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\""] -docs = ["furo", "repoze.sphinx.autointerface", "sphinx", "sphinxcontrib-programoutput", "zope.schema"] -monitor = ["psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""] -recommended = ["cffi (>=1.12.2) ; platform_python_implementation == \"CPython\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""] -test = ["cffi (>=1.12.2) ; platform_python_implementation == \"CPython\"", "coverage (>=5.0) ; sys_platform != \"win32\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "objgraph", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\"", "requests", "setuptools"] - -[[package]] -name = "greenlet" -version = "3.2.3" -description = "Lightweight in-process concurrent programming" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "greenlet-3.2.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:1afd685acd5597349ee6d7a88a8bec83ce13c106ac78c196ee9dde7c04fe87be"}, - {file = "greenlet-3.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:761917cac215c61e9dc7324b2606107b3b292a8349bdebb31503ab4de3f559ac"}, - {file = "greenlet-3.2.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a433dbc54e4a37e4fff90ef34f25a8c00aed99b06856f0119dcf09fbafa16392"}, - {file = "greenlet-3.2.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:72e77ed69312bab0434d7292316d5afd6896192ac4327d44f3d613ecb85b037c"}, - {file = "greenlet-3.2.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:68671180e3849b963649254a882cd544a3c75bfcd2c527346ad8bb53494444db"}, - {file = "greenlet-3.2.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49c8cfb18fb419b3d08e011228ef8a25882397f3a859b9fe1436946140b6756b"}, - {file = "greenlet-3.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:efc6dc8a792243c31f2f5674b670b3a95d46fa1c6a912b8e310d6f542e7b0712"}, - {file = "greenlet-3.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:731e154aba8e757aedd0781d4b240f1225b075b4409f1bb83b05ff410582cf00"}, - {file = "greenlet-3.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:96c20252c2f792defe9a115d3287e14811036d51e78b3aaddbee23b69b216302"}, - {file = "greenlet-3.2.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:784ae58bba89fa1fa5733d170d42486580cab9decda3484779f4759345b29822"}, - {file = "greenlet-3.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0921ac4ea42a5315d3446120ad48f90c3a6b9bb93dd9b3cf4e4d84a66e42de83"}, - {file = "greenlet-3.2.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d2971d93bb99e05f8c2c0c2f4aa9484a18d98c4c3bd3c62b65b7e6ae33dfcfaf"}, - {file = "greenlet-3.2.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c667c0bf9d406b77a15c924ef3285e1e05250948001220368e039b6aa5b5034b"}, - {file = "greenlet-3.2.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:592c12fb1165be74592f5de0d70f82bc5ba552ac44800d632214b76089945147"}, - {file = "greenlet-3.2.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29e184536ba333003540790ba29829ac14bb645514fbd7e32af331e8202a62a5"}, - {file = "greenlet-3.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:93c0bb79844a367782ec4f429d07589417052e621aa39a5ac1fb99c5aa308edc"}, - {file = "greenlet-3.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:751261fc5ad7b6705f5f76726567375bb2104a059454e0226e1eef6c756748ba"}, - {file = "greenlet-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:83a8761c75312361aa2b5b903b79da97f13f556164a7dd2d5448655425bd4c34"}, - {file = "greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d"}, - {file = "greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b"}, - {file = "greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d"}, - {file = "greenlet-3.2.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264"}, - {file = "greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688"}, - {file = "greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb"}, - {file = "greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c"}, - {file = "greenlet-3.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163"}, - {file = "greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849"}, - {file = "greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad"}, - {file = "greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef"}, - {file = "greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3"}, - {file = "greenlet-3.2.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95"}, - {file = "greenlet-3.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb"}, - {file = "greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b"}, - {file = "greenlet-3.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0"}, - {file = "greenlet-3.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36"}, - {file = "greenlet-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3"}, - {file = "greenlet-3.2.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3d04332dddb10b4a211b68111dabaee2e1a073663d117dc10247b5b1642bac86"}, - {file = "greenlet-3.2.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8186162dffde068a465deab08fc72c767196895c39db26ab1c17c0b77a6d8b97"}, - {file = "greenlet-3.2.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728"}, - {file = "greenlet-3.2.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:ed6cfa9200484d234d8394c70f5492f144b20d4533f69262d530a1a082f6ee9a"}, - {file = "greenlet-3.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892"}, - {file = "greenlet-3.2.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141"}, - {file = "greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a"}, - {file = "greenlet-3.2.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:42efc522c0bd75ffa11a71e09cd8a399d83fafe36db250a87cf1dacfaa15dc64"}, - {file = "greenlet-3.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d760f9bdfe79bff803bad32b4d8ffb2c1d2ce906313fc10a83976ffb73d64ca7"}, - {file = "greenlet-3.2.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8324319cbd7b35b97990090808fdc99c27fe5338f87db50514959f8059999805"}, - {file = "greenlet-3.2.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:8c37ef5b3787567d322331d5250e44e42b58c8c713859b8a04c6065f27efbf72"}, - {file = "greenlet-3.2.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ce539fb52fb774d0802175d37fcff5c723e2c7d249c65916257f0a940cee8904"}, - {file = "greenlet-3.2.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:003c930e0e074db83559edc8705f3a2d066d4aa8c2f198aff1e454946efd0f26"}, - {file = "greenlet-3.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7e70ea4384b81ef9e84192e8a77fb87573138aa5d4feee541d8014e452b434da"}, - {file = "greenlet-3.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:22eb5ba839c4b2156f18f76768233fe44b23a31decd9cc0d4cc8141c211fd1b4"}, - {file = "greenlet-3.2.3-cp39-cp39-win32.whl", hash = "sha256:4532f0d25df67f896d137431b13f4cdce89f7e3d4a96387a41290910df4d3a57"}, - {file = "greenlet-3.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:aaa7aae1e7f75eaa3ae400ad98f8644bb81e1dc6ba47ce8a93d3f17274e08322"}, - {file = "greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365"}, -] - -[package.extras] -docs = ["Sphinx", "furo"] -test = ["objgraph", "psutil"] - -[[package]] -name = "gremlinpython" -version = "3.4.7" -description = "Gremlin-Python for Apache TinkerPop" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "gremlinpython-3.4.7-py2.py3-none-any.whl", hash = "sha256:3fc60881638d370fdd0acc005a536baf2fdb3539d5150f2c787e460382548ac4"}, - {file = "gremlinpython-3.4.7.tar.gz", hash = "sha256:0ebe51bba36606d7d731bdeb4f8558ea7f88abf15f841693da47b994a29ac424"}, -] - -[package.dependencies] -aenum = ">=1.4.5,<3.0.0" -isodate = ">=0.6.0,<1.0.0" -six = ">=1.10.0,<2.0.0" -tornado = ">=4.4.1,<6.0" - -[[package]] -name = "h11" -version = "0.16.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, - {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, -] - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "imagesize" -version = "1.4.1" -description = "Getting image size from png/jpeg/jpeg2000/gif file" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["main"] -files = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, -] - -[[package]] -name = "isodate" -version = "0.7.2" -description = "An ISO 8601 date/time/duration parser and formatter" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15"}, - {file = "isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6"}, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, - {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "markupsafe" -version = "3.0.2" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, -] - -[[package]] -name = "mdit-py-plugins" -version = "0.4.2" -description = "Collection of plugins for markdown-it-py" -optional = false -python-versions = ">=3.8" -groups = ["main"] -markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\"" -files = [ - {file = "mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636"}, - {file = "mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5"}, -] - -[package.dependencies] -markdown-it-py = ">=1.0.0,<4.0.0" - -[package.extras] -code-style = ["pre-commit"] -rtd = ["myst-parser", "sphinx-book-theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "myst-parser" -version = "4.0.1" -description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," -optional = false -python-versions = ">=3.10" -groups = ["main"] -markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\"" -files = [ - {file = "myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d"}, - {file = "myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4"}, -] - -[package.dependencies] -docutils = ">=0.19,<0.22" -jinja2 = "*" -markdown-it-py = ">=3.0,<4.0" -mdit-py-plugins = ">=0.4.1,<1.0" -pyyaml = "*" -sphinx = ">=7,<9" - -[package.extras] -code-style = ["pre-commit (>=4.0,<5.0)"] -linkify = ["linkify-it-py (>=2.0,<3.0)"] -rtd = ["ipython", "sphinx (>=7)", "sphinx-autodoc2 (>=0.5.0,<0.6.0)", "sphinx-book-theme (>=1.1,<2.0)", "sphinx-copybutton", "sphinx-design", "sphinx-pyscript", "sphinx-tippy (>=0.4.3)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.9.0,<0.10.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] -testing = ["beautifulsoup4", "coverage[toml]", "defusedxml", "pygments (<2.19)", "pytest (>=8,<9)", "pytest-cov", "pytest-param-files (>=0.6.0,<0.7.0)", "pytest-regressions", "sphinx-pytest"] -testing-docutils = ["pygments", "pytest (>=8,<9)", "pytest-param-files (>=0.6.0,<0.7.0)"] - -[[package]] -name = "packaging" -version = "25.0" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, -] - -[[package]] -name = "pycparser" -version = "2.22" -description = "C parser in Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\"" -files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, -] - -[[package]] -name = "pygments" -version = "2.19.2" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pyyaml" -version = "6.0.2" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "recommonmark" -version = "0.7.1" -description = "A docutils-compatibility bridge to CommonMark, enabling you to write CommonMark inside of Docutils & Sphinx projects." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, - {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, -] - -[package.dependencies] -commonmark = ">=0.8.1" -docutils = ">=0.11" -sphinx = ">=1.3.1" - -[[package]] -name = "redirects-cli" -version = "0.1.3" -description = "Generates static redirections from a YAML file." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "redirects_cli-0.1.3-py3-none-any.whl", hash = "sha256:8a7a548d5f45b98db7d110fd8affbbb44b966cf250e35b5f4c9bd6541622272d"}, - {file = "redirects_cli-0.1.3.tar.gz", hash = "sha256:0cc6f35ae372d087d56bc03cfc639d6e2eac0771454c3c173ac6f3dc233969bc"}, -] - -[package.dependencies] -colorama = ">=0.4" -typer = ">=0.3" - -[package.extras] -test = ["pre-commit", "pytest"] - -[[package]] -name = "requests" -version = "2.32.4" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, - {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset_normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "rich" -version = "14.0.0" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.8.0" -groups = ["main"] -files = [ - {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, - {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "scales" -version = "1.0.9" -description = "Stats for Python processes" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "scales-1.0.9.tar.gz", hash = "sha256:8b6930f7d4bf115192290b44c757af5e254e3fcfcb75ff9a51f5c96a404e2753"}, -] - -[package.dependencies] -six = "*" - -[[package]] -name = "scylla-driver" -version = "3.29.3" -description = "Scylla Driver for Apache Cassandra" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [] -develop = true - -[package.dependencies] -geomet = ">=1.1" -pyyaml = ">5.0" - -[package.extras] -cle = ["cryptography (>=35.0)"] -graph = ["gremlinpython (==3.4.6)"] - -[package.source] -type = "directory" -url = ".." - -[[package]] -name = "setuptools" -version = "79.0.1" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "setuptools-79.0.1-py3-none-any.whl", hash = "sha256:e147c0549f27767ba362f9da434eab9c5dc0045d5304feb602a0af001089fc51"}, - {file = "setuptools-79.0.1.tar.gz", hash = "sha256:128ce7b8f33c3079fd1b067ecbb4051a66e8526e7b65f6cec075dfc650ddfa88"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] - -[[package]] -name = "shellingham" -version = "1.5.4" -description = "Tool to Detect Surrounding Shell" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, - {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, -] - -[[package]] -name = "six" -version = "1.17.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "snowballstemmer" -version = "3.0.1" -description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*" -groups = ["main"] -files = [ - {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}, - {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}, -] - -[[package]] -name = "soupsieve" -version = "2.7" -description = "A modern CSS selector implementation for Beautiful Soup." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4"}, - {file = "soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a"}, -] - -[[package]] -name = "sphinx" -version = "7.4.7" -description = "Python documentation generator" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, - {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, -] - -[package.dependencies] -alabaster = ">=0.7.14,<0.8.0" -babel = ">=2.13" -colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} -docutils = ">=0.20,<0.22" -imagesize = ">=1.3" -Jinja2 = ">=3.1" -packaging = ">=23.0" -Pygments = ">=2.17" -requests = ">=2.30.0" -snowballstemmer = ">=2.2" -sphinxcontrib-applehelp = "*" -sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = ">=2.0.0" -sphinxcontrib-jsmath = "*" -sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.9" -tomli = {version = ">=2", markers = "python_version < \"3.11\""} - -[package.extras] -docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=6.0)", "importlib-metadata (>=6.0)", "mypy (==1.10.1)", "pytest (>=6.0)", "ruff (==0.5.2)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-docutils (==0.21.0.20240711)", "types-requests (>=2.30.0)"] -test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] - -[[package]] -name = "sphinx-autobuild" -version = "2024.10.3" -description = "Rebuild Sphinx documentation on changes, with hot reloading in the browser." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "sphinx_autobuild-2024.10.3-py3-none-any.whl", hash = "sha256:158e16c36f9d633e613c9aaf81c19b0fc458ca78b112533b20dafcda430d60fa"}, - {file = "sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1"}, -] - -[package.dependencies] -colorama = ">=0.4.6" -sphinx = "*" -starlette = ">=0.35" -uvicorn = ">=0.25" -watchfiles = ">=0.20" -websockets = ">=11" - -[package.extras] -test = ["httpx", "pytest (>=6)"] - -[[package]] -name = "sphinx-collapse" -version = "0.1.3" -description = "Collapse extension for Sphinx." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "sphinx_collapse-0.1.3-py3-none-any.whl", hash = "sha256:85fadb2ec8769b93fd04276538668fa96239ef60c20c4a9eaa3e480387a6e65b"}, - {file = "sphinx_collapse-0.1.3.tar.gz", hash = "sha256:cae141e6f03ecd52ed246a305a69e1b0d5d05e6cdf3fe803d40d583ad6ad895a"}, -] - -[package.dependencies] -sphinx = ">=3" - -[package.extras] -doc = ["alabaster"] -test = ["pre-commit", "pytest"] - -[[package]] -name = "sphinx-copybutton" -version = "0.5.2" -description = "Add a copy button to each of your code cells." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd"}, - {file = "sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e"}, -] - -[package.dependencies] -sphinx = ">=1.8" - -[package.extras] -code-style = ["pre-commit (==2.12.1)"] -rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme", "sphinx-examples"] - -[[package]] -name = "sphinx-last-updated-by-git" -version = "0.3.8" -description = "Get the \"last updated\" time for each Sphinx page from Git" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "sphinx_last_updated_by_git-0.3.8-py3-none-any.whl", hash = "sha256:6382c8285ac1f222483a58569b78c0371af5e55f7fbf9c01e5e8a72d6fdfa499"}, - {file = "sphinx_last_updated_by_git-0.3.8.tar.gz", hash = "sha256:c145011f4609d841805b69a9300099fc02fed8f5bb9e5bcef77d97aea97b7761"}, -] - -[package.dependencies] -sphinx = ">=1.8" - -[[package]] -name = "sphinx-multiversion-scylla" -version = "0.3.2" -description = "Add support for multiple versions to sphinx" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "sphinx_multiversion_scylla-0.3.2.tar.gz", hash = "sha256:f415311273228f4f766c36256503da8e2ce01f9d13423f3fcee3160d6284852b"}, -] - -[package.dependencies] -sphinx = ">=2.1" - -[[package]] -name = "sphinx-notfound-page" -version = "1.1.0" -description = "Sphinx extension to build a 404 page with absolute URLs" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "sphinx_notfound_page-1.1.0-py3-none-any.whl", hash = "sha256:835dc76ff7914577a1f58d80a2c8418fb6138c0932c8da8adce4d9096fbcd389"}, - {file = "sphinx_notfound_page-1.1.0.tar.gz", hash = "sha256:913e1754370bb3db201d9300d458a8b8b5fb22e9246a816643a819a9ea2b8067"}, -] - -[package.dependencies] -sphinx = ">=5" - -[package.extras] -doc = ["sphinx-autoapi", "sphinx-rtd-theme", "sphinx-tabs", "sphinxemoji"] -test = ["tox"] - -[[package]] -name = "sphinx-scylladb-theme" -version = "1.8.7" -description = "A Sphinx Theme for ScyllaDB documentation projects" -optional = false -python-versions = "<4.0,>=3.10" -groups = ["main"] -files = [ - {file = "sphinx_scylladb_theme-1.8.7-py3-none-any.whl", hash = "sha256:64c86e86737e16d8bbdbec492622865ec1e9c0c3a5915d747a9c109fd69145f1"}, - {file = "sphinx_scylladb_theme-1.8.7.tar.gz", hash = "sha256:7b84fc99e1156ebf14149f5c1f88b61b5ea852e367fb3940eb99f514db0a6c41"}, -] - -[package.dependencies] -beautifulsoup4 = ">=4.12.3,<5.0.0" -pyyaml = ">=6.0.1,<7.0.0" -setuptools = ">=70.1.1,<80.0.0" -sphinx-collapse = ">=0.1.1,<0.2.0" -sphinx-copybutton = ">=0.5.2,<0.6.0" -sphinx-notfound-page = ">=1.0.4,<2.0.0" -Sphinx-Substitution-Extensions = ">=2022.2.16,<2026.0.0" -sphinx-tabs = ">=3.4.5,<4.0.0" -sphinxcontrib-mermaid = ">=1.0.0,<2.0.0" - -[[package]] -name = "sphinx-sitemap" -version = "2.7.2" -description = "Sitemap generator for Sphinx" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "sphinx_sitemap-2.7.2-py3-none-any.whl", hash = "sha256:1a6a8dcecb0ffb85fd37678f785cfcc40adfe3eebafb05e678971e5260b117e4"}, - {file = "sphinx_sitemap-2.7.2.tar.gz", hash = "sha256:819e028e27579b47efa0e2f863b87136b711c45f13e84730610e80316f6883da"}, -] - -[package.dependencies] -sphinx-last-updated-by-git = "*" - -[package.extras] -dev = ["build", "flake8", "pre-commit", "pytest", "sphinx", "sphinx-last-updated-by-git", "tox"] - -[[package]] -name = "sphinx-substitution-extensions" -version = "2025.1.2" -description = "Extensions for Sphinx which allow for substitutions." -optional = false -python-versions = ">=3.10" -groups = ["main"] -markers = "python_version == \"3.10\" or platform_python_implementation != \"CPython\"" -files = [ - {file = "sphinx_substitution_extensions-2025.1.2-py2.py3-none-any.whl", hash = "sha256:ff14f40e4393bd7434a196badb8d47983355d9755af884b902e3023fb456b958"}, - {file = "sphinx_substitution_extensions-2025.1.2.tar.gz", hash = "sha256:53b8d394d5098a09aef36bc687fa310aeb28466319d2c750e996e46400fb2474"}, -] - -[package.dependencies] -beartype = ">=0.18.5" -docutils = ">=0.19" -sphinx = ">=7.3.5" - -[package.extras] -dev = ["actionlint-py (==1.7.5.21)", "check-manifest (==0.50)", "deptry (==0.21.2)", "doc8 (==1.1.2)", "doccmd (==2024.12.26)", "docformatter (==1.7.5)", "interrogate (==1.7.0)", "mypy-strict-kwargs (==2024.12.25)", "mypy[faster-cache] (==1.14.1)", "myst-parser (==4.0.0)", "pre-commit (==4.0.1)", "pyenchant (==3.3.0rc1)", "pylint (==3.3.3)", "pyproject-fmt (==2.5.0)", "pyright (==1.1.391)", "pyroma (==4.2)", "pytest (==8.3.4)", "pytest-cov (==6.0.0)", "ruff (==0.8.4)", "shellcheck-py (==0.10.0.1)", "shfmt-py (==3.7.0.1)", "sphinx-toolbox (==3.8.1)", "sphinx[test] (==8.1.3)", "types-docutils (==0.21.0.20241128)", "vulture (==2.14)", "yamlfix (==1.17.0)"] -release = ["check-wheel-contents (==0.6.1)"] - -[[package]] -name = "sphinx-substitution-extensions" -version = "2025.2.19" -description = "Extensions for Sphinx which allow for substitutions." -optional = false -python-versions = ">=3.11" -groups = ["main"] -markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\"" -files = [ - {file = "sphinx_substitution_extensions-2025.2.19-py2.py3-none-any.whl", hash = "sha256:dfdaa3a925ff5ab450ff89ae08e9989f90f04add362375f5c8e27309573e5343"}, - {file = "sphinx_substitution_extensions-2025.2.19.tar.gz", hash = "sha256:ecbb35e7ae210aef4e213a389e5095df503dd1260374640c426d843ad64c8f86"}, -] - -[package.dependencies] -beartype = ">=0.18.5" -docutils = ">=0.19" -myst-parser = ">=4.0.0" -sphinx = ">=7.3.5" - -[package.extras] -dev = ["actionlint-py (==1.7.7.23)", "check-manifest (==0.50)", "deptry (==0.23.0)", "doc8 (==1.1.2)", "doccmd (==2025.2.19)", "docformatter (==1.7.5)", "interrogate (==1.7.0)", "mypy-strict-kwargs (==2024.12.25)", "mypy[faster-cache] (==1.15.0)", "pre-commit (==4.1.0)", "pyenchant (==3.3.0rc1)", "pylint (==3.3.4)", "pyproject-fmt (==2.5.1)", "pyright (==1.1.394)", "pyroma (==4.2)", "pytest (==8.3.4)", "pytest-cov (==6.0.0)", "ruff (==0.9.6)", "shellcheck-py (==0.10.0.1)", "shfmt-py (==3.7.0.1)", "sphinx-lint (==1.0.0)", "sphinx-toolbox (==3.8.2)", "sphinx[test] (==8.1.3)", "types-docutils (==0.21.0.20241128)", "vulture (==2.14)", "yamlfix (==1.17.0)"] -release = ["check-wheel-contents (==0.6.1)"] - -[[package]] -name = "sphinx-tabs" -version = "3.4.7" -description = "Tabbed views for Sphinx" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "sphinx-tabs-3.4.7.tar.gz", hash = "sha256:991ad4a424ff54119799ba1491701aa8130dd43509474aef45a81c42d889784d"}, - {file = "sphinx_tabs-3.4.7-py3-none-any.whl", hash = "sha256:c12d7a36fd413b369e9e9967a0a4015781b71a9c393575419834f19204bd1915"}, -] - -[package.dependencies] -docutils = "*" -pygments = "*" -sphinx = ">=1.8" - -[package.extras] -code-style = ["pre-commit (==2.13.0)"] -testing = ["bs4", "coverage", "pygments", "pytest (>=7.1,<8)", "pytest-cov", "pytest-regressions", "rinohtype"] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "2.0.0" -description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, - {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "2.0.0" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, - {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.1.0" -description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, - {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["html5lib", "pytest"] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -description = "A sphinx extension which renders display math in HTML via JavaScript" -optional = false -python-versions = ">=3.5" -groups = ["main"] -files = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] - -[package.extras] -test = ["flake8", "mypy", "pytest"] - -[[package]] -name = "sphinxcontrib-mermaid" -version = "1.0.0" -description = "Mermaid diagrams in yours Sphinx powered docs" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "sphinxcontrib_mermaid-1.0.0-py3-none-any.whl", hash = "sha256:60b72710ea02087f212028feb09711225fbc2e343a10d34822fe787510e1caa3"}, - {file = "sphinxcontrib_mermaid-1.0.0.tar.gz", hash = "sha256:2e8ab67d3e1e2816663f9347d026a8dee4a858acdd4ad32dd1c808893db88146"}, -] - -[package.dependencies] -pyyaml = "*" -sphinx = "*" - -[package.extras] -test = ["defusedxml", "myst-parser", "pytest", "ruff", "sphinx"] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "2.0.0" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, - {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["defusedxml (>=0.7.1)", "pytest"] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "2.0.0" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, - {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "starlette" -version = "0.47.1" -description = "The little ASGI library that shines." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "starlette-0.47.1-py3-none-any.whl", hash = "sha256:5e11c9f5c7c3f24959edbf2dffdc01bba860228acf657129467d8a7468591527"}, - {file = "starlette-0.47.1.tar.gz", hash = "sha256:aef012dd2b6be325ffa16698f9dc533614fb1cebd593a906b90dc1025529a79b"}, -] - -[package.dependencies] -anyio = ">=3.6.2,<5" -typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} - -[package.extras] -full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] - -[[package]] -name = "tomli" -version = "2.2.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["main"] -markers = "python_version == \"3.10\"" -files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, -] - -[[package]] -name = "tornado" -version = "4.5.3" -description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "tornado-4.5.3-cp35-cp35m-win32.whl", hash = "sha256:92b7ca81e18ba9ec3031a7ee73d4577ac21d41a0c9b775a9182f43301c3b5f8e"}, - {file = "tornado-4.5.3-cp35-cp35m-win_amd64.whl", hash = "sha256:b36298e9f63f18cad97378db2222c0e0ca6a55f6304e605515e05a25483ed51a"}, - {file = "tornado-4.5.3-cp36-cp36m-win32.whl", hash = "sha256:ab587996fe6fb9ce65abfda440f9b61e4f9f2cf921967723540679176915e4c3"}, - {file = "tornado-4.5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:5ef073ac6180038ccf99411fe05ae9aafb675952a2c8db60592d5daf8401f803"}, - {file = "tornado-4.5.3.tar.gz", hash = "sha256:6d14e47eab0e15799cf3cdcc86b0b98279da68522caace2bd7ce644287685f0a"}, -] - -[[package]] -name = "typer" -version = "0.16.0" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855"}, - {file = "typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b"}, -] - -[package.dependencies] -click = ">=8.0.0" -rich = ">=10.11.0" -shellingham = ">=1.3.0" -typing-extensions = ">=3.7.4.3" - -[[package]] -name = "typing-extensions" -version = "4.14.1" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, - {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, -] - -[[package]] -name = "urllib3" -version = "2.5.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, - {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "uvicorn" -version = "0.35.0" -description = "The lightning-fast ASGI server." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a"}, - {file = "uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01"}, -] - -[package.dependencies] -click = ">=7.0" -h11 = ">=0.8" -typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} - -[package.extras] -standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] - -[[package]] -name = "watchfiles" -version = "1.1.0" -description = "Simple, modern and high performance file watching and code reload in python." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "watchfiles-1.1.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc"}, - {file = "watchfiles-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df"}, - {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68"}, - {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc"}, - {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97"}, - {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c"}, - {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5"}, - {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9"}, - {file = "watchfiles-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72"}, - {file = "watchfiles-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc"}, - {file = "watchfiles-1.1.0-cp310-cp310-win32.whl", hash = "sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587"}, - {file = "watchfiles-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82"}, - {file = "watchfiles-1.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2"}, - {file = "watchfiles-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c"}, - {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d"}, - {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7"}, - {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c"}, - {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575"}, - {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8"}, - {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f"}, - {file = "watchfiles-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4"}, - {file = "watchfiles-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d"}, - {file = "watchfiles-1.1.0-cp311-cp311-win32.whl", hash = "sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2"}, - {file = "watchfiles-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12"}, - {file = "watchfiles-1.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a"}, - {file = "watchfiles-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179"}, - {file = "watchfiles-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5"}, - {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297"}, - {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0"}, - {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e"}, - {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee"}, - {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd"}, - {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f"}, - {file = "watchfiles-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4"}, - {file = "watchfiles-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f"}, - {file = "watchfiles-1.1.0-cp312-cp312-win32.whl", hash = "sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd"}, - {file = "watchfiles-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47"}, - {file = "watchfiles-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6"}, - {file = "watchfiles-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30"}, - {file = "watchfiles-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a"}, - {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc"}, - {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b"}, - {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895"}, - {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a"}, - {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b"}, - {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c"}, - {file = "watchfiles-1.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b"}, - {file = "watchfiles-1.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb"}, - {file = "watchfiles-1.1.0-cp313-cp313-win32.whl", hash = "sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9"}, - {file = "watchfiles-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7"}, - {file = "watchfiles-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5"}, - {file = "watchfiles-1.1.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1"}, - {file = "watchfiles-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339"}, - {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633"}, - {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011"}, - {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670"}, - {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf"}, - {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4"}, - {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20"}, - {file = "watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef"}, - {file = "watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb"}, - {file = "watchfiles-1.1.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297"}, - {file = "watchfiles-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018"}, - {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0"}, - {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12"}, - {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb"}, - {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77"}, - {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92"}, - {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e"}, - {file = "watchfiles-1.1.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b"}, - {file = "watchfiles-1.1.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259"}, - {file = "watchfiles-1.1.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f"}, - {file = "watchfiles-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e"}, - {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa"}, - {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8"}, - {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f"}, - {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e"}, - {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb"}, - {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147"}, - {file = "watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8"}, - {file = "watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db"}, - {file = "watchfiles-1.1.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:865c8e95713744cf5ae261f3067861e9da5f1370ba91fc536431e29b418676fa"}, - {file = "watchfiles-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42f92befc848bb7a19658f21f3e7bae80d7d005d13891c62c2cd4d4d0abb3433"}, - {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa0cc8365ab29487eb4f9979fd41b22549853389e22d5de3f134a6796e1b05a4"}, - {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90ebb429e933645f3da534c89b29b665e285048973b4d2b6946526888c3eb2c7"}, - {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c588c45da9b08ab3da81d08d7987dae6d2a3badd63acdb3e206a42dbfa7cb76f"}, - {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c55b0f9f68590115c25272b06e63f0824f03d4fc7d6deed43d8ad5660cabdbf"}, - {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd17a1e489f02ce9117b0de3c0b1fab1c3e2eedc82311b299ee6b6faf6c23a29"}, - {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da71945c9ace018d8634822f16cbc2a78323ef6c876b1d34bbf5d5222fd6a72e"}, - {file = "watchfiles-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:51556d5004887045dba3acdd1fdf61dddea2be0a7e18048b5e853dcd37149b86"}, - {file = "watchfiles-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04e4ed5d1cd3eae68c89bcc1a485a109f39f2fd8de05f705e98af6b5f1861f1f"}, - {file = "watchfiles-1.1.0-cp39-cp39-win32.whl", hash = "sha256:c600e85f2ffd9f1035222b1a312aff85fd11ea39baff1d705b9b047aad2ce267"}, - {file = "watchfiles-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3aba215958d88182e8d2acba0fdaf687745180974946609119953c0e112397dc"}, - {file = "watchfiles-1.1.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5"}, - {file = "watchfiles-1.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d"}, - {file = "watchfiles-1.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea"}, - {file = "watchfiles-1.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6"}, - {file = "watchfiles-1.1.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3"}, - {file = "watchfiles-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c"}, - {file = "watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432"}, - {file = "watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792"}, - {file = "watchfiles-1.1.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7b3443f4ec3ba5aa00b0e9fa90cf31d98321cbff8b925a7c7b84161619870bc9"}, - {file = "watchfiles-1.1.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7049e52167fc75fc3cc418fc13d39a8e520cbb60ca08b47f6cedb85e181d2f2a"}, - {file = "watchfiles-1.1.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54062ef956807ba806559b3c3d52105ae1827a0d4ab47b621b31132b6b7e2866"}, - {file = "watchfiles-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a7bd57a1bb02f9d5c398c0c1675384e7ab1dd39da0ca50b7f09af45fa435277"}, - {file = "watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575"}, -] - -[package.dependencies] -anyio = ">=3.0.0" - -[[package]] -name = "websockets" -version = "15.0.1" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, - {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, - {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, - {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, - {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, - {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, - {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, - {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, - {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, - {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, - {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, - {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, - {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, - {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, - {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, - {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, - {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, - {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, - {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, - {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, - {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, - {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, - {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, - {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, - {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, - {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, - {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, - {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, - {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, - {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, - {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, - {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, - {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, - {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, - {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, - {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, - {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, - {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, - {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, - {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, - {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, - {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, - {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, - {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, - {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, - {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, - {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, - {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, - {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, - {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, - {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, - {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, - {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, - {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, - {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, - {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, - {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, -] - -[[package]] -name = "zope-event" -version = "5.1" -description = "Very basic event publishing system" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "zope_event-5.1-py3-none-any.whl", hash = "sha256:53de8f0e9f61dc0598141ac591f49b042b6d74784dab49971b9cc91d0f73a7df"}, - {file = "zope_event-5.1.tar.gz", hash = "sha256:a153660e0c228124655748e990396b9d8295d6e4f546fa1b34f3319e1c666e7f"}, -] - -[package.dependencies] -setuptools = "*" - -[package.extras] -docs = ["Sphinx"] -test = ["zope.testrunner"] - -[[package]] -name = "zope-interface" -version = "7.2" -description = "Interfaces for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "zope.interface-7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce290e62229964715f1011c3dbeab7a4a1e4971fd6f31324c4519464473ef9f2"}, - {file = "zope.interface-7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05b910a5afe03256b58ab2ba6288960a2892dfeef01336dc4be6f1b9ed02ab0a"}, - {file = "zope.interface-7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550f1c6588ecc368c9ce13c44a49b8d6b6f3ca7588873c679bd8fd88a1b557b6"}, - {file = "zope.interface-7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ef9e2f865721553c6f22a9ff97da0f0216c074bd02b25cf0d3af60ea4d6931d"}, - {file = "zope.interface-7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27f926f0dcb058211a3bb3e0e501c69759613b17a553788b2caeb991bed3b61d"}, - {file = "zope.interface-7.2-cp310-cp310-win_amd64.whl", hash = "sha256:144964649eba4c5e4410bb0ee290d338e78f179cdbfd15813de1a664e7649b3b"}, - {file = "zope.interface-7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1909f52a00c8c3dcab6c4fad5d13de2285a4b3c7be063b239b8dc15ddfb73bd2"}, - {file = "zope.interface-7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80ecf2451596f19fd607bb09953f426588fc1e79e93f5968ecf3367550396b22"}, - {file = "zope.interface-7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:033b3923b63474800b04cba480b70f6e6243a62208071fc148354f3f89cc01b7"}, - {file = "zope.interface-7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a102424e28c6b47c67923a1f337ede4a4c2bba3965b01cf707978a801fc7442c"}, - {file = "zope.interface-7.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25e6a61dcb184453bb00eafa733169ab6d903e46f5c2ace4ad275386f9ab327a"}, - {file = "zope.interface-7.2-cp311-cp311-win_amd64.whl", hash = "sha256:3f6771d1647b1fc543d37640b45c06b34832a943c80d1db214a37c31161a93f1"}, - {file = "zope.interface-7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:086ee2f51eaef1e4a52bd7d3111a0404081dadae87f84c0ad4ce2649d4f708b7"}, - {file = "zope.interface-7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21328fcc9d5b80768bf051faa35ab98fb979080c18e6f84ab3f27ce703bce465"}, - {file = "zope.interface-7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6dd02ec01f4468da0f234da9d9c8545c5412fef80bc590cc51d8dd084138a89"}, - {file = "zope.interface-7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e7da17f53e25d1a3bde5da4601e026adc9e8071f9f6f936d0fe3fe84ace6d54"}, - {file = "zope.interface-7.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cab15ff4832580aa440dc9790b8a6128abd0b88b7ee4dd56abacbc52f212209d"}, - {file = "zope.interface-7.2-cp312-cp312-win_amd64.whl", hash = "sha256:29caad142a2355ce7cfea48725aa8bcf0067e2b5cc63fcf5cd9f97ad12d6afb5"}, - {file = "zope.interface-7.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:3e0350b51e88658d5ad126c6a57502b19d5f559f6cb0a628e3dc90442b53dd98"}, - {file = "zope.interface-7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15398c000c094b8855d7d74f4fdc9e73aa02d4d0d5c775acdef98cdb1119768d"}, - {file = "zope.interface-7.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:802176a9f99bd8cc276dcd3b8512808716492f6f557c11196d42e26c01a69a4c"}, - {file = "zope.interface-7.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb23f58a446a7f09db85eda09521a498e109f137b85fb278edb2e34841055398"}, - {file = "zope.interface-7.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a71a5b541078d0ebe373a81a3b7e71432c61d12e660f1d67896ca62d9628045b"}, - {file = "zope.interface-7.2-cp313-cp313-win_amd64.whl", hash = "sha256:4893395d5dd2ba655c38ceb13014fd65667740f09fa5bb01caa1e6284e48c0cd"}, - {file = "zope.interface-7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d3a8ffec2a50d8ec470143ea3d15c0c52d73df882eef92de7537e8ce13475e8a"}, - {file = "zope.interface-7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:31d06db13a30303c08d61d5fb32154be51dfcbdb8438d2374ae27b4e069aac40"}, - {file = "zope.interface-7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e204937f67b28d2dca73ca936d3039a144a081fc47a07598d44854ea2a106239"}, - {file = "zope.interface-7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:224b7b0314f919e751f2bca17d15aad00ddbb1eadf1cb0190fa8175edb7ede62"}, - {file = "zope.interface-7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf95683cde5bc7d0e12d8e7588a3eb754d7c4fa714548adcd96bdf90169f021"}, - {file = "zope.interface-7.2-cp38-cp38-win_amd64.whl", hash = "sha256:7dc5016e0133c1a1ec212fc87a4f7e7e562054549a99c73c8896fa3a9e80cbc7"}, - {file = "zope.interface-7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bd449c306ba006c65799ea7912adbbfed071089461a19091a228998b82b1fdb"}, - {file = "zope.interface-7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a19a6cc9c6ce4b1e7e3d319a473cf0ee989cbbe2b39201d7c19e214d2dfb80c7"}, - {file = "zope.interface-7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72cd1790b48c16db85d51fbbd12d20949d7339ad84fd971427cf00d990c1f137"}, - {file = "zope.interface-7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52e446f9955195440e787596dccd1411f543743c359eeb26e9b2c02b077b0519"}, - {file = "zope.interface-7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ad9913fd858274db8dd867012ebe544ef18d218f6f7d1e3c3e6d98000f14b75"}, - {file = "zope.interface-7.2-cp39-cp39-win_amd64.whl", hash = "sha256:1090c60116b3da3bfdd0c03406e2f14a1ff53e5771aebe33fec1edc0a350175d"}, - {file = "zope.interface-7.2.tar.gz", hash = "sha256:8b49f1a3d1ee4cdaf5b32d2e738362c7f5e40ac8b46dd7d1a65e82a4872728fe"}, -] - -[package.dependencies] -setuptools = "*" - -[package.extras] -docs = ["Sphinx", "furo", "repoze.sphinx.autointerface"] -test = ["coverage[toml]", "zope.event", "zope.testing"] -testing = ["coverage[toml]", "zope.event", "zope.testing"] - -[metadata] -lock-version = "2.1" -python-versions = "^3.10" -content-hash = "432ab6b3744422cd7526b9d863d4f2de57fec071049ecde4b690e15b6dd36551" diff --git a/docs/pyproject.toml b/docs/pyproject.toml index 3d178f1936..762a4f2e49 100644 --- a/docs/pyproject.toml +++ b/docs/pyproject.toml @@ -1,30 +1,60 @@ -[tool.poetry] +[project] name = "python-driver-docs" version = "0.1.0" description = "ScyllaDB Python Driver Docs" -authors = ["Python Driver Contributors"] +authors = [{ name = "ScyllaDB" }] package-mode = false +requires-python = ">=3.13,<3.14" -[tool.poetry.dependencies] -eventlet = "^0.33.3" -futures = "2.2.0" -gevent = "^23.9.1" -gremlinpython = "3.4.7" -python = "^3.10" -pygments = "^2.18.0" -recommonmark = "0.7.1" -redirects_cli = "~0.1.2" -sphinx-autobuild = "^2024.4.19" -sphinx-sitemap = "^2.6.0" -sphinx-scylladb-theme = "^1.8.1" -sphinx-multiversion-scylla = "^0.3.1" -Sphinx = "^7.3.7" -scales = "^1.0.9" -six = ">=1.9" -tornado = ">=4.0,<5.0" -scylla-driver = { path = "../", develop = true } +dependencies = [ + "eventlet>=0.40.3,<1.0.0", + "gevent>=25.9.1,<26.0.0", + "gremlinpython==3.7.4", + "pygments>=2.19.2,<3.0.0", + "myst-parser>=5.0.0", + "redirects_cli~=0.1.3", + "sphinx-autobuild>=2025.0.0,<2026.0.0", + "sphinx-sitemap>=2.8.0,<3.0.0", + "sphinx-scylladb-theme>=1.9.1", + "sphinx-multiversion-scylla>=0.3.2,<1.0.0", + "sphinx>=9.0", + "six>=1.9", + "tornado>=6.5,<7.0", +] +[dependency-groups] +# Add any dev-only tools here; example shown +dev = ["hatchling==1.29.0"] + +[tool.uv.sources] +# Keep the driver editable from the parent directory +scylla-driver = { path = "../", editable = true } [build-system] -requires = ["poetry>=1.8.0"] -build-backend = "poetry.masonry.api" +requires = ["hatchling==1.29.0"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] + +# We don't ship a Python package/module; just include the docs tree as data. +# Using 'include' avoids the "Unable to determine which files to ship" error. +include = [ + "**/*.rst", + "**/*.md", + "**/*.txt", + "**/*.py", # e.g., conf.py and any Sphinx helpers + "**/*.yml", + "**/*.yaml", + "**/*.json", + "**/*.css", + "**/*.js", + "**/*.html", + "_static/**", + "_templates/**", +] + +exclude = [ + "**/__pycache__/**", + "**/*.pyc", + ".venv/**", +] diff --git a/docs/scylla-specific.rst b/docs/scylla-specific.rst index e9caaa8793..4b28781f1c 100644 --- a/docs/scylla-specific.rst +++ b/docs/scylla-specific.rst @@ -91,7 +91,7 @@ New Error Types session = cluster.connect() session.execute(""" CREATE KEYSPACE IF NOT EXISTS keyspace1 - WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} + WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '1'} """) session.execute("USE keyspace1") @@ -111,6 +111,40 @@ New Error Types raise +Paging Differences +------------------ + +ScyllaDB has a built-in 1MB page size limit that Cassandra does not have. This means that even if you set a high ``fetch_size`` (e.g., 10000 rows), ScyllaDB may return fewer rows per page if the total response size exceeds 1MB. + +This behavior is particularly noticeable when: + +* Working with wide tables (many columns) +* Using ``NumpyProtocolHandler`` where you want large arrays per page +* Columns contain large values (blobs, long strings, etc.) + +For example, with a table containing 1000 columns, you might receive only 30-50 rows per page even with ``fetch_size=10000``. + +**Workaround:** If you need to receive more rows per page (up to ScyllaDB's 1MB limit), set ``default_fetch_size`` to ``None``: + +.. code:: python + + from cassandra.cluster import Cluster + from cassandra.protocol import NumpyProtocolHandler + from cassandra.query import tuple_factory + + cluster = Cluster() + session = cluster.connect(keyspace="mykeyspace") + session.row_factory = tuple_factory + session.client_protocol_handler = NumpyProtocolHandler + session.default_fetch_size = None # Let ScyllaDB control page sizes + + results = session.execute("SELECT * FROM wide_table") + +With ``default_fetch_size = None``, the driver won't request a specific page size, allowing ScyllaDB to fill pages up to its 1MB limit. This results in larger arrays when using ``NumpyProtocolHandler``. + +For more details on paging, see :ref:`query-paging`. + + Tablet Awareness ---------------- diff --git a/docs/security.rst b/docs/security.rst index c30189562f..5c8645e685 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -37,23 +37,6 @@ If these do not suit your needs, you may need to create your own subclasses of :class:`~.AuthProvider` and :class:`~.Authenticator`. You can use the Sasl classes as example implementations. -Protocol v1 Authentication -^^^^^^^^^^^^^^^^^^^^^^^^^^ -When working with Cassandra 1.2 (or a higher version with -:attr:`~.Cluster.protocol_version` set to ``1``), you will not pass in -an :class:`~.AuthProvider` instance. Instead, you should pass in a -function that takes one argument, the IP address of a host, and returns -a dict of credentials with a ``username`` and ``password`` key: - -.. code-block:: python - - from cassandra.cluster import Cluster - - def get_credentials(host_address): - return {'username': 'joe', 'password': '1234'} - - cluster = Cluster(auth_provider=get_credentials, protocol_version=1) - SSL --- SSL should be used when client encryption is enabled in Cassandra. @@ -77,14 +60,10 @@ as described in the following examples or implement your own :class:`~.connectio :class:`~.connection.EndPointFactory`. -The following examples assume you have generated your Cassandra certificate and -keystore files with these intructions: - -* `Setup SSL Cert `_ +The following examples assume you have generated your Scylla certificate and +keystore files with these instructions: -It might be also useful to learn about the different levels of identity verification to understand the examples: - -* `Using SSL in DSE drivers `_ +* `Scylla TLS/SSL Guide `_ SSL with Twisted or Eventlet ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -187,7 +166,7 @@ The cassandra configuration:: keystore: /path/to/127.0.0.1.keystore keystore_password: myStorePass require_client_auth: true - truststore: /path/to/dse-truststore.jks + truststore: /path/to/truststore.jks truststore_password: myStorePass The Python ``ssl`` APIs require the certificate in PEM format. First, create a certificate @@ -321,101 +300,11 @@ For example: cluster = Cluster(ssl_options=ssl_opts) This is only an example to show how to pass the ssl parameters. Consider reading -the `python ssl documentation `__ for -your configuration. For further reading, Andrew Mussey has published a thorough guide on -`Using SSL with the DataStax Python driver `_. +the `python ssl documentation `__ for +your configuration. SSL with Twisted ++++++++++++++++ In case the twisted event loop is used pyOpenSSL must be installed or an exception will be risen. Also to set the ``ssl_version`` and ``cert_reqs`` in ``ssl_opts`` the appropriate constants from pyOpenSSL are expected. - -DSE Authentication ------------------- -When authenticating against DSE, the Cassandra driver provides two auth providers that work both with legacy kerberos and Cassandra authenticators, -as well as the new DSE Unified Authentication. This allows client to configure this auth provider independently, -and in advance of any server upgrade. These auth providers are configured in the same way as any previous implementation:: - - from cassandra.auth import DSEGSSAPIAuthProvider - auth_provider = DSEGSSAPIAuthProvider(service='dse', qops=["auth"]) - cluster = Cluster(auth_provider=auth_provider) - session = cluster.connect() - -Implementations are :attr:`.DSEPlainTextAuthProvider`, :class:`.DSEGSSAPIAuthProvider` and :class:`.SaslAuthProvider`. - -DSE Unified Authentication -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -With DSE (>=5.1), unified Authentication allows you to: - -* Proxy Login: Authenticate using a fixed set of authentication credentials but allow authorization of resources based another user id. -* Proxy Execute: Authenticate using a fixed set of authentication credentials but execute requests based on another user id. - -Proxy Login -+++++++++++ - -Proxy login allows you to authenticate with a user but act as another one. You need to ensure the authenticated user has the permission to use the authorization of resources of the other user. ie. this example will allow the `server` user to authenticate as usual but use the authorization of `user1`: - -.. code-block:: text - - GRANT PROXY.LOGIN on role user1 to server - -then you can do the proxy authentication.... - -.. code-block:: python - - from cassandra.cluster import Cluster - from cassandra.auth import SaslAuthProvider - - sasl_kwargs = { - "service": 'dse', - "mechanism":"PLAIN", - "username": 'server', - 'password': 'server', - 'authorization_id': 'user1' - } - - auth_provider = SaslAuthProvider(**sasl_kwargs) - c = Cluster(auth_provider=auth_provider) - s = c.connect() - s.execute(...) # all requests will be executed as 'user1' - -If you are using kerberos, you can use directly :class:`.DSEGSSAPIAuthProvider` and pass the authorization_id, like this: - -.. code-block:: python - - from cassandra.cluster import Cluster - from cassandra.auth import DSEGSSAPIAuthProvider - - # Ensure the kerberos ticket of the server user is set with the kinit utility. - auth_provider = DSEGSSAPIAuthProvider(service='dse', qops=["auth"], principal="server@DATASTAX.COM", - authorization_id='user1@DATASTAX.COM') - c = Cluster(auth_provider=auth_provider) - s = c.connect() - s.execute(...) # all requests will be executed as 'user1' - - -Proxy Execute -+++++++++++++ - -Proxy execute allows you to execute requests as another user than the authenticated one. You need to ensure the authenticated user has the permission to use the authorization of resources of the specified user. ie. this example will allow the `server` user to execute requests as `user1`: - -.. code-block:: text - - GRANT PROXY.EXECUTE on role user1 to server - -then you can do a proxy execute... - -.. code-block:: python - - from cassandra.cluster import Cluster - from cassandra.auth import DSEPlainTextAuthProvider, - - auth_provider = DSEPlainTextAuthProvider('server', 'server') - - c = Cluster(auth_provider=auth_provider) - s = c.connect() - s.execute('select * from k.t;', execute_as='user1') # the request will be executed as 'user1' - -Please see the `official documentation `_ for more details on the feature and configuration process. diff --git a/docs/upgrading.rst b/docs/upgrading.rst deleted file mode 100644 index d0d40e46b5..0000000000 --- a/docs/upgrading.rst +++ /dev/null @@ -1,353 +0,0 @@ -Upgrading -========= - -.. toctree:: - :maxdepth: 1 - -Installation -^^^^^^^^^^^^ - -Only the `scylla-driver` package should be installed. `dse-driver` and `dse-graph` -are not required anymore:: - - pip install scylla-driver - -See :doc:`installation` for more details. - -Import from the cassandra module -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -There is no `dse` module, so you should import from the `cassandra` module. You -need to change only the first module of your import statements, not the submodules. - -.. code-block:: python - - from dse.cluster import Cluster, EXEC_PROFILE_GRAPH_DEFAULT - from dse.auth import PlainTextAuthProvider - from dse.policies import WhiteListRoundRobinPolicy - - # becomes - - from cassandra.cluster import Cluster, EXEC_PROFILE_GRAPH_DEFAULT - from cassandra.auth import PlainTextAuthProvider - from cassandra.policies import WhiteListRoundRobinPolicy - -Also note that the cassandra.hosts module doesn't exist in scylla-driver. This -module is named cassandra.pool. - -Session.execute and Session.execute_async API -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Although it is not common to use this API with positional arguments, it is -important to be aware that the `host` and `execute_as` parameters have had -their positional order swapped. This is only because `execute_as` was added -in dse-driver before `host`. - -See :meth:`.Session.execute`. - -Deprecations -^^^^^^^^^^^^ - -These changes are optional, but recommended: - -* Use :class:`~.policies.DefaultLoadBalancingPolicy` instead of DSELoadBalancingPolicy. - -Upgrading to 3.0 ----------------- -Version 3.0 of the DataStax Python driver for Apache Cassandra -adds support for Cassandra 3.0 while maintaining support for -previously supported versions. In addition to substantial internal rework, -there are several updates to the API that integrators will need -to consider: - -Default consistency is now ``LOCAL_ONE`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Previous value was ``ONE``. The new value is introduced to mesh with the default -DC-aware load balancing policy and to match other drivers. - -Execution API Updates -^^^^^^^^^^^^^^^^^^^^^ -Result return normalization ---------------------------- -`PYTHON-368 `_ - -Previously results would be returned as a ``list`` of rows for result rows -up to ``fetch_size``, and ``PagedResult`` afterward. This could break -application code that assumed one type and got another. - -Now, all results are returned as an iterable :class:`~.ResultSet`. - -The preferred way to consume results of unknown size is to iterate through -them, letting automatic paging occur as they are consumed. - -.. code-block:: python - - results = session.execute("SELECT * FROM system.local") - for row in results: - process(row) - -If the expected size of the results is known, it is still possible to -materialize a list using the iterator: - -.. code-block:: python - - results = session.execute("SELECT * FROM system.local") - row_list = list(results) - -For backward compatibility, :class:`~.ResultSet` supports indexing. When -accessed at an index, a `~.ResultSet` object will materialize all its pages: - -.. code-block:: python - - results = session.execute("SELECT * FROM system.local") - first_result = results[0] # materializes results, fetching all pages - -This can send requests and load (possibly large) results into memory, so -`~.ResultSet` will log a warning on implicit materialization. - -Trace information is not attached to executed Statements --------------------------------------------------------- -`PYTHON-318 `_ - -Previously trace data was attached to Statements if tracing was enabled. This -could lead to confusion if the same statement was used for multiple executions. - -Now, trace data is associated with the ``ResponseFuture`` and ``ResultSet`` -returned for each query: - -:meth:`.ResponseFuture.get_query_trace()` - -:meth:`.ResponseFuture.get_all_query_traces()` - -:meth:`.ResultSet.get_query_trace()` - -:meth:`.ResultSet.get_all_query_traces()` - -Binding named parameters now ignores extra names ------------------------------------------------- -`PYTHON-178 `_ - -Previously, :meth:`.BoundStatement.bind()` would raise if a mapping -was passed with extra names not found in the prepared statement. - -Behavior in 3.0+ is to ignore extra names. - -blist removed as soft dependency -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -`PYTHON-385 `_ - -Previously the driver had a soft dependency on ``blist sortedset``, using -that where available and using an internal fallback where possible. - -Now, the driver never chooses the ``blist`` variant, instead returning the -internal :class:`.util.SortedSet` for all ``set`` results. The class implements -all standard set operations, so no integration code should need to change unless -it explicitly checks for ``sortedset`` type. - -Metadata API Updates -^^^^^^^^^^^^^^^^^^^^ -`PYTHON-276 `_, `PYTHON-408 `_, `PYTHON-400 `_, `PYTHON-422 `_ - -Cassandra 3.0 brought a substantial overhaul to the internal schema metadata representation. -This version of the driver supports that metadata in addition to the legacy version. Doing so -also brought some changes to the metadata model. - -The present API is documented: :any:`cassandra.metadata`. Changes highlighted below: - -* All types are now exposed as CQL types instead of types derived from the internal server implementation -* Some metadata attributes have changed names to match current nomenclature (for example, :attr:`.Index.kind` in place of ``Index.type``). -* Some metadata attributes removed - - * ``TableMetadata.keyspace`` reference replaced with :attr:`.TableMetadata.keyspace_name` - * ``ColumnMetadata.index`` is removed table- and keyspace-level mappings are still maintained - -Several deprecated features are removed -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -`PYTHON-292 `_ - -* ``ResponseFuture.result`` timeout parameter is removed, use ``Session.execute`` timeout instead (`031ebb0 `_) -* ``Cluster.refresh_schema`` removed, use ``Cluster.refresh_*_metadata`` instead (`419fcdf `_) -* ``Cluster.submit_schema_refresh`` removed (`574266d `_) -* ``cqltypes`` time/date functions removed, use ``util`` entry points instead (`bb984ee `_) -* ``decoder`` module removed (`e16a073 `_) -* ``TableMetadata.keyspace`` attribute replaced with ``keyspace_name`` (`cc94073 `_) -* ``cqlengine.columns.TimeUUID.from_datetime`` removed, use ``util`` variant instead (`96489cc `_) -* ``cqlengine.columns.Float(double_precision)`` parameter removed, use ``columns.Double`` instead (`a2d3a98 `_) -* ``cqlengine`` keyspace management functions are removed in favor of the strategy-specific entry points (`4bd5909 `_) -* ``cqlengine.Model.__polymorphic_*__`` attributes removed, use ``__discriminator*`` attributes instead (`9d98c8e `_) -* ``cqlengine.statements`` will no longer warn about list list prepend behavior (`79efe97 `_) - - -Upgrading to 2.1 from 2.0 -------------------------- -Version 2.1 of the DataStax Python driver for Apache Cassandra -adds support for Cassandra 2.1 and version 3 of the native protocol. - -Cassandra 1.2, 2.0, and 2.1 are all supported. However, 1.2 only -supports protocol version 1, and 2.0 only supports versions 1 and -2, so some features may not be available. - -Using the v3 Native Protocol -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -By default, the driver will attempt to use version 2 of the -native protocol. To use version 3, you must explicitly -set the :attr:`~.Cluster.protocol_version`: - -.. code-block:: python - - from cassandra.cluster import Cluster - - cluster = Cluster(protocol_version=3) - -Note that protocol version 3 is only supported by Cassandra 2.1+. - -In future releases, the driver may default to using protocol version -3. - -Working with User-Defined Types -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Cassandra 2.1 introduced the ability to define new types:: - - USE KEYSPACE mykeyspace; - - CREATE TYPE address (street text, city text, zip int); - -The driver generally expects you to use instances of a specific -class to represent column values of this type. You can let the -driver know what class to use with :meth:`.Cluster.register_user_type`: - -.. code-block:: python - - cluster = Cluster() - - class Address(object): - - def __init__(self, street, city, zipcode): - self.street = street - self.city = text - self.zipcode = zipcode - - cluster.register_user_type('mykeyspace', 'address', Address) - -When inserting data for ``address`` columns, you should pass in -instances of ``Address``. When querying data, ``address`` column -values will be instances of ``Address``. - -If no class is registered for a user-defined type, query results -will use a ``namedtuple`` class and data may only be inserted -though prepared statements. - -See :ref:`udts` for more details. - -Customizing Encoders for Non-prepared Statements -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Starting with version 2.1 of the driver, it is possible to customize -how Python types are converted to CQL literals when working with -non-prepared statements. This is done on a per-:class:`~.Session` -basis through :attr:`.Session.encoder`: - -.. code-block:: python - - cluster = Cluster() - session = cluster.connect() - session.encoder.mapping[tuple] = session.encoder.cql_encode_tuple - -See :ref:`type-conversions` for the table of default CQL literal conversions. - -Using Client-Side Protocol-Level Timestamps -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -With version 3 of the native protocol, timestamps may be supplied by the -client at the protocol level. (Normally, if they are not specified within -the CQL query itself, a timestamp is generated server-side.) - -When :attr:`~.Cluster.protocol_version` is set to 3 or higher, the driver -will automatically use client-side timestamps with microsecond precision -unless :attr:`.Session.use_client_timestamp` is changed to :const:`False`. -If a timestamp is specified within the CQL query, it will override the -timestamp generated by the driver. - -Upgrading to 2.0 from 1.x -------------------------- -Version 2.0 of the DataStax Python driver for Apache Cassandra -includes some notable improvements over version 1.x. This version -of the driver supports Cassandra 1.2, 2.0, and 2.1. However, not -all features may be used with Cassandra 1.2, and some new features -in 2.1 are not yet supported. - -Using the v2 Native Protocol -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -By default, the driver will attempt to use version 2 of Cassandra's -native protocol. You can explicitly set the protocol version to -2, though: - -.. code-block:: python - - from cassandra.cluster import Cluster - - cluster = Cluster(protocol_version=2) - -When working with Cassandra 1.2, you will need to -explicitly set the :attr:`~.Cluster.protocol_version` to 1: - -.. code-block:: python - - from cassandra.cluster import Cluster - - cluster = Cluster(protocol_version=1) - -Automatic Query Paging -^^^^^^^^^^^^^^^^^^^^^^ -Version 2 of the native protocol adds support for automatic query -paging, which can make dealing with large result sets much simpler. - -See :ref:`query-paging` for full details. - -Protocol-Level Batch Statements -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -With version 1 of the native protocol, batching of statements required -using a `BATCH cql query `_. -With version 2 of the native protocol, you can now batch statements at -the protocol level. This allows you to use many different prepared -statements within a single batch. - -See :class:`~.query.BatchStatement` for details and usage examples. - -SASL-based Authentication -^^^^^^^^^^^^^^^^^^^^^^^^^ -Also new in version 2 of the native protocol is SASL-based authentication. -See the section on :ref:`security` for details and examples. - -Lightweight Transactions -^^^^^^^^^^^^^^^^^^^^^^^^ -`Lightweight transactions `_ are another new feature. To use lightweight transactions, add ``IF`` clauses -to your CQL queries and set the :attr:`~.Statement.serial_consistency_level` -on your statements. - -Calling Cluster.shutdown() -^^^^^^^^^^^^^^^^^^^^^^^^^^ -In order to fix some issues around garbage collection and unclean interpreter -shutdowns, version 2.0 of the driver requires you to call :meth:`.Cluster.shutdown()` -on your :class:`~.Cluster` objects when you are through with them. -This helps to guarantee a clean shutdown. - -Deprecations -^^^^^^^^^^^^ -The following functions have moved from ``cassandra.decoder`` to ``cassandra.query``. -The original functions have been left in place with a :exc:`DeprecationWarning` for -now: - -* :attr:`cassandra.decoder.tuple_factory` has moved to - :attr:`cassandra.query.tuple_factory` -* :attr:`cassandra.decoder.named_tuple_factory` has moved to - :attr:`cassandra.query.named_tuple_factory` -* :attr:`cassandra.decoder.dict_factory` has moved to - :attr:`cassandra.query.dict_factory` -* :attr:`cassandra.decoder.ordered_dict_factory` has moved to - :attr:`cassandra.query.ordered_dict_factory` - -Dependency Changes -^^^^^^^^^^^^^^^^^^ -The following dependencies have officially been made optional: - -* ``scales`` -* ``blist`` diff --git a/docs/uv.lock b/docs/uv.lock new file mode 100644 index 0000000000..515e37abba --- /dev/null +++ b/docs/uv.lock @@ -0,0 +1,1211 @@ +version = 1 +revision = 3 +requires-python = "==3.13.*" + +[[package]] +name = "aenum" +version = "3.1.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/7a/61ed58e8be9e30c3fe518899cc78c284896d246d51381bab59b5db11e1f3/aenum-3.1.16.tar.gz", hash = "sha256:bfaf9589bdb418ee3a986d85750c7318d9d2839c1b1a1d6fe8fc53ec201cf140", size = 137693, upload-time = "2026-01-12T22:34:38.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/52/6ad8f63ec8da1bf40f96996d25d5b650fdd38f5975f8c813732c47388f18/aenum-3.1.16-py3-none-any.whl", hash = "sha256:9035092855a98e41b66e3d0998bd7b96280e85ceb3a04cc035636138a1943eaf", size = 165627, upload-time = "2025-04-25T03:17:58.89Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, +] + +[[package]] +name = "async-timeout" +version = "4.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/d6/21b30a550dafea84b1b8eee21b5e23fa16d010ae006011221f33dcd8d7f8/async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", size = 8345, upload-time = "2023-08-10T16:35:56.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/fa/e01228c2938de91d47b307831c62ab9e4001e747789d0b05baf779a6488c/async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028", size = 5721, upload-time = "2023-08-10T16:35:55.203Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "beartype" +version = "0.22.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/1d/794ae2acaa67c8b216d91d5919da2606c2bb14086849ffde7f5555f3a3a5/beartype-0.22.8.tar.gz", hash = "sha256:b19b21c9359722ee3f7cc433f063b3e13997b27ae8226551ea5062e621f61165", size = 1602262, upload-time = "2025-12-03T05:11:10.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2a/fbcbf5a025d3e71ddafad7efd43e34ec4362f4d523c3c471b457148fb211/beartype-0.22.8-py3-none-any.whl", hash = "sha256:b832882d04e41a4097bab9f63e6992bc6de58c414ee84cba9b45b67314f5ab2e", size = 1331895, upload-time = "2025-12-03T05:11:08.373Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[package]] +name = "eventlet" +version = "0.40.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "greenlet" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/d8/f72d8583db7c559445e0e9500a9b9787332370c16980802204a403634585/eventlet-0.40.4.tar.gz", hash = "sha256:69bef712b1be18b4930df6f0c495d2a882bf7b63aa111e7b6eeff461cfcaf26f", size = 565920, upload-time = "2025-11-26T13:57:31.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/6d/8e1fa901f6a8307f90e7bd932064e27a0062a4a7a16af38966a9c3293c52/eventlet-0.40.4-py3-none-any.whl", hash = "sha256:6326c6d0bf55810bece151f7a5750207c610f389ba110ffd1541ed6e5215485b", size = 364588, upload-time = "2025-11-26T13:57:29.09Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "gevent" +version = "25.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation == 'CPython' and sys_platform == 'win32'" }, + { name = "greenlet", marker = "platform_python_implementation == 'CPython'" }, + { name = "zope-event" }, + { name = "zope-interface" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/48/b3ef2673ffb940f980966694e40d6d32560f3ffa284ecaeb5ea3a90a6d3f/gevent-25.9.1.tar.gz", hash = "sha256:adf9cd552de44a4e6754c51ff2e78d9193b7fa6eab123db9578a210e657235dd", size = 5059025, upload-time = "2025-09-17T16:15:34.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/77/b97f086388f87f8ad3e01364f845004aef0123d4430241c7c9b1f9bde742/gevent-25.9.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:4f84591d13845ee31c13f44bdf6bd6c3dbf385b5af98b2f25ec328213775f2ed", size = 2973739, upload-time = "2025-09-17T14:53:30.279Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/9d5f204ead343e5b27bbb2fedaec7cd0009d50696b2266f590ae845d0331/gevent-25.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9cdbb24c276a2d0110ad5c978e49daf620b153719ac8a548ce1250a7eb1b9245", size = 1809165, upload-time = "2025-09-17T15:41:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/10/3e/791d1bf1eb47748606d5f2c2aa66571f474d63e0176228b1f1fd7b77ab37/gevent-25.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:88b6c07169468af631dcf0fdd3658f9246d6822cc51461d43f7c44f28b0abb82", size = 1890638, upload-time = "2025-09-17T15:49:02.45Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5c/9ad0229b2b4d81249ca41e4f91dd8057deaa0da6d4fbe40bf13cdc5f7a47/gevent-25.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b7bb0e29a7b3e6ca9bed2394aa820244069982c36dc30b70eb1004dd67851a48", size = 1857118, upload-time = "2025-09-17T15:49:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/49/2a/3010ed6c44179a3a5c5c152e6de43a30ff8bc2c8de3115ad8733533a018f/gevent-25.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2951bb070c0ee37b632ac9134e4fdaad70d2e660c931bb792983a0837fe5b7d7", size = 2111598, upload-time = "2025-09-17T15:15:15.226Z" }, + { url = "https://files.pythonhosted.org/packages/08/75/6bbe57c19a7aa4527cc0f9afcdf5a5f2aed2603b08aadbccb5bf7f607ff4/gevent-25.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4e17c2d57e9a42e25f2a73d297b22b60b2470a74be5a515b36c984e1a246d47", size = 1829059, upload-time = "2025-09-17T15:52:42.596Z" }, + { url = "https://files.pythonhosted.org/packages/06/6e/19a9bee9092be45679cb69e4dd2e0bf5f897b7140b4b39c57cc123d24829/gevent-25.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d94936f8f8b23d9de2251798fcb603b84f083fdf0d7f427183c1828fb64f117", size = 2173529, upload-time = "2025-09-17T15:24:13.897Z" }, + { url = "https://files.pythonhosted.org/packages/ca/4f/50de9afd879440e25737e63f5ba6ee764b75a3abe17376496ab57f432546/gevent-25.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:eb51c5f9537b07da673258b4832f6635014fee31690c3f0944d34741b69f92fa", size = 1681518, upload-time = "2025-09-17T19:39:47.488Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, + { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, + { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, + { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, + { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, +] + +[[package]] +name = "gremlinpython" +version = "3.7.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aenum" }, + { name = "aiohttp" }, + { name = "async-timeout" }, + { name = "isodate" }, + { name = "nest-asyncio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/86/a0b20fed6bb699054f19e693b40570c27d06432469d50be677a156b65fcb/gremlinpython-3.7.4.tar.gz", hash = "sha256:d41579a8ef83c1dce9e51ccff2b5fb496170be0fdb0f491d4124c29e7df9b14d", size = 52639, upload-time = "2025-08-08T16:55:23.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/84/7a268ae9d5ae4a64a701fa099497b7531820d42f6c19dfec39dcdb238bf7/gremlinpython-3.7.4-py3-none-any.whl", hash = "sha256:b6b336320d0110382b6a3832bc19b4e2bf72e4b3f38dab25fdbedfa1a3167987", size = 78522, upload-time = "2025-08-08T16:55:22.246Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hatchling" +version = "1.29.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pathspec" }, + { name = "pluggy" }, + { name = "trove-classifiers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/9c/b4cfe330cd4f49cff17fd771154730555fa4123beb7f292cf0098b4e6c20/hatchling-1.29.0.tar.gz", hash = "sha256:793c31816d952cee405b83488ce001c719f325d9cda69f1fc4cd750527640ea6", size = 55656, upload-time = "2026-02-23T19:42:06.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/8a/44032265776062a89171285ede55a0bdaadc8ac00f27f0512a71a9e3e1c8/hatchling-1.29.0-py3-none-any.whl", hash = "sha256:50af9343281f34785fab12da82e445ed987a6efb34fd8c2fc0f6e6630dbcc1b0", size = 76356, upload-time = "2026-02-23T19:42:05.197Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, +] + +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, + { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, + { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, + { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, + { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, + { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, + { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, + { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, + { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, + { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, + { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, + { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, + { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, + { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, + { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, + { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, + { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, +] + +[[package]] +name = "myst-parser" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/fa/7b45eef11b7971f0beb29d27b7bfe0d747d063aa29e170d9edd004733c8a/myst_parser-5.0.0.tar.gz", hash = "sha256:f6f231452c56e8baa662cc352c548158f6a16fcbd6e3800fc594978002b94f3a", size = 98535, upload-time = "2026-01-15T09:08:18.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/ac/686789b9145413f1a61878c407210e41bfdb097976864e0913078b24098c/myst_parser-5.0.0-py3-none-any.whl", hash = "sha256:ab31e516024918296e169139072b81592336f2fef55b8986aa31c9f04b5f7211", size = 84533, upload-time = "2026-01-15T09:08:16.788Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "python-driver-docs" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "eventlet" }, + { name = "gevent" }, + { name = "gremlinpython" }, + { name = "myst-parser" }, + { name = "pygments" }, + { name = "redirects-cli" }, + { name = "six" }, + { name = "sphinx" }, + { name = "sphinx-autobuild" }, + { name = "sphinx-multiversion-scylla" }, + { name = "sphinx-scylladb-theme" }, + { name = "sphinx-sitemap" }, + { name = "tornado" }, +] + +[package.dev-dependencies] +dev = [ + { name = "hatchling" }, +] + +[package.metadata] +requires-dist = [ + { name = "eventlet", specifier = ">=0.40.3,<1.0.0" }, + { name = "gevent", specifier = ">=25.9.1,<26.0.0" }, + { name = "gremlinpython", specifier = "==3.7.4" }, + { name = "myst-parser", specifier = ">=5.0.0" }, + { name = "pygments", specifier = ">=2.19.2,<3.0.0" }, + { name = "redirects-cli", specifier = "~=0.1.3" }, + { name = "six", specifier = ">=1.9" }, + { name = "sphinx", specifier = ">=9.0" }, + { name = "sphinx-autobuild", specifier = ">=2025.0.0,<2026.0.0" }, + { name = "sphinx-multiversion-scylla", specifier = ">=0.3.2,<1.0.0" }, + { name = "sphinx-scylladb-theme", specifier = ">=1.9.1" }, + { name = "sphinx-sitemap", specifier = ">=2.8.0,<3.0.0" }, + { name = "tornado", specifier = ">=6.5,<7.0" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "hatchling", specifier = "==1.29.0" }] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, +] + +[[package]] +name = "redirects-cli" +version = "0.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/3e/942a3d5322f05aa75c903de1bdc101800cc0627e4c6c371768ef9070fa28/redirects_cli-0.1.3.tar.gz", hash = "sha256:0cc6f35ae372d087d56bc03cfc639d6e2eac0771454c3c173ac6f3dc233969bc", size = 4404, upload-time = "2022-11-29T19:11:20.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/a4/829d6901e0c2c492d0d46190aadf3f4b9c6db6594b4e14a814f844014b28/redirects_cli-0.1.3-py3-none-any.whl", hash = "sha256:8a7a548d5f45b98db7d110fd8affbbb44b966cf250e35b5f4c9bd6541622272d", size = 4655, upload-time = "2022-11-29T19:11:18.898Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinx-autobuild" +version = "2025.8.25" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "sphinx" }, + { name = "starlette" }, + { name = "uvicorn" }, + { name = "watchfiles" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/3c/a59a3a453d4133777f7ed2e83c80b7dc817d43c74b74298ca0af869662ad/sphinx_autobuild-2025.8.25.tar.gz", hash = "sha256:9cf5aab32853c8c31af572e4fecdc09c997e2b8be5a07daf2a389e270e85b213", size = 15200, upload-time = "2025-08-25T18:44:55.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/20/56411b52f917696995f5ad27d2ea7e9492c84a043c5b49a3a3173573cd93/sphinx_autobuild-2025.8.25-py3-none-any.whl", hash = "sha256:b750ac7d5a18603e4665294323fd20f6dcc0a984117026d1986704fa68f0379a", size = 12535, upload-time = "2025-08-25T18:44:54.164Z" }, +] + +[[package]] +name = "sphinx-collapse" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/a1/cb5bb03a5081bd1229b3296c2af347b4147017fdb62777d2aad855cd349f/sphinx_collapse-0.1.4.tar.gz", hash = "sha256:ba860e50839c026cd1abcc164e1e7cb18bcc11c8214150e34a6550461be3229f", size = 19412, upload-time = "2026-02-27T17:47:24.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/18/277f4663c97073606917becab629938237f1e03952f4e339f8b7d1f3096b/sphinx_collapse-0.1.4-py3-none-any.whl", hash = "sha256:76e9fa531bafb4984d6ef5f3dbe311982837f5965b7a35eda013bbd9dd41445e", size = 4811, upload-time = "2026-02-27T17:47:22.622Z" }, +] + +[[package]] +name = "sphinx-copybutton" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", size = 23039, upload-time = "2023-04-14T08:10:22.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343, upload-time = "2023-04-14T08:10:20.844Z" }, +] + +[[package]] +name = "sphinx-last-updated-by-git" +version = "0.3.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/fd/de1685b6dab173dff31da24e0d3b29f02873fc24a1cdbb7678721ddc8581/sphinx_last_updated_by_git-0.3.8.tar.gz", hash = "sha256:c145011f4609d841805b69a9300099fc02fed8f5bb9e5bcef77d97aea97b7761", size = 10785, upload-time = "2024-08-11T07:15:54.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/fb/e496f16fa11fbe2dbdd0b5e306ede153dfed050aae4766fc89d500720dc7/sphinx_last_updated_by_git-0.3.8-py3-none-any.whl", hash = "sha256:6382c8285ac1f222483a58569b78c0371af5e55f7fbf9c01e5e8a72d6fdfa499", size = 8580, upload-time = "2024-08-11T07:15:53.244Z" }, +] + +[[package]] +name = "sphinx-multiversion-scylla" +version = "0.3.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/b1/83fb37f6c9038469b3bd01453875bb2127b3c03f9f41247394ad2063645c/sphinx_multiversion_scylla-0.3.7.tar.gz", hash = "sha256:fc1ddd58e82cfd8810c1be6db8717a244043c04c1c632e9bd1436415d1db0d3b", size = 12665, upload-time = "2026-02-27T18:43:17.849Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/94/f5b6219ca1136dc0305aaf3fb6c96aa2dfe65224d6dc147e00a6485a1a22/sphinx_multiversion_scylla-0.3.7-py3-none-any.whl", hash = "sha256:6205d261a77c90b7ea3105311d1d56014736a5148966133c34344512bb8c4e4f", size = 12558, upload-time = "2026-02-27T18:43:16.988Z" }, +] + +[[package]] +name = "sphinx-notfound-page" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/b2/67603444a8ee97b4a8ea71b0a9d6bab1727ed65e362c87e02f818ee57b8a/sphinx_notfound_page-1.1.0.tar.gz", hash = "sha256:913e1754370bb3db201d9300d458a8b8b5fb22e9246a816643a819a9ea2b8067", size = 7392, upload-time = "2025-01-28T18:45:02.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/d4/019fe439c840a7966012bbb95ccbdd81c5c10271749706793b43beb05145/sphinx_notfound_page-1.1.0-py3-none-any.whl", hash = "sha256:835dc76ff7914577a1f58d80a2c8418fb6138c0932c8da8adce4d9096fbcd389", size = 8167, upload-time = "2025-01-28T18:45:00.465Z" }, +] + +[[package]] +name = "sphinx-scylladb-theme" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "pyyaml" }, + { name = "setuptools" }, + { name = "sphinx-collapse" }, + { name = "sphinx-copybutton" }, + { name = "sphinx-notfound-page" }, + { name = "sphinx-substitution-extensions" }, + { name = "sphinx-tabs" }, + { name = "sphinxcontrib-mermaid" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/4e/e49e351d4c429b8fe3090657d39e956d53dff61187d783caac1cba81bd72/sphinx_scylladb_theme-1.9.1.tar.gz", hash = "sha256:2ba6367f005d2c68eee1916cc16385989b8e53bbddcc81193003bdeb3bd3415e", size = 1676201, upload-time = "2026-03-09T18:10:43.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/30/2b2bae1b022d1fabef405a4857f160464548e08d924f24d0b26d0ca6a848/sphinx_scylladb_theme-1.9.1-py3-none-any.whl", hash = "sha256:6156d60befc3da03bd11991fec9bc590e27ce7cc4ab05aa334edd5611424b106", size = 1662204, upload-time = "2026-03-09T18:10:45.638Z" }, +] + +[[package]] +name = "sphinx-sitemap" +version = "2.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx-last-updated-by-git" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/17/56fe0f65e3567f829b2b4153a622be1d4b222b781e0d90d7db5a7738f30f/sphinx_sitemap-2.9.0.tar.gz", hash = "sha256:70f97bcdf444e3d68e118355cf82a1f54c4d3c03d651cd17fe87398b26e25e21", size = 6978, upload-time = "2025-10-06T00:24:00.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/94/3c57e8b1985e755c48972e2ecd59526d4bf0b52a1fe805bc52a8e98cb92d/sphinx_sitemap-2.9.0-py3-none-any.whl", hash = "sha256:f1f1d3a9ad012ba17a7ef0b560d303bff2d0db26647567d6e810bcc754466664", size = 6218, upload-time = "2025-10-06T00:23:58.778Z" }, +] + +[[package]] +name = "sphinx-substitution-extensions" +version = "2025.11.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "docutils" }, + { name = "myst-parser" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/53/feccf1b607de2aef65c6411b4b4a34a91aa8daf397e77258a7774f9d1990/sphinx_substitution_extensions-2025.11.17.tar.gz", hash = "sha256:aae17f8db9efc3d454a304373ae3df763f8739e05e0b98d5381db46f6d250b27", size = 30459, upload-time = "2025-11-17T14:34:45.072Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/df/7e9cd4775c2782c894741c9274cc4c596ad02ab31257e5a5417f0a6af893/sphinx_substitution_extensions-2025.11.17-py2.py3-none-any.whl", hash = "sha256:ac18455bdc8324b337b0fe7498c1c0d0b1cb65c74d131459be4dea9edb6abbef", size = 8741, upload-time = "2025-11-17T14:34:43.66Z" }, +] + +[[package]] +name = "sphinx-tabs" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "pygments" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/53/a9a91995cb365e589f413b77fc75f1c0e9b4ac61bfa8da52a779ad855cc0/sphinx-tabs-3.4.7.tar.gz", hash = "sha256:991ad4a424ff54119799ba1491701aa8130dd43509474aef45a81c42d889784d", size = 15891, upload-time = "2024-10-08T13:37:27.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/c6/f47505b564b918a3ba60c1e99232d4942c4a7e44ecaae603e829e3d05dae/sphinx_tabs-3.4.7-py3-none-any.whl", hash = "sha256:c12d7a36fd413b369e9e9967a0a4015781b71a9c393575419834f19204bd1915", size = 9727, upload-time = "2024-10-08T13:37:26.192Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-mermaid" +version = "1.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/49/c6ddfe709a4ab76ac6e5a00e696f73626b2c189dc1e1965a361ec102e6cc/sphinxcontrib_mermaid-1.2.3.tar.gz", hash = "sha256:358699d0ec924ef679b41873d9edd97d0773446daf9760c75e18dc0adfd91371", size = 18885, upload-time = "2025-11-26T04:18:32.43Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/39/8b54299ffa00e597d3b0b4d042241a0a0b22cb429ad007ccfb9c1745b4d1/sphinxcontrib_mermaid-1.2.3-py3-none-any.whl", hash = "sha256:5be782b27026bef97bfb15ccb2f7868b674a1afc0982b54cb149702cfc25aa02", size = 13413, upload-time = "2025-11-26T04:18:31.269Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, + { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, + { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, +] + +[[package]] +name = "trove-classifiers" +version = "2026.1.14.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/43/7935f8ea93fcb6680bc10a6fdbf534075c198eeead59150dd5ed68449642/trove_classifiers-2026.1.14.14.tar.gz", hash = "sha256:00492545a1402b09d4858605ba190ea33243d361e2b01c9c296ce06b5c3325f3", size = 16997, upload-time = "2026-01-14T14:54:50.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-py3-none-any.whl", hash = "sha256:1f9553927f18d0513d8e5ff80ab8980b8202ce37ecae0e3274ed2ef11880e74d", size = 14197, upload-time = "2026-01-14T14:54:49.067Z" }, +] + +[[package]] +name = "typer" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/28/7c85c8032b91dbe79725b6f17d2fffc595dff06a35c7a30a37bef73a1ab4/typer-0.20.0.tar.gz", hash = "sha256:1aaf6494031793e4876fb0bacfa6a912b551cf43c1e63c800df8b1a866720c37", size = 106492, upload-time = "2025-10-20T17:03:49.445Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a", size = 47028, upload-time = "2025-10-20T17:03:47.617Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] + +[[package]] +name = "zope-event" +version = "6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/33/d3eeac228fc14de76615612ee208be2d8a5b5b0fada36bf9b62d6b40600c/zope_event-6.1.tar.gz", hash = "sha256:6052a3e0cb8565d3d4ef1a3a7809336ac519bc4fe38398cb8d466db09adef4f0", size = 18739, upload-time = "2025-11-07T08:05:49.934Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/b0/956902e5e1302f8c5d124e219c6bf214e2649f92ad5fce85b05c039a04c9/zope_event-6.1-py3-none-any.whl", hash = "sha256:0ca78b6391b694272b23ec1335c0294cc471065ed10f7f606858fc54566c25a0", size = 6414, upload-time = "2025-11-07T08:05:48.874Z" }, +] + +[[package]] +name = "zope-interface" +version = "8.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/c9/5ec8679a04d37c797d343f650c51ad67d178f0001c363e44b6ac5f97a9da/zope_interface-8.1.1.tar.gz", hash = "sha256:51b10e6e8e238d719636a401f44f1e366146912407b58453936b781a19be19ec", size = 254748, upload-time = "2025-11-15T08:32:52.404Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/81/3c3b5386ce4fba4612fd82ffb8a90d76bcfea33ca2b6399f21e94d38484f/zope_interface-8.1.1-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:84f9be6d959640de9da5d14ac1f6a89148b16da766e88db37ed17e936160b0b1", size = 209046, upload-time = "2025-11-15T08:37:01.473Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e3/32b7cb950c4c4326b3760a8e28e5d6f70ad15f852bfd8f9364b58634f74b/zope_interface-8.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:531fba91dcb97538f70cf4642a19d6574269460274e3f6004bba6fe684449c51", size = 209104, upload-time = "2025-11-15T08:37:02.887Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3d/c4c68e1752a5f5effa2c1f5eaa4fea4399433c9b058fb7000a34bfb1c447/zope_interface-8.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:fc65f5633d5a9583ee8d88d1f5de6b46cd42c62e47757cfe86be36fb7c8c4c9b", size = 259277, upload-time = "2025-11-15T08:37:04.389Z" }, + { url = "https://files.pythonhosted.org/packages/fd/5b/cf4437b174af7591ee29bbad728f620cab5f47bd6e9c02f87d59f31a0dda/zope_interface-8.1.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efef80ddec4d7d99618ef71bc93b88859248075ca2e1ae1c78636654d3d55533", size = 264742, upload-time = "2025-11-15T08:37:05.613Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0e/0cf77356862852d3d3e62db9aadae5419a1a7d89bf963b219745283ab5ca/zope_interface-8.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49aad83525eca3b4747ef51117d302e891f0042b06f32aa1c7023c62642f962b", size = 264252, upload-time = "2025-11-15T08:37:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/8a/10/2af54aa88b2fa172d12364116cc40d325fedbb1877c3bb031b0da6052855/zope_interface-8.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:71cf329a21f98cb2bd9077340a589e316ac8a415cac900575a32544b3dffcb98", size = 212330, upload-time = "2025-11-15T08:37:08.14Z" }, +] diff --git a/examples/concurrent_executions/execute_async_with_queue.py b/examples/concurrent_executions/execute_async_with_queue.py index 72d2c101cb..794ac78818 100644 --- a/examples/concurrent_executions/execute_async_with_queue.py +++ b/examples/concurrent_executions/execute_async_with_queue.py @@ -31,7 +31,7 @@ session = cluster.connect() session.execute(("CREATE KEYSPACE IF NOT EXISTS examples " - "WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1' }")) + "WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '1' }")) session.execute("USE examples") session.execute("CREATE TABLE IF NOT EXISTS tbl_sample_kv (id uuid, value text, PRIMARY KEY (id))") prepared_insert = session.prepare("INSERT INTO tbl_sample_kv (id, value) VALUES (?, ?)") diff --git a/examples/concurrent_executions/execute_with_threads.py b/examples/concurrent_executions/execute_with_threads.py index e3c80f5d6b..70893bd5be 100644 --- a/examples/concurrent_executions/execute_with_threads.py +++ b/examples/concurrent_executions/execute_with_threads.py @@ -34,7 +34,7 @@ session = cluster.connect() session.execute(("CREATE KEYSPACE IF NOT EXISTS examples " - "WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1' }")) + "WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '1' }")) session.execute("USE examples") session.execute("CREATE TABLE IF NOT EXISTS tbl_sample_kv (id uuid, value text, PRIMARY KEY (id))") prepared_insert = session.prepare("INSERT INTO tbl_sample_kv (id, value) VALUES (?, ?)") diff --git a/examples/example_core.py b/examples/example_core.py index 01c766e109..ec41ca7fd5 100644 --- a/examples/example_core.py +++ b/examples/example_core.py @@ -36,7 +36,7 @@ def main(): log.info("creating keyspace...") session.execute(""" CREATE KEYSPACE IF NOT EXISTS %s - WITH replication = { 'class': 'SimpleStrategy', 'replication_factor': '2' } + WITH replication = { 'class': 'NetworkTopologyStrategy', 'replication_factor': '2' } """ % KEYSPACE) log.info("setting keyspace...") diff --git a/examples/request_init_listener.py b/examples/request_init_listener.py index 2ca6df495a..e23ac80fbf 100644 --- a/examples/request_init_listener.py +++ b/examples/request_init_listener.py @@ -19,7 +19,7 @@ # this is just demonstrating a way to track a few custom attributes. from cassandra.cluster import Cluster -from greplin import scales +from cassandra.metrics import PmfStat, IntStat, init import pprint pp = pprint.PrettyPrinter(indent=2) @@ -32,11 +32,11 @@ class RequestAnalyzer(object): Also computes statistics on encoded request size. """ - requests = scales.PmfStat('request size') - errors = scales.IntStat('errors') + requests = PmfStat('request size') + errors = IntStat('errors') def __init__(self, session): - scales.init(self, '/cassandra') + init(self, '/cassandra') # each instance will be registered with a session, and receive a callback for each request generated session.add_request_init_listener(self.on_request) @@ -91,7 +91,7 @@ def __str__(self): pass print() -print(ra) # note: the counts are updated, but the stats are not because scales only updates every 20s +print(ra) # 3 requests (1 errors) # Request size statistics: # { '75percentile': 74, diff --git a/pyproject.toml b/pyproject.toml index 196a25b45c..4a40af5378 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,18 +9,19 @@ classifiers = [ 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14', + 'Programming Language :: Python :: Free Threading :: 2 - Beta', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries :: Python Modules', ] dependencies = ['geomet>=1.1', 'pyyaml > 5.0'] dynamic = ["version", "readme"] -license = "Apache-2.0" +license = {text = "Apache-2.0"} requires-python = ">=3.9" [project.urls] @@ -30,25 +31,29 @@ requires-python = ">=3.9" "Issues" = "https://github.com/scylladb/python-driver/issues" [project.optional-dependencies] -graph = ['gremlinpython==3.4.6'] -cle = ['cryptography>=35.0'] +graph = ['gremlinpython>=3.7.4,<4'] +cle = ['cryptography>=42.0'] +compress-lz4 = ['lz4'] +compress-snappy = ['python-snappy'] +auth-kerberos = [ + 'kerberos; platform_system != "Windows"', + 'winkerberos; platform_system == "Windows"', +] [dependency-groups] dev = [ - "pytest", + "pytest~=8.0", "PyYAML", - "pytz", - "sure", - "scales", "pure-sasl", "twisted[tls]", "gevent", "eventlet>=0.33.3", - "cython", - "packaging", + "cython>=3.2", + "packaging>=25.0", "futurist", - "asynctest", "pyyaml", + "numpy", + "objgraph", "ccm @ git+https://git@github.com/scylladb/scylla-ccm.git@master", ] @@ -73,7 +78,7 @@ version = { attr = "cassandra.__version__" } # any module attri readme = { file = "README.rst", content-type = "text/x-rst" } [build-system] -requires = ["setuptools>=42", "Cython"] +requires = ["setuptools>=70", "Cython"] build-backend = "setuptools.build_meta" @@ -105,7 +110,6 @@ cache-keys = [ { env = "CASS_DRIVER_NO_CYTHON" }, { env = "CASS_DRIVER_BUILD_CONCURRENCY" }, { env = "CASS_DRIVER_BUILD_EXTENSIONS_ARE_MUST" }, - { env = "CASS_DRIVER_ALLOWED_CYTHON_VERSION" }, # used by setuptools_scm { git = { commit = true, tags = true } }, @@ -117,6 +121,9 @@ log_level = "DEBUG" log_date_format = "%Y-%m-%d %H:%M:%S" xfail_strict = true addopts = "-rf" +markers = [ + "last: mark test to run last within its module group", +] [tool.setuptools_scm] version_file = "cassandra/_version.py" @@ -128,13 +135,12 @@ tag_regex = '(?P\d*?\.\d*?\.\d*?)-scylla' build-frontend = "build[uv]" environment = { CASS_DRIVER_BUILD_CONCURRENCY = "2", CASS_DRIVER_BUILD_EXTENSIONS_ARE_MUST = "yes", CFLAGS = "-g0 -O3" } skip = [ - "cp2*", - "cp36*", - "pp36*", - "cp37*", - "pp37*", "cp38*", "pp38*", + "cp39*", + "pp39*", + "cp3*t-*", + "pp3*t-*", "*i686", "*musllinux*", ] @@ -146,6 +152,7 @@ manylinux-aarch64-image = "manylinux_2_28" manylinux-pypy_x86_64-image = "manylinux_2_28" manylinux-pypy_aarch64-image = "manylinux_2_28" +enable = ["pypy"] [tool.cibuildwheel.linux] before-build = "rm -rf ~/.pyxbld && rpm --import https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux && yum install -y libffi-devel libev libev-devel openssl openssl-devel" @@ -168,7 +175,3 @@ test-command = [ # TODO: set CASS_DRIVER_BUILD_EXTENSIONS_ARE_MUST to yes when https://github.com/scylladb/python-driver/issues/429 is fixed environment = { CASS_DRIVER_BUILD_CONCURRENCY = "2", CASS_DRIVER_BUILD_EXTENSIONS_ARE_MUST = "no" } - -[[tool.cibuildwheel.overrides]] -select = "pp*" -test-command = [] diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000000..d85ac38c01 --- /dev/null +++ b/renovate.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ], + "packageRules": [ + { + "matchManagers": ["github-actions"], + "pinDigests": true, + "minimumReleaseAge": "90 days" + } + ] +} diff --git a/setup.py b/setup.py index 340ded87ed..52e04a63e5 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,6 @@ # limitations under the License. import os -import shutil import sys import json import warnings @@ -201,8 +200,6 @@ def eval_env_var_as_array(varname): try_murmur3 = try_extensions and "--no-murmur3" not in sys.argv try_libev = try_extensions and "--no-libev" not in sys.argv and not is_pypy and not os.environ.get('CASS_DRIVER_NO_LIBEV') try_cython = try_extensions and "--no-cython" not in sys.argv and not is_pypy and not os.environ.get('CASS_DRIVER_NO_CYTHON') -try_cython &= 'egg_info' not in sys.argv # bypass setup_requires for pip egg_info calls, which will never have --install-option"--no-cython" coming fomr pip - sys.argv = [a for a in sys.argv if a not in ("--no-murmur3", "--no-libev", "--no-cython", "--no-extensions")] build_concurrency = int(os.environ.get('CASS_DRIVER_BUILD_CONCURRENCY', '0')) @@ -358,80 +355,12 @@ def fix_extension_class(ext: Extension) -> Extension: return ext -def pre_build_check(): - """ - Try to verify build tools - """ - if os.environ.get('CASS_DRIVER_NO_PRE_BUILD_CHECK'): - return True - - try: - from setuptools._distutils.ccompiler import new_compiler - from setuptools._distutils.sysconfig import customize_compiler - from setuptools.dist import Distribution - - # base build_ext just to emulate compiler option setup - be = build_ext(Distribution()) - be.initialize_options() - be.finalize_options() - - # First, make sure we have a Python include directory - have_python_include = any(os.path.isfile(os.path.join(p, 'Python.h')) for p in be.include_dirs) - if not have_python_include: - sys.stderr.write("Did not find 'Python.h' in %s.\n" % (be.include_dirs,)) - return False - - compiler = new_compiler(compiler=be.compiler) - customize_compiler(compiler) - - try: - # We must be able to initialize the compiler if it has that method - if hasattr(compiler, "initialize"): - compiler.initialize() - except: - return False - - executables = [] - if compiler.compiler_type in ('unix', 'cygwin'): - executables = [compiler.executables[exe][0] for exe in ('compiler_so', 'linker_so')] - elif compiler.compiler_type == 'nt': - executables = [getattr(compiler, exe) for exe in ('cc', 'linker')] - - if executables: - for exe in executables: - if not shutil.which(exe): - sys.stderr.write("Failed to find %s for compiler type %s.\n" % (exe, compiler.compiler_type)) - return False - - except Exception as exc: - sys.stderr.write('%s\n' % str(exc)) - sys.stderr.write("Failed pre-build check. Attempting anyway.\n") - - # if we are unable to positively id the compiler type, or one of these assumptions fails, - # just proceed as we would have without the check - return True - - def run_setup(extensions): kw = {'cmdclass': {'doc': DocCommand}} kw['cmdclass']['build_ext'] = build_extensions kw['ext_modules'] = [Extension('DUMMY', [])] # dummy extension makes sure build_ext is called for install - if try_cython: - # precheck compiler before adding to setup_requires - # we don't actually negate try_cython because: - # 1.) build_ext eats errors at compile time, letting the install complete while producing useful feedback - # 2.) there could be a case where the python environment has cython installed but the system doesn't have build tools - if pre_build_check(): - cython_dep = 'Cython>=3.0.11,<4' - user_specified_cython_version = os.environ.get('CASS_DRIVER_ALLOWED_CYTHON_VERSION') - if user_specified_cython_version is not None: - cython_dep = 'Cython==%s' % (user_specified_cython_version,) - kw['setup_requires'] = [cython_dep] - else: - sys.stderr.write("Bypassing Cython setup requirement\n") - setup(**kw) run_setup(None) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000..8fd2fc923b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,67 @@ +# Copyright ScyllaDB, Inc. +# +# 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 +# +# http://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 importlib.machinery +import os +import warnings + +# Directory containing the Cython-compiled driver modules. +_CASSANDRA_DIR = os.path.join(os.path.dirname(__file__), os.pardir, "cassandra") + + +def pytest_configure(config): + """Warn when a compiled Cython extension is older than its .py source. + + Python's import system prefers compiled extensions (.so / .pyd) over pure + Python (.py) files. If a developer edits a .py file without rebuilding + the Cython extensions, the tests + will silently run the *old* compiled code, masking any regressions in the + Python source. + + This hook detects such staleness at test-session startup so the developer + is alerted immediately. + """ + stale = [] + # Iterate over .py sources and, for each module, look for the first + # existing compiled extension in EXTENSION_SUFFIXES order. This mirrors + # how Python's import machinery selects an extension module, and avoids + # globbing patterns like "*{suffix}" that can pick up ABI-tagged + # extensions built for other Python versions. + if os.path.isdir(_CASSANDRA_DIR): + for entry in os.listdir(_CASSANDRA_DIR): + if not entry.endswith(".py"): + continue + module_name, _ = os.path.splitext(entry) + py_path = os.path.join(_CASSANDRA_DIR, entry) + # For this module, find the first extension file Python would load. + for suffix in importlib.machinery.EXTENSION_SUFFIXES: + ext_path = os.path.join(_CASSANDRA_DIR, module_name + suffix) + if not os.path.exists(ext_path): + continue + if os.path.getmtime(py_path) > os.path.getmtime(ext_path): + stale.append((module_name, ext_path, py_path)) + # Only consider the first matching suffix; this is the one + # the import system would actually use. + break + + if stale: + names = ", ".join(m for m, _, _ in stale) + warnings.warn( + f"Stale Cython extension(s) detected: {names}. " + f"The .py source is newer than the compiled extension — tests " + f"will run the OLD compiled code, not your latest changes. " + f"Rebuild with: uv sync --reinstall-package scylla-driver\n" + f"Or use 'uv run pytest' which handles rebuilds automatically.", + stacklevel=1, + ) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index f00d4c7126..5701e5b3da 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -14,6 +14,7 @@ import re import os +from typing import Callable from cassandra.cluster import Cluster from tests import connection_class, EVENT_LOOP_MANAGER @@ -44,7 +45,6 @@ try: import ccmlib - from ccmlib.dse_cluster import DseCluster from ccmlib.cluster import Cluster as CCMCluster from ccmlib.scylla_cluster import ScyllaCluster as CCMScyllaCluster from ccmlib.cluster_factory import ClusterFactory as CCMClusterFactory @@ -122,98 +122,32 @@ def cmd_line_args_to_dict(env_var): args[cmd_arg.lstrip('-')] = cmd_arg_value return args - -def _get_cass_version_from_dse(dse_version): - if dse_version.startswith('4.6') or dse_version.startswith('4.5'): - raise Exception("Cassandra Version 2.0 not supported anymore") - elif dse_version.startswith('4.7') or dse_version.startswith('4.8'): - cass_ver = "2.1" - elif dse_version.startswith('5.0'): - cass_ver = "3.0" - elif dse_version.startswith('5.1'): - # TODO: refactor this method to use packaging.Version everywhere - if Version(dse_version) >= Version('5.1.2'): - cass_ver = "3.11" - else: - cass_ver = "3.10" - elif dse_version.startswith('6.0'): - if dse_version == '6.0.0': - cass_ver = '4.0.0.2284' - elif dse_version == '6.0.1': - cass_ver = '4.0.0.2349' - else: - cass_ver = '4.0.0.' + ''.join(dse_version.split('.')) - elif Version(dse_version) >= Version('6.7'): - if dse_version == '6.7.0': - cass_ver = "4.0.0.67" - else: - cass_ver = '4.0.0.' + ''.join(dse_version.split('.')) - elif dse_version.startswith('6.8'): - if dse_version == '6.8.0': - cass_ver = "4.0.0.68" - else: - cass_ver = '4.0.0.' + ''.join(dse_version.split('.')) - else: - log.error("Unknown dse version found {0}, defaulting to 2.1".format(dse_version)) - cass_ver = "2.1" - return Version(cass_ver) - - -def _get_dse_version_from_cass(cass_version): - if cass_version.startswith('2.1'): - dse_ver = "4.8.15" - elif cass_version.startswith('3.0'): - dse_ver = "5.0.12" - elif cass_version.startswith('3.10') or cass_version.startswith('3.11'): - dse_ver = "5.1.7" - elif cass_version.startswith('4.0'): - dse_ver = "6.0" - else: - log.error("Unknown cassandra version found {0}, defaulting to 2.1".format(cass_version)) - dse_ver = "2.1" - return dse_ver - USE_CASS_EXTERNAL = bool(os.getenv('USE_CASS_EXTERNAL', False)) KEEP_TEST_CLUSTER = bool(os.getenv('KEEP_TEST_CLUSTER', False)) SIMULACRON_JAR = os.getenv('SIMULACRON_JAR', None) -CLOUD_PROXY_PATH = os.getenv('CLOUD_PROXY_PATH', None) -# Supported Clusters: Cassandra, DDAC, DSE, Scylla -DSE_VERSION = None +# Supported Clusters: Cassandra, Scylla SCYLLA_VERSION = os.getenv('SCYLLA_VERSION', None) -if os.getenv('DSE_VERSION', None): # we are testing against DSE - DSE_VERSION = Version(os.getenv('DSE_VERSION', None)) - DSE_CRED = os.getenv('DSE_CREDS', None) - CASSANDRA_VERSION = _get_cass_version_from_dse(DSE_VERSION.base_version) - CCM_VERSION = DSE_VERSION.base_version -else: # we are testing against Cassandra,DDAC or Scylla - if SCYLLA_VERSION: - cv_string = SCYLLA_VERSION - mcv_string = os.getenv('MAPPED_SCYLLA_VERSION', '3.11.4') # Assume that scylla matches cassandra `3.11.4` behavior - else: - cv_string = os.getenv('CASSANDRA_VERSION', None) - mcv_string = os.getenv('MAPPED_CASSANDRA_VERSION', None) - try: - cassandra_version = Version(cv_string) # env var is set to test-dse for DDAC - except: - # fallback to MAPPED_CASSANDRA_VERSION - cassandra_version = Version(mcv_string) +if SCYLLA_VERSION: + cv_string = SCYLLA_VERSION + mcv_string = os.getenv('MAPPED_SCYLLA_VERSION', '3.11.4') # Assume that scylla matches cassandra `3.11.4` behavior +else: + cv_string = os.getenv('CASSANDRA_VERSION', None) + mcv_string = os.getenv('MAPPED_CASSANDRA_VERSION', None) +try: + cassandra_version = Version(cv_string) # env var is set to test-dse for DDAC +except: + # fallback to MAPPED_CASSANDRA_VERSION + cassandra_version = Version(mcv_string) - CASSANDRA_VERSION = Version(mcv_string) if mcv_string else cassandra_version - CCM_VERSION = mcv_string if mcv_string else cv_string +CASSANDRA_VERSION = Version(mcv_string) if mcv_string else cassandra_version +CCM_VERSION = mcv_string if mcv_string else cv_string CASSANDRA_IP = os.getenv('CLUSTER_IP', '127.0.0.1') CASSANDRA_DIR = os.getenv('CASSANDRA_DIR', None) CCM_KWARGS = {} -if DSE_VERSION: - log.info('Using DSE version: %s', DSE_VERSION) - if not CASSANDRA_DIR: - CCM_KWARGS['version'] = DSE_VERSION - if DSE_CRED: - log.info("Using DSE credentials file located at {0}".format(DSE_CRED)) - CCM_KWARGS['dse_credentials_file'] = DSE_CRED -elif CASSANDRA_DIR: +if CASSANDRA_DIR: log.info("Using Cassandra dir: %s", CASSANDRA_DIR) CCM_KWARGS['install_dir'] = CASSANDRA_DIR elif os.getenv('SCYLLA_VERSION'): @@ -228,21 +162,13 @@ def _get_dse_version_from_cass(cass_version): def get_default_protocol(): if CASSANDRA_VERSION >= Version('4.0-a'): - if DSE_VERSION: - return ProtocolVersion.DSE_V2 - else: - return ProtocolVersion.V5 + return ProtocolVersion.V5 if CASSANDRA_VERSION >= Version('3.10'): - if DSE_VERSION: - return ProtocolVersion.DSE_V1 - else: - return 4 + return 4 if CASSANDRA_VERSION >= Version('2.2'): return 4 elif CASSANDRA_VERSION >= Version('2.1'): return 3 - elif CASSANDRA_VERSION >= Version('2.0'): - return 2 else: raise Exception("Running tests with an unsupported Cassandra version: {0}".format(CASSANDRA_VERSION)) @@ -260,39 +186,26 @@ def get_scylla_default_protocol(): def get_supported_protocol_versions(): """ - 1.2 -> 1 - 2.0 -> 2, 1 - 2.1 -> 3, 2, 1 - 2.2 -> 4, 3, 2, 1 + 2.1 -> 3 + 2.2 -> 4, 3 3.X -> 4, 3 3.10(C*) -> 5(beta),4,3 - 3.10(DSE) -> DSE_V1,4,3 4.0(C*) -> 6(beta),5,4,3 - 4.0(DSE) -> DSE_v2, DSE_V1,4,3 ` """ if CASSANDRA_VERSION >= Version('4.0-beta5'): - if not DSE_VERSION: - return (3, 4, 5, 6) + return (3, 4, 5) if CASSANDRA_VERSION >= Version('4.0-a'): - if DSE_VERSION: - return (3, 4, ProtocolVersion.DSE_V1, ProtocolVersion.DSE_V2) - else: - return (3, 4, 5) + return (3, 4, 5) elif CASSANDRA_VERSION >= Version('3.10'): - if DSE_VERSION: - return (3, 4, ProtocolVersion.DSE_V1) - else: - return (3, 4) + return (3, 4) elif CASSANDRA_VERSION >= Version('3.0'): return (3, 4) elif CASSANDRA_VERSION >= Version('2.2'): - return (1,2, 3, 4) + return (3, 4) elif CASSANDRA_VERSION >= Version('2.1'): - return (1, 2, 3) - elif CASSANDRA_VERSION >= Version('2.0'): - return (1, 2) + return (3) else: - return (1,) + return (3,) def get_unsupported_lower_protocol(): @@ -317,15 +230,9 @@ def get_unsupported_upper_protocol(): return 5 if CASSANDRA_VERSION >= Version('4.0-a'): - if DSE_VERSION: - return None - else: - return ProtocolVersion.DSE_V1 + return ProtocolVersion.DSE_V1 if CASSANDRA_VERSION >= Version('3.10'): - if DSE_VERSION: - return ProtocolVersion.DSE_V2 - else: - return 5 + return 5 if CASSANDRA_VERSION >= Version('2.2'): return 5 elif CASSANDRA_VERSION >= Version('2.1'): @@ -352,12 +259,16 @@ def _id_and_mark(f): return _id_and_mark +def xfail_scylla_version(filter: Callable[[Version], bool], reason: str, *args, **kwargs): + if SCYLLA_VERSION is None: + return pytest.mark.skipif(False, reason="It is just a NoOP Decor, should not skip anything") + current_version = Version(get_scylla_version(SCYLLA_VERSION)) + + return pytest.mark.xfail(filter(current_version), reason=reason, *args, **kwargs) + local = local_decorator_creator() notprotocolv1 = unittest.skipUnless(PROTOCOL_VERSION > 1, 'Protocol v1 not supported') -lessthenprotocolv4 = unittest.skipUnless(PROTOCOL_VERSION < 4, 'Protocol versions 4 or greater not supported') -lessthanprotocolv3 = unittest.skipUnless(PROTOCOL_VERSION < 3, 'Protocol versions 3 or greater not supported') greaterthanprotocolv3 = unittest.skipUnless(PROTOCOL_VERSION >= 4, 'Protocol versions less than 4 are not supported') -protocolv6 = unittest.skipUnless(6 in get_supported_protocol_versions(), 'Protocol versions less than 6 are not supported') greaterthancass20 = unittest.skipUnless(CASSANDRA_VERSION >= Version('2.1'), 'Cassandra version 2.1 or greater required') greaterthancass21 = unittest.skipUnless(CASSANDRA_VERSION >= Version('2.2'), 'Cassandra version 2.2 or greater required') @@ -368,18 +279,15 @@ def _id_and_mark(f): greaterthanorequalcass3_11 = unittest.skipUnless(CASSANDRA_VERSION >= Version('3.11'), 'Cassandra version 3.11 or greater required') greaterthanorequalcass40 = unittest.skipUnless(CASSANDRA_VERSION >= Version('4.0'), 'Cassandra version 4.0 or greater required') greaterthanorequalcass50 = unittest.skipUnless(CASSANDRA_VERSION >= Version('5.0-beta'), 'Cassandra version 5.0 or greater required') +def _has_vector_type(): + if SCYLLA_VERSION is not None: + return Version(get_scylla_version(SCYLLA_VERSION)) >= Version('2025.4') + return CASSANDRA_VERSION >= Version('5.0-beta') + lessthanorequalcass40 = unittest.skipUnless(CASSANDRA_VERSION <= Version('4.0'), 'Cassandra version less or equal to 4.0 required') lessthancass40 = unittest.skipUnless(CASSANDRA_VERSION < Version('4.0'), 'Cassandra version less than 4.0 required') lessthancass30 = unittest.skipUnless(CASSANDRA_VERSION < Version('3.0'), 'Cassandra version less then 3.0 required') -greaterthanorequaldse68 = unittest.skipUnless(DSE_VERSION and DSE_VERSION >= Version('6.8'), "DSE 6.8 or greater required for this test") -greaterthanorequaldse67 = unittest.skipUnless(DSE_VERSION and DSE_VERSION >= Version('6.7'), "DSE 6.7 or greater required for this test") -greaterthanorequaldse60 = unittest.skipUnless(DSE_VERSION and DSE_VERSION >= Version('6.0'), "DSE 6.0 or greater required for this test") -greaterthanorequaldse51 = unittest.skipUnless(DSE_VERSION and DSE_VERSION >= Version('5.1'), "DSE 5.1 or greater required for this test") -greaterthanorequaldse50 = unittest.skipUnless(DSE_VERSION and DSE_VERSION >= Version('5.0'), "DSE 5.0 or greater required for this test") -lessthandse51 = unittest.skipUnless(DSE_VERSION and DSE_VERSION < Version('5.1'), "DSE version less than 5.1 required") -lessthandse60 = unittest.skipUnless(DSE_VERSION and DSE_VERSION < Version('6.0'), "DSE version less than 6.0 required") - # pytest.mark.xfail instead of unittest.expectedFailure because # 1. unittest doesn't skip setUpClass when used on class and we need it sometimes # 2. unittest doesn't have conditional xfail, and I prefer to use pytest than custom decorator @@ -394,6 +302,9 @@ def _id_and_mark(f): reason='Scylla does not support composite types') requires_custom_payload = pytest.mark.skipif(SCYLLA_VERSION is not None or PROTOCOL_VERSION < 4, reason='Scylla does not support custom payloads. Cassandra requires native protocol v4.0+') +requires_vector_type = unittest.skipUnless( + _has_vector_type(), + 'Cassandra >= 5.0 or Scylla >= 2025.4 required') xfail_scylla = lambda reason, *args, **kwargs: pytest.mark.xfail(SCYLLA_VERSION is not None, reason=reason, *args, **kwargs) incorrect_test = lambda reason='This test seems to be incorrect and should be fixed', *args, **kwargs: pytest.mark.xfail(reason=reason, *args, **kwargs) @@ -401,11 +312,7 @@ def _id_and_mark(f): requiresmallclockgranularity = unittest.skipIf("Windows" in platform.system() or "asyncore" in EVENT_LOOP_MANAGER, "This test is not suitible for environments with large clock granularity") requiressimulacron = unittest.skipIf(SIMULACRON_JAR is None or CASSANDRA_VERSION < Version("2.1"), "Simulacron jar hasn't been specified or C* version is 2.0") -requirecassandra = unittest.skipIf(DSE_VERSION, "Cassandra required") -notdse = unittest.skipIf(DSE_VERSION, "DSE not supported") -requiredse = unittest.skipUnless(DSE_VERSION, "DSE required") -requirescloudproxy = unittest.skipIf(CLOUD_PROXY_PATH is None, "Cloud Proxy path hasn't been specified") - +requirescompactstorage = xfail_scylla_version(lambda v: v >= Version('2025.1.0'), reason="ScyllaDB deprecated compact storage", raises=InvalidRequest) libevtest = unittest.skipUnless(EVENT_LOOP_MANAGER=="libev", "Test timing designed for libev loop") def wait_for_node_socket(node, timeout): @@ -516,15 +423,11 @@ def use_cluster(cluster_name, nodes, ipformat=None, start=True, workloads=None, configuration_options = configuration_options or {} dse_options = dse_options or {} workloads = workloads or [] - dse_cluster = True if DSE_VERSION else False - if ccm_options is None and DSE_VERSION: - ccm_options = {"version": CCM_VERSION} - elif ccm_options is None: + if ccm_options is None: ccm_options = CCM_KWARGS.copy() cassandra_version = ccm_options.get('version', CCM_VERSION) - dse_version = ccm_options.get('version', DSE_VERSION) global CCM_CLUSTER if USE_CASS_EXTERNAL: @@ -539,7 +442,7 @@ def use_cluster(cluster_name, nodes, ipformat=None, start=True, workloads=None, else: log.debug("Using unnamed external cluster") if set_keyspace and start: - setup_keyspace(ipformat=ipformat, wait=False) + setup_keyspace(ipformat=ipformat) return if is_current_cluster(cluster_name, nodes, workloads): @@ -570,88 +473,41 @@ def use_cluster(cluster_name, nodes, ipformat=None, start=True, workloads=None, if os.path.exists(cluster_path): shutil.rmtree(cluster_path) - if dse_cluster: - CCM_CLUSTER = DseCluster(path, cluster_name, **ccm_options) + if SCYLLA_VERSION: + # `experimental: True` enable all experimental features. + # CDC is causing an issue (can't start cluster with multiple seeds) + # Selecting only features we need for tests, i.e. anything but CDC. + CCM_CLUSTER = CCMScyllaCluster(path, cluster_name, **ccm_options) + CCM_CLUSTER.set_configuration_options({'experimental_features': ['lwt', 'udf'], 'start_native_transport': True}) + + CCM_CLUSTER.set_configuration_options({'skip_wait_for_gossip_to_settle': 0}) + # Permit IS NOT NULL restriction on non-primary key columns of a materialized view + # This allows `test_metadata_with_quoted_identifiers` to run + CCM_CLUSTER.set_configuration_options({'strict_is_not_null_in_views': False}) + else: + ccm_cluster_clz = CCMCluster if Version(cassandra_version) < Version( + '4.1') else Cassandra41CCMCluster + CCM_CLUSTER = ccm_cluster_clz(path, cluster_name, **ccm_options) CCM_CLUSTER.set_configuration_options({'start_native_transport': True}) - CCM_CLUSTER.set_configuration_options({'batch_size_warn_threshold_in_kb': 5}) - if Version(dse_version) >= Version('5.0'): - CCM_CLUSTER.set_configuration_options({'enable_user_defined_functions': True}) - CCM_CLUSTER.set_configuration_options({'enable_scripted_user_defined_functions': True}) - if Version(dse_version) >= Version('5.1'): - # For Inet4Address - CCM_CLUSTER.set_dse_configuration_options({ - 'graph': { - 'gremlin_server': { - 'scriptEngines': { - 'gremlin-groovy': { - 'config': { - 'sandbox_rules': { - 'whitelist_packages': ['java.net'] - } - } - } - } - } - } - }) - if 'spark' in workloads: - if Version(dse_version) >= Version('6.8'): - config_options = { - "resource_manager_options": { - "worker_options": { - "cores_total": 0.1, - "memory_total": "64M" - } - } - } + if Version(cassandra_version) >= Version('2.2'): + CCM_CLUSTER.set_configuration_options({'enable_user_defined_functions': True}) + if Version(cassandra_version) >= Version('3.0'): + # The config.yml option below is deprecated in C* 4.0 per CASSANDRA-17280 + if Version(cassandra_version) < Version('4.0'): + CCM_CLUSTER.set_configuration_options({'enable_scripted_user_defined_functions': True}) else: - config_options = {"initial_spark_worker_resources": 0.1} - - if Version(dse_version) >= Version('6.7'): - log.debug("Disabling AlwaysON SQL for a DSE 6.7 Cluster") - config_options['alwayson_sql_options'] = {'enabled': False} - CCM_CLUSTER.set_dse_configuration_options(config_options) - common.switch_cluster(path, cluster_name) - CCM_CLUSTER.set_configuration_options(configuration_options) - CCM_CLUSTER.populate(nodes, ipformat=ipformat) - - CCM_CLUSTER.set_dse_configuration_options(dse_options) - else: - if SCYLLA_VERSION: - # `experimental: True` enable all experimental features. - # CDC is causing an issue (can't start cluster with multiple seeds) - # Selecting only features we need for tests, i.e. anything but CDC. - CCM_CLUSTER = CCMScyllaCluster(path, cluster_name, **ccm_options) - CCM_CLUSTER.set_configuration_options({'experimental_features': ['lwt', 'udf'], 'start_native_transport': True}) - - CCM_CLUSTER.set_configuration_options({'skip_wait_for_gossip_to_settle': 0}) - # Permit IS NOT NULL restriction on non-primary key columns of a materialized view - # This allows `test_metadata_with_quoted_identifiers` to run - CCM_CLUSTER.set_configuration_options({'strict_is_not_null_in_views': False}) - else: - ccm_cluster_clz = CCMCluster if Version(cassandra_version) < Version( - '4.1') else Cassandra41CCMCluster - CCM_CLUSTER = ccm_cluster_clz(path, cluster_name, **ccm_options) - CCM_CLUSTER.set_configuration_options({'start_native_transport': True}) - if Version(cassandra_version) >= Version('2.2'): - CCM_CLUSTER.set_configuration_options({'enable_user_defined_functions': True}) - if Version(cassandra_version) >= Version('3.0'): - # The config.yml option below is deprecated in C* 4.0 per CASSANDRA-17280 - if Version(cassandra_version) < Version('4.0'): - CCM_CLUSTER.set_configuration_options({'enable_scripted_user_defined_functions': True}) - else: - # Cassandra version >= 4.0 - CCM_CLUSTER.set_configuration_options({ - 'enable_materialized_views': True, - 'enable_sasi_indexes': True, - 'enable_transient_replication': True, - }) - - common.switch_cluster(path, cluster_name) - CCM_CLUSTER.set_configuration_options(configuration_options) - # Since scylla CCM doesn't yet support this options, we skip it - # , use_single_interface=use_single_interface) - CCM_CLUSTER.populate(nodes, ipformat=ipformat) + # Cassandra version >= 4.0 + CCM_CLUSTER.set_configuration_options({ + 'enable_materialized_views': True, + 'enable_sasi_indexes': True, + 'enable_transient_replication': True, + }) + + common.switch_cluster(path, cluster_name) + CCM_CLUSTER.set_configuration_options(configuration_options) + # Since scylla CCM doesn't yet support this options, we skip it + # , use_single_interface=use_single_interface) + CCM_CLUSTER.populate(nodes, ipformat=ipformat) try: jvm_args = [] @@ -744,7 +600,7 @@ def execute_with_long_wait_retry(session, query, timeout=30): del tb tries += 1 - raise RuntimeError("Failed to execute query after 100 attempts: {0}".format(query)) + raise RuntimeError("Failed to execute query after 10 attempts: {0}".format(query)) def execute_with_retry_tolerant(session, query, retry_exceptions, escape_exception): @@ -776,11 +632,7 @@ def drop_keyspace_shutdown_cluster(keyspace_name, session, cluster): cluster.shutdown() -def setup_keyspace(ipformat=None, wait=True, protocol_version=None, port=9042): - # wait for nodes to startup - if wait: - time.sleep(10) - +def setup_keyspace(ipformat=None, protocol_version=None, port=9042): if protocol_version: _protocol_version = protocol_version else: @@ -799,17 +651,17 @@ def setup_keyspace(ipformat=None, wait=True, protocol_version=None, port=9042): ddl = ''' CREATE KEYSPACE test3rf - WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '3'}''' + WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '3'}''' execute_with_long_wait_retry(session, ddl) ddl = ''' CREATE KEYSPACE test2rf - WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '2'}''' + WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '2'}''' execute_with_long_wait_retry(session, ddl) ddl = ''' CREATE KEYSPACE test1rf - WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}''' + WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '1'}''' execute_with_long_wait_retry(session, ddl) ddl_3f = ''' @@ -835,32 +687,55 @@ def is_scylla_enterprise(version: Version) -> bool: return version > Version('2000.1.1') -def xfail_scylla_version_lt(reason, oss_scylla_version, ent_scylla_version, *args, **kwargs): +def xfail_scylla_version_lt(reason, scylla_version, *args, **kwargs): """ It is used to mark tests that are going to fail on certain scylla versions. :param reason: message to fail test with - :param oss_scylla_version: str, oss version from which test supposed to succeed - :param ent_scylla_version: str, enterprise version from which test supposed to succeed. It should end with `.1.1` + :param scylla_version: str, version from which test supposed to succeed """ - if not reason.startswith("scylladb/scylladb#"): - raise ValueError('reason should start with scylladb/scylladb# to reference issue in scylla repo') - - if not isinstance(ent_scylla_version, str): - raise ValueError('ent_scylla_version should be a str') + if not (reason.startswith("scylladb/scylladb#") or reason.startswith("scylladb/scylla-enterprise#")): + raise ValueError('reason should start with scylladb/scylladb# or scylladb/scylla-enterprise# to reference issue in scylla repo') - if not ent_scylla_version.endswith("1.1"): - raise ValueError('ent_scylla_version should end with "1.1"') + if not isinstance(scylla_version, str): + raise ValueError('scylla_version should be a str') if SCYLLA_VERSION is None: return pytest.mark.skipif(False, reason="It is just a NoOP Decor, should not skip anything") current_version = Version(get_scylla_version(SCYLLA_VERSION)) - if is_scylla_enterprise(current_version): - return pytest.mark.xfail(current_version < Version(ent_scylla_version), - reason=reason, *args, **kwargs) + return pytest.mark.xfail(current_version < Version(scylla_version), reason=reason, *args, **kwargs) + - return pytest.mark.xfail(current_version < Version(oss_scylla_version), reason=reason, *args, **kwargs) +def get_tablets_disabled_ddl_suffix(scylla_version='2026.1'): + """ + Returns DDL option string for disabling tablets on ScyllaDB versions older than scylla_version. + Used to work around features not yet supported with tablets (e.g. MVs, secondary indexes, counters). + :param scylla_version: str, version from which tablets support the feature + """ + if SCYLLA_VERSION is not None and Version(get_scylla_version(SCYLLA_VERSION)) < Version(scylla_version): + return " AND tablets = {'enabled': false}" + return "" + + +def skip_scylla_version_lt(reason, scylla_version): + """ + Skip tests on scylla versions older than the specified thresholds. + :param reason: message explaining why the test is skipped + :param scylla_version: str, version from which test supposed to work + """ + if not (reason.startswith("scylladb/scylladb#") or reason.startswith("scylladb/scylla-enterprise#")): + raise ValueError('reason should start with scylladb/scylladb# or scylladb/scylla-enterprise# to reference issue in scylla repo') + + if not isinstance(scylla_version, str): + raise ValueError('scylla_version should be a str') + + if SCYLLA_VERSION is None: + return pytest.mark.skipif(False, reason="It is just a NoOP Decor, should not skip anything") + + current_version = Version(get_scylla_version(SCYLLA_VERSION)) + + return pytest.mark.skipif(current_version < Version(scylla_version), reason=reason) class UpDownWaiter(object): @@ -910,7 +785,7 @@ def drop_keyspace(cls): @classmethod def create_keyspace(cls, rf): - ddl = "CREATE KEYSPACE {0} WITH replication = {{'class': 'SimpleStrategy', 'replication_factor': '{1}'}}".format(cls.ks_name, rf) + ddl = "CREATE KEYSPACE {0} WITH replication = {{'class': 'NetworkTopologyStrategy', 'replication_factor': '{1}'}}".format(cls.ks_name, rf) execute_with_long_wait_retry(cls.session, ddl) @classmethod @@ -1096,13 +971,6 @@ def tearDown(self): self.cluster.shutdown() -def assert_startswith(s, prefix): - if not s.startswith(prefix): - raise AssertionError( - '{} does not start with {}'.format(repr(s), repr(prefix)) - ) - - class TestCluster(object): __test__ = False @@ -1153,4 +1021,4 @@ def _get_config_val(self, k, v): def set_configuration_options(self, values=None, *args, **kwargs): new_values = {self._get_config_key(k, str(v)):self._get_config_val(k, str(v)) for (k,v) in values.items()} - super(Cassandra41CCMCluster, self).set_configuration_options(values=new_values, *args, **kwargs) \ No newline at end of file + super(Cassandra41CCMCluster, self).set_configuration_options(values=new_values, *args, **kwargs) diff --git a/tests/integration/advanced/__init__.py b/tests/integration/advanced/__init__.py index dffaccd190..2c9ca172f8 100644 --- a/tests/integration/advanced/__init__.py +++ b/tests/integration/advanced/__init__.py @@ -11,152 +11,3 @@ # 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 unittest - -from urllib.request import build_opener, Request, HTTPHandler -import re -import os -import time -from os.path import expanduser - -from ccmlib import common - -from tests.integration import get_server_versions, BasicKeyspaceUnitTestCase, \ - drop_keyspace_shutdown_cluster, get_node, USE_CASS_EXTERNAL, TestCluster -from tests.integration import use_singledc, use_single_node, wait_for_node_socket, CASSANDRA_IP - -home = expanduser('~') - -# Home directory of the Embedded Apache Directory Server to use -ADS_HOME = os.getenv('ADS_HOME', home) - - -def find_spark_master(session): - - # Iterate over the nodes the one with port 7080 open is the spark master - for host in session.hosts: - ip = host.address - port = 7077 - spark_master = (ip, port) - if common.check_socket_listening(spark_master, timeout=3): - return spark_master[0] - return None - - -def wait_for_spark_workers(num_of_expected_workers, timeout): - """ - This queries the spark master and checks for the expected number of workers - """ - start_time = time.time() - while True: - opener = build_opener(HTTPHandler) - request = Request("http://{0}:7080".format(CASSANDRA_IP)) - request.get_method = lambda: 'GET' - connection = opener.open(request) - match = re.search('Alive Workers:.*(\d+)', connection.read().decode('utf-8')) - num_workers = int(match.group(1)) - if num_workers == num_of_expected_workers: - match = True - break - elif time.time() - start_time > timeout: - match = True - break - time.sleep(1) - return match - - -def use_single_node_with_graph(start=True, options={}, dse_options={}): - use_single_node(start=start, workloads=['graph'], configuration_options=options, dse_options=dse_options) - - -def use_single_node_with_graph_and_spark(start=True, options={}): - use_single_node(start=start, workloads=['graph', 'spark'], configuration_options=options) - - -def use_single_node_with_graph_and_solr(start=True, options={}): - use_single_node(start=start, workloads=['graph', 'solr'], configuration_options=options) - - -def use_singledc_wth_graph(start=True): - use_singledc(start=start, workloads=['graph']) - - -def use_singledc_wth_graph_and_spark(start=True): - use_cluster_with_graph(3) - - -def use_cluster_with_graph(num_nodes): - """ - This is a work around to account for the fact that spark nodes will conflict over master assignment - when started all at once. - """ - if USE_CASS_EXTERNAL: - return - - # Create the cluster but don't start it. - use_singledc(start=False, workloads=['graph', 'spark']) - # Start first node. - get_node(1).start(wait_for_binary_proto=True) - # Wait binary protocol port to open - wait_for_node_socket(get_node(1), 120) - # Wait for spark master to start up - spark_master_http = ("localhost", 7080) - common.check_socket_listening(spark_master_http, timeout=60) - tmp_cluster = TestCluster() - - # Start up remaining nodes. - try: - session = tmp_cluster.connect() - statement = "ALTER KEYSPACE dse_leases WITH REPLICATION = {'class': 'NetworkTopologyStrategy', 'dc1': '%d'}" % (num_nodes) - session.execute(statement) - finally: - tmp_cluster.shutdown() - - for i in range(1, num_nodes+1): - if i is not 1: - node = get_node(i) - node.start(wait_for_binary_proto=True) - wait_for_node_socket(node, 120) - - # Wait for workers to show up as Alive on master - wait_for_spark_workers(3, 120) - - -class BasicGeometricUnitTestCase(BasicKeyspaceUnitTestCase): - """ - This base test class is used by all the geomteric tests. It contains class level teardown and setup - methods. It also contains the test fixtures used by those tests - """ - - @classmethod - def common_dse_setup(cls, rf, keyspace_creation=True): - cls.cluster = TestCluster() - cls.session = cls.cluster.connect() - cls.ks_name = cls.__name__.lower() - if keyspace_creation: - cls.create_keyspace(rf) - cls.cass_version, cls.cql_version = get_server_versions() - cls.session.set_keyspace(cls.ks_name) - - @classmethod - def setUpClass(cls): - cls.common_dse_setup(1) - cls.initalizeTables() - - @classmethod - def tearDownClass(cls): - drop_keyspace_shutdown_cluster(cls.ks_name, cls.session, cls.cluster) - - @classmethod - def initalizeTables(cls): - udt_type = "CREATE TYPE udt1 (g {0})".format(cls.cql_type_name) - large_table = "CREATE TABLE tbl (k uuid PRIMARY KEY, g {0}, l list<{0}>, s set<{0}>, m0 map<{0},int>, m1 map, t tuple<{0},{0},{0}>, u frozen)".format( - cls.cql_type_name) - simple_table = "CREATE TABLE tblpk (k {0} primary key, v int)".format(cls.cql_type_name) - cluster_table = "CREATE TABLE tblclustering (k0 int, k1 {0}, v int, primary key (k0, k1))".format( - cls.cql_type_name) - cls.session.execute(udt_type) - cls.session.execute(large_table) - cls.session.execute(simple_table) - cls.session.execute(cluster_table) diff --git a/tests/integration/advanced/graph/__init__.py b/tests/integration/advanced/graph/__init__.py deleted file mode 100644 index cc40c6906a..0000000000 --- a/tests/integration/advanced/graph/__init__.py +++ /dev/null @@ -1,1195 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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 sys -import logging -import inspect -from packaging.version import Version -import ipaddress -from uuid import UUID -from decimal import Decimal -import datetime - -from cassandra.util import Point, LineString, Polygon, Duration - -from cassandra.cluster import EXEC_PROFILE_GRAPH_DEFAULT, EXEC_PROFILE_GRAPH_ANALYTICS_DEFAULT -from cassandra.cluster import GraphAnalyticsExecutionProfile, GraphExecutionProfile, EXEC_PROFILE_GRAPH_SYSTEM_DEFAULT, \ - default_lbp_factory -from cassandra.policies import DSELoadBalancingPolicy - -from cassandra.graph import GraphSON1Deserializer -from cassandra.graph.graphson import InetTypeIO, GraphSON2Deserializer, GraphSON3Deserializer -from cassandra.graph import Edge, Vertex, Path -from cassandra.graph.query import GraphOptions, GraphProtocol, graph_graphson2_row_factory, \ - graph_graphson3_row_factory - -from tests.integration import DSE_VERSION -from tests.integration.advanced import * - - -def setup_module(): - if DSE_VERSION: - dse_options = {'graph': {'realtime_evaluation_timeout_in_seconds': 60}} - use_single_node_with_graph(dse_options=dse_options) - - -log = logging.getLogger(__name__) - -MAX_LONG = 9223372036854775807 -MIN_LONG = -9223372036854775808 -ZERO_LONG = 0 - -MAKE_STRICT = "schema.config().option('graph.schema_mode').set('production')" -MAKE_NON_STRICT = "schema.config().option('graph.schema_mode').set('development')" -ALLOW_SCANS = "schema.config().option('graph.allow_scan').set('true')" - -deserializer_plus_to_ipaddressv4 = lambda x: ipaddress.IPv4Address(GraphSON1Deserializer.deserialize_inet(x)) -deserializer_plus_to_ipaddressv6 = lambda x: ipaddress.IPv6Address(GraphSON1Deserializer.deserialize_inet(x)) - - -def generic_ip_deserializer(string_ip_address): - if ":" in string_ip_address: - return deserializer_plus_to_ipaddressv6(string_ip_address) - return deserializer_plus_to_ipaddressv4(string_ip_address) - - -class GenericIpAddressIO(InetTypeIO): - @classmethod - def deserialize(cls, value, reader=None): - return generic_ip_deserializer(value) - -GraphSON2Deserializer._deserializers[GenericIpAddressIO.graphson_type] = GenericIpAddressIO -GraphSON3Deserializer._deserializers[GenericIpAddressIO.graphson_type] = GenericIpAddressIO - -if DSE_VERSION: - if DSE_VERSION >= Version('6.8.0'): - CREATE_CLASSIC_GRAPH = "system.graph(name).engine(Classic).create()" - else: - CREATE_CLASSIC_GRAPH = "system.graph(name).create()" - - -def reset_graph(session, graph_name): - ks = list(session.execute( - "SELECT * FROM system_schema.keyspaces WHERE keyspace_name = '{}';".format(graph_name))) - if ks: - try: - session.execute_graph('system.graph(name).drop()', {'name': graph_name}, - execution_profile=EXEC_PROFILE_GRAPH_SYSTEM_DEFAULT) - except: - pass - - session.execute_graph(CREATE_CLASSIC_GRAPH, {'name': graph_name}, - execution_profile=EXEC_PROFILE_GRAPH_SYSTEM_DEFAULT) - wait_for_graph_inserted(session, graph_name) - - -def wait_for_graph_inserted(session, graph_name): - count = 0 - exists = session.execute_graph('system.graph(name).exists()', {'name': graph_name}, - execution_profile=EXEC_PROFILE_GRAPH_SYSTEM_DEFAULT)[0].value - while not exists and count < 50: - time.sleep(1) - exists = session.execute_graph('system.graph(name).exists()', {'name': graph_name}, - execution_profile=EXEC_PROFILE_GRAPH_SYSTEM_DEFAULT)[0].value - return exists - - -class BasicGraphUnitTestCase(BasicKeyspaceUnitTestCase): - """ - This is basic graph unit test case that provides various utility methods that can be leveraged for testcase setup and tear - down - """ - - @property - def graph_name(self): - return self._testMethodName.lower() - - def session_setup(self): - lbp = DSELoadBalancingPolicy(default_lbp_factory()) - - ep_graphson2 = GraphExecutionProfile( - request_timeout=60, - load_balancing_policy=lbp, - graph_options=GraphOptions( - graph_name=self.graph_name, - graph_protocol=GraphProtocol.GRAPHSON_2_0 - ), - row_factory=graph_graphson2_row_factory) - - ep_graphson3 = GraphExecutionProfile( - request_timeout=60, - load_balancing_policy=lbp, - graph_options=GraphOptions( - graph_name=self.graph_name, - graph_protocol=GraphProtocol.GRAPHSON_3_0 - ), - row_factory=graph_graphson3_row_factory) - - ep_graphson1 = GraphExecutionProfile( - request_timeout=60, - load_balancing_policy=lbp, - graph_options=GraphOptions( - graph_name=self.graph_name - ) - ) - - ep_analytics = GraphAnalyticsExecutionProfile( - request_timeout=60, - load_balancing_policy=lbp, - graph_options=GraphOptions( - graph_source=b'a', - graph_language=b'gremlin-groovy', - graph_name=self.graph_name - ) - ) - - self.cluster = TestCluster(execution_profiles={ - EXEC_PROFILE_GRAPH_DEFAULT: ep_graphson1, - EXEC_PROFILE_GRAPH_ANALYTICS_DEFAULT: ep_analytics, - "graphson1": ep_graphson1, - "graphson2": ep_graphson2, - "graphson3": ep_graphson3 - }) - - self.session = self.cluster.connect() - self.ks_name = self._testMethodName.lower() - self.cass_version, self.cql_version = get_server_versions() - - def setUp(self): - self.session_setup() - self.reset_graph() - self.clear_schema() - # enable dev and scan modes - self.session.execute_graph(MAKE_NON_STRICT) - self.session.execute_graph(ALLOW_SCANS) - - def tearDown(self): - self.cluster.shutdown() - - def clear_schema(self): - self.session.execute_graph(""" - schema.clear(); - """) - - def reset_graph(self): - reset_graph(self.session, self.graph_name) - - def wait_for_graph_inserted(self): - wait_for_graph_inserted(self.session, self.graph_name) - - def _execute(self, query, graphson, params=None, execution_profile_options=None, **kwargs): - queries = query if isinstance(query, list) else [query] - ep = self.get_execution_profile(graphson) - if execution_profile_options: - ep = self.session.execution_profile_clone_update(ep, **execution_profile_options) - - results = [] - for query in queries: - log.debug(query) - rf = self.session.execute_graph_async(query, parameters=params, execution_profile=ep, **kwargs) - results.append(rf.result()) - self.assertEqual(rf.message.custom_payload['graph-results'], graphson) - - return results[0] if len(results) == 1 else results - - def get_execution_profile(self, graphson, traversal=False): - ep = 'graphson1' - if graphson == GraphProtocol.GRAPHSON_2_0: - ep = 'graphson2' - elif graphson == GraphProtocol.GRAPHSON_3_0: - ep = 'graphson3' - - return ep if traversal is False else 'traversal_' + ep - - def resultset_to_list(self, rs): - results_list = [] - for result in rs: - try: - results_list.append(result.value) - except: - results_list.append(result) - - return results_list - - -class GraphUnitTestCase(BasicKeyspaceUnitTestCase): - - @property - def graph_name(self): - return self._testMethodName.lower() - - def session_setup(self): - lbp = DSELoadBalancingPolicy(default_lbp_factory()) - - ep_graphson2 = GraphExecutionProfile( - request_timeout=60, - load_balancing_policy=lbp, - graph_options=GraphOptions( - graph_name=self.graph_name, - graph_protocol=GraphProtocol.GRAPHSON_2_0 - ), - row_factory=graph_graphson2_row_factory) - - ep_graphson3 = GraphExecutionProfile( - request_timeout=60, - load_balancing_policy=lbp, - graph_options=GraphOptions( - graph_name=self.graph_name, - graph_protocol=GraphProtocol.GRAPHSON_3_0 - ), - row_factory=graph_graphson3_row_factory) - - ep_graphson1 = GraphExecutionProfile( - request_timeout=60, - load_balancing_policy=lbp, - graph_options=GraphOptions( - graph_name=self.graph_name, - graph_language='gremlin-groovy' - ) - ) - - ep_analytics = GraphAnalyticsExecutionProfile( - request_timeout=60, - load_balancing_policy=lbp, - graph_options=GraphOptions( - graph_source=b'a', - graph_language=b'gremlin-groovy', - graph_name=self.graph_name - ) - ) - - self.cluster = TestCluster(execution_profiles={ - EXEC_PROFILE_GRAPH_DEFAULT: ep_graphson1, - EXEC_PROFILE_GRAPH_ANALYTICS_DEFAULT: ep_analytics, - "graphson1": ep_graphson1, - "graphson2": ep_graphson2, - "graphson3": ep_graphson3 - }) - - self.session = self.cluster.connect() - self.ks_name = self._testMethodName.lower() - self.cass_version, self.cql_version = get_server_versions() - - def setUp(self): - """basic setup only""" - self.session_setup() - - def setup_graph(self, schema): - """Config dependant setup""" - schema.drop_graph(self.session, self.graph_name) - schema.create_graph(self.session, self.graph_name) - schema.clear(self.session) - if schema is ClassicGraphSchema: - # enable dev and scan modes - self.session.execute_graph(MAKE_NON_STRICT) - self.session.execute_graph(ALLOW_SCANS) - - def teardown_graph(self, schema): - schema.drop_graph(self.session, self.graph_name) - - def tearDown(self): - self.cluster.shutdown() - - def execute_graph_queries(self, queries, params=None, execution_profile=EXEC_PROFILE_GRAPH_DEFAULT, - verify_graphson=False, **kwargs): - results = [] - for query in queries: - log.debug(query) - rf = self.session.execute_graph_async(query, parameters=params, - execution_profile=execution_profile, **kwargs) - if verify_graphson: - self.assertEqual(rf.message.custom_payload['graph-results'], verify_graphson) - results.append(rf.result()) - - return results - - def execute_graph(self, query, graphson, params=None, execution_profile_options=None, traversal=False, **kwargs): - queries = query if isinstance(query, list) else [query] - ep = self.get_execution_profile(graphson) - if traversal: - ep = 'traversal_' + ep - if execution_profile_options: - ep = self.session.execution_profile_clone_update(ep, **execution_profile_options) - - results = self.execute_graph_queries(queries, params, ep, verify_graphson=graphson, **kwargs) - - return results[0] if len(results) == 1 else results - - def get_execution_profile(self, graphson, traversal=False): - ep = 'graphson1' - if graphson == GraphProtocol.GRAPHSON_2_0: - ep = 'graphson2' - elif graphson == GraphProtocol.GRAPHSON_3_0: - ep = 'graphson3' - - return ep if traversal is False else 'traversal_' + ep - - def resultset_to_list(self, rs): - results_list = [] - for result in rs: - try: - results_list.append(result.value) - except: - results_list.append(result) - - return results_list - - -class BasicSharedGraphUnitTestCase(BasicKeyspaceUnitTestCase): - """ - This is basic graph unit test case that provides various utility methods that can be leveraged for testcase setup and tear - down - """ - - @classmethod - def session_setup(cls): - cls.cluster = TestCluster() - cls.session = cls.cluster.connect() - cls.ks_name = cls.__name__.lower() - cls.cass_version, cls.cql_version = get_server_versions() - cls.graph_name = cls.__name__.lower() - - @classmethod - def setUpClass(cls): - if DSE_VERSION: - cls.session_setup() - cls.reset_graph() - profiles = cls.cluster.profile_manager.profiles - profiles[EXEC_PROFILE_GRAPH_DEFAULT].request_timeout = 60 - profiles[EXEC_PROFILE_GRAPH_DEFAULT].graph_options.graph_name = cls.graph_name - profiles[EXEC_PROFILE_GRAPH_ANALYTICS_DEFAULT].request_timeout = 60 - profiles[EXEC_PROFILE_GRAPH_ANALYTICS_DEFAULT].graph_options.graph_name = cls.graph_name - - @classmethod - def tearDownClass(cls): - if DSE_VERSION: - cls.cluster.shutdown() - - @classmethod - def clear_schema(self): - self.session.execute_graph('schema.clear()') - - @classmethod - def reset_graph(self): - reset_graph(self.session, self.graph_name) - - def wait_for_graph_inserted(self): - wait_for_graph_inserted(self.session, self.graph_name) - - -class GraphFixtures(object): - - @staticmethod - def line(length, single_script=True): - raise NotImplementedError() - - @staticmethod - def classic(): - raise NotImplementedError() - - @staticmethod - def multiple_fields(): - raise NotImplementedError() - - @staticmethod - def large(): - raise NotImplementedError() - - -class ClassicGraphFixtures(GraphFixtures): - - @staticmethod - def datatypes(): - data = { - "boolean1": ["Boolean()", True, None], - "boolean2": ["Boolean()", False, None], - "point1": ["Point()", Point(.5, .13), GraphSON1Deserializer.deserialize_point], - "point2": ["Point()", Point(-5, .0), GraphSON1Deserializer.deserialize_point], - - "linestring1": ["Linestring()", LineString(((1.0, 2.0), (3.0, 4.0), (-89.0, 90.0))), - GraphSON1Deserializer.deserialize_linestring], - "polygon1": ["Polygon()", Polygon([(10.0, 10.0), (80.0, 10.0), (80., 88.0), (10., 89.0), (10., 10.0)], - [[(20., 20.0), (20., 30.0), (30., 30.0), (30., 20.0), (20., 20.0)], - [(40., 20.0), (40., 30.0), (50., 30.0), (50., 20.0), (40., 20.0)]]), - GraphSON1Deserializer.deserialize_polygon], - "int1": ["Int()", 2, GraphSON1Deserializer.deserialize_int], - "smallint1": ["Smallint()", 1, GraphSON1Deserializer.deserialize_smallint], - "bigint1": ["Bigint()", MAX_LONG, GraphSON1Deserializer.deserialize_bigint], - "bigint2": ["Bigint()", MIN_LONG, GraphSON1Deserializer.deserialize_bigint], - "bigint3": ["Bigint()", ZERO_LONG, GraphSON1Deserializer.deserialize_bigint], - "varint1": ["Varint()", 2147483647, GraphSON1Deserializer.deserialize_varint], - "int1": ["Int()", 100, GraphSON1Deserializer.deserialize_int], - "float1": ["Float()", 0.3415681, GraphSON1Deserializer.deserialize_float], - "double1": ["Double()", 0.34156811237335205, GraphSON1Deserializer.deserialize_double], - "uuid1": ["Uuid()", UUID('12345678123456781234567812345678'), GraphSON1Deserializer.deserialize_uuid], - "decimal1": ["Decimal()", Decimal(10), GraphSON1Deserializer.deserialize_decimal], - "blob1": ["Blob()", bytearray(b"Hello World"), GraphSON1Deserializer.deserialize_blob], - - "timestamp1": ["Timestamp()", datetime.datetime.utcnow().replace(microsecond=0), - GraphSON1Deserializer.deserialize_timestamp], - "timestamp2": ["Timestamp()", datetime.datetime.max.replace(microsecond=0), - GraphSON1Deserializer.deserialize_timestamp], - # These are valid values but are pending for DSP-14093 to be fixed - #"timestamp3": ["Timestamp()", datetime.datetime(159, 1, 1, 23, 59, 59), - # GraphSON1TypeDeserializer.deserialize_timestamp], - #"timestamp4": ["Timestamp()", datetime.datetime.min, - # GraphSON1TypeDeserializer.deserialize_timestamp], - "inet1": ["Inet()", ipaddress.IPv4Address(u"127.0.0.1"), deserializer_plus_to_ipaddressv4], - "inet2": ["Inet()", ipaddress.IPv6Address(u"2001:db8:85a3:8d3:1319:8a2e:370:7348"), - deserializer_plus_to_ipaddressv6], - "duration1": ["Duration()", datetime.timedelta(1, 16, 0), - GraphSON1Deserializer.deserialize_duration], - "duration2": ["Duration()", datetime.timedelta(days=1, seconds=16, milliseconds=15), - GraphSON1Deserializer.deserialize_duration], - "blob3": ["Blob()", bytes(b"Hello World Again"), GraphSON1Deserializer.deserialize_blob], - "blob4": ["Blob()", memoryview(b"And Again Hello World"), GraphSON1Deserializer.deserialize_blob] - } - - if DSE_VERSION >= Version("5.1"): - data["time1"] = ["Time()", datetime.time(12, 6, 12, 444), GraphSON1Deserializer.deserialize_time] - data["time2"] = ["Time()", datetime.time(12, 6, 12), GraphSON1Deserializer.deserialize_time] - data["time3"] = ["Time()", datetime.time(12, 6), GraphSON1Deserializer.deserialize_time] - data["time4"] = ["Time()", datetime.time.min, GraphSON1Deserializer.deserialize_time] - data["time5"] = ["Time()", datetime.time.max, GraphSON1Deserializer.deserialize_time] - data["blob5"] = ["Blob()", bytearray(b"AKDLIElksadlaswqA" * 10000), GraphSON1Deserializer.deserialize_blob] - data["datetime1"] = ["Date()", datetime.date.today(), GraphSON1Deserializer.deserialize_date] - data["datetime2"] = ["Date()", datetime.date(159, 1, 3), GraphSON1Deserializer.deserialize_date] - data["datetime3"] = ["Date()", datetime.date.min, GraphSON1Deserializer.deserialize_date] - data["datetime4"] = ["Date()", datetime.date.max, GraphSON1Deserializer.deserialize_date] - data["time1"] = ["Time()", datetime.time(12, 6, 12, 444), GraphSON1Deserializer.deserialize_time] - data["time2"] = ["Time()", datetime.time(12, 6, 12), GraphSON1Deserializer.deserialize_time] - data["time3"] = ["Time()", datetime.time(12, 6), GraphSON1Deserializer.deserialize_time] - data["time4"] = ["Time()", datetime.time.min, GraphSON1Deserializer.deserialize_time] - data["time5"] = ["Time()", datetime.time.max, GraphSON1Deserializer.deserialize_time] - - return data - - @staticmethod - def line(length, single_script=False): - queries = [ALLOW_SCANS + ';', - """schema.propertyKey('index').Int().ifNotExists().create(); - schema.propertyKey('distance').Int().ifNotExists().create(); - schema.vertexLabel('lp').properties('index').ifNotExists().create(); - schema.edgeLabel('goesTo').properties('distance').connection('lp', 'lp').ifNotExists().create();"""] - - vertex_script = ["Vertex vertex0 = graph.addVertex(label, 'lp', 'index', 0);"] - for index in range(1, length): - if not single_script and len(vertex_script) > 25: - queries.append("\n".join(vertex_script)) - vertex_script = [ - "Vertex vertex{pindex} = g.V().hasLabel('lp').has('index', {pindex}).next()".format( - pindex=index-1)] - - vertex_script.append(''' - Vertex vertex{vindex} = graph.addVertex(label, 'lp', 'index', {vindex}); - vertex{pindex}.addEdge('goesTo', vertex{vindex}, 'distance', 5); '''.format( - vindex=index, pindex=index - 1)) - - queries.append("\n".join(vertex_script)) - return queries - - @staticmethod - def classic(): - queries = [ALLOW_SCANS, - '''schema.propertyKey('name').Text().ifNotExists().create(); - schema.propertyKey('age').Int().ifNotExists().create(); - schema.propertyKey('lang').Text().ifNotExists().create(); - schema.propertyKey('weight').Float().ifNotExists().create(); - schema.vertexLabel('person').properties('name', 'age').ifNotExists().create(); - schema.vertexLabel('software').properties('name', 'lang').ifNotExists().create(); - schema.edgeLabel('created').properties('weight').connection('person', 'software').ifNotExists().create(); - schema.edgeLabel('created').connection('software', 'software').add(); - schema.edgeLabel('knows').properties('weight').connection('person', 'person').ifNotExists().create();''', - - '''Vertex marko = graph.addVertex(label, 'person', 'name', 'marko', 'age', 29); - Vertex vadas = graph.addVertex(label, 'person', 'name', 'vadas', 'age', 27); - Vertex lop = graph.addVertex(label, 'software', 'name', 'lop', 'lang', 'java'); - Vertex josh = graph.addVertex(label, 'person', 'name', 'josh', 'age', 32); - Vertex ripple = graph.addVertex(label, 'software', 'name', 'ripple', 'lang', 'java'); - Vertex peter = graph.addVertex(label, 'person', 'name', 'peter', 'age', 35); - Vertex carl = graph.addVertex(label, 'person', 'name', 'carl', 'age', 35); - marko.addEdge('knows', vadas, 'weight', 0.5f); - marko.addEdge('knows', josh, 'weight', 1.0f); - marko.addEdge('created', lop, 'weight', 0.4f); - josh.addEdge('created', ripple, 'weight', 1.0f); - josh.addEdge('created', lop, 'weight', 0.4f); - peter.addEdge('created', lop, 'weight', 0.2f);'''] - - return "\n".join(queries) - - @staticmethod - def multiple_fields(): - query_params = {} - queries= [ALLOW_SCANS, - '''schema.propertyKey('shortvalue').Smallint().ifNotExists().create(); - schema.vertexLabel('shortvertex').properties('shortvalue').ifNotExists().create(); - short s1 = 5000; graph.addVertex(label, "shortvertex", "shortvalue", s1); - schema.propertyKey('intvalue').Int().ifNotExists().create(); - schema.vertexLabel('intvertex').properties('intvalue').ifNotExists().create(); - int i1 = 1000000000; graph.addVertex(label, "intvertex", "intvalue", i1); - schema.propertyKey('intvalue2').Int().ifNotExists().create(); - schema.vertexLabel('intvertex2').properties('intvalue2').ifNotExists().create(); - Integer i2 = 100000000; graph.addVertex(label, "intvertex2", "intvalue2", i2); - schema.propertyKey('longvalue').Bigint().ifNotExists().create(); - schema.vertexLabel('longvertex').properties('longvalue').ifNotExists().create(); - long l1 = 9223372036854775807; graph.addVertex(label, "longvertex", "longvalue", l1); - schema.propertyKey('longvalue2').Bigint().ifNotExists().create(); - schema.vertexLabel('longvertex2').properties('longvalue2').ifNotExists().create(); - Long l2 = 100000000000000000L; graph.addVertex(label, "longvertex2", "longvalue2", l2); - schema.propertyKey('floatvalue').Float().ifNotExists().create(); - schema.vertexLabel('floatvertex').properties('floatvalue').ifNotExists().create(); - float f1 = 3.5f; graph.addVertex(label, "floatvertex", "floatvalue", f1); - schema.propertyKey('doublevalue').Double().ifNotExists().create(); - schema.vertexLabel('doublevertex').properties('doublevalue').ifNotExists().create(); - double d1 = 3.5e40; graph.addVertex(label, "doublevertex", "doublevalue", d1); - schema.propertyKey('doublevalue2').Double().ifNotExists().create(); - schema.vertexLabel('doublevertex2').properties('doublevalue2').ifNotExists().create(); - Double d2 = 3.5e40d; graph.addVertex(label, "doublevertex2", "doublevalue2", d2);'''] - - if DSE_VERSION >= Version('5.1'): - queries.append('''schema.propertyKey('datevalue1').Date().ifNotExists().create(); - schema.vertexLabel('datevertex1').properties('datevalue1').ifNotExists().create(); - schema.propertyKey('negdatevalue2').Date().ifNotExists().create(); - schema.vertexLabel('negdatevertex2').properties('negdatevalue2').ifNotExists().create();''') - - for i in range(1, 4): - queries.append('''schema.propertyKey('timevalue{0}').Time().ifNotExists().create(); - schema.vertexLabel('timevertex{0}').properties('timevalue{0}').ifNotExists().create();'''.format( - i)) - - queries.append('graph.addVertex(label, "datevertex1", "datevalue1", date1);') - query_params['date1'] = '1999-07-29' - - queries.append('graph.addVertex(label, "negdatevertex2", "negdatevalue2", date2);') - query_params['date2'] = '-1999-07-28' - - queries.append('graph.addVertex(label, "timevertex1", "timevalue1", time1);') - query_params['time1'] = '14:02' - queries.append('graph.addVertex(label, "timevertex2", "timevalue2", time2);') - query_params['time2'] = '14:02:20' - queries.append('graph.addVertex(label, "timevertex3", "timevalue3", time3);') - query_params['time3'] = '14:02:20.222' - - return queries, query_params - - @staticmethod - def large(): - query_parts = [''' - int size = 2000; - List ids = new ArrayList(); - schema.propertyKey('ts').Int().single().ifNotExists().create(); - schema.propertyKey('sin').Int().single().ifNotExists().create(); - schema.propertyKey('cos').Int().single().ifNotExists().create(); - schema.propertyKey('ii').Int().single().ifNotExists().create(); - schema.vertexLabel('lcg').properties('ts', 'sin', 'cos', 'ii').ifNotExists().create(); - schema.edgeLabel('linked').connection('lcg', 'lcg').ifNotExists().create(); - Vertex v = graph.addVertex(label, 'lcg'); - v.property("ts", 100001); - v.property("sin", 0); - v.property("cos", 1); - v.property("ii", 0); - ids.add(v.id()); - Random rand = new Random(); - for (int ii = 1; ii < size; ii++) { - v = graph.addVertex(label, 'lcg'); - v.property("ii", ii); - v.property("ts", 100001 + ii); - v.property("sin", Math.sin(ii/5.0)); - v.property("cos", Math.cos(ii/5.0)); - Vertex u = g.V(ids.get(rand.nextInt(ids.size()))).next(); - v.addEdge("linked", u); - ids.add(v.id()); - } - g.V().count();'''] - - return "\n".join(query_parts) - - @staticmethod - def address_book(): - p1 = "Point()" - p2 = "Point()" - if DSE_VERSION >= Version('5.1'): - p1 = "Point().withBounds(-100, -100, 100, 100)" - p2 = "Point().withGeoBounds()" - - queries = [ - ALLOW_SCANS, - "schema.propertyKey('name').Text().ifNotExists().create()", - "schema.propertyKey('pointPropWithBoundsWithSearchIndex').{}.ifNotExists().create()".format(p1), - "schema.propertyKey('pointPropWithBounds').{}.ifNotExists().create()".format(p1), - "schema.propertyKey('pointPropWithGeoBoundsWithSearchIndex').{}.ifNotExists().create()".format(p2), - "schema.propertyKey('pointPropWithGeoBounds').{}.ifNotExists().create()".format(p2), - "schema.propertyKey('city').Text().ifNotExists().create()", - "schema.propertyKey('state').Text().ifNotExists().create()", - "schema.propertyKey('description').Text().ifNotExists().create()", - "schema.vertexLabel('person').properties('name', 'city', 'state', 'description', 'pointPropWithBoundsWithSearchIndex', 'pointPropWithBounds', 'pointPropWithGeoBoundsWithSearchIndex', 'pointPropWithGeoBounds').ifNotExists().create()", - "schema.vertexLabel('person').index('searchPointWithBounds').secondary().by('pointPropWithBounds').ifNotExists().add()", - "schema.vertexLabel('person').index('searchPointWithGeoBounds').secondary().by('pointPropWithGeoBounds').ifNotExists().add()", - - "g.addV('person').property('name', 'Paul Thomas Joe').property('city', 'Rochester').property('state', 'MN').property('pointPropWithBoundsWithSearchIndex', Geo.point(-92.46295, 44.0234)).property('pointPropWithBounds', Geo.point(-92.46295, 44.0234)).property('pointPropWithGeoBoundsWithSearchIndex', Geo.point(-92.46295, 44.0234)).property('pointPropWithGeoBounds', Geo.point(-92.46295, 44.0234)).property('description', 'Lives by the hospital').next()", - "g.addV('person').property('name', 'George Bill Steve').property('city', 'Minneapolis').property('state', 'MN').property('pointPropWithBoundsWithSearchIndex', Geo.point(-93.266667, 44.093333)).property('pointPropWithBounds', Geo.point(-93.266667, 44.093333)).property('pointPropWithGeoBoundsWithSearchIndex', Geo.point(-93.266667, 44.093333)).property('pointPropWithGeoBounds', Geo.point(-93.266667, 44.093333)).property('description', 'A cold dude').next()", - "g.addV('person').property('name', 'James Paul Smith').property('city', 'Chicago').property('state', 'IL').property('pointPropWithBoundsWithSearchIndex', Geo.point(-87.684722, 41.836944)).property('description', 'Likes to hang out').next()", - "g.addV('person').property('name', 'Jill Alice').property('city', 'Atlanta').property('state', 'GA').property('pointPropWithBoundsWithSearchIndex', Geo.point(-84.39, 33.755)).property('description', 'Enjoys a nice cold coca cola').next()" - ] - - if not Version('5.0') <= DSE_VERSION < Version('5.1'): - queries.append("schema.vertexLabel('person').index('search').search().by('pointPropWithBoundsWithSearchIndex').withError(0.00001, 0.0).by('pointPropWithGeoBoundsWithSearchIndex').withError(0.00001, 0.0).ifNotExists().add()") - - return "\n".join(queries) - - -class CoreGraphFixtures(GraphFixtures): - - @staticmethod - def datatypes(): - data = ClassicGraphFixtures.datatypes() - del data['duration1'] - del data['duration2'] - - # Core Graphs only types - data["map1"] = ["mapOf(Text, Text)", {'test': 'test'}, None] - data["map2"] = ["mapOf(Text, Point)", {'test': Point(.5, .13)}, None] - data["map3"] = ["frozen(mapOf(Int, Varchar))", {42: 'test'}, None] - - data["list1"] = ["listOf(Text)", ['test', 'hello', 'world'], None] - data["list2"] = ["listOf(Int)", [42, 632, 32], None] - data["list3"] = ["listOf(Point)", [Point(.5, .13), Point(42.5, .13)], None] - data["list4"] = ["frozen(listOf(Int))", [42, 55, 33], None] - - data["set1"] = ["setOf(Text)", {'test', 'hello', 'world'}, None] - data["set2"] = ["setOf(Int)", {42, 632, 32}, None] - data["set3"] = ["setOf(Point)", {Point(.5, .13), Point(42.5, .13)}, None] - data["set4"] = ["frozen(setOf(Int))", {42, 55, 33}, None] - - data["tuple1"] = ["tupleOf(Int, Text)", (42, "world"), None] - data["tuple2"] = ["tupleOf(Int, tupleOf(Text, tupleOf(Text, Point)))", (42, ("world", ('this', Point(.5, .13)))), None] - data["tuple3"] = ["tupleOf(Int, tupleOf(Text, frozen(mapOf(Text, Text))))", (42, ("world", {'test': 'test'})), None] - data["tuple4"] = ["tupleOf(Int, tupleOf(Text, frozen(listOf(Int))))", (42, ("world", [65, 89])), None] - data["tuple5"] = ["tupleOf(Int, tupleOf(Text, frozen(setOf(Int))))", (42, ("world", {65, 55})), None] - data["tuple6"] = ["tupleOf(Int, tupleOf(Text, tupleOf(Text, LineString)))", - (42, ("world", ('this', LineString(((1.0, 2.0), (3.0, 4.0), (-89.0, 90.0)))))), None] - - data["tuple7"] = ["tupleOf(Int, tupleOf(Text, tupleOf(Text, Polygon)))", - (42, ("world", ('this', Polygon([(10.0, 10.0), (80.0, 10.0), (80., 88.0), (10., 89.0), (10., 10.0)], - [[(20., 20.0), (20., 30.0), (30., 30.0), (30., 20.0), (20., 20.0)], - [(40., 20.0), (40., 30.0), (50., 30.0), (50., 20.0), (40., 20.0)]])))), None] - data["dse_duration1"] = ["Duration()", Duration(42, 12, 10303312), None] - data["dse_duration2"] = ["Duration()", Duration(50, 32, 11), None] - - return data - - @staticmethod - def line(length, single_script=False): - queries = [""" - schema.vertexLabel('lp').ifNotExists().partitionBy('index', Int).create(); - schema.edgeLabel('goesTo').ifNotExists().from('lp').to('lp').property('distance', Int).create(); - """] - - vertex_script = ["g.addV('lp').property('index', 0).next();"] - for index in range(1, length): - if not single_script and len(vertex_script) > 25: - queries.append("\n".join(vertex_script)) - vertex_script = [] - - vertex_script.append(''' - g.addV('lp').property('index', {index}).next(); - g.V().hasLabel('lp').has('index', {pindex}).as('pp').V().hasLabel('lp').has('index', {index}).as('p'). - addE('goesTo').from('pp').to('p').property('distance', 5).next(); - '''.format( - index=index, pindex=index - 1)) - - queries.append("\n".join(vertex_script)) - return queries - - @staticmethod - def classic(): - queries = [ - ''' - schema.vertexLabel('person').ifNotExists().partitionBy('name', Text).property('age', Int).create(); - schema.vertexLabel('software')ifNotExists().partitionBy('name', Text).property('lang', Text).create(); - schema.edgeLabel('created').ifNotExists().from('person').to('software').property('weight', Double).create(); - schema.edgeLabel('knows').ifNotExists().from('person').to('person').property('weight', Double).create(); - ''', - - ''' - Vertex marko = g.addV('person').property('name', 'marko').property('age', 29).next(); - Vertex vadas = g.addV('person').property('name', 'vadas').property('age', 27).next(); - Vertex lop = g.addV('software').property('name', 'lop').property('lang', 'java').next(); - Vertex josh = g.addV('person').property('name', 'josh').property('age', 32).next(); - Vertex peter = g.addV('person').property('name', 'peter').property('age', 35).next(); - Vertex carl = g.addV('person').property('name', 'carl').property('age', 35).next(); - Vertex ripple = g.addV('software').property('name', 'ripple').property('lang', 'java').next(); - - // TODO, switch to VertexReference and use v.id() - g.V().hasLabel('person').has('name', 'vadas').as('v').V().hasLabel('person').has('name', 'marko').as('m').addE('knows').from('m').to('v').property('weight', 0.5d).next(); - g.V().hasLabel('person').has('name', 'josh').as('j').V().hasLabel('person').has('name', 'marko').as('m').addE('knows').from('m').to('j').property('weight', 1.0d).next(); - g.V().hasLabel('software').has('name', 'lop').as('l').V().hasLabel('person').has('name', 'marko').as('m').addE('created').from('m').to('l').property('weight', 0.4d).next(); - g.V().hasLabel('software').has('name', 'ripple').as('r').V().hasLabel('person').has('name', 'josh').as('j').addE('created').from('j').to('r').property('weight', 1.0d).next(); - g.V().hasLabel('software').has('name', 'lop').as('l').V().hasLabel('person').has('name', 'josh').as('j').addE('created').from('j').to('l').property('weight', 0.4d).next(); - g.V().hasLabel('software').has('name', 'lop').as('l').V().hasLabel('person').has('name', 'peter').as('p').addE('created').from('p').to('l').property('weight', 0.2d).next(); - - '''] - - return queries - - @staticmethod - def multiple_fields(): - ## no generic test currently needs this - raise NotImplementedError() - - @staticmethod - def large(): - query_parts = [ - ''' - schema.vertexLabel('lcg').ifNotExists().partitionBy('ts', Int).property('sin', Double). - property('cos', Double).property('ii', Int).create(); - schema.edgeLabel('linked').ifNotExists().from('lcg').to('lcg').create(); - ''', - - ''' - int size = 2000; - List ids = new ArrayList(); - v = g.addV('lcg').property('ts', 100001).property('sin', 0d).property('cos', 1d).property('ii', 0).next(); - ids.add(v.id()); - Random rand = new Random(); - for (int ii = 1; ii < size; ii++) { - v = g.addV('lcg').property('ts', 100001 + ii).property('sin', Math.sin(ii/5.0)).property('cos', Math.cos(ii/5.0)).property('ii', ii).next(); - - uid = ids.get(rand.nextInt(ids.size())) - g.V(v.id()).as('v').V(uid).as('u').addE('linked').from('v').to('u').next(); - ids.add(v.id()); - } - g.V().count();''' - ] - - return query_parts - - @staticmethod - def address_book(): - queries = [ - "schema.vertexLabel('person').ifNotExists().partitionBy('name', Text)." - "property('pointPropWithBoundsWithSearchIndex', Point)." - "property('pointPropWithBounds', Point)." - "property('pointPropWithGeoBoundsWithSearchIndex', Point)." - "property('pointPropWithGeoBounds', Point)." - "property('city', Text)." - "property('state', Text)." - "property('description', Text).create()", - "schema.vertexLabel('person').searchIndex().by('name').by('pointPropWithBounds').by('pointPropWithGeoBounds').by('description').asText().create()", - "g.addV('person').property('name', 'Paul Thomas Joe').property('city', 'Rochester').property('state', 'MN').property('pointPropWithBoundsWithSearchIndex', Geo.point(-92.46295, 44.0234)).property('pointPropWithBounds', Geo.point(-92.46295, 44.0234)).property('pointPropWithGeoBoundsWithSearchIndex', Geo.point(-92.46295, 44.0234)).property('pointPropWithGeoBounds', Geo.point(-92.46295, 44.0234)).property('description', 'Lives by the hospital').next()", - "g.addV('person').property('name', 'George Bill Steve').property('city', 'Minneapolis').property('state', 'MN').property('pointPropWithBoundsWithSearchIndex', Geo.point(-93.266667, 44.093333)).property('pointPropWithBounds', Geo.point(-93.266667, 44.093333)).property('pointPropWithGeoBoundsWithSearchIndex', Geo.point(-93.266667, 44.093333)).property('pointPropWithGeoBounds', Geo.point(-93.266667, 44.093333)).property('description', 'A cold dude').next()", - "g.addV('person').property('name', 'James Paul Smith').property('city', 'Chicago').property('state', 'IL').property('pointPropWithBoundsWithSearchIndex', Geo.point(-87.684722, 41.836944)).property('description', 'Likes to hang out').next()", - "g.addV('person').property('name', 'Jill Alice').property('city', 'Atlanta').property('state', 'GA').property('pointPropWithBoundsWithSearchIndex', Geo.point(-84.39, 33.755)).property('description', 'Enjoys a nice cold coca cola').next()" - ] - - if not Version('5.0') <= DSE_VERSION < Version('5.1'): - queries.append("schema.vertexLabel('person').searchIndex().by('pointPropWithBoundsWithSearchIndex').by('pointPropWithGeoBounds')" - ".by('pointPropWithGeoBoundsWithSearchIndex').create()") - - return queries - - -def validate_classic_vertex(test, vertex): - vertex_props = vertex.properties.keys() - test.assertEqual(len(vertex_props), 2) - test.assertIn('name', vertex_props) - test.assertTrue('lang' in vertex_props or 'age' in vertex_props) - - -def validate_classic_vertex_return_type(test, vertex): - validate_generic_vertex_result_type(vertex) - vertex_props = vertex.properties - test.assertIn('name', vertex_props) - test.assertTrue('lang' in vertex_props or 'age' in vertex_props) - - -def validate_generic_vertex_result_type(test, vertex): - test.assertIsInstance(vertex, Vertex) - for attr in ('id', 'type', 'label', 'properties'): - test.assertIsNotNone(getattr(vertex, attr)) - - -def validate_classic_edge_properties(test, edge_properties): - test.assertEqual(len(edge_properties.keys()), 1) - test.assertIn('weight', edge_properties) - test.assertIsInstance(edge_properties, dict) - - -def validate_classic_edge(test, edge): - validate_generic_edge_result_type(test, edge) - validate_classic_edge_properties(test, edge.properties) - - -def validate_line_edge(test, edge): - validate_generic_edge_result_type(test, edge) - edge_props = edge.properties - test.assertEqual(len(edge_props.keys()), 1) - test.assertIn('distance', edge_props) - - -def validate_generic_edge_result_type(test, edge): - test.assertIsInstance(edge, Edge) - for attr in ('properties', 'outV', 'outVLabel', 'inV', 'inVLabel', 'label', 'type', 'id'): - test.assertIsNotNone(getattr(edge, attr)) - - -def validate_path_result_type(test, path): - test.assertIsInstance(path, Path) - test.assertIsNotNone(path.labels) - for obj in path.objects: - if isinstance(obj, Edge): - validate_classic_edge(test, obj) - elif isinstance(obj, Vertex): - validate_classic_vertex(test, obj) - else: - test.fail("Invalid object found in path " + str(object.type)) - - -class GraphTestConfiguration(object): - """Possible Configurations: - ClassicGraphSchema: - graphson1 - graphson2 - graphson3 - - CoreGraphSchema - graphson3 - """ - - @classmethod - def schemas(cls): - schemas = [ClassicGraphSchema] - if DSE_VERSION >= Version("6.8"): - schemas.append(CoreGraphSchema) - return schemas - - @classmethod - def graphson_versions(cls): - graphson_versions = [GraphProtocol.GRAPHSON_1_0] - if DSE_VERSION >= Version("6.0"): - graphson_versions.append(GraphProtocol.GRAPHSON_2_0) - if DSE_VERSION >= Version("6.8"): - graphson_versions.append(GraphProtocol.GRAPHSON_3_0) - return graphson_versions - - @classmethod - def schema_configurations(cls, schema=None): - schemas = cls.schemas() if schema is None else [schema] - configurations = [] - for s in schemas: - configurations.append(s) - - return configurations - - @classmethod - def configurations(cls, schema=None, graphson=None): - schemas = cls.schemas() if schema is None else [schema] - graphson_versions = cls.graphson_versions() if graphson is None else [graphson] - - configurations = [] - for s in schemas: - for g in graphson_versions: - if s is CoreGraphSchema and g != GraphProtocol.GRAPHSON_3_0: - continue - configurations.append((s, g)) - - return configurations - - @staticmethod - def _make_graph_schema_test_method(func, schema): - def test_input(self): - self.setup_graph(schema) - try: - func(self, schema) - except: - raise - finally: - self.teardown_graph(schema) - - schema_name = 'classic' if schema is ClassicGraphSchema else 'core' - test_input.__name__ = '{func}_{schema}'.format( - func=func.__name__.lstrip('_'), schema=schema_name) - return test_input - - @staticmethod - def _make_graph_test_method(func, schema, graphson): - def test_input(self): - self.setup_graph(schema) - try: - func(self, schema, graphson) - except: - raise - finally: - self.teardown_graph(schema) - - graphson_name = 'graphson1' - if graphson == GraphProtocol.GRAPHSON_2_0: - graphson_name = 'graphson2' - elif graphson == GraphProtocol.GRAPHSON_3_0: - graphson_name = 'graphson3' - - schema_name = 'classic' if schema is ClassicGraphSchema else 'core' - - # avoid keyspace name too long issue - if DSE_VERSION < Version('6.7'): - schema_name = schema_name[0] - graphson_name = 'g' + graphson_name[-1] - - test_input.__name__ = '{func}_{schema}_{graphson}'.format( - func=func.__name__.lstrip('_'), schema=schema_name, graphson=graphson_name) - return test_input - - @classmethod - def generate_tests(cls, schema=None, graphson=None, traversal=False): - """Generate tests for a graph configuration""" - def decorator(klass): - if DSE_VERSION: - predicate = inspect.isfunction - for name, func in inspect.getmembers(klass, predicate=predicate): - if not name.startswith('_test'): - continue - for _schema, _graphson in cls.configurations(schema, graphson): - if traversal and _graphson == GraphProtocol.GRAPHSON_1_0: - continue - test_input = cls._make_graph_test_method(func, _schema, _graphson) - log.debug("Generated test '{}.{}'".format(klass.__name__, test_input.__name__)) - setattr(klass, test_input.__name__, test_input) - return klass - - return decorator - - @classmethod - def generate_schema_tests(cls, schema=None): - """Generate schema tests for a graph configuration""" - def decorator(klass): - if DSE_VERSION: - predicate = inspect.isfunction - for name, func in inspect.getmembers(klass, predicate=predicate): - if not name.startswith('_test'): - continue - for _schema in cls.schema_configurations(schema): - test_input = cls._make_graph_schema_test_method(func, _schema) - log.debug("Generated test '{}.{}'".format(klass.__name__, test_input.__name__)) - setattr(klass, test_input.__name__, test_input) - return klass - - return decorator - - -class VertexLabel(object): - """ - Helper that represents a new VertexLabel: - - VertexLabel(['Int()', 'Float()']) # a vertex with 2 properties named property1 and property2 - VertexLabel([('int1', 'Int()'), 'Float()']) # a vertex with 2 properties named int1 and property1 - """ - - id = 0 - label = None - properties = None - - def __init__(self, properties): - VertexLabel.id += 1 - self.id = VertexLabel.id - self.label = "vertex{}".format(self.id) - self.properties = {'pkid': self.id} - property_count = 0 - for p in properties: - if isinstance(p, tuple): - name, typ = p - else: - property_count += 1 - name = "property-v{}-{}".format(self.id, property_count) - typ = p - self.properties[name] = typ - - @property - def non_pk_properties(self): - return {p: v for p, v in self.properties.items() if p != 'pkid'} - - -class GraphSchema(object): - - has_geo_bounds = DSE_VERSION and DSE_VERSION >= Version('5.1') - fixtures = GraphFixtures - - @classmethod - def sanitize_type(cls, typ): - if typ.lower().startswith("point"): - return cls.sanitize_point_type() - elif typ.lower().startswith("line"): - return cls.sanitize_line_type() - elif typ.lower().startswith("poly"): - return cls.sanitize_polygon_type() - else: - return typ - - @classmethod - def sanitize_point_type(cls): - return "Point().withGeoBounds()" if cls.has_geo_bounds else "Point()" - - @classmethod - def sanitize_line_type(cls): - return "Linestring().withGeoBounds()" if cls.has_geo_bounds else "Linestring()" - - @classmethod - def sanitize_polygon_type(cls): - return "Polygon().withGeoBounds()" if cls.has_geo_bounds else "Polygon()" - - @staticmethod - def drop_graph(session, graph_name): - ks = list(session.execute( - "SELECT * FROM system_schema.keyspaces WHERE keyspace_name = '{}';".format(graph_name))) - if not ks: - return - - try: - session.execute_graph('system.graph(name).drop()', {'name': graph_name}, - execution_profile=EXEC_PROFILE_GRAPH_SYSTEM_DEFAULT) - except: - pass - - @staticmethod - def create_graph(session, graph_name): - raise NotImplementedError() - - @staticmethod - def clear(session): - pass - - @staticmethod - def create_vertex_label(session, vertex_label, execution_profile=EXEC_PROFILE_GRAPH_DEFAULT): - raise NotImplementedError() - - @staticmethod - def add_vertex(session, vertex_label, name, value, execution_profile=EXEC_PROFILE_GRAPH_DEFAULT): - raise NotImplementedError() - - @classmethod - def ensure_properties(cls, session, obj, execution_profile=EXEC_PROFILE_GRAPH_DEFAULT): - if not isinstance(obj, (Vertex, Edge)): - return - - # This pre-processing is due to a change in TinkerPop - # properties are not returned automatically anymore - # with some queries. - if not obj.properties: - if isinstance(obj, Edge): - obj.properties = {} - for p in cls.get_edge_properties(session, obj, execution_profile=execution_profile): - obj.properties.update(p) - elif isinstance(obj, Vertex): - obj.properties = { - p.label: p - for p in cls.get_vertex_properties(session, obj, execution_profile=execution_profile) - } - - @staticmethod - def get_vertex_properties(session, vertex, execution_profile=EXEC_PROFILE_GRAPH_DEFAULT): - return session.execute_graph("g.V(vertex_id).properties().toList()", {'vertex_id': vertex.id}, - execution_profile=execution_profile) - - @staticmethod - def get_edge_properties(session, edge, execution_profile=EXEC_PROFILE_GRAPH_DEFAULT): - v = session.execute_graph("g.E(edge_id).properties().toList()", {'edge_id': edge.id}, - execution_profile=execution_profile) - return v - - -class ClassicGraphSchema(GraphSchema): - - fixtures = ClassicGraphFixtures - - @staticmethod - def create_graph(session, graph_name): - session.execute_graph(CREATE_CLASSIC_GRAPH, {'name': graph_name}, - execution_profile=EXEC_PROFILE_GRAPH_SYSTEM_DEFAULT) - wait_for_graph_inserted(session, graph_name) - - @staticmethod - def clear(session): - session.execute_graph('schema.clear()') - - @classmethod - def create_vertex_label(cls, session, vertex_label, execution_profile=EXEC_PROFILE_GRAPH_DEFAULT): - statements = ["schema.propertyKey('pkid').Int().ifNotExists().create();"] - for k, v in vertex_label.non_pk_properties.items(): - typ = cls.sanitize_type(v) - statements.append("schema.propertyKey('{name}').{type}.create();".format( - name=k, type=typ - )) - - statements.append("schema.vertexLabel('{label}').partitionKey('pkid').properties(".format( - label=vertex_label.label)) - property_names = [name for name in vertex_label.non_pk_properties.keys()] - statements.append(", ".join(["'{}'".format(p) for p in property_names])) - statements.append(").create();") - - to_run = "\n".join(statements) - session.execute_graph(to_run, execution_profile=execution_profile) - - @staticmethod - def add_vertex(session, vertex_label, name, value, execution_profile=EXEC_PROFILE_GRAPH_DEFAULT): - statement = "g.addV('{label}').property('pkid', {pkid}).property('{property_name}', val);".format( - pkid=vertex_label.id, label=vertex_label.label, property_name=name) - parameters = {'val': value} - return session.execute_graph(statement, parameters, execution_profile=execution_profile) - - -class CoreGraphSchema(GraphSchema): - - fixtures = CoreGraphFixtures - - @classmethod - def sanitize_type(cls, typ): - typ = super(CoreGraphSchema, cls).sanitize_type(typ) - return typ.replace('()', '') - - @classmethod - def sanitize_point_type(cls): - return "Point" - - @classmethod - def sanitize_line_type(cls): - return "LineString" - - @classmethod - def sanitize_polygon_type(cls): - return "Polygon" - - @staticmethod - def create_graph(session, graph_name): - session.execute_graph('system.graph(name).create()', {'name': graph_name}, - execution_profile=EXEC_PROFILE_GRAPH_SYSTEM_DEFAULT) - wait_for_graph_inserted(session, graph_name) - - @classmethod - def create_vertex_label(cls, session, vertex_label, execution_profile=EXEC_PROFILE_GRAPH_DEFAULT): - statements = ["schema.vertexLabel('{label}').partitionBy('pkid', Int)".format( - label=vertex_label.label)] - - for name, typ in vertex_label.non_pk_properties.items(): - typ = cls.sanitize_type(typ) - statements.append(".property('{name}', {type})".format(name=name, type=typ)) - statements.append(".create();") - - to_run = "\n".join(statements) - session.execute_graph(to_run, execution_profile=execution_profile) - - @staticmethod - def add_vertex(session, vertex_label, name, value, execution_profile=EXEC_PROFILE_GRAPH_DEFAULT): - statement = "g.addV('{label}').property('pkid', {pkid}).property('{property_name}', val);".format( - pkid=vertex_label.id, label=vertex_label.label, property_name=name) - parameters = {'val': value} - return session.execute_graph(statement, parameters, execution_profile=execution_profile) diff --git a/tests/integration/advanced/graph/fluent/__init__.py b/tests/integration/advanced/graph/fluent/__init__.py deleted file mode 100644 index 155de026c5..0000000000 --- a/tests/integration/advanced/graph/fluent/__init__.py +++ /dev/null @@ -1,718 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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 sys -import datetime -import time -from collections import namedtuple -from packaging.version import Version - -from cassandra.datastax.graph.fluent import DseGraph -from cassandra.graph import VertexProperty, GraphProtocol -from cassandra.util import Point, Polygon, LineString - -from gremlin_python.process.graph_traversal import GraphTraversal, GraphTraversalSource -from gremlin_python.process.traversal import P -from gremlin_python.structure.graph import Edge as TravEdge -from gremlin_python.structure.graph import Vertex as TravVertex, VertexProperty as TravVertexProperty - -from tests.util import wait_until_not_raised -from tests.integration import DSE_VERSION -from tests.integration.advanced.graph import ( - GraphUnitTestCase, ClassicGraphSchema, CoreGraphSchema, - VertexLabel) -from tests.integration import requiredse - -import unittest - - -import ipaddress - - -def check_equality_base(testcase, original, read_value): - if isinstance(original, float): - testcase.assertAlmostEqual(original, read_value, delta=.01) - elif isinstance(original, ipaddress.IPv4Address): - testcase.assertAlmostEqual(original, ipaddress.IPv4Address(read_value)) - elif isinstance(original, ipaddress.IPv6Address): - testcase.assertAlmostEqual(original, ipaddress.IPv6Address(read_value)) - else: - testcase.assertEqual(original, read_value) - - -def create_traversal_profiles(cluster, graph_name): - ep_graphson2 = DseGraph().create_execution_profile( - graph_name, graph_protocol=GraphProtocol.GRAPHSON_2_0) - ep_graphson3 = DseGraph().create_execution_profile( - graph_name, graph_protocol=GraphProtocol.GRAPHSON_3_0) - - cluster.add_execution_profile('traversal_graphson2', ep_graphson2) - cluster.add_execution_profile('traversal_graphson3', ep_graphson3) - - return ep_graphson2, ep_graphson3 - - -class _AbstractTraversalTest(GraphUnitTestCase): - - def setUp(self): - super(_AbstractTraversalTest, self).setUp() - self.ep_graphson2, self.ep_graphson3 = create_traversal_profiles(self.cluster, self.graph_name) - - def _test_basic_query(self, schema, graphson): - """ - Test to validate that basic graph queries works - - Creates a simple classic tinkerpot graph, and attempts to preform a basic query - using Tinkerpop's GLV with both explicit and implicit execution - ensuring that each one is correct. See reference graph here - http://www.tinkerpop.com/docs/3.0.0.M1/ - - @since 1.0.0 - @jira_ticket PYTHON-641 - @expected_result graph should generate and all vertices and edge results should be - - @test_category dse graph - """ - - g = self.fetch_traversal_source(graphson) - self.execute_graph(schema.fixtures.classic(), graphson) - traversal = g.V().has('name', 'marko').out('knows').values('name') - results_list = self.execute_traversal(traversal, graphson) - self.assertEqual(len(results_list), 2) - self.assertIn('vadas', results_list) - self.assertIn('josh', results_list) - - def _test_classic_graph(self, schema, graphson): - """ - Test to validate that basic graph generation, and vertex and edges are surfaced correctly - - Creates a simple classic tinkerpot graph, and iterates over the the vertices and edges - using Tinkerpop's GLV with both explicit and implicit execution - ensuring that each one iscorrect. See reference graph here - http://www.tinkerpop.com/docs/3.0.0.M1/ - - @since 1.0.0 - @jira_ticket PYTHON-641 - @expected_result graph should generate and all vertices and edge results should be - - @test_category dse graph - """ - - self.execute_graph(schema.fixtures.classic(), graphson) - ep = self.get_execution_profile(graphson) - g = self.fetch_traversal_source(graphson) - traversal = g.V() - vert_list = self.execute_traversal(traversal, graphson) - - for vertex in vert_list: - schema.ensure_properties(self.session, vertex, execution_profile=ep) - self._validate_classic_vertex(g, vertex) - traversal = g.E() - edge_list = self.execute_traversal(traversal, graphson) - for edge in edge_list: - schema.ensure_properties(self.session, edge, execution_profile=ep) - self._validate_classic_edge(g, edge) - - def _test_graph_classic_path(self, schema, graphson): - """ - Test to validate that the path version of the result type is generated correctly. It also - tests basic path results as that is not covered elsewhere - - @since 1.0.0 - @jira_ticket PYTHON-641 - @expected_result path object should be unpacked correctly including all nested edges and vertices - @test_category dse graph - """ - self.execute_graph(schema.fixtures.classic(), graphson) - g = self.fetch_traversal_source(graphson) - traversal = g.V().hasLabel('person').has('name', 'marko').as_('a').outE('knows').inV().as_('c', 'd').outE('created').as_('e', 'f', 'g').inV().path() - path_list = self.execute_traversal(traversal, graphson) - self.assertEqual(len(path_list), 2) - for path in path_list: - self._validate_path_result_type(g, path) - - def _test_range_query(self, schema, graphson): - """ - Test to validate range queries are handled correctly. - - Creates a very large line graph script and executes it. Then proceeds to to a range - limited query against it, and ensure that the results are formated correctly and that - the result set is properly sized. - - @since 1.0.0 - @jira_ticket PYTHON-641 - @expected_result result set should be properly formated and properly sized - - @test_category dse graph - """ - - self.execute_graph(schema.fixtures.line(150), graphson) - ep = self.get_execution_profile(graphson) - g = self.fetch_traversal_source(graphson) - - traversal = g.E().range(0, 10) - edges = self.execute_traversal(traversal, graphson) - self.assertEqual(len(edges), 10) - for edge in edges: - schema.ensure_properties(self.session, edge, execution_profile=ep) - self._validate_line_edge(g, edge) - - def _test_result_types(self, schema, graphson): - """ - Test to validate that the edge and vertex version of results are constructed correctly. - - @since 1.0.0 - @jira_ticket PYTHON-641 - @expected_result edge/vertex result types should be unpacked correctly. - @test_category dse graph - """ - self.execute_graph(schema.fixtures.line(150), graphson) - g = self.fetch_traversal_source(graphson) - traversal = g.V() - vertices = self.execute_traversal(traversal, graphson) - for vertex in vertices: - self._validate_type(g, vertex) - - def _test_large_result_set(self, schema, graphson): - """ - Test to validate that large result sets return correctly. - - Creates a very large graph. Ensures that large result sets are handled appropriately. - - @since 1.0.0 - @jira_ticket PYTHON-641 - @expected_result when limits of result sets are hit errors should be surfaced appropriately - - @test_category dse graph - """ - self.execute_graph(schema.fixtures.large(), graphson) - g = self.fetch_traversal_source(graphson) - traversal = g.V() - vertices = self.execute_traversal(traversal, graphson) - for vertex in vertices: - self._validate_generic_vertex_result_type(g, vertex) - - def _test_vertex_meta_properties(self, schema, graphson): - """ - Test verifying vertex property properties - - @since 1.0.0 - @jira_ticket PYTHON-641 - - @test_category dse graph - """ - if schema is not ClassicGraphSchema: - raise unittest.SkipTest('skipped because multiple properties are only supported with classic graphs') - - s = self.session - s.execute_graph("schema.propertyKey('k0').Text().ifNotExists().create();") - s.execute_graph("schema.propertyKey('k1').Text().ifNotExists().create();") - s.execute_graph("schema.propertyKey('key').Text().properties('k0', 'k1').ifNotExists().create();") - s.execute_graph("schema.vertexLabel('MLP').properties('key').ifNotExists().create();") - s.execute_graph("schema.config().option('graph.allow_scan').set('true');") - v = s.execute_graph('''v = graph.addVertex('MLP') - v.property('key', 'meta_prop', 'k0', 'v0', 'k1', 'v1') - v''')[0] - - g = self.fetch_traversal_source(graphson) - - traversal = g.V() - # This should contain key, and value where value is a property - # This should be a vertex property and should contain sub properties - results = self.execute_traversal(traversal, graphson) - self._validate_meta_property(g, results[0]) - - def _test_vertex_multiple_properties(self, schema, graphson): - """ - Test verifying vertex property form for various Cardinality - - All key types are encoded as a list, regardless of cardinality - - Single cardinality properties have only one value -- the last one added - - Default is single (this is config dependent) - - @since 1.0.0 - @jira_ticket PYTHON-641 - - @test_category dse graph - """ - if schema is not ClassicGraphSchema: - raise unittest.SkipTest('skipped because multiple properties are only supported with classic graphs') - - s = self.session - s.execute_graph('''Schema schema = graph.schema(); - schema.propertyKey('mult_key').Text().multiple().ifNotExists().create(); - schema.propertyKey('single_key').Text().single().ifNotExists().create(); - schema.vertexLabel('MPW1').properties('mult_key').ifNotExists().create(); - schema.vertexLabel('MPW2').properties('mult_key').ifNotExists().create(); - schema.vertexLabel('SW1').properties('single_key').ifNotExists().create();''') - - mpw1v = s.execute_graph('''v = graph.addVertex('MPW1') - v.property('mult_key', 'value') - v''')[0] - - mpw2v = s.execute_graph('''g.addV('MPW2').property('mult_key', 'value0').property('mult_key', 'value1')''')[0] - - g = self.fetch_traversal_source(graphson) - traversal = g.V(mpw1v.id).properties() - - vertex_props = self.execute_traversal(traversal, graphson) - - self.assertEqual(len(vertex_props), 1) - - self.assertEqual(self.fetch_key_from_prop(vertex_props[0]), "mult_key") - self.assertEqual(vertex_props[0].value, "value") - - # multiple_with_two_values - #v = s.execute_graph('''g.addV(label, 'MPW2', 'mult_key', 'value0', 'mult_key', 'value1')''')[0] - traversal = g.V(mpw2v.id).properties() - - vertex_props = self.execute_traversal(traversal, graphson) - - self.assertEqual(len(vertex_props), 2) - self.assertEqual(self.fetch_key_from_prop(vertex_props[0]), 'mult_key') - self.assertEqual(self.fetch_key_from_prop(vertex_props[1]), 'mult_key') - self.assertEqual(vertex_props[0].value, 'value0') - self.assertEqual(vertex_props[1].value, 'value1') - - # single_with_one_value - v = s.execute_graph('''v = graph.addVertex('SW1') - v.property('single_key', 'value') - v''')[0] - traversal = g.V(v.id).properties() - vertex_props = self.execute_traversal(traversal, graphson) - self.assertEqual(len(vertex_props), 1) - self.assertEqual(self.fetch_key_from_prop(vertex_props[0]), "single_key") - self.assertEqual(vertex_props[0].value, "value") - - def should_parse_meta_properties(self): - g = self.fetch_traversal_source() - g.addV("meta_v").property("meta_prop", "hello", "sub_prop", "hi", "sub_prop2", "hi2") - - def _test_all_graph_types_with_schema(self, schema, graphson): - """ - Exhaustively goes through each type that is supported by dse_graph. - creates a vertex for each type using a dse-tinkerpop traversal, - It then attempts to fetch it from the server and compares it to what was inserted - Prime the graph with the correct schema first - - @since 1.0.0 - @jira_ticket PYTHON-641 - @expected_result inserted objects are equivalent to those retrieved - - @test_category dse graph - """ - self._write_and_read_data_types(schema, graphson) - - def _test_all_graph_types_without_schema(self, schema, graphson): - """ - Exhaustively goes through each type that is supported by dse_graph. - creates a vertex for each type using a dse-tinkerpop traversal, - It then attempts to fetch it from the server and compares it to what was inserted - Do not prime the graph with the correct schema first - @since 1.0.0 - @jira_ticket PYTHON-641 - @expected_result inserted objects are equivalent to those retrieved - @test_category dse graph - """ - if schema is not ClassicGraphSchema: - raise unittest.SkipTest('schema-less is only for classic graphs') - self._write_and_read_data_types(schema, graphson, use_schema=False) - - def _test_dsl(self, schema, graphson): - """ - The test creates a SocialTraversal and a SocialTraversalSource as part of - a DSL. Then calls it's method and checks the results to verify - we have the expected results - - @since @since 1.1.0a1 - @jira_ticket PYTHON-790 - @expected_result only the vertex corresponding to marko is in the result - - @test_category dse graph - """ - class SocialTraversal(GraphTraversal): - def knows(self, person_name): - return self.out("knows").hasLabel("person").has("name", person_name).in_() - - class SocialTraversalSource(GraphTraversalSource): - def __init__(self, *args, **kwargs): - super(SocialTraversalSource, self).__init__(*args, **kwargs) - self.graph_traversal = SocialTraversal - - def people(self, *names): - return self.get_graph_traversal().V().has("name", P.within(*names)) - - self.execute_graph(schema.fixtures.classic(), graphson) - if schema is CoreGraphSchema: - self.execute_graph(""" - schema.edgeLabel('knows').from('person').to('person').materializedView('person__knows__person_by_in_name'). - ifNotExists().partitionBy('in_name').clusterBy('out_name', Asc).create() - """, graphson) - time.sleep(1) # give some time to the MV to be populated - g = self.fetch_traversal_source(graphson, traversal_class=SocialTraversalSource) - - traversal = g.people("marko", "albert").knows("vadas") - results = self.execute_traversal(traversal, graphson) - self.assertEqual(len(results), 1) - only_vertex = results[0] - schema.ensure_properties(self.session, only_vertex, - execution_profile=self.get_execution_profile(graphson)) - self._validate_classic_vertex(g, only_vertex) - - def _test_bulked_results(self, schema, graphson): - """ - Send a query expecting a bulked result and the driver "undoes" - the bulk and returns the expected list - - @since 1.1.0a1 - @jira_ticket PYTHON-771 - @expected_result the expanded list - - @test_category dse graph - """ - self.execute_graph(schema.fixtures.classic(), graphson) - g = self.fetch_traversal_source(graphson) - barrier_traversal = g.E().label().barrier() - results = self.execute_traversal(barrier_traversal, graphson) - self.assertEqual(sorted(["created", "created", "created", "created", "knows", "knows"]), sorted(results)) - - def _test_udt_with_classes(self, schema, graphson): - class Address(object): - - def __init__(self, address, city, state): - self.address = address - self.city = city - self.state = state - - def __eq__(self, other): - return self.address == other.address and self.city == other.city and self.state == other.state - - class AddressWithTags(object): - - def __init__(self, address, city, state, tags): - self.address = address - self.city = city - self.state = state - self.tags = tags - - def __eq__(self, other): - return (self.address == other.address and self.city == other.city - and self.state == other.state and self.tags == other.tags) - - class ComplexAddress(object): - - def __init__(self, address, address_tags, city, state, props): - self.address = address - self.address_tags = address_tags - self.city = city - self.state = state - self.props = props - - def __eq__(self, other): - return (self.address == other.address and self.address_tags == other.address_tags - and self.city == other.city and self.state == other.state - and self.props == other.props) - - class ComplexAddressWithOwners(object): - - def __init__(self, address, address_tags, city, state, props, owners): - self.address = address - self.address_tags = address_tags - self.city = city - self.state = state - self.props = props - self.owners = owners - - def __eq__(self, other): - return (self.address == other.address and self.address_tags == other.address_tags - and self.city == other.city and self.state == other.state - and self.props == other.props and self.owners == other.owners) - - self.__test_udt(schema, graphson, Address, AddressWithTags, ComplexAddress, ComplexAddressWithOwners) - - def _test_udt_with_namedtuples(self, schema, graphson): - AddressTuple = namedtuple('Address', ('address', 'city', 'state')) - AddressWithTagsTuple = namedtuple('AddressWithTags', ('address', 'city', 'state', 'tags')) - ComplexAddressTuple = namedtuple('ComplexAddress', ('address', 'address_tags', 'city', 'state', 'props')) - ComplexAddressWithOwnersTuple = namedtuple('ComplexAddressWithOwners', ('address', 'address_tags', 'city', - 'state', 'props', 'owners')) - - self.__test_udt(schema, graphson, AddressTuple, AddressWithTagsTuple, - ComplexAddressTuple, ComplexAddressWithOwnersTuple) - - def _write_and_read_data_types(self, schema, graphson, use_schema=True): - g = self.fetch_traversal_source(graphson) - ep = self.get_execution_profile(graphson) - for data in schema.fixtures.datatypes().values(): - typ, value, deserializer = data - vertex_label = VertexLabel([typ]) - property_name = next(iter(vertex_label.non_pk_properties.keys())) - if use_schema or schema is CoreGraphSchema: - schema.create_vertex_label(self.session, vertex_label, execution_profile=ep) - - write_traversal = g.addV(str(vertex_label.label)).property('pkid', vertex_label.id).\ - property(property_name, value) - self.execute_traversal(write_traversal, graphson) - - read_traversal = g.V().hasLabel(str(vertex_label.label)).has(property_name).properties() - results = self.execute_traversal(read_traversal, graphson) - - for result in results: - if result.label == 'pkid': - continue - self._check_equality(g, value, result.value) - - def __test_udt(self, schema, graphson, address_class, address_with_tags_class, - complex_address_class, complex_address_with_owners_class): - if schema is not CoreGraphSchema or DSE_VERSION < Version('6.8'): - raise unittest.SkipTest("Graph UDT is only supported with DSE 6.8+ and Core graphs.") - - ep = self.get_execution_profile(graphson) - - Address = address_class - AddressWithTags = address_with_tags_class - ComplexAddress = complex_address_class - ComplexAddressWithOwners = complex_address_with_owners_class - - # setup udt - self.session.execute_graph(""" - schema.type('address').property('address', Text).property('city', Text).property('state', Text).create(); - schema.type('addressTags').property('address', Text).property('city', Text).property('state', Text). - property('tags', setOf(Text)).create(); - schema.type('complexAddress').property('address', Text).property('address_tags', frozen(typeOf('addressTags'))). - property('city', Text).property('state', Text).property('props', mapOf(Text, Int)).create(); - schema.type('complexAddressWithOwners').property('address', Text). - property('address_tags', frozen(typeOf('addressTags'))). - property('city', Text).property('state', Text).property('props', mapOf(Text, Int)). - property('owners', frozen(listOf(tupleOf(Text, Int)))).create(); - """, execution_profile=ep) - - # wait max 10 seconds to get the UDT discovered. - wait_until_not_raised( - lambda: self.session.cluster.register_user_type(self.graph_name, 'address', Address), - 1, 10) - wait_until_not_raised( - lambda: self.session.cluster.register_user_type(self.graph_name, 'addressTags', AddressWithTags), - 1, 10) - wait_until_not_raised( - lambda: self.session.cluster.register_user_type(self.graph_name, 'complexAddress', ComplexAddress), - 1, 10) - wait_until_not_raised( - lambda: self.session.cluster.register_user_type(self.graph_name, 'complexAddressWithOwners', ComplexAddressWithOwners), - 1, 10) - - data = { - "udt1": ["typeOf('address')", Address('1440 Rd Smith', 'Quebec', 'QC')], - "udt2": ["tupleOf(typeOf('address'), Text)", (Address('1440 Rd Smith', 'Quebec', 'QC'), 'hello')], - "udt3": ["tupleOf(frozen(typeOf('address')), Text)", (Address('1440 Rd Smith', 'Quebec', 'QC'), 'hello')], - "udt4": ["tupleOf(tupleOf(Int, typeOf('address')), Text)", - ((42, Address('1440 Rd Smith', 'Quebec', 'QC')), 'hello')], - "udt5": ["tupleOf(tupleOf(Int, typeOf('addressTags')), Text)", - ((42, AddressWithTags('1440 Rd Smith', 'Quebec', 'QC', {'t1', 't2'})), 'hello')], - "udt6": ["tupleOf(tupleOf(Int, typeOf('complexAddress')), Text)", - ((42, ComplexAddress('1440 Rd Smith', - AddressWithTags('1440 Rd Smith', 'Quebec', 'QC', {'t1', 't2'}), - 'Quebec', 'QC', {'p1': 42, 'p2': 33})), 'hello')], - "udt7": ["tupleOf(tupleOf(Int, frozen(typeOf('complexAddressWithOwners'))), Text)", - ((42, ComplexAddressWithOwners( - '1440 Rd Smith', - AddressWithTags('1440 CRd Smith', 'Quebec', 'QC', {'t1', 't2'}), - 'Quebec', 'QC', {'p1': 42, 'p2': 33}, [('Mike', 43), ('Gina', 39)]) - ), 'hello')] - } - - g = self.fetch_traversal_source(graphson) - for typ, value in data.values(): - vertex_label = VertexLabel([typ]) - property_name = next(iter(vertex_label.non_pk_properties.keys())) - schema.create_vertex_label(self.session, vertex_label, execution_profile=ep) - - write_traversal = g.addV(str(vertex_label.label)).property('pkid', vertex_label.id). \ - property(property_name, value) - self.execute_traversal(write_traversal, graphson) - - #vertex = list(schema.add_vertex(self.session, vertex_label, property_name, value, execution_profile=ep))[0] - #vertex_properties = list(schema.get_vertex_properties( - # self.session, vertex, execution_profile=ep)) - - read_traversal = g.V().hasLabel(str(vertex_label.label)).has(property_name).properties() - vertex_properties = self.execute_traversal(read_traversal, graphson) - - self.assertEqual(len(vertex_properties), 2) # include pkid - for vp in vertex_properties: - if vp.label == 'pkid': - continue - - self.assertIsInstance(vp, (VertexProperty, TravVertexProperty)) - self.assertEqual(vp.label, property_name) - self.assertEqual(vp.value, value) - - @staticmethod - def fetch_edge_props(g, edge): - edge_props = g.E(edge.id).properties().toList() - return edge_props - - @staticmethod - def fetch_vertex_props(g, vertex): - - vertex_props = g.V(vertex.id).properties().toList() - return vertex_props - - def _check_equality(self, g, original, read_value): - return check_equality_base(self, original, read_value) - - -def _validate_prop(key, value, unittest): - if key == 'index': - return - - if any(key.startswith(t) for t in ('int', 'short')): - typ = int - - elif any(key.startswith(t) for t in ('long',)): - if sys.version_info >= (3, 0): - typ = int - else: - typ = long - elif any(key.startswith(t) for t in ('float', 'double')): - typ = float - elif any(key.startswith(t) for t in ('polygon',)): - typ = Polygon - elif any(key.startswith(t) for t in ('point',)): - typ = Point - elif any(key.startswith(t) for t in ('Linestring',)): - typ = LineString - elif any(key.startswith(t) for t in ('neg',)): - typ = str - elif any(key.startswith(t) for t in ('date',)): - typ = datetime.date - elif any(key.startswith(t) for t in ('time',)): - typ = datetime.time - else: - unittest.fail("Received unexpected type: %s" % key) - - -@requiredse -class BaseImplicitExecutionTest(GraphUnitTestCase): - """ - This test class will execute all tests of the AbstractTraversalTestClass using implicit execution - This all traversal will be run directly using toList() - """ - def setUp(self): - super(BaseImplicitExecutionTest, self).setUp() - if DSE_VERSION: - self.ep = DseGraph().create_execution_profile(self.graph_name) - self.cluster.add_execution_profile(self.graph_name, self.ep) - - @staticmethod - def fetch_key_from_prop(property): - return property.key - - def fetch_traversal_source(self, graphson, **kwargs): - ep = self.get_execution_profile(graphson, traversal=True) - return DseGraph().traversal_source(self.session, self.graph_name, execution_profile=ep, **kwargs) - - def execute_traversal(self, traversal, graphson=None): - return traversal.toList() - - def _validate_classic_vertex(self, g, vertex): - # Checks the properties on a classic vertex for correctness - vertex_props = self.fetch_vertex_props(g, vertex) - vertex_prop_keys = [vp.key for vp in vertex_props] - self.assertEqual(len(vertex_prop_keys), 2) - self.assertIn('name', vertex_prop_keys) - self.assertTrue('lang' in vertex_prop_keys or 'age' in vertex_prop_keys) - - def _validate_generic_vertex_result_type(self, g, vertex): - # Checks a vertex object for it's generic properties - properties = self.fetch_vertex_props(g, vertex) - for attr in ('id', 'label'): - self.assertIsNotNone(getattr(vertex, attr)) - self.assertTrue(len(properties) > 2) - - def _validate_classic_edge_properties(self, g, edge): - # Checks the properties on a classic edge for correctness - edge_props = self.fetch_edge_props(g, edge) - edge_prop_keys = [ep.key for ep in edge_props] - self.assertEqual(len(edge_prop_keys), 1) - self.assertIn('weight', edge_prop_keys) - - def _validate_classic_edge(self, g, edge): - self._validate_generic_edge_result_type(edge) - self._validate_classic_edge_properties(g, edge) - - def _validate_line_edge(self, g, edge): - self._validate_generic_edge_result_type(edge) - edge_props = self.fetch_edge_props(g, edge) - edge_prop_keys = [ep.key for ep in edge_props] - self.assertEqual(len(edge_prop_keys), 1) - self.assertIn('distance', edge_prop_keys) - - def _validate_generic_edge_result_type(self, edge): - self.assertIsInstance(edge, TravEdge) - - for attr in ('outV', 'inV', 'label', 'id'): - self.assertIsNotNone(getattr(edge, attr)) - - def _validate_path_result_type(self, g, objects_path): - for obj in objects_path: - if isinstance(obj, TravEdge): - self._validate_classic_edge(g, obj) - elif isinstance(obj, TravVertex): - self._validate_classic_vertex(g, obj) - else: - self.fail("Invalid object found in path " + str(obj.type)) - - def _validate_meta_property(self, g, vertex): - meta_props = g.V(vertex.id).properties().toList() - self.assertEqual(len(meta_props), 1) - meta_prop = meta_props[0] - self.assertEqual(meta_prop.value, "meta_prop") - self.assertEqual(meta_prop.key, "key") - - nested_props = g.V(vertex.id).properties().properties().toList() - self.assertEqual(len(nested_props), 2) - for nested_prop in nested_props: - self.assertTrue(nested_prop.key in ['k0', 'k1']) - self.assertTrue(nested_prop.value in ['v0', 'v1']) - - def _validate_type(self, g, vertex): - props = self.fetch_vertex_props(g, vertex) - for prop in props: - value = prop.value - key = prop.key - _validate_prop(key, value, self) - - -class BaseExplicitExecutionTest(GraphUnitTestCase): - - def fetch_traversal_source(self, graphson, **kwargs): - ep = self.get_execution_profile(graphson, traversal=True) - return DseGraph().traversal_source(self.session, self.graph_name, execution_profile=ep, **kwargs) - - def execute_traversal(self, traversal, graphson): - ep = self.get_execution_profile(graphson, traversal=True) - ep = self.session.get_execution_profile(ep) - context = None - if graphson == GraphProtocol.GRAPHSON_3_0: - context = { - 'cluster': self.cluster, - 'graph_name': ep.graph_options.graph_name.decode('utf-8') if ep.graph_options.graph_name else None - } - query = DseGraph.query_from_traversal(traversal, graphson, context=context) - # Use an ep that is configured with the correct row factory, and bytecode-json language flat set - result_set = self.execute_graph(query, graphson, traversal=True) - return list(result_set) diff --git a/tests/integration/advanced/graph/fluent/test_graph.py b/tests/integration/advanced/graph/fluent/test_graph.py deleted file mode 100644 index 911e6d5d57..0000000000 --- a/tests/integration/advanced/graph/fluent/test_graph.py +++ /dev/null @@ -1,241 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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. - -from cassandra import cluster -from cassandra.cluster import ContinuousPagingOptions -from cassandra.datastax.graph.fluent import DseGraph -from cassandra.graph import VertexProperty - -from tests.integration import greaterthanorequaldse68 -from tests.integration.advanced.graph import ( - GraphUnitTestCase, ClassicGraphSchema, CoreGraphSchema, - VertexLabel, GraphTestConfiguration -) -from tests.integration import greaterthanorequaldse60 -from tests.integration.advanced.graph.fluent import ( - BaseExplicitExecutionTest, create_traversal_profiles, check_equality_base) - -import unittest - - -@greaterthanorequaldse60 -@GraphTestConfiguration.generate_tests(traversal=True) -class BatchStatementTests(BaseExplicitExecutionTest): - - def setUp(self): - super(BatchStatementTests, self).setUp() - self.ep_graphson2, self.ep_graphson3 = create_traversal_profiles(self.cluster, self.graph_name) - - def _test_batch_with_schema(self, schema, graphson): - """ - Sends a Batch statement and verifies it has succeeded with a schema created - - @since 1.1.0 - @jira_ticket PYTHON-789 - @expected_result ValueError is arisen - - @test_category dse graph - """ - self._send_batch_and_read_results(schema, graphson) - - def _test_batch_without_schema(self, schema, graphson): - """ - Sends a Batch statement and verifies it has succeeded without a schema created - - @since 1.1.0 - @jira_ticket PYTHON-789 - @expected_result ValueError is arisen - - @test_category dse graph - """ - if schema is not ClassicGraphSchema: - raise unittest.SkipTest('schema-less is only for classic graphs') - self._send_batch_and_read_results(schema, graphson, use_schema=False) - - def _test_batch_with_schema_add_all(self, schema, graphson): - """ - Sends a Batch statement and verifies it has succeeded with a schema created. - Uses :method:`dse_graph.query._BatchGraphStatement.add_all` to add the statements - instead of :method:`dse_graph.query._BatchGraphStatement.add` - - @since 1.1.0 - @jira_ticket PYTHON-789 - @expected_result ValueError is arisen - - @test_category dse graph - """ - self._send_batch_and_read_results(schema, graphson, add_all=True) - - def _test_batch_without_schema_add_all(self, schema, graphson): - """ - Sends a Batch statement and verifies it has succeeded without a schema created - Uses :method:`dse_graph.query._BatchGraphStatement.add_all` to add the statements - instead of :method:`dse_graph.query._BatchGraphStatement.add` - - @since 1.1.0 - @jira_ticket PYTHON-789 - @expected_result ValueError is arisen - - @test_category dse graph - """ - if schema is not ClassicGraphSchema: - raise unittest.SkipTest('schema-less is only for classic graphs') - self._send_batch_and_read_results(schema, graphson, add_all=True, use_schema=False) - - def test_only_graph_traversals_are_accepted(self): - """ - Verifies that ValueError is risen if the parameter add is not a traversal - - @since 1.1.0 - @jira_ticket PYTHON-789 - @expected_result ValueError is arisen - - @test_category dse graph - """ - batch = DseGraph.batch() - self.assertRaises(ValueError, batch.add, '{"@value":{"step":[["addV","poc_int"],' - '["property","bigint1value",{"@value":12,"@type":"g:Int32"}]]},' - '"@type":"g:Bytecode"}') - another_batch = DseGraph.batch() - self.assertRaises(ValueError, batch.add, another_batch) - - def _send_batch_and_read_results(self, schema, graphson, add_all=False, use_schema=True): - traversals = [] - datatypes = schema.fixtures.datatypes() - values = {} - g = self.fetch_traversal_source(graphson) - ep = self.get_execution_profile(graphson) - batch = DseGraph.batch(session=self.session, - execution_profile=self.get_execution_profile(graphson, traversal=True)) - for data in datatypes.values(): - typ, value, deserializer = data - vertex_label = VertexLabel([typ]) - property_name = next(iter(vertex_label.non_pk_properties.keys())) - values[property_name] = value - if use_schema or schema is CoreGraphSchema: - schema.create_vertex_label(self.session, vertex_label, execution_profile=ep) - - traversal = g.addV(str(vertex_label.label)).property('pkid', vertex_label.id).property(property_name, value) - if not add_all: - batch.add(traversal) - traversals.append(traversal) - - if add_all: - batch.add_all(traversals) - - self.assertEqual(len(datatypes), len(batch)) - - batch.execute() - - vertices = self.execute_traversal(g.V(), graphson) - self.assertEqual(len(vertices), len(datatypes), "g.V() returned {}".format(vertices)) - - # Iterate over all the vertices and check that they match the original input - for vertex in vertices: - schema.ensure_properties(self.session, vertex, execution_profile=ep) - key = [k for k in list(vertex.properties.keys()) if k != 'pkid'][0].replace("value", "") - original = values[key] - self._check_equality(original, vertex) - - def _check_equality(self, original, vertex): - for key in vertex.properties: - if key == 'pkid': - continue - value = vertex.properties[key].value \ - if isinstance(vertex.properties[key], VertexProperty) else vertex.properties[key][0].value - check_equality_base(self, original, value) - - -class ContinuousPagingOptionsForTests(ContinuousPagingOptions): - def __init__(self, - page_unit=ContinuousPagingOptions.PagingUnit.ROWS, max_pages=1, # max_pages=1 - max_pages_per_second=0, max_queue_size=4): - super(ContinuousPagingOptionsForTests, self).__init__(page_unit, max_pages, max_pages_per_second, - max_queue_size) - - -def reset_paging_options(): - cluster.ContinuousPagingOptions = ContinuousPagingOptions - - -@greaterthanorequaldse68 -@GraphTestConfiguration.generate_tests(schema=CoreGraphSchema) -class GraphPagingTest(GraphUnitTestCase): - - def setUp(self): - super(GraphPagingTest, self).setUp() - self.addCleanup(reset_paging_options) - self.ep_graphson2, self.ep_graphson3 = create_traversal_profiles(self.cluster, self.graph_name) - - def _setup_data(self, schema, graphson): - self.execute_graph( - "schema.vertexLabel('person').ifNotExists().partitionBy('name', Text).property('age', Int).create();", - graphson) - for i in range(100): - self.execute_graph("g.addV('person').property('name', 'batman-{}')".format(i), graphson) - - def _test_cont_paging_is_enabled_by_default(self, schema, graphson): - """ - Test that graph paging is automatically enabled with a >=6.8 cluster. - - @jira_ticket PYTHON-1045 - @expected_result the default continuous paging options are used - - @test_category dse graph - """ - # with traversals... I don't have access to the response future... so this is a hack to ensure paging is on - cluster.ContinuousPagingOptions = ContinuousPagingOptionsForTests - ep = self.get_execution_profile(graphson, traversal=True) - self._setup_data(schema, graphson) - self.session.default_fetch_size = 10 - g = DseGraph.traversal_source(self.session, execution_profile=ep) - results = g.V().toList() - self.assertEqual(len(results), 10) # only 10 results due to our hack - - def _test_cont_paging_can_be_disabled(self, schema, graphson): - """ - Test that graph paging can be disabled. - - @jira_ticket PYTHON-1045 - @expected_result the default continuous paging options are not used - - @test_category dse graph - """ - # with traversals... I don't have access to the response future... so this is a hack to ensure paging is on - cluster.ContinuousPagingOptions = ContinuousPagingOptionsForTests - ep = self.get_execution_profile(graphson, traversal=True) - ep = self.session.execution_profile_clone_update(ep, continuous_paging_options=None) - self._setup_data(schema, graphson) - self.session.default_fetch_size = 10 - g = DseGraph.traversal_source(self.session, execution_profile=ep) - results = g.V().toList() - self.assertEqual(len(results), 100) # 100 results since paging is disabled - - def _test_cont_paging_with_custom_options(self, schema, graphson): - """ - Test that we can specify custom paging options. - - @jira_ticket PYTHON-1045 - @expected_result we get only the desired number of results - - @test_category dse graph - """ - ep = self.get_execution_profile(graphson, traversal=True) - ep = self.session.execution_profile_clone_update(ep, - continuous_paging_options=ContinuousPagingOptions(max_pages=1)) - self._setup_data(schema, graphson) - self.session.default_fetch_size = 10 - g = DseGraph.traversal_source(self.session, execution_profile=ep) - results = g.V().toList() - self.assertEqual(len(results), 10) # only 10 results since paging is disabled diff --git a/tests/integration/advanced/graph/fluent/test_graph_explicit_execution.py b/tests/integration/advanced/graph/fluent/test_graph_explicit_execution.py deleted file mode 100644 index 1a5846203d..0000000000 --- a/tests/integration/advanced/graph/fluent/test_graph_explicit_execution.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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. - -from cassandra.graph import Vertex, Edge - -from tests.integration.advanced.graph import ( - validate_classic_vertex, validate_classic_edge, validate_generic_vertex_result_type, - validate_classic_edge_properties, validate_line_edge, - validate_generic_edge_result_type, validate_path_result_type) - -from tests.integration import requiredse, DSE_VERSION -from tests.integration.advanced import use_single_node_with_graph -from tests.integration.advanced.graph import GraphTestConfiguration -from tests.integration.advanced.graph.fluent import ( - BaseExplicitExecutionTest, _AbstractTraversalTest, _validate_prop) - - -def setup_module(): - if DSE_VERSION: - dse_options = {'graph': {'realtime_evaluation_timeout_in_seconds': 60}} - use_single_node_with_graph(dse_options=dse_options) - - -@requiredse -@GraphTestConfiguration.generate_tests(traversal=True) -class ExplicitExecutionTest(BaseExplicitExecutionTest, _AbstractTraversalTest): - """ - This test class will execute all tests of the AbstractTraversalTestClass using Explicit execution - All queries will be run by converting them to byte code, and calling execute graph explicitly with a generated ep. - """ - @staticmethod - def fetch_key_from_prop(property): - return property.label - - def _validate_classic_vertex(self, g, vertex): - validate_classic_vertex(self, vertex) - - def _validate_generic_vertex_result_type(self, g, vertex): - validate_generic_vertex_result_type(self, vertex) - - def _validate_classic_edge_properties(self, g, edge): - validate_classic_edge_properties(self, edge) - - def _validate_classic_edge(self, g, edge): - validate_classic_edge(self, edge) - - def _validate_line_edge(self, g, edge): - validate_line_edge(self, edge) - - def _validate_generic_edge_result_type(self, edge): - validate_generic_edge_result_type(self, edge) - - def _validate_type(self, g, vertex): - for key in vertex.properties: - value = vertex.properties[key][0].value - _validate_prop(key, value, self) - - def _validate_path_result_type(self, g, path_obj): - # This pre-processing is due to a change in TinkerPop - # properties are not returned automatically anymore - # with some queries. - for obj in path_obj.objects: - if not obj.properties: - props = [] - if isinstance(obj, Edge): - obj.properties = { - p.key: p.value - for p in self.fetch_edge_props(g, obj) - } - elif isinstance(obj, Vertex): - obj.properties = { - p.label: p.value - for p in self.fetch_vertex_props(g, obj) - } - - validate_path_result_type(self, path_obj) - - def _validate_meta_property(self, g, vertex): - - self.assertEqual(len(vertex.properties), 1) - self.assertEqual(len(vertex.properties['key']), 1) - p = vertex.properties['key'][0] - self.assertEqual(p.label, 'key') - self.assertEqual(p.value, 'meta_prop') - self.assertEqual(p.properties, {'k0': 'v0', 'k1': 'v1'}) diff --git a/tests/integration/advanced/graph/fluent/test_graph_implicit_execution.py b/tests/integration/advanced/graph/fluent/test_graph_implicit_execution.py deleted file mode 100644 index 50e6795867..0000000000 --- a/tests/integration/advanced/graph/fluent/test_graph_implicit_execution.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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. - -from concurrent.futures import Future -from cassandra.datastax.graph.fluent import DseGraph - -from tests.integration import requiredse, DSE_VERSION -from tests.integration.advanced import use_single_node_with_graph -from tests.integration.advanced.graph import GraphTestConfiguration -from tests.integration.advanced.graph.fluent import ( - BaseImplicitExecutionTest, create_traversal_profiles, _AbstractTraversalTest) - - -def setup_module(): - if DSE_VERSION: - dse_options = {'graph': {'realtime_evaluation_timeout_in_seconds': 60}} - use_single_node_with_graph(dse_options=dse_options) - - -@requiredse -@GraphTestConfiguration.generate_tests(traversal=True) -class ImplicitExecutionTest(BaseImplicitExecutionTest, _AbstractTraversalTest): - def _test_iterate_step(self, schema, graphson): - """ - Test to validate that the iterate() step work on all dse versions. - @jira_ticket PYTHON-1155 - @expected_result iterate step works - @test_category dse graph - """ - - g = self.fetch_traversal_source(graphson) - self.execute_graph(schema.fixtures.classic(), graphson) - g.addV('person').property('name', 'Person1').iterate() - - -@requiredse -@GraphTestConfiguration.generate_tests(traversal=True) -class ImplicitAsyncExecutionTest(BaseImplicitExecutionTest): - """ - Test to validate that the traversal async execution works properly. - - @since 3.21.0 - @jira_ticket PYTHON-1129 - - @test_category dse graph - """ - - def setUp(self): - super(ImplicitAsyncExecutionTest, self).setUp() - self.ep_graphson2, self.ep_graphson3 = create_traversal_profiles(self.cluster, self.graph_name) - - def _validate_results(self, results): - results = list(results) - self.assertEqual(len(results), 2) - self.assertIn('vadas', results) - self.assertIn('josh', results) - - def _test_promise(self, schema, graphson): - self.execute_graph(schema.fixtures.classic(), graphson) - g = self.fetch_traversal_source(graphson) - traversal_future = g.V().has('name', 'marko').out('knows').values('name').promise() - self._validate_results(traversal_future.result()) - - def _test_promise_error_is_propagated(self, schema, graphson): - self.execute_graph(schema.fixtures.classic(), graphson) - g = DseGraph().traversal_source(self.session, 'wrong_graph', execution_profile=self.ep) - traversal_future = g.V().has('name', 'marko').out('knows').values('name').promise() - with self.assertRaises(Exception): - traversal_future.result() - - def _test_promise_callback(self, schema, graphson): - self.execute_graph(schema.fixtures.classic(), graphson) - g = self.fetch_traversal_source(graphson) - future = Future() - - def cb(f): - future.set_result(f.result()) - - traversal_future = g.V().has('name', 'marko').out('knows').values('name').promise() - traversal_future.add_done_callback(cb) - self._validate_results(future.result()) - - def _test_promise_callback_on_error(self, schema, graphson): - self.execute_graph(schema.fixtures.classic(), graphson) - g = DseGraph().traversal_source(self.session, 'wrong_graph', execution_profile=self.ep) - future = Future() - - def cb(f): - try: - f.result() - except Exception as e: - future.set_exception(e) - - traversal_future = g.V().has('name', 'marko').out('knows').values('name').promise() - traversal_future.add_done_callback(cb) - with self.assertRaises(Exception): - future.result() diff --git a/tests/integration/advanced/graph/fluent/test_search.py b/tests/integration/advanced/graph/fluent/test_search.py deleted file mode 100644 index d50016d576..0000000000 --- a/tests/integration/advanced/graph/fluent/test_search.py +++ /dev/null @@ -1,539 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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. - -from cassandra.util import Distance -from cassandra import InvalidRequest -from cassandra.graph import GraphProtocol -from cassandra.datastax.graph.fluent import DseGraph -from cassandra.datastax.graph.fluent.predicates import Search, Geo, GeoUnit, CqlCollection - -from tests.integration.advanced import use_single_node_with_graph_and_solr -from tests.integration.advanced.graph import GraphUnitTestCase, CoreGraphSchema, ClassicGraphSchema, GraphTestConfiguration -from tests.integration import greaterthanorequaldse51, DSE_VERSION, requiredse - - -def setup_module(): - if DSE_VERSION: - use_single_node_with_graph_and_solr() - - -class AbstractSearchTest(GraphUnitTestCase): - - def setUp(self): - super(AbstractSearchTest, self).setUp() - self.ep_graphson2 = DseGraph().create_execution_profile(self.graph_name, - graph_protocol=GraphProtocol.GRAPHSON_2_0) - self.ep_graphson3 = DseGraph().create_execution_profile(self.graph_name, - graph_protocol=GraphProtocol.GRAPHSON_3_0) - - self.cluster.add_execution_profile('traversal_graphson2', self.ep_graphson2) - self.cluster.add_execution_profile('traversal_graphson3', self.ep_graphson3) - - def fetch_traversal_source(self, graphson): - ep = self.get_execution_profile(graphson, traversal=True) - return DseGraph().traversal_source(self.session, self.graph_name, execution_profile=ep) - - def _test_search_by_prefix(self, schema, graphson): - """ - Test to validate that solr searches by prefix function. - - @since 1.0.0 - @jira_ticket PYTHON-660 - @expected_result all names starting with Paul should be returned - - @test_category dse graph - """ - self.execute_graph(schema.fixtures.address_book(), graphson) - g = self.fetch_traversal_source(graphson) - traversal = g.V().has("person", "name", Search.prefix("Paul")).values("name") - results_list = self.execute_traversal(traversal, graphson) - self.assertEqual(len(results_list), 1) - self.assertEqual(results_list[0], "Paul Thomas Joe") - - def _test_search_by_regex(self, schema, graphson): - """ - Test to validate that solr searches by regex function. - - @since 1.0.0 - @jira_ticket PYTHON-660 - @expected_result all names containing Paul should be returned - - @test_category dse graph - """ - self.execute_graph(schema.fixtures.address_book(), graphson) - g = self.fetch_traversal_source(graphson) - traversal = g.V().has("person", "name", Search.regex(".*Paul.*")).values("name") - results_list = self.execute_traversal(traversal, graphson) - self.assertEqual(len(results_list), 2) - self.assertIn("Paul Thomas Joe", results_list) - self.assertIn("James Paul Smith", results_list) - - def _test_search_by_token(self, schema, graphson): - """ - Test to validate that solr searches by token. - - @since 1.0.0 - @jira_ticket PYTHON-660 - @expected_result all names with description containing could shoud be returned - - @test_category dse graph - """ - self.execute_graph(schema.fixtures.address_book(), graphson) - g = self.fetch_traversal_source(graphson) - traversal = g.V().has("person", "description", Search.token("cold")).values("name") - results_list = self.execute_traversal(traversal, graphson) - self.assertEqual(len(results_list), 2) - self.assertIn("Jill Alice", results_list) - self.assertIn("George Bill Steve", results_list) - - def _test_search_by_token_prefix(self, schema, graphson): - """ - Test to validate that solr searches by token prefix. - - @since 1.0.0 - @jira_ticket PYTHON-660 - @expected_result all names with description containing a token starting with h are returned - - @test_category dse graph - """ - self.execute_graph(schema.fixtures.address_book(), graphson) - g = self.fetch_traversal_source(graphson) - traversal = g.V().has("person", "description", Search.token_prefix("h")).values("name") - results_list = self.execute_traversal(traversal, graphson) - self.assertEqual(len(results_list), 2) - self.assertIn("Paul Thomas Joe", results_list) - self.assertIn( "James Paul Smith", results_list) - - def _test_search_by_token_regex(self, schema, graphson): - """ - Test to validate that solr searches by token regex. - - @since 1.0.0 - @jira_ticket PYTHON-660 - @expected_result all names with description containing nice or hospital are returned - - @test_category dse graph - """ - self.execute_graph(schema.fixtures.address_book(), graphson) - g = self.fetch_traversal_source(graphson) - traversal = g.V().has("person", "description", Search.token_regex("(nice|hospital)")).values("name") - results_list = self.execute_traversal(traversal, graphson) - self.assertEqual(len(results_list), 2) - self.assertIn("Paul Thomas Joe", results_list ) - self.assertIn( "Jill Alice", results_list ) - - def _assert_in_distance(self, schema, graphson, inside, names): - """ - Helper function that asserts that an exception is arisen if geodetic predicates are used - in cartesian geometry. Also asserts that the expected list is equal to the returned from - the transversal using different search indexes. - """ - def assert_equal_list(L1, L2): - return len(L1) == len(L2) and sorted(L1) == sorted(L2) - - self.execute_graph(schema.fixtures.address_book(), graphson) - g = self.fetch_traversal_source(graphson) - - traversal = g.V().has("person", "pointPropWithBoundsWithSearchIndex", inside).values("name") - if schema is ClassicGraphSchema: - # throws an exception because of a SOLR/Search limitation in the indexing process - # may be resolved in the future - self.assertRaises(InvalidRequest, self.execute_traversal, traversal, graphson) - else: - traversal = g.V().has("person", "pointPropWithBoundsWithSearchIndex", inside).values("name") - results_list = self.execute_traversal(traversal, graphson) - assert_equal_list(names, results_list) - - traversal = g.V().has("person", "pointPropWithBounds", inside).values("name") - results_list = self.execute_traversal(traversal, graphson) - assert_equal_list(names, results_list) - - traversal = g.V().has("person", "pointPropWithGeoBoundsWithSearchIndex", inside).values("name") - results_list = self.execute_traversal(traversal, graphson) - assert_equal_list(names, results_list) - - traversal = g.V().has("person", "pointPropWithGeoBounds", inside).values("name") - results_list = self.execute_traversal(traversal, graphson) - assert_equal_list(names, results_list) - - @greaterthanorequaldse51 - def _test_search_by_distance(self, schema, graphson): - """ - Test to validate that solr searches by distance. - - @since 1.0.0 - @jira_ticket PYTHON-660 - @expected_result all names with a geo location within a 2 degree distance of -92,44 are returned - - @test_category dse graph - """ - self._assert_in_distance(schema, graphson, - Geo.inside(Distance(-92, 44, 2)), - ["Paul Thomas Joe", "George Bill Steve"] - ) - - @greaterthanorequaldse51 - def _test_search_by_distance_meters_units(self, schema, graphson): - """ - Test to validate that solr searches by distance. - - @since 2.0.0 - @jira_ticket PYTHON-698 - @expected_result all names with a geo location within a 56k-meter radius of -92,44 are returned - - @test_category dse graph - """ - self._assert_in_distance(schema, graphson, - Geo.inside(Distance(-92, 44, 56000), GeoUnit.METERS), - ["Paul Thomas Joe"] - ) - - @greaterthanorequaldse51 - def _test_search_by_distance_miles_units(self, schema, graphson): - """ - Test to validate that solr searches by distance. - - @since 2.0.0 - @jira_ticket PYTHON-698 - @expected_result all names with a geo location within a 70-mile radius of -92,44 are returned - - @test_category dse graph - """ - self._assert_in_distance(schema, graphson, - Geo.inside(Distance(-92, 44, 70), GeoUnit.MILES), - ["Paul Thomas Joe", "George Bill Steve"] - ) - - @greaterthanorequaldse51 - def _test_search_by_distance_check_limit(self, schema, graphson): - """ - Test to validate that solr searches by distance using several units. It will also validate - that and exception is arisen if geodetic predicates are used against cartesian geometry - - @since 2.0.0 - @jira_ticket PYTHON-698 - @expected_result if the search distance is below the real distance only one - name will be in the list, otherwise, two - - @test_category dse graph - """ - # Paul Thomas Joe and George Bill Steve are 64.6923761881464 km apart - self._assert_in_distance(schema, graphson, - Geo.inside(Distance(-92.46295, 44.0234, 65), GeoUnit.KILOMETERS), - ["George Bill Steve", "Paul Thomas Joe"] - ) - - self._assert_in_distance(schema, graphson, - Geo.inside(Distance(-92.46295, 44.0234, 64), GeoUnit.KILOMETERS), - ["Paul Thomas Joe"] - ) - - # Paul Thomas Joe and George Bill Steve are 40.19797892069464 miles apart - self._assert_in_distance(schema, graphson, - Geo.inside(Distance(-92.46295, 44.0234, 41), GeoUnit.MILES), - ["George Bill Steve", "Paul Thomas Joe"] - ) - - self._assert_in_distance(schema, graphson, - Geo.inside(Distance(-92.46295, 44.0234, 40), GeoUnit.MILES), - ["Paul Thomas Joe"] - ) - - @greaterthanorequaldse51 - def _test_search_by_fuzzy(self, schema, graphson): - """ - Test to validate that solr searches by distance. - - @since 1.0.0 - @jira_ticket PYTHON-664 - @expected_result all names with a geo location within a 2 radius distance of -92,44 are returned - - @test_category dse graph - """ - self.execute_graph(schema.fixtures.address_book(), graphson) - g = self.fetch_traversal_source(graphson) - traversal = g.V().has("person", "name", Search.fuzzy("Paul Thamas Joe", 1)).values("name") - results_list = self.execute_traversal(traversal, graphson) - self.assertEqual(len(results_list), 1) - self.assertIn("Paul Thomas Joe", results_list) - - traversal = g.V().has("person", "name", Search.fuzzy("Paul Thames Joe", 1)).values("name") - results_list = self.execute_traversal(traversal, graphson) - self.assertEqual(len(results_list), 0) - - @greaterthanorequaldse51 - def _test_search_by_fuzzy_token(self, schema, graphson): - """ - Test to validate that fuzzy searches. - - @since 1.0.0 - @jira_ticket PYTHON-664 - @expected_result all names with that differ from the search criteria by one letter should be returned - - @test_category dse graph - """ - self.execute_graph(schema.fixtures.address_book(), graphson) - g = self.fetch_traversal_source(graphson) - traversal = g.V().has("person", "description", Search.token_fuzzy("lives", 1)).values("name") - # Should match 'Paul Thomas Joe' since description contains 'Lives' - # Should match 'James Paul Joe' since description contains 'Likes' - results_list = self.execute_traversal(traversal, graphson) - self.assertEqual(len(results_list), 2) - self.assertIn("Paul Thomas Joe", results_list) - self.assertIn("James Paul Smith", results_list) - - traversal = g.V().has("person", "description", Search.token_fuzzy("loues", 1)).values("name") - results_list = self.execute_traversal(traversal, graphson) - self.assertEqual(len(results_list), 0) - - @greaterthanorequaldse51 - def _test_search_by_phrase(self, schema, graphson): - """ - Test to validate that phrase searches. - - @since 1.0.0 - @jira_ticket PYTHON-664 - @expected_result all names with that differ from the search phrase criteria by two letter should be returned - - @test_category dse graph - """ - self.execute_graph(schema.fixtures.address_book(), graphson) - g = self.fetch_traversal_source(graphson) - traversal = g.V().has("person", "description", Search.phrase("a cold", 2)).values("name") - #Should match 'George Bill Steve' since 'A cold dude' is at distance of 0 for 'a cold'. - #Should match 'Jill Alice' since 'Enjoys a very nice cold coca cola' is at distance of 2 for 'a cold'. - results_list = self.execute_traversal(traversal, graphson) - self.assertEqual(len(results_list), 2) - self.assertIn('George Bill Steve', results_list) - self.assertIn('Jill Alice', results_list) - - traversal = g.V().has("person", "description", Search.phrase("a bald", 2)).values("name") - results_list = self.execute_traversal(traversal, graphson) - self.assertEqual(len(results_list), 0) - - -@requiredse -@GraphTestConfiguration.generate_tests(traversal=True) -class ImplicitSearchTest(AbstractSearchTest): - """ - This test class will execute all tests of the AbstractSearchTest using implicit execution - All traversals will be run directly using toList() - """ - def fetch_key_from_prop(self, property): - return property.key - - def execute_traversal(self, traversal, graphson=None): - return traversal.toList() - - -@requiredse -@GraphTestConfiguration.generate_tests(traversal=True) -class ExplicitSearchTest(AbstractSearchTest): - """ - This test class will execute all tests of the AbstractSearchTest using implicit execution - All traversals will be converted to byte code then they will be executed explicitly. - """ - - def execute_traversal(self, traversal, graphson): - ep = self.get_execution_profile(graphson, traversal=True) - ep = self.session.get_execution_profile(ep) - context = None - if graphson == GraphProtocol.GRAPHSON_3_0: - context = { - 'cluster': self.cluster, - 'graph_name': ep.graph_options.graph_name.decode('utf-8') if ep.graph_options.graph_name else None - } - query = DseGraph.query_from_traversal(traversal, graphson, context=context) - #Use an ep that is configured with the correct row factory, and bytecode-json language flat set - result_set = self.execute_graph(query, graphson, traversal=True) - return list(result_set) - - -@requiredse -class BaseCqlCollectionPredicatesTest(GraphUnitTestCase): - - def setUp(self): - super(BaseCqlCollectionPredicatesTest, self).setUp() - self.ep_graphson3 = DseGraph().create_execution_profile(self.graph_name, - graph_protocol=GraphProtocol.GRAPHSON_3_0) - self.cluster.add_execution_profile('traversal_graphson3', self.ep_graphson3) - - def fetch_traversal_source(self, graphson): - ep = self.get_execution_profile(graphson, traversal=True) - return DseGraph().traversal_source(self.session, self.graph_name, execution_profile=ep) - - def setup_vertex_label(self, graphson): - ep = self.get_execution_profile(graphson) - self.session.execute_graph(""" - schema.vertexLabel('cqlcollections').ifNotExists().partitionBy('name', Varchar) - .property('list', listOf(Text)) - .property('frozen_list', frozen(listOf(Text))) - .property('set', setOf(Text)) - .property('frozen_set', frozen(setOf(Text))) - .property('map_keys', mapOf(Int, Text)) - .property('map_values', mapOf(Int, Text)) - .property('map_entries', mapOf(Int, Text)) - .property('frozen_map', frozen(mapOf(Int, Text))) - .create() - """, execution_profile=ep) - - self.session.execute_graph(""" - schema.vertexLabel('cqlcollections').secondaryIndex('list').by('list').create(); - schema.vertexLabel('cqlcollections').secondaryIndex('frozen_list').by('frozen_list').indexFull().create(); - schema.vertexLabel('cqlcollections').secondaryIndex('set').by('set').create(); - schema.vertexLabel('cqlcollections').secondaryIndex('frozen_set').by('frozen_set').indexFull().create(); - schema.vertexLabel('cqlcollections').secondaryIndex('map_keys').by('map_keys').indexKeys().create(); - schema.vertexLabel('cqlcollections').secondaryIndex('map_values').by('map_values').indexValues().create(); - schema.vertexLabel('cqlcollections').secondaryIndex('map_entries').by('map_entries').indexEntries().create(); - schema.vertexLabel('cqlcollections').secondaryIndex('frozen_map').by('frozen_map').indexFull().create(); - """, execution_profile=ep) - - def _test_contains_list(self, schema, graphson): - """ - Test to validate that the cql predicate contains works with list - - @since TODO dse 6.8 - @jira_ticket PYTHON-1039 - @expected_result contains predicate work on a list - - @test_category dse graph - """ - self.setup_vertex_label(graphson) - g = self.fetch_traversal_source(graphson) - traversal = g.addV("cqlcollections").property("name", "list1").property("list", ['item1', 'item2']) - self.execute_traversal(traversal, graphson) - traversal = g.addV("cqlcollections").property("name", "list2").property("list", ['item3', 'item4']) - self.execute_traversal(traversal, graphson) - traversal = g.V().has("cqlcollections", "list", CqlCollection.contains("item1")).values("name") - results_list = self.execute_traversal(traversal, graphson) - self.assertEqual(len(results_list), 1) - self.assertIn("list1", results_list) - - def _test_contains_set(self, schema, graphson): - """ - Test to validate that the cql predicate contains works with set - - @since TODO dse 6.8 - @jira_ticket PYTHON-1039 - @expected_result contains predicate work on a set - - @test_category dse graph - """ - self.setup_vertex_label(graphson) - g = self.fetch_traversal_source(graphson) - traversal = g.addV("cqlcollections").property("name", "set1").property("set", {'item1', 'item2'}) - self.execute_traversal(traversal, graphson) - traversal = g.addV("cqlcollections").property("name", "set2").property("set", {'item3', 'item4'}) - self.execute_traversal(traversal, graphson) - traversal = g.V().has("cqlcollections", "set", CqlCollection.contains("item1")).values("name") - results_list = self.execute_traversal(traversal, graphson) - self.assertEqual(len(results_list), 1) - self.assertIn("set1", results_list) - - def _test_contains_key_map(self, schema, graphson): - """ - Test to validate that the cql predicate contains_key works with map - - @since TODO dse 6.8 - @jira_ticket PYTHON-1039 - @expected_result contains_key predicate work on a map - - @test_category dse graph - """ - self.setup_vertex_label(graphson) - g = self.fetch_traversal_source(graphson) - traversal = g.addV("cqlcollections").property("name", "map1").property("map_keys", {0: 'item1', 1: 'item2'}) - self.execute_traversal(traversal, graphson) - traversal = g.addV("cqlcollections").property("name", "map2").property("map_keys", {2: 'item3', 3: 'item4'}) - self.execute_traversal(traversal, graphson) - traversal = g.V().has("cqlcollections", "map_keys", CqlCollection.contains_key(0)).values("name") - results_list = self.execute_traversal(traversal, graphson) - self.assertEqual(len(results_list), 1) - self.assertIn("map1", results_list) - - def _test_contains_value_map(self, schema, graphson): - """ - Test to validate that the cql predicate contains_value works with map - - @since TODO dse 6.8 - @jira_ticket PYTHON-1039 - @expected_result contains_value predicate work on a map - - @test_category dse graph - """ - self.setup_vertex_label(graphson) - g = self.fetch_traversal_source(graphson) - traversal = g.addV("cqlcollections").property("name", "map1").property("map_values", {0: 'item1', 1: 'item2'}) - self.execute_traversal(traversal, graphson) - traversal = g.addV("cqlcollections").property("name", "map2").property("map_values", {2: 'item3', 3: 'item4'}) - self.execute_traversal(traversal, graphson) - traversal = g.V().has("cqlcollections", "map_values", CqlCollection.contains_value('item3')).values("name") - results_list = self.execute_traversal(traversal, graphson) - self.assertEqual(len(results_list), 1) - self.assertIn("map2", results_list) - - def _test_entry_eq_map(self, schema, graphson): - """ - Test to validate that the cql predicate entry_eq works with map - - @since TODO dse 6.8 - @jira_ticket PYTHON-1039 - @expected_result entry_eq predicate work on a map - - @test_category dse graph - """ - self.setup_vertex_label(graphson) - g = self.fetch_traversal_source(graphson) - traversal = g.addV("cqlcollections").property("name", "map1").property("map_entries", {0: 'item1', 1: 'item2'}) - self.execute_traversal(traversal, graphson) - traversal = g.addV("cqlcollections").property("name", "map2").property("map_entries", {2: 'item3', 3: 'item4'}) - self.execute_traversal(traversal, graphson) - traversal = g.V().has("cqlcollections", "map_entries", CqlCollection.entry_eq([2, 'item3'])).values("name") - results_list = self.execute_traversal(traversal, graphson) - self.assertEqual(len(results_list), 1) - self.assertIn("map2", results_list) - - -@requiredse -@GraphTestConfiguration.generate_tests(traversal=True, schema=CoreGraphSchema) -class ImplicitCqlCollectionPredicatesTest(BaseCqlCollectionPredicatesTest): - """ - This test class will execute all tests of the BaseCqlCollectionTest using implicit execution - All traversals will be run directly using toList() - """ - - def execute_traversal(self, traversal, graphson=None): - return traversal.toList() - - -@requiredse -@GraphTestConfiguration.generate_tests(traversal=True, schema=CoreGraphSchema) -class ExplicitCqlCollectionPredicatesTest(BaseCqlCollectionPredicatesTest): - """ - This test class will execute all tests of the AbstractSearchTest using implicit execution - All traversals will be converted to byte code then they will be executed explicitly. - """ - - def execute_traversal(self, traversal, graphson): - ep = self.get_execution_profile(graphson, traversal=True) - ep = self.session.get_execution_profile(ep) - context = None - if graphson == GraphProtocol.GRAPHSON_3_0: - context = { - 'cluster': self.cluster, - 'graph_name': ep.graph_options.graph_name.decode('utf-8') if ep.graph_options.graph_name else None - } - query = DseGraph.query_from_traversal(traversal, graphson, context=context) - result_set = self.execute_graph(query, graphson, traversal=True) - return list(result_set) diff --git a/tests/integration/advanced/graph/test_graph.py b/tests/integration/advanced/graph/test_graph.py deleted file mode 100644 index 7f55229911..0000000000 --- a/tests/integration/advanced/graph/test_graph.py +++ /dev/null @@ -1,270 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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 re - -from cassandra import OperationTimedOut, InvalidRequest -from cassandra.protocol import SyntaxException -from cassandra.policies import WhiteListRoundRobinPolicy -from cassandra.cluster import NoHostAvailable -from cassandra.cluster import EXEC_PROFILE_GRAPH_DEFAULT, GraphExecutionProfile -from cassandra.graph import single_object_row_factory, Vertex, graph_object_row_factory, \ - graph_graphson2_row_factory, graph_graphson3_row_factory -from cassandra.util import SortedSet - -from tests.integration import DSE_VERSION, greaterthanorequaldse51, greaterthanorequaldse68, \ - requiredse, TestCluster -from tests.integration.advanced.graph import BasicGraphUnitTestCase, GraphUnitTestCase, \ - GraphProtocol, ClassicGraphSchema, CoreGraphSchema, use_single_node_with_graph - - -def setup_module(): - if DSE_VERSION: - dse_options = {'graph': {'realtime_evaluation_timeout_in_seconds': 60}} - use_single_node_with_graph(dse_options=dse_options) - - -@requiredse -class GraphTimeoutTests(BasicGraphUnitTestCase): - - def test_should_wait_indefinitely_by_default(self): - """ - Tests that by default the client should wait indefinitely for server timeouts - - @since 1.0.0 - @jira_ticket PYTHON-589 - - @test_category dse graph - """ - desired_timeout = 1000 - - graph_source = "test_timeout_1" - ep_name = graph_source - ep = self.session.execution_profile_clone_update(EXEC_PROFILE_GRAPH_DEFAULT) - ep.graph_options = ep.graph_options.copy() - ep.graph_options.graph_source = graph_source - self.cluster.add_execution_profile(ep_name, ep) - - to_run = '''graph.schema().config().option("graph.traversal_sources.{0}.evaluation_timeout").set('{1} ms')'''.format( - graph_source, desired_timeout) - self.session.execute_graph(to_run, execution_profile=ep_name) - with self.assertRaises(InvalidRequest) as ir: - self.session.execute_graph("java.util.concurrent.TimeUnit.MILLISECONDS.sleep(35000L);1+1", - execution_profile=ep_name) - self.assertTrue("evaluation exceeded the configured threshold of 1000" in str(ir.exception) or - "evaluation exceeded the configured threshold of evaluation_timeout at 1000" in str( - ir.exception)) - - def test_request_timeout_less_then_server(self): - """ - Tests that with explicit request_timeouts set, that a server timeout is honored if it's relieved prior to the - client timeout - - @since 1.0.0 - @jira_ticket PYTHON-589 - - @test_category dse graph - """ - desired_timeout = 1000 - graph_source = "test_timeout_2" - ep_name = graph_source - ep = self.session.execution_profile_clone_update(EXEC_PROFILE_GRAPH_DEFAULT, request_timeout=32) - ep.graph_options = ep.graph_options.copy() - ep.graph_options.graph_source = graph_source - self.cluster.add_execution_profile(ep_name, ep) - - to_run = '''graph.schema().config().option("graph.traversal_sources.{0}.evaluation_timeout").set('{1} ms')'''.format( - graph_source, desired_timeout) - self.session.execute_graph(to_run, execution_profile=ep_name) - with self.assertRaises(InvalidRequest) as ir: - self.session.execute_graph("java.util.concurrent.TimeUnit.MILLISECONDS.sleep(35000L);1+1", - execution_profile=ep_name) - self.assertTrue("evaluation exceeded the configured threshold of 1000" in str(ir.exception) or - "evaluation exceeded the configured threshold of evaluation_timeout at 1000" in str( - ir.exception)) - - def test_server_timeout_less_then_request(self): - """ - Tests that with explicit request_timeouts set, that a client timeout is honored if it's triggered prior to the - server sending a timeout. - - @since 1.0.0 - @jira_ticket PYTHON-589 - - @test_category dse graph - """ - graph_source = "test_timeout_3" - ep_name = graph_source - ep = self.session.execution_profile_clone_update(EXEC_PROFILE_GRAPH_DEFAULT, request_timeout=1) - ep.graph_options = ep.graph_options.copy() - ep.graph_options.graph_source = graph_source - self.cluster.add_execution_profile(ep_name, ep) - server_timeout = 10000 - to_run = '''graph.schema().config().option("graph.traversal_sources.{0}.evaluation_timeout").set('{1} ms')'''.format( - graph_source, server_timeout) - self.session.execute_graph(to_run, execution_profile=ep_name) - - with self.assertRaises(Exception) as e: - self.session.execute_graph("java.util.concurrent.TimeUnit.MILLISECONDS.sleep(35000L);1+1", - execution_profile=ep_name) - self.assertTrue(isinstance(e, InvalidRequest) or isinstance(e, OperationTimedOut)) - - -@requiredse -class GraphProfileTests(BasicGraphUnitTestCase): - def test_graph_profile(self): - """ - Test verifying various aspects of graph config properties. - - @since 1.0.0 - @jira_ticket PYTHON-570 - - @test_category dse graph - """ - hosts = self.cluster.metadata.all_hosts() - first_host = hosts[0].address - second_hosts = "1.2.3.4" - - self._execute(ClassicGraphSchema.fixtures.classic(), graphson=GraphProtocol.GRAPHSON_1_0) - # Create various execution policies - exec_dif_factory = GraphExecutionProfile(row_factory=single_object_row_factory) - exec_dif_factory.graph_options.graph_name = self.graph_name - exec_dif_lbp = GraphExecutionProfile(load_balancing_policy=WhiteListRoundRobinPolicy([first_host])) - exec_dif_lbp.graph_options.graph_name = self.graph_name - exec_bad_lbp = GraphExecutionProfile(load_balancing_policy=WhiteListRoundRobinPolicy([second_hosts])) - exec_dif_lbp.graph_options.graph_name = self.graph_name - exec_short_timeout = GraphExecutionProfile(request_timeout=1, - load_balancing_policy=WhiteListRoundRobinPolicy([first_host])) - exec_short_timeout.graph_options.graph_name = self.graph_name - - # Add a single execution policy on cluster creation - local_cluster = TestCluster(execution_profiles={"exec_dif_factory": exec_dif_factory}) - local_session = local_cluster.connect() - self.addCleanup(local_cluster.shutdown) - - rs1 = self.session.execute_graph('g.V()') - rs2 = local_session.execute_graph('g.V()', execution_profile='exec_dif_factory') - - # Verify default and non default policy works - self.assertFalse(isinstance(rs2[0], Vertex)) - self.assertTrue(isinstance(rs1[0], Vertex)) - # Add other policies validate that lbp are honored - local_cluster.add_execution_profile("exec_dif_ldp", exec_dif_lbp) - local_session.execute_graph('g.V()', execution_profile="exec_dif_ldp") - local_cluster.add_execution_profile("exec_bad_lbp", exec_bad_lbp) - with self.assertRaises(NoHostAvailable): - local_session.execute_graph('g.V()', execution_profile="exec_bad_lbp") - - # Try with missing EP - with self.assertRaises(ValueError): - local_session.execute_graph('g.V()', execution_profile='bad_exec_profile') - - # Validate that timeout is honored - local_cluster.add_execution_profile("exec_short_timeout", exec_short_timeout) - with self.assertRaises(Exception) as e: - self.assertTrue(isinstance(e, InvalidRequest) or isinstance(e, OperationTimedOut)) - local_session.execute_graph('java.util.concurrent.TimeUnit.MILLISECONDS.sleep(2000L);', - execution_profile='exec_short_timeout') - - -@requiredse -class GraphMetadataTest(BasicGraphUnitTestCase): - - @greaterthanorequaldse51 - def test_dse_workloads(self): - """ - Test to ensure dse_workloads is populated appropriately. - Field added in DSE 5.1 - - @since DSE 2.0 - @jira_ticket PYTHON-667 - @expected_result dse_workloads set is set on host model - - @test_category metadata - """ - for host in self.cluster.metadata.all_hosts(): - self.assertIsInstance(host.dse_workloads, SortedSet) - self.assertIn("Cassandra", host.dse_workloads) - self.assertIn("Graph", host.dse_workloads) - - -@requiredse -class GraphExecutionProfileOptionsResolveTest(GraphUnitTestCase): - """ - Test that the execution profile options are properly resolved for graph queries. - - @since DSE 6.8 - @jira_ticket PYTHON-1004 PYTHON-1056 - @expected_result execution profile options are properly determined following the rules. - """ - - def test_default_options(self): - ep = self.session.get_execution_profile(EXEC_PROFILE_GRAPH_DEFAULT) - self.assertEqual(ep.graph_options.graph_protocol, None) - self.assertEqual(ep.row_factory, None) - self.session._resolve_execution_profile_options(ep) - self.assertEqual(ep.graph_options.graph_protocol, GraphProtocol.GRAPHSON_1_0) - self.assertEqual(ep.row_factory, graph_object_row_factory) - - def test_default_options_when_not_groovy(self): - ep = self.session.get_execution_profile(EXEC_PROFILE_GRAPH_DEFAULT) - self.assertEqual(ep.graph_options.graph_protocol, None) - self.assertEqual(ep.row_factory, None) - ep.graph_options.graph_language = 'whatever' - self.session._resolve_execution_profile_options(ep) - self.assertEqual(ep.graph_options.graph_protocol, GraphProtocol.GRAPHSON_2_0) - self.assertEqual(ep.row_factory, graph_graphson2_row_factory) - - def test_default_options_when_explicitly_specified(self): - ep = self.session.get_execution_profile(EXEC_PROFILE_GRAPH_DEFAULT) - self.assertEqual(ep.graph_options.graph_protocol, None) - self.assertEqual(ep.row_factory, None) - obj = object() - ep.graph_options.graph_protocol = obj - ep.row_factory = obj - self.session._resolve_execution_profile_options(ep) - self.assertEqual(ep.graph_options.graph_protocol, obj) - self.assertEqual(ep.row_factory, obj) - - @greaterthanorequaldse68 - def test_graph_protocol_default_for_core_is_graphson3(self): - """Test that graphson3 is automatically resolved for a core graph query""" - self.setup_graph(CoreGraphSchema) - ep = self.session.get_execution_profile(EXEC_PROFILE_GRAPH_DEFAULT) - self.assertEqual(ep.graph_options.graph_protocol, None) - self.assertEqual(ep.row_factory, None) - # Ensure we have the graph metadata - self.session.cluster.refresh_schema_metadata() - self.session._resolve_execution_profile_options(ep) - self.assertEqual(ep.graph_options.graph_protocol, GraphProtocol.GRAPHSON_3_0) - self.assertEqual(ep.row_factory, graph_graphson3_row_factory) - - self.execute_graph_queries(CoreGraphSchema.fixtures.classic(), verify_graphson=GraphProtocol.GRAPHSON_3_0) - - @greaterthanorequaldse68 - def test_graph_protocol_default_for_core_fallback_to_graphson1_if_no_graph_name(self): - """Test that graphson1 is set when we cannot detect if it's a core graph""" - self.setup_graph(CoreGraphSchema) - default_ep = self.session.get_execution_profile(EXEC_PROFILE_GRAPH_DEFAULT) - graph_options = default_ep.graph_options.copy() - graph_options.graph_name = None - ep = self.session.execution_profile_clone_update(EXEC_PROFILE_GRAPH_DEFAULT, graph_options=graph_options) - self.session._resolve_execution_profile_options(ep) - self.assertEqual(ep.graph_options.graph_protocol, GraphProtocol.GRAPHSON_1_0) - self.assertEqual(ep.row_factory, graph_object_row_factory) - - regex = re.compile(".*Variable.*is unknown.*", re.S) - with self.assertRaisesRegex(SyntaxException, regex): - self.execute_graph_queries(CoreGraphSchema.fixtures.classic(), - execution_profile=ep, verify_graphson=GraphProtocol.GRAPHSON_1_0) diff --git a/tests/integration/advanced/graph/test_graph_cont_paging.py b/tests/integration/advanced/graph/test_graph_cont_paging.py deleted file mode 100644 index 065d01d939..0000000000 --- a/tests/integration/advanced/graph/test_graph_cont_paging.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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. - -from cassandra.cluster import ContinuousPagingOptions - -from tests.integration import greaterthanorequaldse68 -from tests.integration.advanced.graph import GraphUnitTestCase, CoreGraphSchema, GraphTestConfiguration - - -@greaterthanorequaldse68 -@GraphTestConfiguration.generate_tests(schema=CoreGraphSchema) -class GraphPagingTest(GraphUnitTestCase): - - def _setup_data(self, schema, graphson): - self.execute_graph("schema.vertexLabel('person').ifNotExists().partitionBy('name', Text).property('age', Int).create();", graphson) - for i in range(100): - self.execute_graph("g.addV('person').property('name', 'batman-{}')".format(i), graphson) - - def _test_cont_paging_is_enabled_by_default(self, schema, graphson): - """ - Test that graph paging is automatically enabled with a >=6.8 cluster. - - @jira_ticket PYTHON-1045 - @expected_result the response future has a continuous_paging_session since graph paging is enabled - - @test_category dse graph - """ - ep = self.get_execution_profile(graphson) - self._setup_data(schema, graphson) - rf = self.session.execute_graph_async("g.V()", execution_profile=ep) - results = list(rf.result()) - self.assertIsNotNone(rf._continuous_paging_session) - self.assertEqual(len(results), 100) - - def _test_cont_paging_can_be_disabled(self, schema, graphson): - """ - Test that graph paging can be disabled. - - @jira_ticket PYTHON-1045 - @expected_result the response future doesn't have a continuous_paging_session since graph paging is disabled - - @test_category dse graph - """ - ep = self.get_execution_profile(graphson) - new_ep = self.session.execution_profile_clone_update(ep, continuous_paging_options=None) - self._setup_data(schema, graphson) - rf = self.session.execute_graph_async("g.V()", execution_profile=new_ep) - results = list(rf.result()) - self.assertIsNone(rf._continuous_paging_session) - self.assertEqual(len(results), 100) - - def _test_cont_paging_with_custom_options(self, schema, graphson): - """ - Test that we can specify custom paging options. - - @jira_ticket PYTHON-1045 - @expected_result we get only the desired number of results - - @test_category dse graph - """ - ep = self.get_execution_profile(graphson) - new_ep = self.session.execution_profile_clone_update( - ep, continuous_paging_options=ContinuousPagingOptions(max_pages=1)) - self._setup_data(schema, graphson) - self.session.default_fetch_size = 10 - results = list(self.session.execute_graph("g.V()", execution_profile=new_ep)) - self.assertEqual(len(results), 10) diff --git a/tests/integration/advanced/graph/test_graph_datatype.py b/tests/integration/advanced/graph/test_graph_datatype.py deleted file mode 100644 index 8a261c94d9..0000000000 --- a/tests/integration/advanced/graph/test_graph_datatype.py +++ /dev/null @@ -1,266 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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 unittest - -import time -import logging -from packaging.version import Version -from collections import namedtuple - -from cassandra.cluster import EXEC_PROFILE_GRAPH_DEFAULT -from cassandra.graph import graph_result_row_factory -from cassandra.graph.query import GraphProtocol -from cassandra.graph.types import VertexProperty - -from tests.util import wait_until -from tests.integration.advanced.graph import BasicGraphUnitTestCase, ClassicGraphFixtures, \ - ClassicGraphSchema, CoreGraphSchema -from tests.integration.advanced.graph import VertexLabel, GraphTestConfiguration, GraphUnitTestCase -from tests.integration import DSE_VERSION, requiredse - -log = logging.getLogger(__name__) - - -@requiredse -class GraphBasicDataTypesTests(BasicGraphUnitTestCase): - - def test_result_types(self): - """ - Test to validate that the edge and vertex version of results are constructed correctly. - - @since 1.0.0 - @jira_ticket PYTHON-479 - @expected_result edge/vertex result types should be unpacked correctly. - @test_category dse graph - """ - queries, params = ClassicGraphFixtures.multiple_fields() - for query in queries: - self.session.execute_graph(query, params) - - prof = self.session.execution_profile_clone_update(EXEC_PROFILE_GRAPH_DEFAULT, row_factory=graph_result_row_factory) # requires simplified row factory to avoid shedding id/~type information used for validation below - rs = self.session.execute_graph("g.V()", execution_profile=prof) - - for result in rs: - self._validate_type(result) - - def _validate_type(self, vertex): - for properties in vertex.properties.values(): - prop = properties[0] - - if DSE_VERSION >= Version("5.1"): - type_indicator = prop['id']['~label'] - else: - type_indicator = prop['id']['~type'] - - if any(type_indicator.startswith(t) for t in - ('int', 'short', 'long', 'bigint', 'decimal', 'smallint', 'varint')): - typ = int - elif any(type_indicator.startswith(t) for t in ('float', 'double')): - typ = float - elif any(type_indicator.startswith(t) for t in ('duration', 'date', 'negdate', 'time', - 'blob', 'timestamp', 'point', 'linestring', 'polygon', - 'inet', 'uuid')): - typ = str - else: - pass - self.fail("Received unexpected type: %s" % type_indicator) - self.assertIsInstance(prop['value'], typ) - - -class GenericGraphDataTypeTest(GraphUnitTestCase): - - def _test_all_datatypes(self, schema, graphson): - ep = self.get_execution_profile(graphson) - - for data in schema.fixtures.datatypes().values(): - typ, value, deserializer = data - vertex_label = VertexLabel([typ]) - property_name = next(iter(vertex_label.non_pk_properties.keys())) - schema.create_vertex_label(self.session, vertex_label, execution_profile=ep) - vertex = list(schema.add_vertex(self.session, vertex_label, property_name, value, execution_profile=ep))[0] - - def get_vertex_properties(): - return list(schema.get_vertex_properties( - self.session, vertex, execution_profile=ep)) - - prop_returned = 1 if DSE_VERSION < Version('5.1') else 2 # include pkid >=5.1 - wait_until( - lambda: len(get_vertex_properties()) == prop_returned, 0.2, 15) - - vertex_properties = get_vertex_properties() - if graphson == GraphProtocol.GRAPHSON_1_0: - vertex_properties = [vp.as_vertex_property() for vp in vertex_properties] - - for vp in vertex_properties: - if vp.label == 'pkid': - continue - - self.assertIsInstance(vp, VertexProperty) - self.assertEqual(vp.label, property_name) - if graphson == GraphProtocol.GRAPHSON_1_0: - deserialized_value = deserializer(vp.value) if deserializer else vp.value - self.assertEqual(deserialized_value, value) - else: - self.assertEqual(vp.value, value) - - def __test_udt(self, schema, graphson, address_class, address_with_tags_class, - complex_address_class, complex_address_with_owners_class): - if schema is not CoreGraphSchema or DSE_VERSION < Version('6.8'): - raise unittest.SkipTest("Graph UDT is only supported with DSE 6.8+ and Core graphs.") - - ep = self.get_execution_profile(graphson) - - Address = address_class - AddressWithTags = address_with_tags_class - ComplexAddress = complex_address_class - ComplexAddressWithOwners = complex_address_with_owners_class - - # setup udt - self.session.execute_graph(""" - schema.type('address').property('address', Text).property('city', Text).property('state', Text).create(); - schema.type('addressTags').property('address', Text).property('city', Text).property('state', Text). - property('tags', setOf(Text)).create(); - schema.type('complexAddress').property('address', Text).property('address_tags', frozen(typeOf('addressTags'))). - property('city', Text).property('state', Text).property('props', mapOf(Text, Int)).create(); - schema.type('complexAddressWithOwners').property('address', Text). - property('address_tags', frozen(typeOf('addressTags'))). - property('city', Text).property('state', Text).property('props', mapOf(Text, Int)). - property('owners', frozen(listOf(tupleOf(Text, Int)))).create(); - """, execution_profile=ep) - - time.sleep(2) # wait the UDT to be discovered - self.session.cluster.register_user_type(self.graph_name, 'address', Address) - self.session.cluster.register_user_type(self.graph_name, 'addressTags', AddressWithTags) - self.session.cluster.register_user_type(self.graph_name, 'complexAddress', ComplexAddress) - self.session.cluster.register_user_type(self.graph_name, 'complexAddressWithOwners', ComplexAddressWithOwners) - - data = { - "udt1": ["typeOf('address')", Address('1440 Rd Smith', 'Quebec', 'QC')], - "udt2": ["tupleOf(typeOf('address'), Text)", (Address('1440 Rd Smith', 'Quebec', 'QC'), 'hello')], - "udt3": ["tupleOf(frozen(typeOf('address')), Text)", (Address('1440 Rd Smith', 'Quebec', 'QC'), 'hello')], - "udt4": ["tupleOf(tupleOf(Int, typeOf('address')), Text)", - ((42, Address('1440 Rd Smith', 'Quebec', 'QC')), 'hello')], - "udt5": ["tupleOf(tupleOf(Int, typeOf('addressTags')), Text)", - ((42, AddressWithTags('1440 Rd Smith', 'Quebec', 'QC', {'t1', 't2'})), 'hello')], - "udt6": ["tupleOf(tupleOf(Int, typeOf('complexAddress')), Text)", - ((42, ComplexAddress('1440 Rd Smith', - AddressWithTags('1440 Rd Smith', 'Quebec', 'QC', {'t1', 't2'}), - 'Quebec', 'QC', {'p1': 42, 'p2': 33})), 'hello')], - "udt7": ["tupleOf(tupleOf(Int, frozen(typeOf('complexAddressWithOwners'))), Text)", - ((42, ComplexAddressWithOwners( - '1440 Rd Smith', - AddressWithTags('1440 CRd Smith', 'Quebec', 'QC', {'t1', 't2'}), - 'Quebec', 'QC', {'p1': 42, 'p2': 33}, [('Mike', 43), ('Gina', 39)]) - ), 'hello')] - } - - for typ, value in data.values(): - vertex_label = VertexLabel([typ]) - property_name = next(iter(vertex_label.non_pk_properties.keys())) - schema.create_vertex_label(self.session, vertex_label, execution_profile=ep) - - vertex = list(schema.add_vertex(self.session, vertex_label, property_name, value, execution_profile=ep))[0] - - def get_vertex_properties(): - return list(schema.get_vertex_properties( - self.session, vertex, execution_profile=ep)) - - wait_until( - lambda: len(get_vertex_properties()) == 2, 0.2, 15) - - vertex_properties = get_vertex_properties() - for vp in vertex_properties: - if vp.label == 'pkid': - continue - - self.assertIsInstance(vp, VertexProperty) - self.assertEqual(vp.label, property_name) - self.assertEqual(vp.value, value) - - def _test_udt_with_classes(self, schema, graphson): - class Address(object): - - def __init__(self, address, city, state): - self.address = address - self.city = city - self.state = state - - def __eq__(self, other): - return self.address == other.address and self.city == other.city and self.state == other.state - - class AddressWithTags(object): - - def __init__(self, address, city, state, tags): - self.address = address - self.city = city - self.state = state - self.tags = tags - - def __eq__(self, other): - return (self.address == other.address and self.city == other.city - and self.state == other.state and self.tags == other.tags) - - class ComplexAddress(object): - - def __init__(self, address, address_tags, city, state, props): - self.address = address - self.address_tags = address_tags - self.city = city - self.state = state - self.props = props - - def __eq__(self, other): - return (self.address == other.address and self.address_tags == other.address_tags - and self.city == other.city and self.state == other.state - and self.props == other.props) - - class ComplexAddressWithOwners(object): - - def __init__(self, address, address_tags, city, state, props, owners): - self.address = address - self.address_tags = address_tags - self.city = city - self.state = state - self.props = props - self.owners = owners - - def __eq__(self, other): - return (self.address == other.address and self.address_tags == other.address_tags - and self.city == other.city and self.state == other.state - and self.props == other.props and self.owners == other.owners) - - self.__test_udt(schema, graphson, Address, AddressWithTags, ComplexAddress, ComplexAddressWithOwners) - - def _test_udt_with_namedtuples(self, schema, graphson): - AddressTuple = namedtuple('Address', ('address', 'city', 'state')) - AddressWithTagsTuple = namedtuple('AddressWithTags', ('address', 'city', 'state', 'tags')) - ComplexAddressTuple = namedtuple('ComplexAddress', ('address', 'address_tags', 'city', 'state', 'props')) - ComplexAddressWithOwnersTuple = namedtuple('ComplexAddressWithOwners', ('address', 'address_tags', 'city', - 'state', 'props', 'owners')) - - self.__test_udt(schema, graphson, AddressTuple, AddressWithTagsTuple, - ComplexAddressTuple, ComplexAddressWithOwnersTuple) - - -@requiredse -@GraphTestConfiguration.generate_tests(schema=ClassicGraphSchema) -class ClassicGraphDataTypeTest(GenericGraphDataTypeTest): - pass - - -@requiredse -@GraphTestConfiguration.generate_tests(schema=CoreGraphSchema) -class CoreGraphDataTypeTest(GenericGraphDataTypeTest): - pass diff --git a/tests/integration/advanced/graph/test_graph_query.py b/tests/integration/advanced/graph/test_graph_query.py deleted file mode 100644 index 0c889938d8..0000000000 --- a/tests/integration/advanced/graph/test_graph_query.py +++ /dev/null @@ -1,594 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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 sys -from packaging.version import Version - -from copy import copy -from itertools import chain -import json -import time - -import unittest - -from cassandra import OperationTimedOut, ConsistencyLevel, InvalidRequest -from cassandra.cluster import EXEC_PROFILE_GRAPH_DEFAULT, NoHostAvailable -from cassandra.protocol import ServerError, SyntaxException -from cassandra.query import QueryTrace -from cassandra.util import Point -from cassandra.graph import (SimpleGraphStatement, single_object_row_factory, - Result, GraphOptions, GraphProtocol, to_bigint) -from cassandra.datastax.graph.query import _graph_options -from cassandra.datastax.graph.types import T - -from tests.integration import DSE_VERSION, requiredse, greaterthanorequaldse68 -from tests.integration.advanced.graph import BasicGraphUnitTestCase, GraphTestConfiguration, \ - validate_classic_vertex, GraphUnitTestCase, validate_classic_edge, validate_path_result_type, \ - validate_line_edge, validate_generic_vertex_result_type, \ - ClassicGraphSchema, CoreGraphSchema, VertexLabel - - -@requiredse -class BasicGraphQueryTest(BasicGraphUnitTestCase): - - def test_consistency_passing(self): - """ - Test to validated that graph consistency levels are properly surfaced to the base driver - - @since 1.0.0 - @jira_ticket PYTHON-509 - @expected_result graph consistency levels are surfaced correctly - @test_category dse graph - """ - cl_attrs = ('graph_read_consistency_level', 'graph_write_consistency_level') - - # Iterates over the graph options and constructs an array containing - # The graph_options that correlate to graoh read and write consistency levels - graph_params = [a[2] for a in _graph_options if a[0] in cl_attrs] - - s = self.session - default_profile = s.cluster.profile_manager.profiles[EXEC_PROFILE_GRAPH_DEFAULT] - default_graph_opts = default_profile.graph_options - try: - # Checks the default graph attributes and ensures that both graph_read_consistency_level and graph_write_consistency_level - # Are None by default - for attr in cl_attrs: - self.assertIsNone(getattr(default_graph_opts, attr)) - - res = s.execute_graph("null") - for param in graph_params: - self.assertNotIn(param, res.response_future.message.custom_payload) - - # session defaults are passed - opts = GraphOptions() - opts.update(default_graph_opts) - cl = {0: ConsistencyLevel.ONE, 1: ConsistencyLevel.LOCAL_QUORUM} - for k, v in cl.items(): - setattr(opts, cl_attrs[k], v) - default_profile.graph_options = opts - - res = s.execute_graph("null") - - for k, v in cl.items(): - self.assertEqual(res.response_future.message.custom_payload[graph_params[k]], ConsistencyLevel.value_to_name[v].encode()) - - # passed profile values override session defaults - cl = {0: ConsistencyLevel.ALL, 1: ConsistencyLevel.QUORUM} - opts = GraphOptions() - opts.update(default_graph_opts) - for k, v in cl.items(): - attr_name = cl_attrs[k] - setattr(opts, attr_name, v) - self.assertNotEqual(getattr(default_profile.graph_options, attr_name), getattr(opts, attr_name)) - tmp_profile = s.execution_profile_clone_update(EXEC_PROFILE_GRAPH_DEFAULT, graph_options=opts) - res = s.execute_graph("null", execution_profile=tmp_profile) - - for k, v in cl.items(): - self.assertEqual(res.response_future.message.custom_payload[graph_params[k]], ConsistencyLevel.value_to_name[v].encode()) - finally: - default_profile.graph_options = default_graph_opts - - def test_execute_graph_row_factory(self): - s = self.session - - # default Results - default_profile = s.cluster.profile_manager.profiles[EXEC_PROFILE_GRAPH_DEFAULT] - self.assertEqual(default_profile.row_factory, None) # will be resolved to graph_object_row_factory - result = s.execute_graph("123")[0] - self.assertIsInstance(result, Result) - self.assertEqual(result.value, 123) - - # other via parameter - prof = s.execution_profile_clone_update(EXEC_PROFILE_GRAPH_DEFAULT, row_factory=single_object_row_factory) - rs = s.execute_graph("123", execution_profile=prof) - self.assertEqual(rs.response_future.row_factory, single_object_row_factory) - self.assertEqual(json.loads(rs[0]), {'result': 123}) - - def test_execute_graph_timeout(self): - s = self.session - - value = [1, 2, 3] - query = "[%r]" % (value,) - - # default is passed down - default_graph_profile = s.cluster.profile_manager.profiles[EXEC_PROFILE_GRAPH_DEFAULT] - rs = self.session.execute_graph(query) - self.assertEqual(rs[0].value, value) - self.assertEqual(rs.response_future.timeout, default_graph_profile.request_timeout) - - # tiny timeout times out as expected - tmp_profile = copy(default_graph_profile) - tmp_profile.request_timeout = sys.float_info.min - - max_retry_count = 10 - for _ in range(max_retry_count): - start = time.time() - try: - with self.assertRaises(OperationTimedOut): - s.execute_graph(query, execution_profile=tmp_profile) - break - except: - end = time.time() - self.assertAlmostEqual(start, end, 1) - else: - raise Exception("session.execute_graph didn't time out in {0} tries".format(max_retry_count)) - - def test_profile_graph_options(self): - s = self.session - statement = SimpleGraphStatement("true") - ep = self.session.execution_profile_clone_update(EXEC_PROFILE_GRAPH_DEFAULT) - self.assertTrue(s.execute_graph(statement, execution_profile=ep)[0].value) - - # bad graph name to verify it's passed - ep.graph_options = ep.graph_options.copy() - ep.graph_options.graph_name = "definitely_not_correct" - try: - s.execute_graph(statement, execution_profile=ep) - except NoHostAvailable: - self.assertTrue(DSE_VERSION >= Version("6.0")) - except InvalidRequest: - self.assertTrue(DSE_VERSION >= Version("5.0")) - else: - if DSE_VERSION < Version("6.8"): # >6.8 returns true - self.fail("Should have risen ServerError or InvalidRequest") - - def test_additional_custom_payload(self): - s = self.session - custom_payload = {'some': 'example'.encode('utf-8'), 'items': 'here'.encode('utf-8')} - sgs = SimpleGraphStatement("null", custom_payload=custom_payload) - future = s.execute_graph_async(sgs) - - default_profile = s.cluster.profile_manager.profiles[EXEC_PROFILE_GRAPH_DEFAULT] - default_graph_opts = default_profile.graph_options - for k, v in chain(custom_payload.items(), default_graph_opts.get_options_map().items()): - self.assertEqual(future.message.custom_payload[k], v) - - -class GenericGraphQueryTest(GraphUnitTestCase): - - def _test_basic_query(self, schema, graphson): - """ - Test to validate that basic graph query results can be executed with a sane result set. - - Creates a simple classic tinkerpot graph, and attempts to find all vertices - related the vertex marco, that have a label of knows. - See reference graph here - http://www.tinkerpop.com/docs/3.0.0.M1/ - - @since 1.0.0 - @jira_ticket PYTHON-457 - @expected_result graph should find two vertices related to marco via 'knows' edges. - - @test_category dse graph - """ - self.execute_graph(schema.fixtures.classic(), graphson) - rs = self.execute_graph('''g.V().has('name','marko').out('knows').values('name')''', graphson) - self.assertFalse(rs.has_more_pages) - results_list = self.resultset_to_list(rs) - self.assertEqual(len(results_list), 2) - self.assertIn('vadas', results_list) - self.assertIn('josh', results_list) - - def _test_geometric_graph_types(self, schema, graphson): - """ - Test to validate that geometric types function correctly - - Creates a very simple graph, and tries to insert a simple point type - - @since 1.0.0 - @jira_ticket DSP-8087 - @expected_result json types associated with insert is parsed correctly - - @test_category dse graph - """ - vertex_label = VertexLabel([('pointP', "Point()")]) - ep = self.get_execution_profile(graphson) - schema.create_vertex_label(self.session, vertex_label, ep) - # import org.apache.cassandra.db.marshal.geometry.Point; - rs = schema.add_vertex(self.session, vertex_label, 'pointP', Point(0, 1), ep) - - # if result set is not parsed correctly this will throw an exception - self.assertIsNotNone(rs) - - def _test_execute_graph_trace(self, schema, graphson): - value = [1, 2, 3] - query = "[%r]" % (value,) - - # default is no trace - rs = self.execute_graph(query, graphson) - results = self.resultset_to_list(rs) - self.assertEqual(results[0], value) - self.assertIsNone(rs.get_query_trace()) - - # request trace - rs = self.execute_graph(query, graphson, trace=True) - results = self.resultset_to_list(rs) - self.assertEqual(results[0], value) - qt = rs.get_query_trace(max_wait_sec=10) - self.assertIsInstance(qt, QueryTrace) - self.assertIsNotNone(qt.duration) - - def _test_range_query(self, schema, graphson): - """ - Test to validate range queries are handled correctly. - - Creates a very large line graph script and executes it. Then proceeds to to a range - limited query against it, and ensure that the results are formatted correctly and that - the result set is properly sized. - - @since 1.0.0 - @jira_ticket PYTHON-457 - @expected_result result set should be properly formatted and properly sized - - @test_category dse graph - """ - self.execute_graph(schema.fixtures.line(150), graphson) - rs = self.execute_graph("g.E().range(0,10)", graphson) - self.assertFalse(rs.has_more_pages) - results = self.resultset_to_list(rs) - self.assertEqual(len(results), 10) - ep = self.get_execution_profile(graphson) - for result in results: - schema.ensure_properties(self.session, result, execution_profile=ep) - validate_line_edge(self, result) - - def _test_classic_graph(self, schema, graphson): - """ - Test to validate that basic graph generation, and vertex and edges are surfaced correctly - - Creates a simple classic tinkerpot graph, and iterates over the the vertices and edges - ensureing that each one is correct. See reference graph here - http://www.tinkerpop.com/docs/3.0.0.M1/ - - @since 1.0.0 - @jira_ticket PYTHON-457 - @expected_result graph should generate and all vertices and edge results should be - - @test_category dse graph - """ - self.execute_graph(schema.fixtures.classic(), graphson) - rs = self.execute_graph('g.V()', graphson) - ep = self.get_execution_profile(graphson) - for vertex in rs: - schema.ensure_properties(self.session, vertex, execution_profile=ep) - validate_classic_vertex(self, vertex) - rs = self.execute_graph('g.E()', graphson) - for edge in rs: - schema.ensure_properties(self.session, edge, execution_profile=ep) - validate_classic_edge(self, edge) - - def _test_graph_classic_path(self, schema, graphson): - """ - Test to validate that the path version of the result type is generated correctly. It also - tests basic path results as that is not covered elsewhere - - @since 1.0.0 - @jira_ticket PYTHON-479 - @expected_result path object should be unpacked correctly including all nested edges and verticies - @test_category dse graph - """ - self.execute_graph(schema.fixtures.classic(), graphson) - rs = self.execute_graph("g.V().hasLabel('person').has('name', 'marko').as('a').outE('knows').inV().as('c', 'd')." - " outE('created').as('e', 'f', 'g').inV().path()", - graphson) - rs_list = list(rs) - self.assertEqual(len(rs_list), 2) - for result in rs_list: - try: - path = result.as_path() - except: - path = result - - ep = self.get_execution_profile(graphson) - for obj in path.objects: - schema.ensure_properties(self.session, obj, ep) - - validate_path_result_type(self, path) - - def _test_large_create_script(self, schema, graphson): - """ - Test to validate that server errors due to large groovy scripts are properly surfaced - - Creates a very large line graph script and executes it. Then proceeds to create a line graph script - that is to large for the server to handle expects a server error to be returned - - @since 1.0.0 - @jira_ticket PYTHON-457 - @expected_result graph should generate and all vertices and edge results should be - - @test_category dse graph - """ - self.execute_graph(schema.fixtures.line(150), graphson) - self.execute_graph(schema.fixtures.line(300), graphson) # This should passed since the queries are splitted - self.assertRaises(SyntaxException, self.execute_graph, schema.fixtures.line(300, single_script=True), graphson) # this is not and too big - - def _test_large_result_set(self, schema, graphson): - """ - Test to validate that large result sets return correctly. - - Creates a very large graph. Ensures that large result sets are handled appropriately. - - @since 1.0.0 - @jira_ticket PYTHON-457 - @expected_result when limits of result sets are hit errors should be surfaced appropriately - - @test_category dse graph - """ - self.execute_graph(schema.fixtures.large(), graphson, execution_profile_options={'request_timeout': 32}) - rs = self.execute_graph("g.V()", graphson) - for result in rs: - validate_generic_vertex_result_type(self, result) - - def _test_param_passing(self, schema, graphson): - """ - Test to validate that parameter passing works as expected - - @since 1.0.0 - @jira_ticket PYTHON-457 - @expected_result parameters work as expected - - @test_category dse graph - """ - - # unused parameters are passed, but ignored - self.execute_graph("null", graphson, params={"doesn't": "matter", "what's": "passed"}) - - # multiple params - rs = self.execute_graph("[a, b]", graphson, params={'a': 0, 'b': 1}) - results = self.resultset_to_list(rs) - self.assertEqual(results[0], 0) - self.assertEqual(results[1], 1) - - if graphson == GraphProtocol.GRAPHSON_1_0: - # different value types - for param in (None, "string", 1234, 5.678, True, False): - result = self.resultset_to_list(self.execute_graph('x', graphson, params={'x': param}))[0] - self.assertEqual(result, param) - - def _test_vertex_property_properties(self, schema, graphson): - """ - Test verifying vertex property properties - - @since 1.0.0 - @jira_ticket PYTHON-487 - - @test_category dse graph - """ - if schema is not ClassicGraphSchema: - raise unittest.SkipTest('skipped because rich properties are only supported with classic graphs') - - self.execute_graph("schema.propertyKey('k0').Text().ifNotExists().create();", graphson) - self.execute_graph("schema.propertyKey('k1').Text().ifNotExists().create();", graphson) - self.execute_graph("schema.propertyKey('key').Text().properties('k0', 'k1').ifNotExists().create();", graphson) - self.execute_graph("schema.vertexLabel('MLP').properties('key').ifNotExists().create();", graphson) - v = self.execute_graph('''v = graph.addVertex('MLP') - v.property('key', 'value', 'k0', 'v0', 'k1', 'v1') - v''', graphson)[0] - self.assertEqual(len(v.properties), 1) - self.assertEqual(len(v.properties['key']), 1) - p = v.properties['key'][0] - self.assertEqual(p.label, 'key') - self.assertEqual(p.value, 'value') - self.assertEqual(p.properties, {'k0': 'v0', 'k1': 'v1'}) - - def _test_vertex_multiple_properties(self, schema, graphson): - """ - Test verifying vertex property form for various Cardinality - - All key types are encoded as a list, regardless of cardinality - - Single cardinality properties have only one value -- the last one added - - Default is single (this is config dependent) - - @since 1.0.0 - @jira_ticket PYTHON-487 - - @test_category dse graph - """ - if schema is not ClassicGraphSchema: - raise unittest.SkipTest('skipped because multiple properties are only supported with classic graphs') - - self.execute_graph('''Schema schema = graph.schema(); - schema.propertyKey('mult_key').Text().multiple().ifNotExists().create(); - schema.propertyKey('single_key').Text().single().ifNotExists().create(); - schema.vertexLabel('MPW1').properties('mult_key').ifNotExists().create(); - schema.vertexLabel('SW1').properties('single_key').ifNotExists().create();''', graphson) - - v = self.execute_graph('''v = graph.addVertex('MPW1') - v.property('mult_key', 'value') - v''', graphson)[0] - self.assertEqual(len(v.properties), 1) - self.assertEqual(len(v.properties['mult_key']), 1) - self.assertEqual(v.properties['mult_key'][0].label, 'mult_key') - self.assertEqual(v.properties['mult_key'][0].value, 'value') - - # multiple_with_two_values - v = self.execute_graph('''g.addV('MPW1').property('mult_key', 'value0').property('mult_key', 'value1')''', graphson)[0] - self.assertEqual(len(v.properties), 1) - self.assertEqual(len(v.properties['mult_key']), 2) - self.assertEqual(v.properties['mult_key'][0].label, 'mult_key') - self.assertEqual(v.properties['mult_key'][1].label, 'mult_key') - self.assertEqual(v.properties['mult_key'][0].value, 'value0') - self.assertEqual(v.properties['mult_key'][1].value, 'value1') - - # single_with_one_value - v = self.execute_graph('''v = graph.addVertex('SW1') - v.property('single_key', 'value') - v''', graphson)[0] - self.assertEqual(len(v.properties), 1) - self.assertEqual(len(v.properties['single_key']), 1) - self.assertEqual(v.properties['single_key'][0].label, 'single_key') - self.assertEqual(v.properties['single_key'][0].value, 'value') - - if DSE_VERSION < Version('6.8'): - # single_with_two_values - with self.assertRaises(InvalidRequest): - v = self.execute_graph(''' - v = graph.addVertex('SW1') - v.property('single_key', 'value0').property('single_key', 'value1').next() - v - ''', graphson)[0] - else: - # >=6.8 single_with_two_values, first one wins - v = self.execute_graph('''v = graph.addVertex('SW1') - v.property('single_key', 'value0').property('single_key', 'value1') - v''', graphson)[0] - self.assertEqual(v.properties['single_key'][0].value, 'value0') - - def _test_result_forms(self, schema, graphson): - """ - Test to validate that geometric types function correctly - - Creates a very simple graph, and tries to insert a simple point type - - @since 1.0.0 - @jira_ticket DSP-8087 - @expected_result json types associated with insert is parsed correctly - - @test_category dse graph - """ - self.execute_graph(schema.fixtures.classic(), graphson) - ep = self.get_execution_profile(graphson) - - results = self.resultset_to_list(self.session.execute_graph('g.V()', execution_profile=ep)) - self.assertGreater(len(results), 0, "Result set was empty this was not expected") - for v in results: - schema.ensure_properties(self.session, v, ep) - validate_classic_vertex(self, v) - - results = self.resultset_to_list(self.session.execute_graph('g.E()', execution_profile=ep)) - self.assertGreater(len(results), 0, "Result set was empty this was not expected") - for e in results: - schema.ensure_properties(self.session, e, ep) - validate_classic_edge(self, e) - - def _test_query_profile(self, schema, graphson): - """ - Test to validate profiling results are deserialized properly. - - @since 1.6.0 - @jira_ticket PYTHON-1057 - @expected_result TraversalMetrics and Metrics are deserialized properly - - @test_category dse graph - """ - if graphson == GraphProtocol.GRAPHSON_1_0: - raise unittest.SkipTest('skipped because there is no metrics deserializer with graphson1') - - ep = self.get_execution_profile(graphson) - results = list(self.session.execute_graph("g.V().profile()", execution_profile=ep)) - self.assertEqual(len(results), 1) - self.assertIn('metrics', results[0]) - self.assertIn('dur', results[0]) - self.assertEqual(len(results[0]['metrics']), 2) - self.assertIn('dur', results[0]['metrics'][0]) - - def _test_query_bulkset(self, schema, graphson): - """ - Test to validate bulkset results are deserialized properly. - - @since 1.6.0 - @jira_ticket PYTHON-1060 - @expected_result BulkSet is deserialized properly to a list - - @test_category dse graph - """ - self.execute_graph(schema.fixtures.classic(), graphson) - ep = self.get_execution_profile(graphson) - results = list(self.session.execute_graph( - 'g.V().hasLabel("person").aggregate("x").by("age").cap("x")', - execution_profile=ep)) - self.assertEqual(len(results), 1) - results = results[0] - if type(results) is Result: - results = results.value - else: - self.assertEqual(len(results), 5) - self.assertEqual(results.count(35), 2) - - @greaterthanorequaldse68 - def _test_elementMap_query(self, schema, graphson): - """ - Test to validate that an elementMap can be serialized properly. - """ - self.execute_graph(schema.fixtures.classic(), graphson) - rs = self.execute_graph('''g.V().has('name','marko').elementMap()''', graphson) - results_list = self.resultset_to_list(rs) - self.assertEqual(len(results_list), 1) - row = results_list[0] - if graphson == GraphProtocol.GRAPHSON_3_0: - self.assertIn(T.id, row) - self.assertIn(T.label, row) - if schema is CoreGraphSchema: - self.assertEqual(row[T.id], 'dseg:/person/marko') - self.assertEqual(row[T.label], 'person') - else: - self.assertIn('id', row) - self.assertIn('label', row) - - -@GraphTestConfiguration.generate_tests(schema=ClassicGraphSchema) -class ClassicGraphQueryTest(GenericGraphQueryTest): - pass - - -@GraphTestConfiguration.generate_tests(schema=CoreGraphSchema) -class CoreGraphQueryTest(GenericGraphQueryTest): - pass - - -@GraphTestConfiguration.generate_tests(schema=CoreGraphSchema) -class CoreGraphQueryWithTypeWrapperTest(GraphUnitTestCase): - - def _test_basic_query_with_type_wrapper(self, schema, graphson): - """ - Test to validate that a query using a type wrapper works. - - @since 2.8.0 - @jira_ticket PYTHON-1051 - @expected_result graph query works and doesn't raise an exception - - @test_category dse graph - """ - ep = self.get_execution_profile(graphson) - vl = VertexLabel(['tupleOf(Int, Bigint)']) - schema.create_vertex_label(self.session, vl, execution_profile=ep) - - prop_name = next(iter(vl.non_pk_properties.keys())) - with self.assertRaises(InvalidRequest): - schema.add_vertex(self.session, vl, prop_name, (1, 42), execution_profile=ep) - - schema.add_vertex(self.session, vl, prop_name, (1, to_bigint(42)), execution_profile=ep) diff --git a/tests/integration/advanced/test_adv_metadata.py b/tests/integration/advanced/test_adv_metadata.py deleted file mode 100644 index 66f682fd49..0000000000 --- a/tests/integration/advanced/test_adv_metadata.py +++ /dev/null @@ -1,392 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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. - -from packaging.version import Version - -from tests.integration import (BasicExistingKeyspaceUnitTestCase, BasicSharedKeyspaceUnitTestCase, - BasicSharedKeyspaceUnitTestCaseRF1, - greaterthanorequaldse51, greaterthanorequaldse60, - greaterthanorequaldse68, use_single_node, - DSE_VERSION, requiredse, TestCluster) - -import unittest - -import logging -import time - - -log = logging.getLogger(__name__) - - -def setup_module(): - if DSE_VERSION: - use_single_node() - - -@requiredse -@greaterthanorequaldse60 -class FunctionAndAggregateMetadataTests(BasicSharedKeyspaceUnitTestCaseRF1): - - @classmethod - def setUpClass(cls): - if DSE_VERSION: - super(FunctionAndAggregateMetadataTests, cls).setUpClass() - - @classmethod - def tearDownClass(cls): - if DSE_VERSION: - super(FunctionAndAggregateMetadataTests, cls).tearDownClass() - - def setUp(self): - self.func_name = self.function_table_name + '_func' - self.agg_name = self.function_table_name + '_agg(int)' - - def _populated_ks_meta_attr(self, attr_name): - val, start_time = None, time.time() - while not val: - self.cluster.refresh_schema_metadata() - val = getattr(self.cluster.metadata.keyspaces[self.keyspace_name], - attr_name) - self.assertLess(time.time(), start_time + 30, - 'did not see func in metadata in 30s') - log.debug('done blocking; dict is populated: {}'.format(val)) - return val - - def test_monotonic_on_and_deterministic_function(self): - self.session.execute(""" - CREATE FUNCTION {ksn}.{ftn}(key int, val int) - RETURNS NULL ON NULL INPUT - RETURNS int - DETERMINISTIC - MONOTONIC ON val - LANGUAGE java AS 'return key+val;'; - """.format(ksn=self.keyspace_name, - ftn=self.func_name)) - fn = self._populated_ks_meta_attr('functions')[ - '{}(int,int)'.format(self.func_name) - ] - self.assertEqual(fn.monotonic_on, ['val']) - # monotonic is not set by MONOTONIC ON - self.assertFalse(fn.monotonic) - self.assertTrue(fn.deterministic) - self.assertEqual('CREATE FUNCTION {ksn}.{ftn}(key int, val int) ' - 'RETURNS NULL ON NULL INPUT ' - 'RETURNS int DETERMINISTIC MONOTONIC ON val ' - 'LANGUAGE java AS $$return key+val;$$' - ''.format(ksn=self.keyspace_name, - ftn=self.func_name), - fn.as_cql_query()) - self.session.execute('DROP FUNCTION {}.{}'.format(self.keyspace_name, - self.func_name)) - self.session.execute(fn.as_cql_query()) - - def test_monotonic_all_and_nondeterministic_function(self): - self.session.execute(""" - CREATE FUNCTION {ksn}.{ftn}(key int, val int) - RETURNS NULL ON NULL INPUT - RETURNS int - MONOTONIC - LANGUAGE java AS 'return key+val;'; - """.format(ksn=self.keyspace_name, - ftn=self.func_name)) - fn = self._populated_ks_meta_attr('functions')[ - '{}(int,int)'.format(self.func_name) - ] - self.assertEqual(set(fn.monotonic_on), {'key', 'val'}) - self.assertTrue(fn.monotonic) - self.assertFalse(fn.deterministic) - self.assertEqual('CREATE FUNCTION {ksn}.{ftn}(key int, val int) ' - 'RETURNS NULL ON NULL INPUT RETURNS int MONOTONIC ' - 'LANGUAGE java AS $$return key+val;$$' - ''.format(ksn=self.keyspace_name, - ftn=self.func_name), - fn.as_cql_query()) - self.session.execute('DROP FUNCTION {}.{}'.format(self.keyspace_name, - self.func_name)) - self.session.execute(fn.as_cql_query()) - - def _create_func_for_aggregate(self): - self.session.execute(""" - CREATE FUNCTION {ksn}.{ftn}(key int, val int) - RETURNS NULL ON NULL INPUT - RETURNS int - DETERMINISTIC - LANGUAGE java AS 'return key+val;'; - """.format(ksn=self.keyspace_name, - ftn=self.func_name)) - - def test_deterministic_aggregate(self): - self._create_func_for_aggregate() - self.session.execute(""" - CREATE AGGREGATE {ksn}.{an} - SFUNC {ftn} - STYPE int - INITCOND 0 - DETERMINISTIC - """.format(ksn=self.keyspace_name, - ftn=self.func_name, - an=self.agg_name)) - ag = self._populated_ks_meta_attr('aggregates')[self.agg_name] - self.assertTrue(ag.deterministic) - self.assertEqual( - 'CREATE AGGREGATE {ksn}.{an} SFUNC ' - '{ftn} STYPE int INITCOND 0 DETERMINISTIC' - ''.format(ksn=self.keyspace_name, - ftn=self.func_name, - an=self.agg_name), - ag.as_cql_query()) - self.session.execute('DROP AGGREGATE {}.{}'.format(self.keyspace_name, - self.agg_name)) - self.session.execute(ag.as_cql_query()) - - def test_nondeterministic_aggregate(self): - self._create_func_for_aggregate() - self.session.execute(""" - CREATE AGGREGATE {ksn}.{an} - SFUNC {ftn} - STYPE int - INITCOND 0 - """.format(ksn=self.keyspace_name, - ftn=self.func_name, - an=self.agg_name)) - ag = self._populated_ks_meta_attr('aggregates')[self.agg_name] - self.assertFalse(ag.deterministic) - self.assertEqual( - 'CREATE AGGREGATE {ksn}.{an} SFUNC ' - '{ftn} STYPE int INITCOND 0' - ''.format(ksn=self.keyspace_name, - ftn=self.func_name, - an=self.agg_name), - ag.as_cql_query()) - self.session.execute('DROP AGGREGATE {}.{}'.format(self.keyspace_name, - self.agg_name)) - self.session.execute(ag.as_cql_query()) - - -@requiredse -class RLACMetadataTests(BasicSharedKeyspaceUnitTestCase): - - @classmethod - def setUpClass(cls): - if DSE_VERSION: - super(RLACMetadataTests, cls).setUpClass() - - @classmethod - def tearDownClass(cls): - if DSE_VERSION: - super(RLACMetadataTests, cls).setUpClass() - - @greaterthanorequaldse51 - def test_rlac_on_table(self): - """ - Checks to ensure that the RLAC table extension appends the proper cql on export to tables - - @since 3.20 - @jira_ticket PYTHON-638 - @expected_result Invalid hosts on the contact list should be excluded - - @test_category metadata - """ - self.session.execute("CREATE TABLE {0}.reports (" - " report_user text, " - " report_number int, " - " report_month int, " - " report_year int, " - " report_text text," - " PRIMARY KEY (report_user, report_number))".format(self.keyspace_name)) - restrict_cql = "RESTRICT ROWS ON {0}.reports USING report_user".format(self.keyspace_name) - self.session.execute(restrict_cql) - table_meta = self.cluster.metadata.keyspaces[self.keyspace_name].tables['reports'] - self.assertTrue(restrict_cql in table_meta.export_as_string()) - - @unittest.skip("Dse 5.1 doesn't support MV and RLAC remove after update") - @greaterthanorequaldse51 - def test_rlac_on_mv(self): - """ - Checks to ensure that the RLAC table extension appends the proper cql to export on mV's - - @since 3.20 - @jira_ticket PYTHON-682 - @expected_result Invalid hosts on the contact list should be excluded - - @test_category metadata - """ - self.session.execute("CREATE TABLE {0}.reports2 (" - " report_user text, " - " report_number int, " - " report_month int, " - " report_year int, " - " report_text text," - " PRIMARY KEY (report_user, report_number))".format(self.keyspace_name)) - self.session.execute("CREATE MATERIALIZED VIEW {0}.reports_by_year AS " - " SELECT report_year, report_user, report_number, report_text FROM {0}.reports2 " - " WHERE report_user IS NOT NULL AND report_number IS NOT NULL AND report_year IS NOT NULL " - " PRIMARY KEY ((report_year, report_user), report_number)".format(self.keyspace_name)) - - restrict_cql_table = "RESTRICT ROWS ON {0}.reports2 USING report_user".format(self.keyspace_name) - self.session.execute(restrict_cql_table) - restrict_cql_view = "RESTRICT ROWS ON {0}.reports_by_year USING report_user".format(self.keyspace_name) - self.session.execute(restrict_cql_view) - table_cql = self.cluster.metadata.keyspaces[self.keyspace_name].tables['reports2'].export_as_string() - view_cql = self.cluster.metadata.keyspaces[self.keyspace_name].tables['reports2'].views["reports_by_year"].export_as_string() - self.assertTrue(restrict_cql_table in table_cql) - self.assertTrue(restrict_cql_view in table_cql) - self.assertTrue(restrict_cql_view in view_cql) - self.assertTrue(restrict_cql_table not in view_cql) - - -@requiredse -class NodeSyncMetadataTests(BasicSharedKeyspaceUnitTestCase): - - @classmethod - def setUpClass(cls): - if DSE_VERSION: - super(NodeSyncMetadataTests, cls).setUpClass() - - @classmethod - def tearDownClass(cls): - if DSE_VERSION: - super(NodeSyncMetadataTests, cls).setUpClass() - - @greaterthanorequaldse60 - def test_nodesync_on_table(self): - """ - Checks to ensure that nodesync is visible through driver metadata - - @since 3.20 - @jira_ticket PYTHON-799 - @expected_result nodesync should be enabled - - @test_category metadata - """ - self.session.execute("CREATE TABLE {0}.reports (" - " report_user text PRIMARY KEY" - ") WITH nodesync = {{" - "'enabled': 'true', 'deadline_target_sec' : 86400 }};".format( - self.keyspace_name - )) - table_meta = self.cluster.metadata.keyspaces[self.keyspace_name].tables['reports'] - self.assertIn('nodesync =', table_meta.export_as_string()) - self.assertIn('nodesync', table_meta.options) - - -@greaterthanorequaldse68 -class GraphMetadataTests(BasicExistingKeyspaceUnitTestCase): - """ - Various tests to ensure that graph metadata are visible through driver metadata - @since DSE6.8 - @jira_ticket PYTHON-996 - @expected_result graph metadata are fetched - @test_category metadata - """ - - @classmethod - def setUpClass(cls): - if DSE_VERSION and DSE_VERSION >= Version('6.8'): - super(GraphMetadataTests, cls).setUpClass() - cls.session.execute(""" - CREATE KEYSPACE ks_no_graph_engine WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}; - """) - cls.session.execute(""" - CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1} and graph_engine = 'Core'; - """ % (cls.ks_name,)) - - cls.session.execute(""" - CREATE TABLE %s.person (name text PRIMARY KEY) WITH VERTEX LABEL; - """ % (cls.ks_name,)) - - cls.session.execute(""" - CREATE TABLE %s.software(company text, name text, version int, PRIMARY KEY((company, name), version)) WITH VERTEX LABEL rocksolidsoftware; - """ % (cls.ks_name,)) - - cls.session.execute(""" - CREATE TABLE %s.contributors (contributor text, company_name text, software_name text, software_version int, - PRIMARY KEY (contributor, company_name, software_name, software_version) ) - WITH CLUSTERING ORDER BY (company_name ASC, software_name ASC, software_version ASC) - AND EDGE LABEL contrib FROM person(contributor) TO rocksolidsoftware((company_name, software_name), software_version); - """ % (cls.ks_name,)) - - @classmethod - def tearDownClass(cls): - if DSE_VERSION and DSE_VERSION >= Version('6.8'): - cls.session.execute('DROP KEYSPACE {0}'.format('ks_no_graph_engine')) - cls.session.execute('DROP KEYSPACE {0}'.format(cls.ks_name)) - cls.cluster.shutdown() - - def test_keyspace_metadata(self): - self.assertIsNone(self.cluster.metadata.keyspaces['ks_no_graph_engine'].graph_engine, None) - self.assertEqual(self.cluster.metadata.keyspaces[self.ks_name].graph_engine, 'Core') - - def test_keyspace_metadata_alter_graph_engine(self): - self.session.execute("ALTER KEYSPACE %s WITH graph_engine = 'Tinker'" % (self.ks_name,)) - self.assertEqual(self.cluster.metadata.keyspaces[self.ks_name].graph_engine, 'Tinker') - self.session.execute("ALTER KEYSPACE %s WITH graph_engine = 'Core'" % (self.ks_name,)) - self.assertEqual(self.cluster.metadata.keyspaces[self.ks_name].graph_engine, 'Core') - - def test_vertex_metadata(self): - vertex_meta = self.cluster.metadata.keyspaces[self.ks_name].tables['person'].vertex - self.assertEqual(vertex_meta.keyspace_name, self.ks_name) - self.assertEqual(vertex_meta.table_name, 'person') - self.assertEqual(vertex_meta.label_name, 'person') - - vertex_meta = self.cluster.metadata.keyspaces[self.ks_name].tables['software'].vertex - self.assertEqual(vertex_meta.keyspace_name, self.ks_name) - self.assertEqual(vertex_meta.table_name, 'software') - self.assertEqual(vertex_meta.label_name, 'rocksolidsoftware') - - def test_edge_metadata(self): - edge_meta = self.cluster.metadata.keyspaces[self.ks_name].tables['contributors'].edge - self.assertEqual(edge_meta.keyspace_name, self.ks_name) - self.assertEqual(edge_meta.table_name, 'contributors') - self.assertEqual(edge_meta.label_name, 'contrib') - self.assertEqual(edge_meta.from_table, 'person') - self.assertEqual(edge_meta.from_label, 'person') - self.assertEqual(edge_meta.from_partition_key_columns, ['contributor']) - self.assertEqual(edge_meta.from_clustering_columns, []) - self.assertEqual(edge_meta.to_table, 'software') - self.assertEqual(edge_meta.to_label, 'rocksolidsoftware') - self.assertEqual(edge_meta.to_partition_key_columns, ['company_name', 'software_name']) - self.assertEqual(edge_meta.to_clustering_columns, ['software_version']) - - -@greaterthanorequaldse68 -class GraphMetadataSchemaErrorTests(BasicExistingKeyspaceUnitTestCase): - """ - Test that we can connect when the graph schema is broken. - """ - - def test_connection_on_graph_schema_error(self): - self.session = self.cluster.connect() - - self.session.execute(""" - CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1} and graph_engine = 'Core'; - """ % (self.ks_name,)) - - self.session.execute(""" - CREATE TABLE %s.person (name text PRIMARY KEY) WITH VERTEX LABEL; - """ % (self.ks_name,)) - - self.session.execute(""" - CREATE TABLE %s.software(company text, name text, version int, PRIMARY KEY((company, name), version)) WITH VERTEX LABEL rocksolidsoftware; - """ % (self.ks_name,)) - - self.session.execute(""" - CREATE TABLE %s.contributors (contributor text, company_name text, software_name text, software_version int, - PRIMARY KEY (contributor, company_name, software_name, software_version) ) - WITH CLUSTERING ORDER BY (company_name ASC, software_name ASC, software_version ASC) - AND EDGE LABEL contrib FROM person(contributor) TO rocksolidsoftware((company_name, software_name), software_version); - """ % (self.ks_name,)) - - self.session.execute('TRUNCATE system_schema.vertices') - TestCluster().connect().shutdown() diff --git a/tests/integration/advanced/test_auth.py b/tests/integration/advanced/test_auth.py deleted file mode 100644 index 438d4e8018..0000000000 --- a/tests/integration/advanced/test_auth.py +++ /dev/null @@ -1,532 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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 unittest -import logging -import os -import subprocess -import time - -from ccmlib.dse_cluster import DseCluster -from nose.plugins.attrib import attr -from packaging.version import Version - -from cassandra.auth import (DSEGSSAPIAuthProvider, DSEPlainTextAuthProvider, - SaslAuthProvider, TransitionalModePlainTextAuthProvider) -from cassandra.cluster import EXEC_PROFILE_GRAPH_DEFAULT, NoHostAvailable -from cassandra.protocol import Unauthorized -from cassandra.query import SimpleStatement -from tests.integration import (get_cluster, greaterthanorequaldse51, - remove_cluster, requiredse, DSE_VERSION, TestCluster) -from tests.integration.advanced import ADS_HOME, use_single_node_with_graph -from tests.integration.advanced.graph import reset_graph, ClassicGraphFixtures - - -log = logging.getLogger(__name__) - - -def setup_module(): - if DSE_VERSION: - use_single_node_with_graph() - - -def teardown_module(): - if DSE_VERSION: - remove_cluster() # this test messes with config - - -def wait_role_manager_setup_then_execute(session, statements): - for s in statements: - exc = None - for attempt in range(3): - try: - session.execute(s) - break - except Exception as e: - exc = e - time.sleep(5) - else: # if we didn't reach `break` - if exc is not None: - raise exc - - -@attr('long') -@requiredse -class BasicDseAuthTest(unittest.TestCase): - - @classmethod - def setUpClass(self): - """ - This will setup the necessary infrastructure to run our authentication tests. It requres the ADS_HOME environment variable - and our custom embedded apache directory server jar in order to run. - """ - if not DSE_VERSION: - return - - clear_kerberos_tickets() - self.cluster = None - - # Setup variables for various keytab and other files - self.conf_file_dir = os.path.join(ADS_HOME, "conf/") - self.krb_conf = os.path.join(self.conf_file_dir, "krb5.conf") - self.dse_keytab = os.path.join(self.conf_file_dir, "dse.keytab") - self.dseuser_keytab = os.path.join(self.conf_file_dir, "dseuser.keytab") - self.cassandra_keytab = os.path.join(self.conf_file_dir, "cassandra.keytab") - self.bob_keytab = os.path.join(self.conf_file_dir, "bob.keytab") - self.charlie_keytab = os.path.join(self.conf_file_dir, "charlie.keytab") - actual_jar = os.path.join(ADS_HOME, "embedded-ads.jar") - - # Create configuration directories if they don't already exists - if not os.path.exists(self.conf_file_dir): - os.makedirs(self.conf_file_dir) - if not os.path.exists(actual_jar): - raise RuntimeError('could not find {}'.format(actual_jar)) - log.warning("Starting adserver") - # Start the ADS, this will create the keytab con configuration files listed above - self.proc = subprocess.Popen(['java', '-jar', actual_jar, '-k', '--confdir', self.conf_file_dir], shell=False) - time.sleep(10) - # TODO poll for server to come up - - log.warning("Starting adserver started") - ccm_cluster = get_cluster() - log.warning("fetching tickets") - # Stop cluster if running and configure it with the correct options - ccm_cluster.stop() - if isinstance(ccm_cluster, DseCluster): - # Setup kerberos options in cassandra.yaml - config_options = {'kerberos_options': {'keytab': self.dse_keytab, - 'service_principal': 'dse/_HOST@DATASTAX.COM', - 'qop': 'auth'}, - 'authentication_options': {'enabled': 'true', - 'default_scheme': 'kerberos', - 'scheme_permissions': 'true', - 'allow_digest_with_kerberos': 'true', - 'plain_text_without_ssl': 'warn', - 'transitional_mode': 'disabled'}, - 'authorization_options': {'enabled': 'true'}} - - krb5java = "-Djava.security.krb5.conf=" + self.krb_conf - # Setup dse authenticator in cassandra.yaml - ccm_cluster.set_configuration_options({ - 'authenticator': 'com.datastax.bdp.cassandra.auth.DseAuthenticator', - 'authorizer': 'com.datastax.bdp.cassandra.auth.DseAuthorizer' - }) - ccm_cluster.set_dse_configuration_options(config_options) - ccm_cluster.start(wait_for_binary_proto=True, wait_other_notice=True, jvm_args=[krb5java]) - else: - log.error("Cluster is not dse cluster test will fail") - - @classmethod - def tearDownClass(self): - """ - Terminates running ADS (Apache directory server). - """ - if not DSE_VERSION: - return - - self.proc.terminate() - - def tearDown(self): - """ - This will clear any existing kerberos tickets by using kdestroy - """ - clear_kerberos_tickets() - if self.cluster: - self.cluster.shutdown() - - def refresh_kerberos_tickets(self, keytab_file, user_name, krb_conf): - """ - Fetches a new ticket for using the keytab file and username provided. - """ - self.ads_pid = subprocess.call(['kinit', '-t', keytab_file, user_name], env={'KRB5_CONFIG': krb_conf}, shell=False) - - def connect_and_query(self, auth_provider, query=None): - """ - Runs a simple system query with the auth_provided specified. - """ - os.environ['KRB5_CONFIG'] = self.krb_conf - self.cluster = TestCluster(auth_provider=auth_provider) - self.session = self.cluster.connect() - query = query if query else "SELECT * FROM system.local WHERE key='local'" - statement = SimpleStatement(query) - rs = self.session.execute(statement) - return rs - - def test_should_not_authenticate_with_bad_user_ticket(self): - """ - This tests will attempt to authenticate with a user that has a valid ticket, but is not a valid dse user. - @since 3.20 - @jira_ticket PYTHON-457 - @test_category dse auth - @expected_result NoHostAvailable exception should be thrown - - """ - self.refresh_kerberos_tickets(self.dseuser_keytab, "dseuser@DATASTAX.COM", self.krb_conf) - auth_provider = DSEGSSAPIAuthProvider(service='dse', qops=["auth"]) - self.assertRaises(NoHostAvailable, self.connect_and_query, auth_provider) - - def test_should_not_athenticate_without_ticket(self): - """ - This tests will attempt to authenticate with a user that is valid but has no ticket - @since 3.20 - @jira_ticket PYTHON-457 - @test_category dse auth - @expected_result NoHostAvailable exception should be thrown - - """ - auth_provider = DSEGSSAPIAuthProvider(service='dse', qops=["auth"]) - self.assertRaises(NoHostAvailable, self.connect_and_query, auth_provider) - - def test_connect_with_kerberos(self): - """ - This tests will attempt to authenticate with a user that is valid and has a ticket - @since 3.20 - @jira_ticket PYTHON-457 - @test_category dse auth - @expected_result Client should be able to connect and run a basic query - - """ - self.refresh_kerberos_tickets(self.cassandra_keytab, "cassandra@DATASTAX.COM", self.krb_conf) - auth_provider = DSEGSSAPIAuthProvider() - rs = self.connect_and_query(auth_provider) - self.assertIsNotNone(rs) - connections = [c for holders in self.cluster.get_connection_holders() for c in holders.get_connections()] - # Check to make sure our server_authenticator class is being set appropriate - for connection in connections: - self.assertTrue('DseAuthenticator' in connection.authenticator.server_authenticator_class) - - def test_connect_with_kerberos_and_graph(self): - """ - This tests will attempt to authenticate with a user and execute a graph query - @since 3.20 - @jira_ticket PYTHON-457 - @test_category dse auth - @expected_result Client should be able to connect and run a basic graph query with authentication - - """ - self.refresh_kerberos_tickets(self.cassandra_keytab, "cassandra@DATASTAX.COM", self.krb_conf) - - auth_provider = DSEGSSAPIAuthProvider(service='dse', qops=["auth"]) - rs = self.connect_and_query(auth_provider) - self.assertIsNotNone(rs) - reset_graph(self.session, self._testMethodName.lower()) - profiles = self.cluster.profile_manager.profiles - profiles[EXEC_PROFILE_GRAPH_DEFAULT].graph_options.graph_name = self._testMethodName.lower() - self.session.execute_graph(ClassicGraphFixtures.classic()) - - rs = self.session.execute_graph('g.V()') - self.assertIsNotNone(rs) - - def test_connect_with_kerberos_host_not_resolved(self): - """ - This tests will attempt to authenticate with IP, this will fail on osx. - The success or failure of this test is dependent on a reverse dns lookup which can be impacted by your environment - if it fails don't panic. - @since 3.20 - @jira_ticket PYTHON-566 - @test_category dse auth - @expected_result Client should error when ip is used - - """ - self.refresh_kerberos_tickets(self.cassandra_keytab, "cassandra@DATASTAX.COM", self.krb_conf) - DSEGSSAPIAuthProvider(service='dse', qops=["auth"], resolve_host_name=False) - - def test_connect_with_explicit_principal(self): - """ - This tests will attempt to authenticate using valid and invalid user principals - @since 3.20 - @jira_ticket PYTHON-574 - @test_category dse auth - @expected_result Client principals should be used by the underlying mechanism - - """ - - # Connect with valid principal - self.refresh_kerberos_tickets(self.cassandra_keytab, "cassandra@DATASTAX.COM", self.krb_conf) - auth_provider = DSEGSSAPIAuthProvider(service='dse', qops=["auth"], principal="cassandra@DATASTAX.COM") - self.connect_and_query(auth_provider) - connections = [c for holders in self.cluster.get_connection_holders() for c in holders.get_connections()] - - # Check to make sure our server_authenticator class is being set appropriate - for connection in connections: - self.assertTrue('DseAuthenticator' in connection.authenticator.server_authenticator_class) - - # Use invalid principal - auth_provider = DSEGSSAPIAuthProvider(service='dse', qops=["auth"], principal="notauser@DATASTAX.COM") - self.assertRaises(NoHostAvailable, self.connect_and_query, auth_provider) - - @greaterthanorequaldse51 - def test_proxy_login_with_kerberos(self): - """ - Test that the proxy login works with kerberos. - """ - # Set up users for proxy login test - self._setup_for_proxy() - - query = "select * from testkrbproxy.testproxy" - - # Try normal login with Charlie - self.refresh_kerberos_tickets(self.charlie_keytab, "charlie@DATASTAX.COM", self.krb_conf) - auth_provider = DSEGSSAPIAuthProvider(service='dse', qops=["auth"], principal="charlie@DATASTAX.COM") - self.connect_and_query(auth_provider, query=query) - - # Try proxy login with bob - self.refresh_kerberos_tickets(self.bob_keytab, "bob@DATASTAX.COM", self.krb_conf) - auth_provider = DSEGSSAPIAuthProvider(service='dse', qops=["auth"], principal="bob@DATASTAX.COM", - authorization_id='charlie@DATASTAX.COM') - self.connect_and_query(auth_provider, query=query) - - # Try logging with bob without mentioning charlie - self.refresh_kerberos_tickets(self.bob_keytab, "bob@DATASTAX.COM", self.krb_conf) - auth_provider = DSEGSSAPIAuthProvider(service='dse', qops=["auth"], principal="bob@DATASTAX.COM") - self.assertRaises(Unauthorized, self.connect_and_query, auth_provider, query=query) - - self._remove_proxy_setup() - - @greaterthanorequaldse51 - def test_proxy_login_with_kerberos_forbidden(self): - """ - Test that the proxy login fail when proxy role is not granted - """ - # Set up users for proxy login test - self._setup_for_proxy(False) - query = "select * from testkrbproxy.testproxy" - - # Try normal login with Charlie - self.refresh_kerberos_tickets(self.bob_keytab, "bob@DATASTAX.COM", self.krb_conf) - auth_provider = DSEGSSAPIAuthProvider(service='dse', qops=["auth"], principal="bob@DATASTAX.COM", - authorization_id='charlie@DATASTAX.COM') - self.assertRaises(NoHostAvailable, self.connect_and_query, auth_provider, query=query) - - self.refresh_kerberos_tickets(self.bob_keytab, "bob@DATASTAX.COM", self.krb_conf) - auth_provider = DSEGSSAPIAuthProvider(service='dse', qops=["auth"], principal="bob@DATASTAX.COM") - self.assertRaises(Unauthorized, self.connect_and_query, auth_provider, query=query) - - self._remove_proxy_setup() - - def _remove_proxy_setup(self): - os.environ['KRB5_CONFIG'] = self.krb_conf - self.refresh_kerberos_tickets(self.cassandra_keytab, "cassandra@DATASTAX.COM", self.krb_conf) - auth_provider = DSEGSSAPIAuthProvider(service='dse', qops=["auth"], principal='cassandra@DATASTAX.COM') - cluster = TestCluster(auth_provider=auth_provider) - session = cluster.connect() - - session.execute("REVOKE PROXY.LOGIN ON ROLE '{0}' FROM '{1}'".format('charlie@DATASTAX.COM', 'bob@DATASTAX.COM')) - - session.execute("DROP ROLE IF EXISTS '{0}';".format('bob@DATASTAX.COM')) - session.execute("DROP ROLE IF EXISTS '{0}';".format('charlie@DATASTAX.COM')) - - # Create a keyspace and allow only charlie to query it. - - session.execute("DROP KEYSPACE testkrbproxy") - - cluster.shutdown() - - def _setup_for_proxy(self, grant=True): - os.environ['KRB5_CONFIG'] = self.krb_conf - self.refresh_kerberos_tickets(self.cassandra_keytab, "cassandra@DATASTAX.COM", self.krb_conf) - auth_provider = DSEGSSAPIAuthProvider(service='dse', qops=["auth"], principal='cassandra@DATASTAX.COM') - cluster = TestCluster(auth_provider=auth_provider) - session = cluster.connect() - - stmts = [ - "CREATE ROLE IF NOT EXISTS '{0}' WITH LOGIN = TRUE;".format('bob@DATASTAX.COM'), - "CREATE ROLE IF NOT EXISTS '{0}' WITH LOGIN = TRUE;".format('bob@DATASTAX.COM'), - "GRANT EXECUTE ON ALL AUTHENTICATION SCHEMES to 'bob@DATASTAX.COM'", - "CREATE ROLE IF NOT EXISTS '{0}' WITH LOGIN = TRUE;".format('charlie@DATASTAX.COM'), - "GRANT EXECUTE ON ALL AUTHENTICATION SCHEMES to 'charlie@DATASTAX.COM'", - # Create a keyspace and allow only charlie to query it. - "CREATE KEYSPACE testkrbproxy WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}", - "CREATE TABLE testkrbproxy.testproxy (id int PRIMARY KEY, value text)", - "GRANT ALL PERMISSIONS ON KEYSPACE testkrbproxy to '{0}'".format('charlie@DATASTAX.COM'), - ] - - if grant: - stmts.append("GRANT PROXY.LOGIN ON ROLE '{0}' to '{1}'".format('charlie@DATASTAX.COM', 'bob@DATASTAX.COM')) - - wait_role_manager_setup_then_execute(session, stmts) - - cluster.shutdown() - - -def clear_kerberos_tickets(): - subprocess.call(['kdestroy'], shell=False) - - -@attr('long') -@requiredse -class BaseDseProxyAuthTest(unittest.TestCase): - - @classmethod - def setUpClass(self): - """ - This will setup the necessary infrastructure to run unified authentication tests. - """ - if not DSE_VERSION or DSE_VERSION < Version('5.1'): - return - self.cluster = None - - ccm_cluster = get_cluster() - # Stop cluster if running and configure it with the correct options - ccm_cluster.stop() - if isinstance(ccm_cluster, DseCluster): - # Setup dse options in dse.yaml - config_options = {'authentication_options': {'enabled': 'true', - 'default_scheme': 'internal', - 'scheme_permissions': 'true', - 'transitional_mode': 'normal'}, - 'authorization_options': {'enabled': 'true'} - } - - # Setup dse authenticator in cassandra.yaml - ccm_cluster.set_configuration_options({ - 'authenticator': 'com.datastax.bdp.cassandra.auth.DseAuthenticator', - 'authorizer': 'com.datastax.bdp.cassandra.auth.DseAuthorizer' - }) - ccm_cluster.set_dse_configuration_options(config_options) - ccm_cluster.start(wait_for_binary_proto=True, wait_other_notice=True) - else: - log.error("Cluster is not dse cluster test will fail") - - # Create users and test keyspace - self.user_role = 'user1' - self.server_role = 'server' - self.root_cluster = TestCluster(auth_provider=DSEPlainTextAuthProvider('cassandra', 'cassandra')) - self.root_session = self.root_cluster.connect() - - stmts = [ - "CREATE USER {0} WITH PASSWORD '{1}'".format(self.server_role, self.server_role), - "CREATE USER {0} WITH PASSWORD '{1}'".format(self.user_role, self.user_role), - "CREATE KEYSPACE testproxy WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}", - "CREATE TABLE testproxy.testproxy (id int PRIMARY KEY, value text)", - "GRANT ALL PERMISSIONS ON KEYSPACE testproxy to {0}".format(self.user_role) - ] - - wait_role_manager_setup_then_execute(self.root_session, stmts) - - @classmethod - def tearDownClass(self): - """ - Shutdown the root session. - """ - if not DSE_VERSION or DSE_VERSION < Version('5.1'): - return - self.root_session.execute('DROP KEYSPACE testproxy;') - self.root_session.execute('DROP USER {0}'.format(self.user_role)) - self.root_session.execute('DROP USER {0}'.format(self.server_role)) - self.root_cluster.shutdown() - - def tearDown(self): - """ - Shutdown the cluster and reset proxy permissions - """ - self.cluster.shutdown() - - self.root_session.execute("REVOKE PROXY.LOGIN ON ROLE {0} from {1}".format(self.user_role, self.server_role)) - self.root_session.execute("REVOKE PROXY.EXECUTE ON ROLE {0} from {1}".format(self.user_role, self.server_role)) - - def grant_proxy_login(self): - """ - Grant PROXY.LOGIN permission on a role to a specific user. - """ - self.root_session.execute("GRANT PROXY.LOGIN on role {0} to {1}".format(self.user_role, self.server_role)) - - def grant_proxy_execute(self): - """ - Grant PROXY.EXECUTE permission on a role to a specific user. - """ - self.root_session.execute("GRANT PROXY.EXECUTE on role {0} to {1}".format(self.user_role, self.server_role)) - - -@attr('long') -@greaterthanorequaldse51 -class DseProxyAuthTest(BaseDseProxyAuthTest): - """ - Tests Unified Auth. Proxy Login using SASL and Proxy Execute. - """ - - @classmethod - def get_sasl_options(self, mechanism='PLAIN'): - sasl_options = { - "service": 'dse', - "username": 'server', - "mechanism": mechanism, - 'password': self.server_role, - 'authorization_id': self.user_role - } - return sasl_options - - def connect_and_query(self, auth_provider, execute_as=None, query="SELECT * FROM testproxy.testproxy"): - self.cluster = TestCluster(auth_provider=auth_provider) - self.session = self.cluster.connect() - rs = self.session.execute(query, execute_as=execute_as) - return rs - - def test_proxy_login_forbidden(self): - """ - Test that a proxy login is forbidden by default for a user. - @since 3.20 - @jira_ticket PYTHON-662 - @test_category dse auth - @expected_result connect and query should not be allowed - """ - auth_provider = SaslAuthProvider(**self.get_sasl_options()) - with self.assertRaises(Unauthorized): - self.connect_and_query(auth_provider) - - def test_proxy_login_allowed(self): - """ - Test that a proxy login is allowed with proper permissions. - @since 3.20 - @jira_ticket PYTHON-662 - @test_category dse auth - @expected_result connect and query should be allowed - """ - auth_provider = SaslAuthProvider(**self.get_sasl_options()) - self.grant_proxy_login() - self.connect_and_query(auth_provider) - - def test_proxy_execute_forbidden(self): - """ - Test that a proxy execute is forbidden by default for a user. - @since 3.20 - @jira_ticket PYTHON-662 - @test_category dse auth - @expected_result connect and query should not be allowed - """ - auth_provider = DSEPlainTextAuthProvider(self.server_role, self.server_role) - with self.assertRaises(Unauthorized): - self.connect_and_query(auth_provider, execute_as=self.user_role) - - def test_proxy_execute_allowed(self): - """ - Test that a proxy execute is allowed with proper permissions. - @since 3.20 - @jira_ticket PYTHON-662 - @test_category dse auth - @expected_result connect and query should be allowed - """ - auth_provider = DSEPlainTextAuthProvider(self.server_role, self.server_role) - self.grant_proxy_execute() - self.connect_and_query(auth_provider, execute_as=self.user_role) - - def test_connection_with_transitional_mode(self): - """ - Test that the driver can connect using TransitionalModePlainTextAuthProvider - @since 3.20 - @jira_ticket PYTHON-831 - @test_category dse auth - @expected_result connect and query should be allowed - """ - auth_provider = TransitionalModePlainTextAuthProvider() - self.assertIsNotNone(self.connect_and_query(auth_provider, query="SELECT * from system.local WHERE key='local'")) diff --git a/tests/integration/advanced/test_cont_paging.py b/tests/integration/advanced/test_cont_paging.py deleted file mode 100644 index 99de82647d..0000000000 --- a/tests/integration/advanced/test_cont_paging.py +++ /dev/null @@ -1,243 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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. - -from tests.integration import use_singledc, greaterthanorequaldse51, BasicSharedKeyspaceUnitTestCaseRF3WM, \ - DSE_VERSION, ProtocolVersion, greaterthanorequaldse60, requiredse, TestCluster - -import logging -log = logging.getLogger(__name__) - -import unittest - -from itertools import cycle, count -from packaging.version import Version -import time - -from cassandra.cluster import ExecutionProfile, ContinuousPagingOptions -from cassandra.concurrent import execute_concurrent -from cassandra.query import SimpleStatement - - -def setup_module(): - if DSE_VERSION: - use_singledc() - - -@requiredse -class BaseContPagingTests(): - @classmethod - def setUpClass(cls): - if not DSE_VERSION or DSE_VERSION < cls.required_dse_version: - return - - cls.execution_profiles = {"CONTDEFAULT": ExecutionProfile(continuous_paging_options=ContinuousPagingOptions()), - "ONEPAGE": ExecutionProfile( - continuous_paging_options=ContinuousPagingOptions(max_pages=1)), - "MANYPAGES": ExecutionProfile( - continuous_paging_options=ContinuousPagingOptions(max_pages=10)), - "BYTES": ExecutionProfile(continuous_paging_options=ContinuousPagingOptions( - page_unit=ContinuousPagingOptions.PagingUnit.BYTES)), - "SLOW": ExecutionProfile( - continuous_paging_options=ContinuousPagingOptions(max_pages_per_second=1)), } - cls.sane_eps = ["CONTDEFAULT", "BYTES"] - - @classmethod - def tearDownClass(cls): - if not DSE_VERSION or DSE_VERSION < cls.required_dse_version: - return - - @classmethod - def create_cluster(cls): - - cls.cluster_with_profiles = TestCluster(protocol_version=cls.protocol_version, execution_profiles=cls.execution_profiles) - - cls.session_with_profiles = cls.cluster_with_profiles.connect(wait_for_all_pools=True) - statements_and_params = zip( - cycle(["INSERT INTO " + cls.ks_name + "." + cls.ks_name + " (k, v) VALUES (%s, 0)"]), - [(i,) for i in range(150)]) - execute_concurrent(cls.session_with_profiles, list(statements_and_params)) - - cls.select_all_statement = "SELECT * FROM {0}.{0}".format(cls.ks_name) - - def test_continous_paging(self): - """ - Test to ensure that various continuous paging schemes return the full set of results. - @since 3.20 - @jira_ticket PYTHON-615 - @expected_result various continous paging options should fetch all the results - - @test_category queries - """ - for ep in self.execution_profiles.keys(): - results = list(self.session_with_profiles.execute(self.select_all_statement, execution_profile= ep)) - self.assertEqual(len(results), 150) - - - def test_page_fetch_size(self): - """ - Test to ensure that continuous paging works appropriately with fetch size. - @since 3.20 - @jira_ticket PYTHON-615 - @expected_result continuous paging options should work sensibly with various fetch size - - @test_category queries - """ - - # Since we fetch one page at a time results should match fetch size - for fetch_size in (2, 3, 7, 10, 99, 100, 101, 150): - self.session_with_profiles.default_fetch_size = fetch_size - results = list(self.session_with_profiles.execute(self.select_all_statement, execution_profile= "ONEPAGE")) - self.assertEqual(len(results), fetch_size) - - # Since we fetch ten pages at a time results should match fetch size * 10 - for fetch_size in (2, 3, 7, 10, 15): - self.session_with_profiles.default_fetch_size = fetch_size - results = list(self.session_with_profiles.execute(self.select_all_statement, execution_profile= "MANYPAGES")) - self.assertEqual(len(results), fetch_size*10) - - # Default settings for continuous paging should be able to fetch all results regardless of fetch size - # Changing the units should, not affect the number of results, if max_pages is not set - for profile in self.sane_eps: - for fetch_size in (2, 3, 7, 10, 15): - self.session_with_profiles.default_fetch_size = fetch_size - results = list(self.session_with_profiles.execute(self.select_all_statement, execution_profile= profile)) - self.assertEqual(len(results), 150) - - # This should take around 3 seconds to fetch but should still complete with all results - self.session_with_profiles.default_fetch_size = 50 - results = list(self.session_with_profiles.execute(self.select_all_statement, execution_profile= "SLOW")) - self.assertEqual(len(results), 150) - - def test_paging_cancel(self): - """ - Test to ensure we can cancel a continuous paging session once it's started - @since 3.20 - @jira_ticket PYTHON-615 - @expected_result This query should be canceled before any sizable amount of results can be returned - @test_category queries - """ - - self.session_with_profiles.default_fetch_size = 1 - # This combination should fetch one result a second. We should see a very few results - results = self.session_with_profiles.execute_async(self.select_all_statement, execution_profile= "SLOW") - result_set =results.result() - result_set.cancel_continuous_paging() - result_lst =list(result_set) - self.assertLess(len(result_lst), 2, "Cancel should have aborted fetch immediately") - - def test_con_paging_verify_writes(self): - """ - Test to validate results with a few continuous paging options - @since 3.20 - @jira_ticket PYTHON-615 - @expected_result all results should be returned correctly - @test_category queries - """ - prepared = self.session_with_profiles.prepare(self.select_all_statement) - - - for ep in self.sane_eps: - for fetch_size in (2, 3, 7, 10, 99, 100, 101, 10000): - self.session_with_profiles.default_fetch_size = fetch_size - results = self.session_with_profiles.execute(self.select_all_statement, execution_profile=ep) - result_array = set() - result_set = set() - for result in results: - result_array.add(result.k) - result_set.add(result.v) - - self.assertEqual(set(range(150)), result_array) - self.assertEqual(set([0]), result_set) - - statement = SimpleStatement(self.select_all_statement) - results = self.session_with_profiles.execute(statement, execution_profile=ep) - result_array = set() - result_set = set() - for result in results: - result_array.add(result.k) - result_set.add(result.v) - - self.assertEqual(set(range(150)), result_array) - self.assertEqual(set([0]), result_set) - - results = self.session_with_profiles.execute(prepared, execution_profile=ep) - result_array = set() - result_set = set() - for result in results: - result_array.add(result.k) - result_set.add(result.v) - - self.assertEqual(set(range(150)), result_array) - self.assertEqual(set([0]), result_set) - - def test_can_get_results_when_no_more_pages(self): - """ - Test to validate that the resutls can be fetched when - has_more_pages is False - @since 3.20 - @jira_ticket PYTHON-946 - @expected_result the results can be fetched - @test_category queries - """ - generator_expanded = [] - def get_all_rows(generator, future, generator_expanded): - self.assertFalse(future.has_more_pages) - - generator_expanded.extend(list(generator)) - print("Setting generator_expanded to True") - - future = self.session_with_profiles.execute_async("SELECT * from system.local LIMIT 10", - execution_profile="CONTDEFAULT") - future.add_callback(get_all_rows, future, generator_expanded) - time.sleep(5) - self.assertTrue(generator_expanded) - - -@requiredse -@greaterthanorequaldse51 -class ContPagingTestsDSEV1(BaseContPagingTests, BasicSharedKeyspaceUnitTestCaseRF3WM): - @classmethod - def setUpClass(cls): - cls.required_dse_version = BaseContPagingTests.required_dse_version = Version('5.1') - if not DSE_VERSION or DSE_VERSION < cls.required_dse_version: - return - - BasicSharedKeyspaceUnitTestCaseRF3WM.setUpClass() - BaseContPagingTests.setUpClass() - - cls.protocol_version = ProtocolVersion.DSE_V1 - cls.create_cluster() - - -@requiredse -@greaterthanorequaldse60 -class ContPagingTestsDSEV2(BaseContPagingTests, BasicSharedKeyspaceUnitTestCaseRF3WM): - @classmethod - def setUpClass(cls): - cls.required_dse_version = BaseContPagingTests.required_dse_version = Version('6.0') - if not DSE_VERSION or DSE_VERSION < cls.required_dse_version: - return - - BasicSharedKeyspaceUnitTestCaseRF3WM.setUpClass() - BaseContPagingTests.setUpClass() - - more_profiles = { - "SMALL_QUEUE": ExecutionProfile(continuous_paging_options=ContinuousPagingOptions(max_queue_size=2)), - "BIG_QUEUE": ExecutionProfile(continuous_paging_options=ContinuousPagingOptions(max_queue_size=400)) - } - cls.sane_eps += ["SMALL_QUEUE", "BIG_QUEUE"] - cls.execution_profiles.update(more_profiles) - - cls.protocol_version = ProtocolVersion.DSE_V2 - cls.create_cluster() diff --git a/tests/integration/advanced/test_cqlengine_where_operators.py b/tests/integration/advanced/test_cqlengine_where_operators.py deleted file mode 100644 index b2e4d4ba9e..0000000000 --- a/tests/integration/advanced/test_cqlengine_where_operators.py +++ /dev/null @@ -1,110 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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 unittest - -import os -import time - -from cassandra.cqlengine import columns, connection, models -from cassandra.cqlengine.management import (CQLENG_ALLOW_SCHEMA_MANAGEMENT, - create_keyspace_simple, drop_table, - sync_table) -from cassandra.cqlengine.statements import IsNotNull -from tests.integration import DSE_VERSION, requiredse, CASSANDRA_IP, greaterthanorequaldse60, TestCluster -from tests.integration.advanced import use_single_node_with_graph_and_solr -from tests.integration.cqlengine import DEFAULT_KEYSPACE - - -class SimpleNullableModel(models.Model): - __keyspace__ = DEFAULT_KEYSPACE - partition = columns.Integer(primary_key=True) - nullable = columns.Integer(required=False) - # nullable = columns.Integer(required=False, custom_index=True) - - -def setup_module(): - if DSE_VERSION: - os.environ[CQLENG_ALLOW_SCHEMA_MANAGEMENT] = '1' - use_single_node_with_graph_and_solr() - setup_connection(DEFAULT_KEYSPACE) - create_keyspace_simple(DEFAULT_KEYSPACE, 1) - sync_table(SimpleNullableModel) - - -def setup_connection(keyspace_name): - connection.setup([CASSANDRA_IP], - # consistency=ConsistencyLevel.ONE, - # protocol_version=PROTOCOL_VERSION, - default_keyspace=keyspace_name) - - -def teardown_module(): - if DSE_VERSION: - drop_table(SimpleNullableModel) - - -@requiredse -class IsNotNullTests(unittest.TestCase): - - @classmethod - def setUpClass(cls): - if DSE_VERSION: - cls.cluster = TestCluster() - - @greaterthanorequaldse60 - def test_is_not_null_execution(self): - """ - Verify that CQL statements have correct syntax when executed - If we wanted them to return something meaningful and not a InvalidRequest - we'd have to create an index in search for the column we are using - IsNotNull - - @since 3.20 - @jira_ticket PYTHON-968 - @expected_result InvalidRequest is arisen - - @test_category cqlengine - """ - cluster = TestCluster() - self.addCleanup(cluster.shutdown) - session = cluster.connect() - - SimpleNullableModel.create(partition=1, nullable=2) - SimpleNullableModel.create(partition=2, nullable=None) - - self.addCleanup(session.execute, "DROP SEARCH INDEX ON {}".format( - SimpleNullableModel.column_family_name())) - create_index_stmt = ( - "CREATE SEARCH INDEX ON {} WITH COLUMNS nullable " - "".format(SimpleNullableModel.column_family_name())) - session.execute(create_index_stmt) - - SimpleNullableModel.create(partition=1, nullable=1) - SimpleNullableModel.create(partition=2, nullable=None) - - # TODO: block on indexing more precisely - time.sleep(5) - - self.assertEqual(len(list(SimpleNullableModel.objects.all())), 2) - self.assertEqual( - len(list( - SimpleNullableModel.filter(IsNotNull("nullable"), partition__eq=2) - )), - 0) - self.assertEqual( - len(list( - SimpleNullableModel.filter(IsNotNull("nullable"), partition__eq=1) - )), - 1) diff --git a/tests/integration/advanced/test_geometry.py b/tests/integration/advanced/test_geometry.py deleted file mode 100644 index 6a6737bd50..0000000000 --- a/tests/integration/advanced/test_geometry.py +++ /dev/null @@ -1,249 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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. - - -from tests.integration import DSE_VERSION, requiredse -from tests.integration.advanced import BasicGeometricUnitTestCase, use_single_node_with_graph -from cassandra.util import OrderedMap, sortedset -from collections import namedtuple - -import unittest -from uuid import uuid1 -from cassandra.util import Point, LineString, Polygon -from cassandra.cqltypes import LineStringType, PointType, PolygonType - - -def setup_module(): - if DSE_VERSION: - use_single_node_with_graph() - - -class AbstractGeometricTypeTest(): - - original_value = "" - - def test_should_insert_simple(self): - """ - This tests will attempt to insert a point, polygon, or line, using simple inline formating. - @since 3.20 - @jira_ticket PYTHON-456 - @test_category dse geometric - @expected_result geometric types should be able to be inserted and queried. - """ - uuid_key = uuid1() - self.session.execute("INSERT INTO tbl (k, g) VALUES (%s, %s)", [uuid_key, self.original_value]) - self.validate('g', uuid_key, self.original_value) - - def test_should_insert_simple_prepared(self): - """ - This tests will attempt to insert a point, polygon, or line, using prepared statements. - @since 3.20 - @jira_ticket PYTHON-456 - @test_category dse geometric - @expected_result geometric types should be able to be inserted and queried. - """ - uuid_key = uuid1() - prepared = self.session.prepare("INSERT INTO tbl (k, g) VALUES (?, ?)") - self.session.execute(prepared, (uuid_key, self.original_value)) - self.validate('g', uuid_key, self.original_value) - - def test_should_insert_simple_prepared_with_bound(self): - """ - This tests will attempt to insert a point, polygon, or line, using prepared statements and bind. - @since 3.20 - @jira_ticket PYTHON-456 - @test_category dse geometric - @expected_result geometric types should be able to be inserted and queried. - """ - uuid_key = uuid1() - prepared = self.session.prepare("INSERT INTO tbl (k, g) VALUES (?, ?)") - bound_statement = prepared.bind((uuid_key, self.original_value)) - self.session.execute(bound_statement) - self.validate('g', uuid_key, self.original_value) - - def test_should_insert_as_list(self): - """ - This tests will attempt to insert a point, polygon, or line, as values of list. - @since 3.20 - @jira_ticket PYTHON-456 - @test_category dse geometric - @expected_result geometric types should be able to be inserted and queried as a list. - """ - uuid_key = uuid1() - prepared = self.session.prepare("INSERT INTO tbl (k, l) VALUES (?, ?)") - bound_statement = prepared.bind((uuid_key, [self.original_value])) - self.session.execute(bound_statement) - self.validate('l', uuid_key, [self.original_value]) - - def test_should_insert_as_set(self): - """ - This tests will attempt to insert a point, polygon, or line, as values of set. - @since 3.20 - @jira_ticket PYTHON-456 - @test_category dse geometric - @expected_result geometric types should be able to be inserted and queried as a set. - """ - uuid_key = uuid1() - prepared = self.session.prepare("INSERT INTO tbl (k, s) VALUES (?, ?)") - bound_statement = prepared.bind((uuid_key, sortedset([self.original_value]))) - self.session.execute(bound_statement) - self.validate('s', uuid_key, sortedset([self.original_value])) - - def test_should_insert_as_map_keys(self): - """ - This tests will attempt to insert a point, polygon, or line, as keys of a map. - @since 3.20 - @jira_ticket PYTHON-456 - @test_category dse geometric - @expected_result geometric types should be able to be inserted and queried as keys of a map. - """ - uuid_key = uuid1() - prepared = self.session.prepare("INSERT INTO tbl (k, m0) VALUES (?, ?)") - bound_statement = prepared.bind((uuid_key, OrderedMap(zip([self.original_value], [1])))) - self.session.execute(bound_statement) - self.validate('m0', uuid_key, OrderedMap(zip([self.original_value], [1]))) - - def test_should_insert_as_map_values(self): - """ - This tests will attempt to insert a point, polygon, or line, as values of a map. - @since 3.20 - @jira_ticket PYTHON-456 - @test_category dse geometric - @expected_result geometric types should be able to be inserted and queried as values of a map. - """ - uuid_key = uuid1() - prepared = self.session.prepare("INSERT INTO tbl (k, m1) VALUES (?, ?)") - bound_statement = prepared.bind((uuid_key, OrderedMap(zip([1], [self.original_value])))) - self.session.execute(bound_statement) - self.validate('m1', uuid_key, OrderedMap(zip([1], [self.original_value]))) - - def test_should_insert_as_tuple(self): - """ - This tests will attempt to insert a point, polygon, or line, as values of a tuple. - @since 3.20 - @jira_ticket PYTHON-456 - @test_category dse geometric - @expected_result geometric types should be able to be inserted and queried as values of a tuple. - """ - uuid_key = uuid1() - prepared = self.session.prepare("INSERT INTO tbl (k, t) VALUES (?, ?)") - bound_statement = prepared.bind((uuid_key, (self.original_value, self.original_value, self.original_value))) - self.session.execute(bound_statement) - self.validate('t', uuid_key, (self.original_value, self.original_value, self.original_value)) - - def test_should_insert_as_udt(self): - """ - This tests will attempt to insert a point, polygon, or line, as members of a udt. - @since 3.20 - @jira_ticket PYTHON-456 - @test_category dse geometric - @expected_result geometric types should be able to be inserted and queried as members of a udt. - """ - UDT1 = namedtuple('udt1', ('g')) - self.cluster.register_user_type(self.ks_name, 'udt1', UDT1) - uuid_key = uuid1() - prepared = self.session.prepare("INSERT INTO tbl (k, u) values (?, ?)") - bound_statement = prepared.bind((uuid_key, UDT1(self.original_value))) - self.session.execute(bound_statement) - rs = self.session.execute("SELECT {0} from {1} where k={2}".format('u', 'tbl', uuid_key)) - retrieved_udt = rs[0]._asdict()['u'] - - self.assertEqual(retrieved_udt.g, self.original_value) - - def test_should_accept_as_partition_key(self): - """ - This tests will attempt to insert a point, polygon, or line, as a partition key. - @since 3.20 - @jira_ticket PYTHON-456 - @test_category dse geometric - @expected_result geometric types should be able to be inserted and queried as a partition key. - """ - prepared = self.session.prepare("INSERT INTO tblpk (k, v) VALUES (?, ?)") - bound_statement = prepared.bind((self.original_value, 1)) - self.session.execute(bound_statement) - rs = self.session.execute("SELECT k, v FROM tblpk") - foundpk = rs[0]._asdict()['k'] - self.assertEqual(foundpk, self.original_value) - - def validate(self, value, key, expected): - """ - Simple utility method used for validation of inserted types. - """ - rs = self.session.execute("SELECT {0} from tbl where k={1}".format(value, key)) - retrieved = rs[0]._asdict()[value] - self.assertEqual(expected, retrieved) - - def test_insert_empty_with_string(self): - """ - This tests will attempt to insert a point, polygon, or line, as Empty - @since 3.20 - @jira_ticket PYTHON-481 - @test_category dse geometric - @expected_result EMPTY as a keyword should be honored - """ - uuid_key = uuid1() - self.session.execute("INSERT INTO tbl (k, g) VALUES (%s, %s)", [uuid_key, self.empty_statement]) - self.validate('g', uuid_key, self.empty_value) - - def test_insert_empty_with_object(self): - """ - This tests will attempt to insert a point, polygon, or line, as Empty - @since 3.20 - @jira_ticket PYTHON-481 - @test_category dse geometric - @expected_result EMPTY as a keyword should be used with empty objects - """ - uuid_key = uuid1() - prepared = self.session.prepare("INSERT INTO tbl (k, g) VALUES (?, ?)") - self.session.execute(prepared, (uuid_key, self.empty_value)) - self.validate('g', uuid_key, self.empty_value) - - -@requiredse -class BasicGeometricPointTypeTest(AbstractGeometricTypeTest, BasicGeometricUnitTestCase): - """ - Runs all the geometric tests against PointType - """ - cql_type_name = "'{0}'".format(PointType.typename) - original_value = Point(.5, .13) - - @unittest.skip("Empty String") - def test_insert_empty_with_string(self): - pass - - @unittest.skip("Empty String") - def test_insert_empty_with_object(self): - pass - - -@requiredse -class BasicGeometricLineStringTypeTest(AbstractGeometricTypeTest, BasicGeometricUnitTestCase): - """ - Runs all the geometric tests against LineStringType - """ - cql_type_name = cql_type_name = "'{0}'".format(LineStringType.typename) - original_value = LineString(((1, 2), (3, 4), (9871234, 1235487215))) - empty_statement = 'LINESTRING EMPTY' - empty_value = LineString() - - -@requiredse -class BasicGeometricPolygonTypeTest(AbstractGeometricTypeTest, BasicGeometricUnitTestCase): - """ - Runs all the geometric tests against PolygonType - """ - cql_type_name = cql_type_name = "'{0}'".format(PolygonType.typename) - original_value = Polygon([(10.0, 10.0), (110.0, 10.0), (110., 110.0), (10., 110.0), (10., 10.0)], [[(20., 20.0), (20., 30.0), (30., 30.0), (30., 20.0), (20., 20.0)], [(40., 20.0), (40., 30.0), (50., 30.0), (50., 20.0), (40., 20.0)]]) - empty_statement = 'POLYGON EMPTY' - empty_value = Polygon() diff --git a/tests/integration/advanced/test_spark.py b/tests/integration/advanced/test_spark.py deleted file mode 100644 index a307913abb..0000000000 --- a/tests/integration/advanced/test_spark.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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 logging - -from cassandra.cluster import EXEC_PROFILE_GRAPH_ANALYTICS_DEFAULT -from cassandra.graph import SimpleGraphStatement -from tests.integration import DSE_VERSION, requiredse -from tests.integration.advanced import use_singledc_wth_graph_and_spark, find_spark_master -from tests.integration.advanced.graph import BasicGraphUnitTestCase, ClassicGraphFixtures -log = logging.getLogger(__name__) - - -def setup_module(): - if DSE_VERSION: - use_singledc_wth_graph_and_spark() - - -@requiredse -class SparkLBTests(BasicGraphUnitTestCase): - """ - Test to validate that analtics query can run in a multi-node enviroment. Also check to to ensure - that the master spark node is correctly targeted when OLAP queries are run - - @since 3.20 - @jira_ticket PYTHON-510 - @expected_result OLAP results should come back correctly, master spark coordinator should always be picked. - @test_category dse graph - """ - def test_spark_analytic_query(self): - self.session.execute_graph(ClassicGraphFixtures.classic()) - spark_master = find_spark_master(self.session) - - # Run multipltle times to ensure we don't round robin - for i in range(3): - to_run = SimpleGraphStatement("g.V().count()") - rs = self.session.execute_graph(to_run, execution_profile=EXEC_PROFILE_GRAPH_ANALYTICS_DEFAULT) - self.assertEqual(rs[0].value, 7) - self.assertEqual(rs.response_future._current_host.address, spark_master) diff --git a/tests/integration/cloud/__init__.py b/tests/integration/cloud/__init__.py deleted file mode 100644 index a6a4ab7a5d..0000000000 --- a/tests/integration/cloud/__init__.py +++ /dev/null @@ -1,113 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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 -from cassandra.cluster import Cluster - -import unittest - -import os -import subprocess - -from tests.integration import CLOUD_PROXY_PATH, USE_CASS_EXTERNAL - - -def setup_package(): - if CLOUD_PROXY_PATH and not USE_CASS_EXTERNAL: - start_cloud_proxy() - - -def teardown_package(): - if not USE_CASS_EXTERNAL: - stop_cloud_proxy() - - -class CloudProxyCluster(unittest.TestCase): - - creds_dir = os.path.join(os.path.abspath(CLOUD_PROXY_PATH or ''), 'certs/bundles/') - creds = os.path.join(creds_dir, 'creds-v1.zip') - creds_no_auth = os.path.join(creds_dir, 'creds-v1-wo-creds.zip') - creds_unreachable = os.path.join(creds_dir, 'creds-v1-unreachable.zip') - creds_invalid_ca = os.path.join(creds_dir, 'creds-v1-invalid-ca.zip') - - cluster, connect = None, False - session = None - - @classmethod - def connect(cls, creds, **kwargs): - cloud_config = { - 'secure_connect_bundle': creds, - } - cls.cluster = Cluster(cloud=cloud_config, protocol_version=4, **kwargs) - cls.session = cls.cluster.connect(wait_for_all_pools=True) - - def tearDown(self): - if self.cluster: - self.cluster.shutdown() - - -class CloudProxyServer(object): - """ - Class for starting and stopping the proxy (sni_single_endpoint) - """ - - ccm_command = 'docker exec $(docker ps -a -q --filter ancestor=single_endpoint) ccm {}' - - def __init__(self, CLOUD_PROXY_PATH): - self.CLOUD_PROXY_PATH = CLOUD_PROXY_PATH - self.running = False - - def start(self): - return_code = subprocess.call( - ['REQUIRE_CLIENT_CERTIFICATE=true ./run.sh'], - cwd=self.CLOUD_PROXY_PATH, - shell=True) - if return_code != 0: - raise Exception("Error while starting proxy server") - self.running = True - - def stop(self): - if self.is_running(): - subprocess.call( - ["docker kill $(docker ps -a -q --filter ancestor=single_endpoint)"], - shell=True) - self.running = False - - def is_running(self): - return self.running - - def start_node(self, id): - subcommand = 'node{} start --jvm_arg "-Ddse.product_type=DATASTAX_APOLLO" --root --wait-for-binary-proto'.format(id) - subprocess.call( - [self.ccm_command.format(subcommand)], - shell=True) - - def stop_node(self, id): - subcommand = 'node{} stop'.format(id) - subprocess.call( - [self.ccm_command.format(subcommand)], - shell=True) - - -CLOUD_PROXY_SERVER = CloudProxyServer(CLOUD_PROXY_PATH) - - -def start_cloud_proxy(): - """ - Starts and waits for the proxy to run - """ - CLOUD_PROXY_SERVER.stop() - CLOUD_PROXY_SERVER.start() - - -def stop_cloud_proxy(): - CLOUD_PROXY_SERVER.stop() diff --git a/tests/integration/cloud/conftest.py b/tests/integration/cloud/conftest.py deleted file mode 100644 index fb08b04194..0000000000 --- a/tests/integration/cloud/conftest.py +++ /dev/null @@ -1,9 +0,0 @@ -import pytest - -from tests.integration.cloud import setup_package, teardown_package - -@pytest.fixture(scope='session', autouse=True) -def setup_and_teardown_packages(): - setup_package() - yield - teardown_package() \ No newline at end of file diff --git a/tests/integration/cloud/test_cloud.py b/tests/integration/cloud/test_cloud.py deleted file mode 100644 index 514314d81e..0000000000 --- a/tests/integration/cloud/test_cloud.py +++ /dev/null @@ -1,240 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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 -from cassandra.datastax.cloud import parse_metadata_info -from cassandra.query import SimpleStatement -from cassandra.cqlengine import connection -from cassandra.cqlengine.management import sync_table, create_keyspace_simple -from cassandra.cqlengine.models import Model -from cassandra.cqlengine import columns -from cassandra import DriverException, ConsistencyLevel, InvalidRequest -from cassandra.cluster import NoHostAvailable, ExecutionProfile, Cluster, _execution_profile_to_string -from cassandra.connection import SniEndPoint -from cassandra.auth import PlainTextAuthProvider -from cassandra.policies import TokenAwarePolicy, DCAwareRoundRobinPolicy, ConstantReconnectionPolicy - -from ssl import SSLContext, PROTOCOL_TLS -from unittest.mock import patch - -from tests.integration import requirescloudproxy -from tests.util import wait_until_not_raised -from tests.integration.cloud import CloudProxyCluster, CLOUD_PROXY_SERVER - -DISALLOWED_CONSISTENCIES = [ - ConsistencyLevel.ANY, - ConsistencyLevel.ONE, - ConsistencyLevel.LOCAL_ONE -] - - -@requirescloudproxy -class CloudTests(CloudProxyCluster): - def hosts_up(self): - return [h for h in self.cluster.metadata.all_hosts() if h.is_up] - - def test_resolve_and_connect(self): - self.connect(self.creds) - - self.assertEqual(len(self.hosts_up()), 3) - for host in self.cluster.metadata.all_hosts(): - self.assertTrue(host.is_up) - self.assertIsInstance(host.endpoint, SniEndPoint) - self.assertEqual(str(host.endpoint), "{}:{}:{}".format( - host.endpoint.address, host.endpoint.port, host.host_id)) - self.assertIn(host.endpoint._resolved_address, ("127.0.0.1", '::1')) - - def test_match_system_local(self): - self.connect(self.creds) - - self.assertEqual(len(self.hosts_up()), 3) - for host in self.cluster.metadata.all_hosts(): - row = self.session.execute("SELECT * FROM system.local WHERE key='local'", host=host).one() - self.assertEqual(row.host_id, host.host_id) - self.assertEqual(row.rpc_address, host.broadcast_rpc_address) - - def test_set_auth_provider(self): - self.connect(self.creds) - self.assertIsInstance(self.cluster.auth_provider, PlainTextAuthProvider) - self.assertEqual(self.cluster.auth_provider.username, 'user1') - self.assertEqual(self.cluster.auth_provider.password, 'user1') - - def test_support_leaving_the_auth_unset(self): - with self.assertRaises(NoHostAvailable): - self.connect(self.creds_no_auth) - self.assertIsNone(self.cluster.auth_provider) - - def test_support_overriding_auth_provider(self): - try: - self.connect(self.creds, auth_provider=PlainTextAuthProvider('invalid', 'invalid')) - except: - pass # this will fail soon when sni_single_endpoint is updated - self.assertIsInstance(self.cluster.auth_provider, PlainTextAuthProvider) - self.assertEqual(self.cluster.auth_provider.username, 'invalid') - self.assertEqual(self.cluster.auth_provider.password, 'invalid') - - def test_error_overriding_ssl_context(self): - with self.assertRaises(ValueError) as cm: - self.connect(self.creds, ssl_context=SSLContext(PROTOCOL_TLS)) - - self.assertIn('cannot be specified with a cloud configuration', str(cm.exception)) - - def test_error_overriding_ssl_options(self): - with self.assertRaises(ValueError) as cm: - self.connect(self.creds, ssl_options={'check_hostname': True}) - - self.assertIn('cannot be specified with a cloud configuration', str(cm.exception)) - - def _bad_hostname_metadata(self, config, http_data): - config = parse_metadata_info(config, http_data) - config.sni_host = "127.0.0.1" - return config - - def test_verify_hostname(self): - with patch('cassandra.datastax.cloud.parse_metadata_info', wraps=self._bad_hostname_metadata): - with self.assertRaises(NoHostAvailable) as e: - self.connect(self.creds) - self.assertIn("hostname", str(e.exception).lower()) - - def test_error_when_bundle_doesnt_exist(self): - try: - self.connect('/invalid/path/file.zip') - except Exception as e: - self.assertIsInstance(e, FileNotFoundError) - - def test_load_balancing_policy_is_dcawaretokenlbp(self): - self.connect(self.creds) - self.assertIsInstance(self.cluster.profile_manager.default.load_balancing_policy, - TokenAwarePolicy) - self.assertIsInstance(self.cluster.profile_manager.default.load_balancing_policy._child_policy, - DCAwareRoundRobinPolicy) - - def test_resolve_and_reconnect_on_node_down(self): - - self.connect(self.creds, - idle_heartbeat_interval=1, idle_heartbeat_timeout=1, - reconnection_policy=ConstantReconnectionPolicy(120)) - - self.assertEqual(len(self.hosts_up()), 3) - CLOUD_PROXY_SERVER.stop_node(1) - wait_until_not_raised( - lambda: self.assertEqual(len(self.hosts_up()), 2), - 0.02, 250) - - host = [h for h in self.cluster.metadata.all_hosts() if not h.is_up][0] - with patch.object(SniEndPoint, "resolve", wraps=host.endpoint.resolve) as mocked_resolve: - CLOUD_PROXY_SERVER.start_node(1) - wait_until_not_raised( - lambda: self.assertEqual(len(self.hosts_up()), 3), - 0.02, 250) - mocked_resolve.assert_called() - - def test_metadata_unreachable(self): - with self.assertRaises(DriverException) as cm: - self.connect(self.creds_unreachable, connect_timeout=1) - - self.assertIn('Unable to connect to the metadata service', str(cm.exception)) - - def test_metadata_ssl_error(self): - with self.assertRaises(DriverException) as cm: - self.connect(self.creds_invalid_ca) - - self.assertIn('Unable to connect to the metadata', str(cm.exception)) - - def test_default_consistency(self): - self.connect(self.creds) - self.assertEqual(self.session.default_consistency_level, ConsistencyLevel.LOCAL_QUORUM) - # Verify EXEC_PROFILE_DEFAULT, EXEC_PROFILE_GRAPH_DEFAULT, - # EXEC_PROFILE_GRAPH_SYSTEM_DEFAULT, EXEC_PROFILE_GRAPH_ANALYTICS_DEFAULT - for ep_key in self.cluster.profile_manager.profiles.keys(): - ep = self.cluster.profile_manager.profiles[ep_key] - self.assertEqual( - ep.consistency_level, - ConsistencyLevel.LOCAL_QUORUM, - "Expecting LOCAL QUORUM for profile {}, but got {} instead".format( - _execution_profile_to_string(ep_key), ConsistencyLevel.value_to_name[ep.consistency_level] - )) - - def test_default_consistency_of_execution_profiles(self): - cloud_config = {'secure_connect_bundle': self.creds} - self.cluster = Cluster(cloud=cloud_config, protocol_version=4, execution_profiles={ - 'pre_create_default_ep': ExecutionProfile(), - 'pre_create_changed_ep': ExecutionProfile( - consistency_level=ConsistencyLevel.LOCAL_ONE, - ), - }) - self.cluster.add_execution_profile('pre_connect_default_ep', ExecutionProfile()) - self.cluster.add_execution_profile( - 'pre_connect_changed_ep', - ExecutionProfile( - consistency_level=ConsistencyLevel.LOCAL_ONE, - ) - ) - session = self.cluster.connect(wait_for_all_pools=True) - - self.cluster.add_execution_profile('post_connect_default_ep', ExecutionProfile()) - self.cluster.add_execution_profile( - 'post_connect_changed_ep', - ExecutionProfile( - consistency_level=ConsistencyLevel.LOCAL_ONE, - ) - ) - - for default in ['pre_create_default_ep', 'pre_connect_default_ep', 'post_connect_default_ep']: - cl = self.cluster.profile_manager.profiles[default].consistency_level - self.assertEqual( - cl, ConsistencyLevel.LOCAL_QUORUM, - "Expecting LOCAL QUORUM for profile {}, but got {} instead".format(default, cl) - ) - for changed in ['pre_create_changed_ep', 'pre_connect_changed_ep', 'post_connect_changed_ep']: - cl = self.cluster.profile_manager.profiles[changed].consistency_level - self.assertEqual( - cl, ConsistencyLevel.LOCAL_ONE, - "Expecting LOCAL ONE for profile {}, but got {} instead".format(default, cl) - ) - - def test_consistency_guardrails(self): - self.connect(self.creds) - self.session.execute( - "CREATE KEYSPACE IF NOT EXISTS test_consistency_guardrails " - "with replication={'class': 'SimpleStrategy', 'replication_factor': 1}" - ) - self.session.execute("CREATE TABLE IF NOT EXISTS test_consistency_guardrails.guardrails (id int primary key)") - for consistency in DISALLOWED_CONSISTENCIES: - statement = SimpleStatement( - "INSERT INTO test_consistency_guardrails.guardrails (id) values (1)", - consistency_level=consistency - ) - with self.assertRaises(InvalidRequest) as e: - self.session.execute(statement) - self.assertIn('not allowed for Write Consistency Level', str(e.exception)) - - # Sanity check to make sure we can do a normal insert - statement = SimpleStatement( - "INSERT INTO test_consistency_guardrails.guardrails (id) values (1)", - consistency_level=ConsistencyLevel.LOCAL_QUORUM - ) - try: - self.session.execute(statement) - except InvalidRequest: - self.fail("InvalidRequest was incorrectly raised for write query at LOCAL QUORUM!") - - def test_cqlengine_can_connect(self): - class TestModel(Model): - id = columns.Integer(primary_key=True) - val = columns.Text() - - connection.setup(None, "test", cloud={'secure_connect_bundle': self.creds}) - create_keyspace_simple('test', 1) - sync_table(TestModel) - TestModel.objects.create(id=42, value='test') - self.assertEqual(len(TestModel.objects.all()), 1) diff --git a/tests/integration/cloud/test_cloud_schema.py b/tests/integration/cloud/test_cloud_schema.py deleted file mode 100644 index 1d52e8e428..0000000000 --- a/tests/integration/cloud/test_cloud_schema.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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 -""" -This is mostly copypasta from integration/long/test_schema.py - -TODO: Come up with way to run cloud and local tests without duplication -""" - -import logging -import time - -from cassandra import ConsistencyLevel -from cassandra.cluster import Cluster -from cassandra.query import SimpleStatement - -from tests.integration import execute_until_pass -from tests.integration.cloud import CloudProxyCluster - -log = logging.getLogger(__name__) - - -class CloudSchemaTests(CloudProxyCluster): - def test_recreates(self): - """ - Basic test for repeated schema creation and use, using many different keyspaces - """ - self.connect(self.creds) - session = self.session - - for _ in self.cluster.metadata.all_hosts(): - for keyspace_number in range(5): - keyspace = "ks_{0}".format(keyspace_number) - - if keyspace in self.cluster.metadata.keyspaces.keys(): - drop = "DROP KEYSPACE {0}".format(keyspace) - log.debug(drop) - execute_until_pass(session, drop) - - create = "CREATE KEYSPACE {0} WITH replication = {{'class': 'SimpleStrategy', 'replication_factor': 3}}".format( - keyspace) - log.debug(create) - execute_until_pass(session, create) - - create = "CREATE TABLE {0}.cf (k int PRIMARY KEY, i int)".format(keyspace) - log.debug(create) - execute_until_pass(session, create) - - use = "USE {0}".format(keyspace) - log.debug(use) - execute_until_pass(session, use) - - insert = "INSERT INTO cf (k, i) VALUES (0, 0)" - log.debug(insert) - ss = SimpleStatement(insert, consistency_level=ConsistencyLevel.QUORUM) - execute_until_pass(session, ss) - - def test_for_schema_disagreement_attribute(self): - """ - Tests to ensure that schema disagreement is properly surfaced on the response future. - - Creates and destroys keyspaces/tables with various schema agreement timeouts set. - First part runs cql create/drop cmds with schema agreement set in such away were it will be impossible for agreement to occur during timeout. - It then validates that the correct value is set on the result. - Second part ensures that when schema agreement occurs, that the result set reflects that appropriately - - @since 3.1.0 - @jira_ticket PYTHON-458 - @expected_result is_schema_agreed is set appropriately on response thefuture - - @test_category schema - """ - # This should yield a schema disagreement - cloud_config = {'secure_connect_bundle': self.creds} - cluster = Cluster(max_schema_agreement_wait=0.001, protocol_version=4, cloud=cloud_config) - session = cluster.connect(wait_for_all_pools=True) - - rs = session.execute( - "CREATE KEYSPACE test_schema_disagreement WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3}") - self.check_and_wait_for_agreement(session, rs, False) - rs = session.execute( - SimpleStatement("CREATE TABLE test_schema_disagreement.cf (key int PRIMARY KEY, value int)", - consistency_level=ConsistencyLevel.ALL)) - self.check_and_wait_for_agreement(session, rs, False) - rs = session.execute("DROP KEYSPACE test_schema_disagreement") - self.check_and_wait_for_agreement(session, rs, False) - cluster.shutdown() - - # These should have schema agreement - cluster = Cluster(protocol_version=4, max_schema_agreement_wait=100, cloud=cloud_config) - session = cluster.connect() - rs = session.execute( - "CREATE KEYSPACE test_schema_disagreement WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3}") - self.check_and_wait_for_agreement(session, rs, True) - rs = session.execute( - SimpleStatement("CREATE TABLE test_schema_disagreement.cf (key int PRIMARY KEY, value int)", - consistency_level=ConsistencyLevel.ALL)) - self.check_and_wait_for_agreement(session, rs, True) - rs = session.execute("DROP KEYSPACE test_schema_disagreement") - self.check_and_wait_for_agreement(session, rs, True) - cluster.shutdown() - - def check_and_wait_for_agreement(self, session, rs, exepected): - # Wait for RESULT_KIND_SCHEMA_CHANGE message to arrive - time.sleep(1) - self.assertEqual(rs.response_future.is_schema_agreed, exepected) - if not rs.response_future.is_schema_agreed: - session.cluster.control_connection.wait_for_schema_agreement(wait_time=1000) \ No newline at end of file diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index a682bcb608..5db8026675 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -17,7 +17,7 @@ def cleanup_clusters(): if not os.environ.get('DISABLE_CLUSTER_CLEANUP'): for cluster_name in [CLUSTER_NAME, SINGLE_NODE_CLUSTER_NAME, MULTIDC_CLUSTER_NAME, - 'shared_aware', 'sni_proxy', 'test_ip_change']: + 'cluster_tests', 'shared_aware', 'sni_proxy', 'test_ip_change', 'test_client_routes_replacement']: try: cluster = CCMClusterFactory.load(ccm_path, cluster_name) logging.debug("Using external CCM cluster {0}".format(cluster.name)) diff --git a/tests/integration/cqlengine/__init__.py b/tests/integration/cqlengine/__init__.py index 204dcb1253..7fae437370 100644 --- a/tests/integration/cqlengine/__init__.py +++ b/tests/integration/cqlengine/__init__.py @@ -77,12 +77,7 @@ def wrapped_function(*args, **kwargs): # DeMonkey Patch our code cassandra.cqlengine.connection.execute = original_function # Check to see if we have a pre-existing test case to work from. - if args: - test_case = args[0] - else: - test_case = unittest.TestCase("__init__") - # Check to see if the count is what you expect - test_case.assertEqual(count.get_counter(), expected, msg="Expected number of cassandra.cqlengine.connection.execute calls ({0}) doesn't match actual number invoked ({1})".format(expected, count.get_counter())) + assert count.get_counter() == expected, "Expected number of cassandra.cqlengine.connection.execute calls ({0}) doesn't match actual number invoked ({1})".format(expected, count.get_counter()) return to_return # Name of the wrapped function must match the original or unittest will error out. wrapped_function.__name__ = fn.__name__ @@ -94,5 +89,3 @@ def wrapped_function(*args, **kwargs): return wrapped_function return innerCounter - - diff --git a/tests/integration/cqlengine/advanced/test_cont_paging.py b/tests/integration/cqlengine/advanced/test_cont_paging.py deleted file mode 100644 index 95fb7dc837..0000000000 --- a/tests/integration/cqlengine/advanced/test_cont_paging.py +++ /dev/null @@ -1,169 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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 unittest - -from packaging.version import Version - -from cassandra.cluster import (EXEC_PROFILE_DEFAULT, - ContinuousPagingOptions, ExecutionProfile, - ProtocolVersion) -from cassandra.cqlengine import columns, connection, models -from cassandra.cqlengine.management import drop_table, sync_table -from tests.integration import (DSE_VERSION, greaterthanorequaldse51, - greaterthanorequaldse60, requiredse, TestCluster) - - -class TestMultiKeyModel(models.Model): - __test__ = False - - partition = columns.Integer(primary_key=True) - cluster = columns.Integer(primary_key=True) - count = columns.Integer(required=False) - text = columns.Text(required=False) - - -def setup_module(): - if DSE_VERSION: - sync_table(TestMultiKeyModel) - for i in range(1000): - TestMultiKeyModel.create(partition=i, cluster=i, count=5, text="text to write") - - -def teardown_module(): - if DSE_VERSION: - drop_table(TestMultiKeyModel) - - -@requiredse -class BasicConcurrentTests(): - required_dse_version = None - protocol_version = None - connections = set() - sane_connections = {"CONTDEFAULT"} - - @classmethod - def setUpClass(cls): - if DSE_VERSION: - cls._create_cluster_with_cp_options("CONTDEFAULT", ContinuousPagingOptions()) - cls._create_cluster_with_cp_options("ONEPAGE", ContinuousPagingOptions(max_pages=1)) - cls._create_cluster_with_cp_options("MANYPAGES", ContinuousPagingOptions(max_pages=10)) - cls._create_cluster_with_cp_options("SLOW", ContinuousPagingOptions(max_pages_per_second=1)) - - @classmethod - def tearDownClass(cls): - if not DSE_VERSION or DSE_VERSION < cls.required_dse_version: - return - - cls.cluster_default.shutdown() - connection.set_default_connection("default") - - @classmethod - def _create_cluster_with_cp_options(cls, name, cp_options): - execution_profiles = {EXEC_PROFILE_DEFAULT: - ExecutionProfile(continuous_paging_options=cp_options)} - cls.cluster_default = TestCluster(protocol_version=cls.protocol_version, - execution_profiles=execution_profiles) - cls.session_default = cls.cluster_default.connect(wait_for_all_pools=True) - connection.register_connection(name, default=True, session=cls.session_default) - cls.connections.add(name) - - def test_continuous_paging_basic(self): - """ - Test to ensure that various continuous paging works with cqlengine - for session - @since DSE 2.4 - @jira_ticket PYTHON-872 - @expected_result various continous paging options should fetch all the results - - @test_category queries - """ - for connection_name in self.sane_connections: - connection.set_default_connection(connection_name) - row = TestMultiKeyModel.get(partition=0, cluster=0) - self.assertEqual(row.partition, 0) - self.assertEqual(row.cluster, 0) - rows = TestMultiKeyModel.objects().allow_filtering() - self.assertEqual(len(rows), 1000) - - def test_fetch_size(self): - """ - Test to ensure that various continuous paging works with different fetch sizes - for session - @since DSE 2.4 - @jira_ticket PYTHON-872 - @expected_result various continous paging options should fetch all the results - - @test_category queries - """ - for connection_name in self.connections: - conn = connection._connections[connection_name] - initial_default = conn.session.default_fetch_size - self.addCleanup( - setattr, - conn.session, - "default_fetch_size", - initial_default - ) - - connection.set_default_connection("ONEPAGE") - for fetch_size in (2, 3, 7, 10, 99, 100, 101, 150): - connection._connections["ONEPAGE"].session.default_fetch_size = fetch_size - rows = TestMultiKeyModel.objects().allow_filtering() - self.assertEqual(fetch_size, len(rows)) - - connection.set_default_connection("MANYPAGES") - for fetch_size in (2, 3, 7, 10, 15): - connection._connections["MANYPAGES"].session.default_fetch_size = fetch_size - rows = TestMultiKeyModel.objects().allow_filtering() - self.assertEqual(fetch_size * 10, len(rows)) - - for connection_name in self.sane_connections: - connection.set_default_connection(connection_name) - for fetch_size in (2, 3, 7, 10, 99, 100, 101, 150): - connection._connections[connection_name].session.default_fetch_size = fetch_size - rows = TestMultiKeyModel.objects().allow_filtering() - self.assertEqual(1000, len(rows)) - - -@requiredse -@greaterthanorequaldse51 -class ContPagingTestsDSEV1(BasicConcurrentTests, unittest.TestCase): - @classmethod - def setUpClass(cls): - BasicConcurrentTests.required_dse_version = Version('5.1') - if not DSE_VERSION or DSE_VERSION < BasicConcurrentTests.required_dse_version: - return - - BasicConcurrentTests.protocol_version = ProtocolVersion.DSE_V1 - BasicConcurrentTests.setUpClass() - -@requiredse -@greaterthanorequaldse60 -class ContPagingTestsDSEV2(BasicConcurrentTests, unittest.TestCase): - @classmethod - def setUpClass(cls): - BasicConcurrentTests.required_dse_version = Version('6.0') - if not DSE_VERSION or DSE_VERSION < BasicConcurrentTests.required_dse_version: - return - BasicConcurrentTests.protocol_version = ProtocolVersion.DSE_V2 - BasicConcurrentTests.setUpClass() - - cls.connections = cls.connections.union({"SMALL_QUEUE", "BIG_QUEUE"}) - cls.sane_connections = cls.sane_connections.union({"SMALL_QUEUE", "BIG_QUEUE"}) - - cls._create_cluster_with_cp_options("SMALL_QUEUE", ContinuousPagingOptions(max_queue_size=2)) - cls._create_cluster_with_cp_options("BIG_QUEUE", ContinuousPagingOptions(max_queue_size=400)) diff --git a/tests/integration/cqlengine/base.py b/tests/integration/cqlengine/base.py index e2c02c82a3..c65554b974 100644 --- a/tests/integration/cqlengine/base.py +++ b/tests/integration/cqlengine/base.py @@ -40,15 +40,3 @@ class BaseCassEngTestCase(unittest.TestCase): def setUp(self): self.session = get_session() - - def assertHasAttr(self, obj, attr): - self.assertTrue(hasattr(obj, attr), - "{0} doesn't have attribute: {1}".format(obj, attr)) - - def assertNotHasAttr(self, obj, attr): - self.assertFalse(hasattr(obj, attr), - "{0} shouldn't have the attribute: {1}".format(obj, attr)) - - if sys.version_info > (3, 0): - def assertItemsEqual(self, first, second, msg=None): - return self.assertCountEqual(first, second, msg) diff --git a/tests/integration/cqlengine/columns/test_container_columns.py b/tests/integration/cqlengine/columns/test_container_columns.py index 4c21ff55d8..6fb2754877 100644 --- a/tests/integration/cqlengine/columns/test_container_columns.py +++ b/tests/integration/cqlengine/columns/test_container_columns.py @@ -30,6 +30,7 @@ from tests.integration.cqlengine import is_prepend_reversed from tests.integration.cqlengine.base import BaseCassEngTestCase from tests.integration import greaterthancass20, CASSANDRA_VERSION +import pytest log = logging.getLogger(__name__) @@ -72,7 +73,8 @@ def tearDownClass(cls): drop_table(TestSetModel) def test_add_none_fails(self): - self.assertRaises(ValidationError, TestSetModel.create, **{'int_set': set([None])}) + with pytest.raises(ValidationError): + TestSetModel.create(int_set=set([None])) def test_empty_set_initial(self): """ @@ -91,7 +93,7 @@ def test_deleting_last_item_should_succeed(self): m.save() m = TestSetModel.get(partition=m.partition) - self.assertTrue(5 not in m.int_set) + assert 5 not in m.int_set def test_blind_deleting_last_item_should_succeed(self): m = TestSetModel.create() @@ -101,7 +103,7 @@ def test_blind_deleting_last_item_should_succeed(self): TestSetModel.objects(partition=m.partition).update(int_set=set()) m = TestSetModel.get(partition=m.partition) - self.assertTrue(5 not in m.int_set) + assert 5 not in m.int_set def test_empty_set_retrieval(self): m = TestSetModel.create() @@ -113,20 +115,21 @@ def test_io_success(self): m1 = TestSetModel.create(int_set=set((1, 2)), text_set=set(('kai', 'andreas'))) m2 = TestSetModel.get(partition=m1.partition) - self.assertIsInstance(m2.int_set, set) - self.assertIsInstance(m2.text_set, set) + assert isinstance(m2.int_set, set) + assert isinstance(m2.text_set, set) - self.assertIn(1, m2.int_set) - self.assertIn(2, m2.int_set) + assert 1 in m2.int_set + assert 2 in m2.int_set - self.assertIn('kai', m2.text_set) - self.assertIn('andreas', m2.text_set) + assert 'kai' in m2.text_set + assert 'andreas' in m2.text_set def test_type_validation(self): """ Tests that attempting to use the wrong types will raise an exception """ - self.assertRaises(ValidationError, TestSetModel.create, **{'int_set': set(('string', True)), 'text_set': set((1, 3.0))}) + with pytest.raises(ValidationError): + TestSetModel.create(int_set=set(('string', True)), text_set=set((1, 3.0))) def test_element_count_validation(self): """ @@ -142,8 +145,9 @@ def test_element_count_validation(self): del tb except OperationTimedOut: #This will happen if the host is remote - self.assertFalse(CASSANDRA_IP.startswith("127.0.0.")) - self.assertRaises(ValidationError, TestSetModel.create, **{'text_set': set(str(uuid4()) for i in range(65536))}) + assert not CASSANDRA_IP.startswith("127.0.0.") + with pytest.raises(ValidationError): + TestSetModel.create(text_set=set(str(uuid4()) for i in range(65536))) def test_partial_updates(self): """ Tests that partial udpates work as expected """ @@ -151,12 +155,12 @@ def test_partial_updates(self): m1.int_set.add(5) m1.int_set.remove(1) - self.assertEqual(m1.int_set, set((2, 3, 4, 5))) + assert m1.int_set == set((2, 3, 4, 5)) m1.save() m2 = TestSetModel.get(partition=m1.partition) - self.assertEqual(m2.int_set, set((2, 3, 4, 5))) + assert m2.int_set == set((2, 3, 4, 5)) def test_instantiation_with_column_class(self): """ @@ -164,23 +168,23 @@ def test_instantiation_with_column_class(self): and that the class is instantiated in the constructor """ column = columns.Set(columns.Text) - self.assertIsInstance(column.value_col, columns.Text) + assert isinstance(column.value_col, columns.Text) def test_instantiation_with_column_instance(self): """ Tests that columns instantiated with a column instance work properly """ column = columns.Set(columns.Text(min_length=100)) - self.assertIsInstance(column.value_col, columns.Text) + assert isinstance(column.value_col, columns.Text) def test_to_python(self): """ Tests that to_python of value column is called """ column = columns.Set(JsonTestColumn) val = set((1, 2, 3)) db_val = column.to_database(val) - self.assertEqual(db_val, set(json.dumps(v) for v in val)) + assert db_val == set(json.dumps(v) for v in val) py_val = column.to_python(db_val) - self.assertEqual(py_val, val) + assert py_val == val def test_default_empty_container_saving(self): """ tests that the default empty container is not saved if it hasn't been updated """ @@ -191,7 +195,7 @@ def test_default_empty_container_saving(self): TestSetModel.create(partition=pkey) m = TestSetModel.get(partition=pkey) - self.assertEqual(m.int_set, set((3, 4))) + assert m.int_set == set((3, 4)) class TestListModel(Model): @@ -227,23 +231,24 @@ def test_io_success(self): m1 = TestListModel.create(int_list=[1, 2], text_list=['kai', 'andreas']) m2 = TestListModel.get(partition=m1.partition) - self.assertIsInstance(m2.int_list, list) - self.assertIsInstance(m2.text_list, list) + assert isinstance(m2.int_list, list) + assert isinstance(m2.text_list, list) - self.assertEqual(len(m2.int_list), 2) - self.assertEqual(len(m2.text_list), 2) + assert len(m2.int_list) == 2 + assert len(m2.text_list) == 2 - self.assertEqual(m2.int_list[0], 1) - self.assertEqual(m2.int_list[1], 2) + assert m2.int_list[0] == 1 + assert m2.int_list[1] == 2 - self.assertEqual(m2.text_list[0], 'kai') - self.assertEqual(m2.text_list[1], 'andreas') + assert m2.text_list[0] == 'kai' + assert m2.text_list[1] == 'andreas' def test_type_validation(self): """ Tests that attempting to use the wrong types will raise an exception """ - self.assertRaises(ValidationError, TestListModel.create, **{'int_list': ['string', True], 'text_list': [1, 3.0]}) + with pytest.raises(ValidationError): + TestListModel.create(int_list=['string', True], text_list=[1, 3.0]) def test_element_count_validation(self): """ @@ -257,7 +262,8 @@ def test_element_count_validation(self): ex_type, ex, tb = sys.exc_info() log.warning("{0}: {1} Backtrace: {2}".format(ex_type.__name__, ex, traceback.extract_tb(tb))) del tb - self.assertRaises(ValidationError, TestListModel.create, **{'text_list': [str(uuid4()) for _ in range(65536)]}) + with pytest.raises(ValidationError): + TestListModel.create(text_list=[str(uuid4()) for _ in range(65536)]) def test_partial_updates(self): """ Tests that partial udpates work as expected """ @@ -275,7 +281,7 @@ def test_partial_updates(self): expected = full m2 = TestListModel.get(partition=m1.partition) - self.assertEqual(list(m2.int_list), expected) + assert list(m2.int_list) == expected def test_instantiation_with_column_class(self): """ @@ -283,23 +289,23 @@ def test_instantiation_with_column_class(self): and that the class is instantiated in the constructor """ column = columns.List(columns.Text) - self.assertIsInstance(column.value_col, columns.Text) + assert isinstance(column.value_col, columns.Text) def test_instantiation_with_column_instance(self): """ Tests that columns instantiated with a column instance work properly """ column = columns.List(columns.Text(min_length=100)) - self.assertIsInstance(column.value_col, columns.Text) + assert isinstance(column.value_col, columns.Text) def test_to_python(self): """ Tests that to_python of value column is called """ column = columns.List(JsonTestColumn) val = [1, 2, 3] db_val = column.to_database(val) - self.assertEqual(db_val, [json.dumps(v) for v in val]) + assert db_val == [json.dumps(v) for v in val] py_val = column.to_python(db_val) - self.assertEqual(py_val, val) + assert py_val == val def test_default_empty_container_saving(self): """ tests that the default empty container is not saved if it hasn't been updated """ @@ -310,7 +316,7 @@ def test_default_empty_container_saving(self): TestListModel.create(partition=pkey) m = TestListModel.get(partition=pkey) - self.assertEqual(m.int_list, [1, 2, 3, 4]) + assert m.int_list == [1, 2, 3, 4] def test_remove_entry_works(self): pkey = uuid4() @@ -318,7 +324,7 @@ def test_remove_entry_works(self): tmp.int_list.pop() tmp.update() tmp = TestListModel.get(partition=pkey) - self.assertEqual(tmp.int_list, [1]) + assert tmp.int_list == [1] def test_update_from_non_empty_to_empty(self): pkey = uuid4() @@ -327,11 +333,12 @@ def test_update_from_non_empty_to_empty(self): tmp.update() tmp = TestListModel.get(partition=pkey) - self.assertEqual(tmp.int_list, []) + assert tmp.int_list == [] def test_insert_none(self): pkey = uuid4() - self.assertRaises(ValidationError, TestListModel.create, **{'partition': pkey, 'int_list': [None]}) + with pytest.raises(ValidationError): + TestListModel.create(partition=pkey, int_list=[None]) def test_blind_list_updates_from_none(self): """ Tests that updates from None work as expected """ @@ -341,12 +348,12 @@ def test_blind_list_updates_from_none(self): m.save() m2 = TestListModel.get(partition=m.partition) - self.assertEqual(m2.int_list, expected) + assert m2.int_list == expected TestListModel.objects(partition=m.partition).update(int_list=[]) m3 = TestListModel.get(partition=m.partition) - self.assertEqual(m3.int_list, []) + assert m3.int_list == [] class TestMapModel(Model): @@ -373,7 +380,8 @@ def test_empty_default(self): tmp.int_map['blah'] = 1 def test_add_none_as_map_key(self): - self.assertRaises(ValidationError, TestMapModel.create, **{'int_map': {None: uuid4()}}) + with pytest.raises(ValidationError): + TestMapModel.create(int_map={None: uuid4()}) def test_empty_retrieve(self): tmp = TestMapModel.create() @@ -388,7 +396,7 @@ def test_remove_last_entry_works(self): tmp.save() tmp = TestMapModel.get(partition=tmp.partition) - self.assertTrue("blah" not in tmp.int_map) + assert "blah" not in tmp.int_map def test_io_success(self): """ Tests that a basic usage works as expected """ @@ -400,22 +408,23 @@ def test_io_success(self): text_map={'now': now, 'then': then}) m2 = TestMapModel.get(partition=m1.partition) - self.assertTrue(isinstance(m2.int_map, dict)) - self.assertTrue(isinstance(m2.text_map, dict)) + assert isinstance(m2.int_map, dict) + assert isinstance(m2.text_map, dict) - self.assertTrue(1 in m2.int_map) - self.assertTrue(2 in m2.int_map) - self.assertEqual(m2.int_map[1], k1) - self.assertEqual(m2.int_map[2], k2) + assert 1 in m2.int_map + assert 2 in m2.int_map + assert m2.int_map[1] == k1 + assert m2.int_map[2] == k2 - self.assertAlmostEqual(get_total_seconds(now - m2.text_map['now']), 0, 2) - self.assertAlmostEqual(get_total_seconds(then - m2.text_map['then']), 0, 2) + assert get_total_seconds(now - m2.text_map['now']) == pytest.approx(0, abs=1e-2) + assert get_total_seconds(then - m2.text_map['then']) == pytest.approx(0, abs=1e-2) def test_type_validation(self): """ Tests that attempting to use the wrong types will raise an exception """ - self.assertRaises(ValidationError, TestMapModel.create, **{'int_map': {'key': 2, uuid4(): 'val'}, 'text_map': {2: 5}}) + with pytest.raises(ValidationError): + TestMapModel.create(int_map={'key': 2, uuid4(): 'val'}, text_map={2: 5}) def test_element_count_validation(self): """ @@ -429,7 +438,8 @@ def test_element_count_validation(self): ex_type, ex, tb = sys.exc_info() log.warning("{0}: {1} Backtrace: {2}".format(ex_type.__name__, ex, traceback.extract_tb(tb))) del tb - self.assertRaises(ValidationError, TestMapModel.create, **{'text_map': dict((str(uuid4()), i) for i in range(65536))}) + with pytest.raises(ValidationError): + TestMapModel.create(text_map=dict((str(uuid4()), i) for i in range(65536))) def test_partial_updates(self): """ Tests that partial udpates work as expected """ @@ -449,7 +459,7 @@ def test_partial_updates(self): m1.save() m2 = TestMapModel.get(partition=m1.partition) - self.assertEqual(m2.text_map, final) + assert m2.text_map == final def test_updates_from_none(self): """ Tests that updates from None work as expected """ @@ -459,12 +469,12 @@ def test_updates_from_none(self): m.save() m2 = TestMapModel.get(partition=m.partition) - self.assertEqual(m2.int_map, expected) + assert m2.int_map == expected m2.int_map = None m2.save() m3 = TestMapModel.get(partition=m.partition) - self.assertNotEqual(m3.int_map, expected) + assert m3.int_map != expected def test_blind_updates_from_none(self): """ Tests that updates from None work as expected """ @@ -474,12 +484,12 @@ def test_blind_updates_from_none(self): m.save() m2 = TestMapModel.get(partition=m.partition) - self.assertEqual(m2.int_map, expected) + assert m2.int_map == expected TestMapModel.objects(partition=m.partition).update(int_map={}) m3 = TestMapModel.get(partition=m.partition) - self.assertNotEqual(m3.int_map, expected) + assert m3.int_map != expected def test_updates_to_none(self): """ Tests that setting the field to None works as expected """ @@ -488,7 +498,7 @@ def test_updates_to_none(self): m.save() m2 = TestMapModel.get(partition=m.partition) - self.assertEqual(m2.int_map, {}) + assert m2.int_map == {} def test_instantiation_with_column_class(self): """ @@ -496,25 +506,25 @@ def test_instantiation_with_column_class(self): and that the class is instantiated in the constructor """ column = columns.Map(columns.Text, columns.Integer) - self.assertIsInstance(column.key_col, columns.Text) - self.assertIsInstance(column.value_col, columns.Integer) + assert isinstance(column.key_col, columns.Text) + assert isinstance(column.value_col, columns.Integer) def test_instantiation_with_column_instance(self): """ Tests that columns instantiated with a column instance work properly """ column = columns.Map(columns.Text(min_length=100), columns.Integer()) - self.assertIsInstance(column.key_col, columns.Text) - self.assertIsInstance(column.value_col, columns.Integer) + assert isinstance(column.key_col, columns.Text) + assert isinstance(column.value_col, columns.Integer) def test_to_python(self): """ Tests that to_python of value column is called """ column = columns.Map(JsonTestColumn, JsonTestColumn) val = {1: 2, 3: 4, 5: 6} db_val = column.to_database(val) - self.assertEqual(db_val, dict((json.dumps(k), json.dumps(v)) for k, v in val.items())) + assert db_val == dict((json.dumps(k), json.dumps(v)) for k, v in val.items()) py_val = column.to_python(db_val) - self.assertEqual(py_val, val) + assert py_val == val def test_default_empty_container_saving(self): """ tests that the default empty container is not saved if it hasn't been updated """ @@ -526,7 +536,7 @@ def test_default_empty_container_saving(self): TestMapModel.create(partition=pkey) m = TestMapModel.get(partition=pkey) - self.assertEqual(m.int_map, tmap) + assert m.int_map == tmap class TestCamelMapModel(Model): @@ -617,13 +627,13 @@ def test_io_success(self): m1 = TestTupleModel.create(int_tuple=(1, 2, 3, 5, 6), text_tuple=('kai', 'andreas'), mixed_tuple=('first', 2, 'Third')) m2 = TestTupleModel.get(partition=m1.partition) - self.assertIsInstance(m2.int_tuple, tuple) - self.assertIsInstance(m2.text_tuple, tuple) - self.assertIsInstance(m2.mixed_tuple, tuple) + assert isinstance(m2.int_tuple, tuple) + assert isinstance(m2.text_tuple, tuple) + assert isinstance(m2.mixed_tuple, tuple) - self.assertEqual((1, 2, 3), m2.int_tuple) - self.assertEqual(('kai', 'andreas'), m2.text_tuple) - self.assertEqual(('first', 2, 'Third'), m2.mixed_tuple) + assert (1, 2, 3) == m2.int_tuple + assert ('kai', 'andreas') == m2.text_tuple + assert ('first', 2, 'Third') == m2.mixed_tuple def test_type_validation(self): """ @@ -635,9 +645,12 @@ def test_type_validation(self): @test_category object_mapper """ - self.assertRaises(ValidationError, TestTupleModel.create, **{'int_tuple': ('string', True), 'text_tuple': ('test', 'test'), 'mixed_tuple': ('one', 2, 'three')}) - self.assertRaises(ValidationError, TestTupleModel.create, **{'int_tuple': ('string', 'string'), 'text_tuple': (1, 3.0), 'mixed_tuple': ('one', 2, 'three')}) - self.assertRaises(ValidationError, TestTupleModel.create, **{'int_tuple': ('string', 'string'), 'text_tuple': ('test', 'test'), 'mixed_tuple': (1, "two", 3)}) + with pytest.raises(ValidationError): + TestTupleModel.create(int_tuple=('string', True), text_tuple=('test', 'test'), mixed_tuple=('one', 2, 'three')) + with pytest.raises(ValidationError): + TestTupleModel.create(int_tuple=('string', 'string'), text_tuple=(1, 3.0), mixed_tuple=('one', 2, 'three')) + with pytest.raises(ValidationError): + TestTupleModel.create(int_tuple=('string', 'string'), text_tuple=('test', 'test'), mixed_tuple=(1, "two", 3)) def test_instantiation_with_column_class(self): """ @@ -651,10 +664,10 @@ def test_instantiation_with_column_class(self): @test_category object_mapper """ mixed_tuple = columns.Tuple(columns.Text, columns.Integer, columns.Text, required=False) - self.assertIsInstance(mixed_tuple.types[0], columns.Text) - self.assertIsInstance(mixed_tuple.types[1], columns.Integer) - self.assertIsInstance(mixed_tuple.types[2], columns.Text) - self.assertEqual(len(mixed_tuple.types), 3) + assert isinstance(mixed_tuple.types[0], columns.Text) + assert isinstance(mixed_tuple.types[1], columns.Integer) + assert isinstance(mixed_tuple.types[2], columns.Text) + assert len(mixed_tuple.types) == 3 def test_default_empty_container_saving(self): """ @@ -673,7 +686,7 @@ def test_default_empty_container_saving(self): TestTupleModel.create(partition=pkey) m = TestTupleModel.get(partition=pkey) - self.assertEqual(m.int_tuple, (1, 2, 3)) + assert m.int_tuple == (1, 2, 3) def test_updates(self): """ @@ -693,7 +706,7 @@ def test_updates(self): m1.save() m2 = TestTupleModel.get(partition=m1.partition) - self.assertEqual(tuple(m2.int_tuple), replacement) + assert tuple(m2.int_tuple) == replacement def test_update_from_non_empty_to_empty(self): """ @@ -711,7 +724,7 @@ def test_update_from_non_empty_to_empty(self): tmp.update() tmp = TestTupleModel.get(partition=pkey) - self.assertEqual(tmp.int_tuple, (None)) + assert tmp.int_tuple == (None) def test_insert_none(self): """ @@ -725,7 +738,7 @@ def test_insert_none(self): """ pkey = uuid4() tmp = TestTupleModel.create(partition=pkey, int_tuple=(None)) - self.assertEqual((None), tmp.int_tuple) + assert (None) == tmp.int_tuple def test_blind_tuple_updates_from_none(self): """ @@ -744,12 +757,12 @@ def test_blind_tuple_updates_from_none(self): m.save() m2 = TestTupleModel.get(partition=m.partition) - self.assertEqual(m2.int_tuple, expected) + assert m2.int_tuple == expected TestTupleModel.objects(partition=m.partition).update(int_tuple=None) m3 = TestTupleModel.get(partition=m.partition) - self.assertEqual(m3.int_tuple, None) + assert m3.int_tuple == None class TestNestedModel(Model): @@ -824,15 +837,15 @@ def test_io_success(self): m1 = TestNestedModel.create(list_list=list_list_master, map_list=map_list_master, set_tuple=set_tuple_master) m2 = TestNestedModel.get(partition=m1.partition) - self.assertIsInstance(m2.list_list, list) - self.assertIsInstance(m2.list_list[0], list) - self.assertIsInstance(m2.map_list, dict) - self.assertIsInstance(m2.map_list.get("key2"), list) + assert isinstance(m2.list_list, list) + assert isinstance(m2.list_list[0], list) + assert isinstance(m2.map_list, dict) + assert isinstance(m2.map_list.get("key2"), list) - self.assertEqual(list_list_master, m2.list_list) - self.assertEqual(map_list_master, m2.map_list) - self.assertEqual(set_tuple_master, m2.set_tuple) - self.assertIsInstance(m2.set_tuple.pop(), tuple) + assert list_list_master == m2.list_list + assert map_list_master == m2.map_list + assert set_tuple_master == m2.set_tuple + assert isinstance(m2.set_tuple.pop(), tuple) def test_type_validation(self): """ @@ -853,12 +866,18 @@ def test_type_validation(self): set_tuple_bad_tuple_value = set((("text", "text"), ("text", "text"), ("text", "text"))) set_tuple_not_set = ['This', 'is', 'not', 'a', 'set'] - self.assertRaises(ValidationError, TestNestedModel.create, **{'list_list': list_list_bad_list_context}) - self.assertRaises(ValidationError, TestNestedModel.create, **{'list_list': list_list_no_list}) - self.assertRaises(ValidationError, TestNestedModel.create, **{'map_list': map_list_bad_value}) - self.assertRaises(ValidationError, TestNestedModel.create, **{'map_list': map_list_bad_key}) - self.assertRaises(ValidationError, TestNestedModel.create, **{'set_tuple': set_tuple_bad_tuple_value}) - self.assertRaises(ValidationError, TestNestedModel.create, **{'set_tuple': set_tuple_not_set}) + with pytest.raises(ValidationError): + TestNestedModel.create(list_list=list_list_bad_list_context) + with pytest.raises(ValidationError): + TestNestedModel.create(list_list=list_list_no_list) + with pytest.raises(ValidationError): + TestNestedModel.create(map_list=map_list_bad_value) + with pytest.raises(ValidationError): + TestNestedModel.create(map_list=map_list_bad_key) + with pytest.raises(ValidationError): + TestNestedModel.create(set_tuple=set_tuple_bad_tuple_value) + with pytest.raises(ValidationError): + TestNestedModel.create(set_tuple=set_tuple_not_set) def test_instantiation_with_column_class(self): """ @@ -875,11 +894,11 @@ def test_instantiation_with_column_class(self): map_list = columns.Map(columns.Text, columns.List(columns.Text), required=False) set_tuple = columns.Set(columns.Tuple(columns.Integer, columns.Integer), required=False) - self.assertIsInstance(list_list, columns.List) - self.assertIsInstance(list_list.types[0], columns.List) - self.assertIsInstance(map_list.types[0], columns.Text) - self.assertIsInstance(map_list.types[1], columns.List) - self.assertIsInstance(set_tuple.types[0], columns.Tuple) + assert isinstance(list_list, columns.List) + assert isinstance(list_list.types[0], columns.List) + assert isinstance(map_list.types[0], columns.Text) + assert isinstance(map_list.types[1], columns.List) + assert isinstance(set_tuple.types[0], columns.Tuple) def test_default_empty_container_saving(self): """ @@ -902,9 +921,9 @@ def test_default_empty_container_saving(self): TestNestedModel.create(partition=pkey) m = TestNestedModel.get(partition=pkey) - self.assertEqual(m.list_list, list_list_master) - self.assertEqual(m.map_list, map_list_master) - self.assertEqual(m.set_tuple, set_tuple_master) + assert m.list_list == list_list_master + assert m.map_list == map_list_master + assert m.set_tuple == set_tuple_master def test_updates(self): """ @@ -931,9 +950,9 @@ def test_updates(self): m1.save() m2 = TestNestedModel.get(partition=m1.partition) - self.assertEqual(m2.list_list, list_list_replacement) - self.assertEqual(m2.map_list, map_list_replacement) - self.assertEqual(m2.set_tuple, set_tuple_replacement) + assert m2.list_list == list_list_replacement + assert m2.map_list == map_list_replacement + assert m2.set_tuple == set_tuple_replacement def test_update_from_non_empty_to_empty(self): """ @@ -955,9 +974,9 @@ def test_update_from_non_empty_to_empty(self): tmp.update() tmp = TestNestedModel.get(partition=tmp.partition) - self.assertEqual(tmp.list_list, []) - self.assertEqual(tmp.map_list, {}) - self.assertEqual(tmp.set_tuple, set()) + assert tmp.list_list == [] + assert tmp.map_list == {} + assert tmp.set_tuple == set() def test_insert_none(self): """ @@ -971,8 +990,6 @@ def test_insert_none(self): """ pkey = uuid4() tmp = TestNestedModel.create(partition=pkey, list_list=(None), map_list=(None), set_tuple=(None)) - self.assertEqual([], tmp.list_list) - self.assertEqual({}, tmp.map_list) - self.assertEqual(set(), tmp.set_tuple) - - + assert [] == tmp.list_list + assert {} == tmp.map_list + assert set() == tmp.set_tuple diff --git a/tests/integration/cqlengine/columns/test_counter_column.py b/tests/integration/cqlengine/columns/test_counter_column.py index 160b98d7c2..5f69475b34 100644 --- a/tests/integration/cqlengine/columns/test_counter_column.py +++ b/tests/integration/cqlengine/columns/test_counter_column.py @@ -13,6 +13,7 @@ # limitations under the License. from uuid import uuid4 +import pytest from cassandra.cqlengine import columns from cassandra.cqlengine.management import sync_table, drop_table @@ -32,37 +33,28 @@ class TestClassConstruction(BaseCassEngTestCase): def test_defining_a_non_counter_column_fails(self): """ Tests that defining a non counter column field in a model with a counter column fails """ - try: + with pytest.raises(ModelDefinitionException): class model(Model): partition = columns.UUID(primary_key=True, default=uuid4) counter = columns.Counter() text = columns.Text() - self.fail("did not raise expected ModelDefinitionException") - except ModelDefinitionException: - pass def test_defining_a_primary_key_counter_column_fails(self): """ Tests that defining primary keys on counter columns fails """ - try: + with pytest.raises(TypeError): class model(Model): partition = columns.UUID(primary_key=True, default=uuid4) cluster = columns.Counter(primary_ley=True) counter = columns.Counter() - self.fail("did not raise expected TypeError") - except TypeError: - pass # force it - try: + with pytest.raises(ModelDefinitionException): class model(Model): partition = columns.UUID(primary_key=True, default=uuid4) cluster = columns.Counter() cluster.primary_key = True counter = columns.Counter() - self.fail("did not raise expected ModelDefinitionException") - except ModelDefinitionException: - pass class TestCounterColumn(BaseCassEngTestCase): @@ -120,12 +112,12 @@ def test_save_after_no_update(self): # read back instance = TestCounterModel.get(partition=instance.partition) - self.assertEqual(instance.counter, expected_value) + assert instance.counter == expected_value # save after doing nothing instance.save() - self.assertEqual(instance.counter, expected_value) + assert instance.counter == expected_value # make sure there was no increment instance = TestCounterModel.get(partition=instance.partition) - self.assertEqual(instance.counter, expected_value) + assert instance.counter == expected_value diff --git a/tests/integration/cqlengine/columns/test_validation.py b/tests/integration/cqlengine/columns/test_validation.py index 32f20d52ff..ebffc0666c 100644 --- a/tests/integration/cqlengine/columns/test_validation.py +++ b/tests/integration/cqlengine/columns/test_validation.py @@ -33,6 +33,7 @@ from tests.integration import PROTOCOL_VERSION, CASSANDRA_VERSION, greaterthanorequalcass30, greaterthanorequalcass3_11 from tests.integration.cqlengine.base import BaseCassEngTestCase +import pytest class TestDatetime(BaseCassEngTestCase): @@ -53,7 +54,7 @@ def test_datetime_io(self): now = datetime.now() self.DatetimeTest.objects.create(test_id=0, created_at=now) dt2 = self.DatetimeTest.objects(test_id=0).first() - self.assertEqual(dt2.created_at.timetuple()[:6], now.timetuple()[:6]) + assert dt2.created_at.timetuple()[:6] == now.timetuple()[:6] def test_datetime_tzinfo_io(self): class TZ(tzinfo): @@ -65,45 +66,45 @@ def dst(self, date_time): now = datetime(1982, 1, 1, tzinfo=TZ()) dt = self.DatetimeTest.objects.create(test_id=1, created_at=now) dt2 = self.DatetimeTest.objects(test_id=1).first() - self.assertEqual(dt2.created_at.timetuple()[:6], (now + timedelta(hours=1)).timetuple()[:6]) + assert dt2.created_at.timetuple()[:6] == (now + timedelta(hours=1)).timetuple()[:6] @greaterthanorequalcass30 def test_datetime_date_support(self): today = date.today() self.DatetimeTest.objects.create(test_id=2, created_at=today) dt2 = self.DatetimeTest.objects(test_id=2).first() - self.assertEqual(dt2.created_at.isoformat(), datetime(today.year, today.month, today.day).isoformat()) + assert dt2.created_at.isoformat() == datetime(today.year, today.month, today.day).isoformat() result = self.DatetimeTest.objects.all().allow_filtering().filter(test_id=2).first() - self.assertEqual(result.created_at, datetime.combine(today, datetime.min.time())) + assert result.created_at == datetime.combine(today, datetime.min.time()) result = self.DatetimeTest.objects.all().allow_filtering().filter(test_id=2, created_at=today).first() - self.assertEqual(result.created_at, datetime.combine(today, datetime.min.time())) + assert result.created_at == datetime.combine(today, datetime.min.time()) def test_datetime_none(self): dt = self.DatetimeTest.objects.create(test_id=3, created_at=None) dt2 = self.DatetimeTest.objects(test_id=3).first() - self.assertIsNone(dt2.created_at) + assert dt2.created_at is None dts = self.DatetimeTest.objects.filter(test_id=3).values_list('created_at') - self.assertIsNone(dts[0][0]) + assert dts[0][0] is None def test_datetime_invalid(self): dt_value= 'INVALID' - with self.assertRaises(TypeError): + with pytest.raises(TypeError): self.DatetimeTest.objects.create(test_id=4, created_at=dt_value) def test_datetime_timestamp(self): dt_value = 1454520554 self.DatetimeTest.objects.create(test_id=5, created_at=dt_value) dt2 = self.DatetimeTest.objects(test_id=5).first() - self.assertEqual(dt2.created_at, datetime.fromtimestamp(dt_value, tz=timezone.utc).replace(tzinfo=None)) + assert dt2.created_at == datetime.fromtimestamp(dt_value, tz=timezone.utc).replace(tzinfo=None) def test_datetime_large(self): dt_value = datetime(2038, 12, 31, 10, 10, 10, 123000) self.DatetimeTest.objects.create(test_id=6, created_at=dt_value) dt2 = self.DatetimeTest.objects(test_id=6).first() - self.assertEqual(dt2.created_at, dt_value) + assert dt2.created_at == dt_value def test_datetime_truncate_microseconds(self): """ @@ -123,7 +124,7 @@ def test_datetime_truncate_microseconds(self): dt_truncated = datetime(2024, 12, 31, 10, 10, 10, 923000) self.DatetimeTest.objects.create(test_id=6, created_at=dt_value) dt2 = self.DatetimeTest.objects(test_id=6).first() - self.assertEqual(dt2.created_at,dt_truncated) + assert dt2.created_at == dt_truncated finally: # We need to always return behavior to default DateTime.truncate_microseconds = False @@ -141,9 +142,9 @@ def setUpClass(cls): def test_default_is_set(self): tmp = self.BoolDefaultValueTest.create(test_id=1) - self.assertEqual(True, tmp.stuff) + assert True == tmp.stuff tmp2 = self.BoolDefaultValueTest.get(test_id=1) - self.assertEqual(True, tmp2.stuff) + assert True == tmp2.stuff class TestBoolValidation(BaseCassEngTestCase): @@ -160,7 +161,7 @@ def test_validation_preserves_none(self): test_obj = self.BoolValidationTest(test_id=1) test_obj.validate() - self.assertIsNone(test_obj.bool_column) + assert test_obj.bool_column is None class TestVarInt(BaseCassEngTestCase): @@ -183,9 +184,9 @@ def test_varint_io(self): long_int = 92834902384092834092384028340283048239048203480234823048230482304820348239 int1 = self.VarIntTest.objects.create(test_id=0, bignum=long_int) int2 = self.VarIntTest.objects(test_id=0).first() - self.assertEqual(int1.bignum, int2.bignum) + assert int1.bignum == int2.bignum - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): self.VarIntTest.objects.create(test_id=0, bignum="not_a_number") @@ -222,10 +223,10 @@ def _check_value_is_correct_in_db(self, value): """ if value is None: result = self.model_class.objects.all().allow_filtering().filter(test_id=0).first() - self.assertIsNone(result.class_param) + assert result.class_param is None result = self.model_class.objects(test_id=0).first() - self.assertIsNone(result.class_param) + assert result.class_param is None else: if not isinstance(value, self.python_klass): @@ -234,16 +235,16 @@ def _check_value_is_correct_in_db(self, value): value_to_compare = value result = self.model_class.objects(test_id=0).first() - self.assertIsInstance(result.class_param, self.python_klass) - self.assertEqual(result.class_param, value_to_compare) + assert isinstance(result.class_param, self.python_klass) + assert result.class_param == value_to_compare result = self.model_class.objects.all().allow_filtering().filter(test_id=0).first() - self.assertIsInstance(result.class_param, self.python_klass) - self.assertEqual(result.class_param, value_to_compare) + assert isinstance(result.class_param, self.python_klass) + assert result.class_param == value_to_compare result = self.model_class.objects.all().allow_filtering().filter(test_id=0, class_param=value).first() - self.assertIsInstance(result.class_param, self.python_klass) - self.assertEqual(result.class_param, value_to_compare) + assert isinstance(result.class_param, self.python_klass) + assert result.class_param == value_to_compare return result @@ -276,10 +277,10 @@ def test_param_none(self): """ self.model_class.objects.create(test_id=1, class_param=None) dt2 = self.model_class.objects(test_id=1).first() - self.assertIsNone(dt2.class_param) + assert dt2.class_param is None dts = self.model_class.objects(test_id=1).values_list('class_param') - self.assertIsNone(dts[0][0]) + assert dts[0][0] is None class TestDate(DataType, BaseCassEngTestCase): @@ -541,22 +542,22 @@ def test_min_length(self): Ascii(min_length=5).validate('kevin') Ascii(min_length=5).validate('kevintastic') - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Ascii(min_length=1).validate('') - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Ascii(min_length=1).validate(None) - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Ascii(min_length=6).validate('') - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Ascii(min_length=6).validate(None) - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Ascii(min_length=6).validate('kevin') - with self.assertRaises(ValueError): + with pytest.raises(ValueError): Ascii(min_length=-1) def test_max_length(self): @@ -573,13 +574,13 @@ def test_max_length(self): Ascii(max_length=5).validate('b') Ascii(max_length=5).validate('blake') - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Ascii(max_length=0).validate('b') - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Ascii(max_length=5).validate('blaketastic') - with self.assertRaises(ValueError): + with pytest.raises(ValueError): Ascii(max_length=-1) def test_length_range(self): @@ -588,10 +589,10 @@ def test_length_range(self): Ascii(min_length=10, max_length=10) Ascii(min_length=10, max_length=11) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): Ascii(min_length=10, max_length=9) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): Ascii(min_length=1, max_length=0) def test_type_checking(self): @@ -599,26 +600,26 @@ def test_type_checking(self): Ascii().validate(u'unicode') Ascii().validate(bytearray('bytearray', encoding='ascii')) - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Ascii().validate(5) - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Ascii().validate(True) Ascii().validate("!#$%&\'()*+,-./") - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Ascii().validate('Beyonc' + chr(233)) if sys.version_info < (3, 1): - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Ascii().validate(u'Beyonc' + unichr(233)) def test_unaltering_validation(self): """ Test the validation step doesn't re-interpret values. """ - self.assertEqual(Ascii().validate(''), '') - self.assertEqual(Ascii().validate(None), None) - self.assertEqual(Ascii().validate('yo'), 'yo') + assert Ascii().validate('') == '' + assert Ascii().validate(None) == None + assert Ascii().validate('yo') == 'yo' def test_non_required_validation(self): """ Tests that validation is ok on none and blank values if required is False. """ @@ -629,26 +630,26 @@ def test_required_validation(self): """ Tests that validation raise on none and blank values if value required. """ Ascii(required=True).validate('k') - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Ascii(required=True).validate('') - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Ascii(required=True).validate(None) # With min_length set. Ascii(required=True, min_length=0).validate('k') Ascii(required=True, min_length=1).validate('k') - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Ascii(required=True, min_length=2).validate('k') # With max_length set. Ascii(required=True, max_length=1).validate('k') - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Ascii(required=True, max_length=2).validate('kevin') - with self.assertRaises(ValueError): + with pytest.raises(ValueError): Ascii(required=True, max_length=0) @@ -668,22 +669,22 @@ def test_min_length(self): Text(min_length=5).validate('blake') Text(min_length=5).validate('blaketastic') - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Text(min_length=1).validate('') - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Text(min_length=1).validate(None) - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Text(min_length=6).validate('') - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Text(min_length=6).validate(None) - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Text(min_length=6).validate('blake') - with self.assertRaises(ValueError): + with pytest.raises(ValueError): Text(min_length=-1) def test_max_length(self): @@ -700,13 +701,13 @@ def test_max_length(self): Text(max_length=5).validate('b') Text(max_length=5).validate('blake') - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Text(max_length=0).validate('b') - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Text(max_length=5).validate('blaketastic') - with self.assertRaises(ValueError): + with pytest.raises(ValueError): Text(max_length=-1) def test_length_range(self): @@ -715,10 +716,10 @@ def test_length_range(self): Text(min_length=10, max_length=10) Text(min_length=10, max_length=11) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): Text(min_length=10, max_length=9) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): Text(min_length=1, max_length=0) def test_type_checking(self): @@ -726,10 +727,10 @@ def test_type_checking(self): Text().validate(u'unicode') Text().validate(bytearray('bytearray', encoding='ascii')) - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Text().validate(5) - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Text().validate(True) Text().validate("!#$%&\'()*+,-./") @@ -739,9 +740,9 @@ def test_type_checking(self): def test_unaltering_validation(self): """ Test the validation step doesn't re-interpret values. """ - self.assertEqual(Text().validate(''), '') - self.assertEqual(Text().validate(None), None) - self.assertEqual(Text().validate('yo'), 'yo') + assert Text().validate('') == '' + assert Text().validate(None) == None + assert Text().validate('yo') == 'yo' def test_non_required_validation(self): """ Tests that validation is ok on none and blank values if required is False """ @@ -752,26 +753,26 @@ def test_required_validation(self): """ Tests that validation raise on none and blank values if value required. """ Text(required=True).validate('b') - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Text(required=True).validate('') - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Text(required=True).validate(None) # With min_length set. Text(required=True, min_length=0).validate('b') Text(required=True, min_length=1).validate('b') - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Text(required=True, min_length=2).validate('b') # With max_length set. Text(required=True, max_length=1).validate('b') - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): Text(required=True, max_length=2).validate('blake') - with self.assertRaises(ValueError): + with pytest.raises(ValueError): Text(required=True, max_length=0) @@ -781,7 +782,7 @@ class TestModel(Model): id = UUID(primary_key=True, default=uuid4) def test_extra_field(self): - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): self.TestModel.create(bacon=5000) @@ -834,5 +835,5 @@ def test_inet_saves(self): def test_non_address_fails(self): # TODO: presently this only tests that the server blows it up. Is there supposed to be local validation? - with self.assertRaises(InvalidRequest): + with pytest.raises(InvalidRequest): self.InetTestModel.create(address="what is going on here?") diff --git a/tests/integration/cqlengine/connections/test_connection.py b/tests/integration/cqlengine/connections/test_connection.py index c63836785e..640c953285 100644 --- a/tests/integration/cqlengine/connections/test_connection.py +++ b/tests/integration/cqlengine/connections/test_connection.py @@ -42,12 +42,12 @@ def tearDown(self): @local def test_connection_setup_with_setup(self): connection.setup(hosts=None, default_keyspace=None) - self.assertIsNotNone(connection.get_connection("default").cluster.metadata.get_host("127.0.0.1")) + assert connection.get_connection("default").cluster.metadata.get_host("127.0.0.1") is not None @local def test_connection_setup_with_default(self): connection.default() - self.assertIsNotNone(connection.get_connection("default").cluster.metadata.get_host("127.0.0.1")) + assert connection.get_connection("default").cluster.metadata.get_host("127.0.0.1") is not None def test_only_one_connection_is_created(self): """ @@ -63,7 +63,7 @@ def test_only_one_connection_is_created(self): number_of_clusters_before = len(_clusters_for_shutdown) connection.default() number_of_clusters_after = len(_clusters_for_shutdown) - self.assertEqual(number_of_clusters_after - number_of_clusters_before, 1) + assert number_of_clusters_after - number_of_clusters_before == 1 class SeveralConnectionsTest(BaseCassEngTestCase): @@ -76,9 +76,9 @@ def setUpClass(cls): super(SeveralConnectionsTest, cls).setUpClass() cls.setup_cluster = TestCluster() cls.setup_session = cls.setup_cluster.connect() - ddl = "CREATE KEYSPACE {0} WITH replication = {{'class': 'SimpleStrategy', 'replication_factor': '{1}'}}".format(cls.keyspace1, 1) + ddl = "CREATE KEYSPACE {0} WITH replication = {{'class': 'NetworkTopologyStrategy', 'replication_factor': '{1}'}}".format(cls.keyspace1, 1) execute_with_long_wait_retry(cls.setup_session, ddl) - ddl = "CREATE KEYSPACE {0} WITH replication = {{'class': 'SimpleStrategy', 'replication_factor': '{1}'}}".format(cls.keyspace2, 1) + ddl = "CREATE KEYSPACE {0} WITH replication = {{'class': 'NetworkTopologyStrategy', 'replication_factor': '{1}'}}".format(cls.keyspace2, 1) execute_with_long_wait_retry(cls.setup_session, ddl) @classmethod @@ -119,11 +119,11 @@ def test_connection_session_switch(self): sync_table(TestConnectModel) TCM2 = TestConnectModel.create(id=1, keyspace=self.keyspace2) connection.set_session(self.session1) - self.assertEqual(1, TestConnectModel.objects.count()) - self.assertEqual(TestConnectModel.objects.first(), TCM1) + assert 1 == TestConnectModel.objects.count() + assert TestConnectModel.objects.first() == TCM1 connection.set_session(self.session2) - self.assertEqual(1, TestConnectModel.objects.count()) - self.assertEqual(TestConnectModel.objects.first(), TCM2) + assert 1 == TestConnectModel.objects.count() + assert TestConnectModel.objects.first() == TCM2 class ConnectionModel(Model): @@ -135,7 +135,7 @@ class ConnectionInitTest(unittest.TestCase): def test_default_connection_uses_legacy(self): connection.default() conn = connection.get_connection() - self.assertEqual(conn.cluster._config_mode, _ConfigMode.LEGACY) + assert conn.cluster._config_mode == _ConfigMode.LEGACY def test_connection_with_legacy_settings(self): connection.setup( @@ -144,7 +144,7 @@ def test_connection_with_legacy_settings(self): consistency=ConsistencyLevel.LOCAL_ONE ) conn = connection.get_connection() - self.assertEqual(conn.cluster._config_mode, _ConfigMode.LEGACY) + assert conn.cluster._config_mode == _ConfigMode.LEGACY def test_connection_from_session_with_execution_profile(self): cluster = TestCluster(execution_profiles={EXEC_PROFILE_DEFAULT: ExecutionProfile(row_factory=dict_factory)}) @@ -152,7 +152,7 @@ def test_connection_from_session_with_execution_profile(self): connection.default() connection.set_session(session) conn = connection.get_connection() - self.assertEqual(conn.cluster._config_mode, _ConfigMode.PROFILES) + assert conn.cluster._config_mode == _ConfigMode.PROFILES def test_connection_from_session_with_legacy_settings(self): cluster = TestCluster(load_balancing_policy=RoundRobinPolicy()) @@ -160,7 +160,7 @@ def test_connection_from_session_with_legacy_settings(self): session.row_factory = dict_factory connection.set_session(session) conn = connection.get_connection() - self.assertEqual(conn.cluster._config_mode, _ConfigMode.LEGACY) + assert conn.cluster._config_mode == _ConfigMode.LEGACY def test_uncommitted_session_uses_legacy(self): cluster = TestCluster() @@ -168,7 +168,7 @@ def test_uncommitted_session_uses_legacy(self): session.row_factory = dict_factory connection.set_session(session) conn = connection.get_connection() - self.assertEqual(conn.cluster._config_mode, _ConfigMode.LEGACY) + assert conn.cluster._config_mode == _ConfigMode.LEGACY def test_legacy_insert_query(self): connection.setup( @@ -176,21 +176,21 @@ def test_legacy_insert_query(self): default_keyspace=DEFAULT_KEYSPACE, consistency=ConsistencyLevel.LOCAL_ONE ) - self.assertEqual(connection.get_connection().cluster._config_mode, _ConfigMode.LEGACY) + assert connection.get_connection().cluster._config_mode == _ConfigMode.LEGACY sync_table(ConnectionModel) ConnectionModel.objects.create(key=0, some_data='text0') ConnectionModel.objects.create(key=1, some_data='text1') - self.assertEqual(ConnectionModel.objects(key=0)[0].some_data, 'text0') + assert ConnectionModel.objects(key=0)[0].some_data == 'text0' def test_execution_profile_insert_query(self): cluster = TestCluster(execution_profiles={EXEC_PROFILE_DEFAULT: ExecutionProfile(row_factory=dict_factory)}) session = cluster.connect() connection.default() connection.set_session(session) - self.assertEqual(connection.get_connection().cluster._config_mode, _ConfigMode.PROFILES) + assert connection.get_connection().cluster._config_mode == _ConfigMode.PROFILES sync_table(ConnectionModel) ConnectionModel.objects.create(key=0, some_data='text0') ConnectionModel.objects.create(key=1, some_data='text1') - self.assertEqual(ConnectionModel.objects(key=0)[0].some_data, 'text0') + assert ConnectionModel.objects(key=0)[0].some_data == 'text0' diff --git a/tests/integration/cqlengine/management/test_compaction_settings.py b/tests/integration/cqlengine/management/test_compaction_settings.py index d58c419d0e..25484b30c9 100644 --- a/tests/integration/cqlengine/management/test_compaction_settings.py +++ b/tests/integration/cqlengine/management/test_compaction_settings.py @@ -20,6 +20,7 @@ from cassandra.cqlengine.models import Model from tests.integration.cqlengine.base import BaseCassEngTestCase +from tests.util import assertRegex class LeveledCompactionTestTable(Model): @@ -53,7 +54,7 @@ class LeveledCompactionChangesDetectionTest(Model): drop_table(LeveledCompactionChangesDetectionTest) sync_table(LeveledCompactionChangesDetectionTest) - self.assertFalse(_update_options(LeveledCompactionChangesDetectionTest)) + assert not _update_options(LeveledCompactionChangesDetectionTest) def test_compaction_not_altered_without_changes_sizetiered(self): class SizeTieredCompactionChangesDetectionTest(Model): @@ -71,7 +72,7 @@ class SizeTieredCompactionChangesDetectionTest(Model): drop_table(SizeTieredCompactionChangesDetectionTest) sync_table(SizeTieredCompactionChangesDetectionTest) - self.assertFalse(_update_options(SizeTieredCompactionChangesDetectionTest)) + assert not _update_options(SizeTieredCompactionChangesDetectionTest) def test_alter_actually_alters(self): tmp = copy.deepcopy(LeveledCompactionTestTable) @@ -82,7 +83,7 @@ def test_alter_actually_alters(self): table_meta = _get_table_metadata(tmp) - self.assertRegex(table_meta.export_as_string(), '.*SizeTieredCompactionStrategy.*') + assertRegex(table_meta.export_as_string(), '.*SizeTieredCompactionStrategy.*') def test_alter_options(self): @@ -96,11 +97,11 @@ class AlterTable(Model): drop_table(AlterTable) sync_table(AlterTable) table_meta = _get_table_metadata(AlterTable) - self.assertRegex(table_meta.export_as_string(), ".*'sstable_size_in_mb': '64'.*") + assertRegex(table_meta.export_as_string(), ".*'sstable_size_in_mb': '64'.*") AlterTable.__options__['compaction']['sstable_size_in_mb'] = '128' sync_table(AlterTable) table_meta = _get_table_metadata(AlterTable) - self.assertRegex(table_meta.export_as_string(), ".*'sstable_size_in_mb': '128'.*") + assertRegex(table_meta.export_as_string(), ".*'sstable_size_in_mb': '128'.*") class OptionsTest(BaseCassEngTestCase): @@ -110,7 +111,7 @@ def _verify_options(self, table_meta, expected_options): for name, value in expected_options.items(): if isinstance(value, str): - self.assertIn("%s = '%s'" % (name, value), cql) + assert "%s = '%s'" % (name, value) in cql else: start = cql.find("%s = {" % (name,)) end = cql.find('}', start) @@ -124,9 +125,9 @@ def _verify_options(self, table_meta, expected_options): attr = "'%s': '%s'" % (subname, subvalue.split('.')[-1]) found_at = cql.find(attr, start) else: - - self.assertTrue(found_at > start) - self.assertTrue(found_at < end) + + assert found_at > start + assert found_at < end def test_all_size_tiered_options(self): class AllSizeTieredOptionsModel(Model): diff --git a/tests/integration/cqlengine/management/test_management.py b/tests/integration/cqlengine/management/test_management.py index ab5ea9f901..1332680cef 100644 --- a/tests/integration/cqlengine/management/test_management.py +++ b/tests/integration/cqlengine/management/test_management.py @@ -23,12 +23,13 @@ from cassandra.cqlengine.models import Model from cassandra.cqlengine import columns -from tests.integration import DSE_VERSION, PROTOCOL_VERSION, greaterthancass20, requires_collection_indexes, \ +from tests.integration import PROTOCOL_VERSION, greaterthancass20, requires_collection_indexes, \ MockLoggingHandler, CASSANDRA_VERSION, SCYLLA_VERSION, xfail_scylla from tests.integration.cqlengine.base import BaseCassEngTestCase from tests.integration.cqlengine.query.test_queryset import TestModel from cassandra.cqlengine.usertype import UserType from tests.integration.cqlengine import DEFAULT_KEYSPACE +import pytest INCLUDE_REPAIR = (not CASSANDRA_VERSION >= Version('4-a')) and SCYLLA_VERSION is None # This should cover DSE 6.0+ @@ -39,20 +40,20 @@ def test_create_drop_succeeeds(self): cluster = get_cluster() keyspace_ss = 'test_ks_ss' - self.assertNotIn(keyspace_ss, cluster.metadata.keyspaces) + assert keyspace_ss not in cluster.metadata.keyspaces management.create_keyspace_simple(keyspace_ss, 2) - self.assertIn(keyspace_ss, cluster.metadata.keyspaces) + assert keyspace_ss in cluster.metadata.keyspaces management.drop_keyspace(keyspace_ss) - self.assertNotIn(keyspace_ss, cluster.metadata.keyspaces) + assert keyspace_ss not in cluster.metadata.keyspaces keyspace_nts = 'test_ks_nts' - self.assertNotIn(keyspace_nts, cluster.metadata.keyspaces) + assert keyspace_nts not in cluster.metadata.keyspaces management.create_keyspace_network_topology(keyspace_nts, {'dc1': 1}) - self.assertIn(keyspace_nts, cluster.metadata.keyspaces) + assert keyspace_nts in cluster.metadata.keyspaces management.drop_keyspace(keyspace_nts) - self.assertNotIn(keyspace_nts, cluster.metadata.keyspaces) + assert keyspace_nts not in cluster.metadata.keyspaces class DropTableTest(BaseCassEngTestCase): @@ -177,30 +178,30 @@ def setUp(self): def test_add_column(self): sync_table(FirstModel) meta_columns = _get_table_metadata(FirstModel).columns - self.assertEqual(set(meta_columns), set(FirstModel._columns)) + assert set(meta_columns) == set(FirstModel._columns) sync_table(SecondModel) meta_columns = _get_table_metadata(FirstModel).columns - self.assertEqual(set(meta_columns), set(SecondModel._columns)) + assert set(meta_columns) == set(SecondModel._columns) sync_table(ThirdModel) meta_columns = _get_table_metadata(FirstModel).columns - self.assertEqual(len(meta_columns), 5) - self.assertEqual(len(ThirdModel._columns), 4) - self.assertIn('fourth_key', meta_columns) - self.assertNotIn('fourth_key', ThirdModel._columns) - self.assertIn('blah', ThirdModel._columns) - self.assertIn('blah', meta_columns) + assert len(meta_columns) == 5 + assert len(ThirdModel._columns) == 4 + assert 'fourth_key' in meta_columns + assert 'fourth_key' not in ThirdModel._columns + assert 'blah' in ThirdModel._columns + assert 'blah' in meta_columns sync_table(FourthModel) meta_columns = _get_table_metadata(FirstModel).columns - self.assertEqual(len(meta_columns), 5) - self.assertEqual(len(ThirdModel._columns), 4) - self.assertIn('fourth_key', meta_columns) - self.assertNotIn('fourth_key', FourthModel._columns) - self.assertIn('renamed', FourthModel._columns) - self.assertNotIn('renamed', meta_columns) - self.assertIn('blah', meta_columns) + assert len(meta_columns) == 5 + assert len(ThirdModel._columns) == 4 + assert 'fourth_key' in meta_columns + assert 'fourth_key' not in FourthModel._columns + assert 'renamed' in FourthModel._columns + assert 'renamed' not in meta_columns + assert 'blah' in meta_columns class ModelWithTableProperties(Model): @@ -239,8 +240,7 @@ def test_set_table_properties(self): expected.update({'read_repair_chance': 0.17985}) options = management._get_table_metadata(ModelWithTableProperties).options - self.assertEqual(dict([(k, options.get(k)) for k in expected.keys()]), - expected) + assert dict([(k, options.get(k)) for k in expected.keys()]) == expected def test_table_property_update(self): ModelWithTableProperties.__options__['bloom_filter_fp_chance'] = 0.66778 @@ -255,14 +255,15 @@ def test_table_property_update(self): table_options = management._get_table_metadata(ModelWithTableProperties).options - self.assertLessEqual(ModelWithTableProperties.__options__.items(), table_options.items()) + assert ModelWithTableProperties.__options__.items() <= table_options.items() def test_bogus_option_update(self): sync_table(ModelWithTableProperties) option = 'no way will this ever be an option' try: ModelWithTableProperties.__options__[option] = 'what was I thinking?' - self.assertRaisesRegex(KeyError, "Invalid table option.*%s.*" % option, sync_table, ModelWithTableProperties) + with pytest.raises(KeyError, match="Invalid table option.*%s.*" % option): + sync_table(ModelWithTableProperties) finally: ModelWithTableProperties.__options__.pop(option, None) @@ -278,14 +279,14 @@ def test_sync_table_works_with_primary_keys_only_tables(self): # blows up with DoesNotExist if table does not exist table_meta = management._get_table_metadata(PrimaryKeysOnlyModel) - self.assertIn('LeveledCompactionStrategy', table_meta.as_cql_query()) + assert 'LeveledCompactionStrategy' in table_meta.as_cql_query() PrimaryKeysOnlyModel.__options__['compaction']['class'] = 'SizeTieredCompactionStrategy' sync_table(PrimaryKeysOnlyModel) table_meta = management._get_table_metadata(PrimaryKeysOnlyModel) - self.assertIn('SizeTieredCompactionStrategy', table_meta.as_cql_query()) + assert 'SizeTieredCompactionStrategy' in table_meta.as_cql_query() def test_primary_key_validation(self): """ @@ -298,9 +299,12 @@ def test_primary_key_validation(self): @test_category object_mapper """ sync_table(PrimaryKeysOnlyModel) - self.assertRaises(CQLEngineException, sync_table, PrimaryKeysModelChanged) - self.assertRaises(CQLEngineException, sync_table, PrimaryKeysAddedClusteringKey) - self.assertRaises(CQLEngineException, sync_table, PrimaryKeysRemovedPk) + with pytest.raises(CQLEngineException): + sync_table(PrimaryKeysModelChanged) + with pytest.raises(CQLEngineException): + sync_table(PrimaryKeysAddedClusteringKey) + with pytest.raises(CQLEngineException): + sync_table(PrimaryKeysRemovedPk) class IndexModel(Model): @@ -364,12 +368,12 @@ def test_sync_warnings(self): with MockLoggingHandler().set_module_name(management.__name__) as mock_handler: sync_table(BaseInconsistent) sync_table(ChangedInconsistent) - self.assertTrue('differing from the model type' in mock_handler.messages.get('warning')[0]) + assert 'differing from the model type' in mock_handler.messages.get('warning')[0] if CASSANDRA_VERSION >= Version('2.1'): sync_type(DEFAULT_KEYSPACE, BaseInconsistentType) mock_handler.reset() sync_type(DEFAULT_KEYSPACE, ChangedInconsistentType) - self.assertTrue('differing from the model user type' in mock_handler.messages.get('warning')[0]) + assert 'differing from the model user type' in mock_handler.messages.get('warning')[0] class TestIndexSetModel(Model): @@ -401,12 +405,12 @@ def test_sync_index(self): """ sync_table(IndexModel) table_meta = management._get_table_metadata(IndexModel) - self.assertIsNotNone(management._get_index_name_by_column(table_meta, 'second_key')) + assert management._get_index_name_by_column(table_meta, 'second_key') is not None # index already exists sync_table(IndexModel) table_meta = management._get_table_metadata(IndexModel) - self.assertIsNotNone(management._get_index_name_by_column(table_meta, 'second_key')) + assert management._get_index_name_by_column(table_meta, 'second_key') is not None def test_sync_index_case_sensitive(self): """ @@ -421,12 +425,12 @@ def test_sync_index_case_sensitive(self): """ sync_table(IndexCaseSensitiveModel) table_meta = management._get_table_metadata(IndexCaseSensitiveModel) - self.assertIsNotNone(management._get_index_name_by_column(table_meta, 'second_key')) + assert management._get_index_name_by_column(table_meta, 'second_key') is not None # index already exists sync_table(IndexCaseSensitiveModel) table_meta = management._get_table_metadata(IndexCaseSensitiveModel) - self.assertIsNotNone(management._get_index_name_by_column(table_meta, 'second_key')) + assert management._get_index_name_by_column(table_meta, 'second_key') is not None @greaterthancass20 @requires_collection_indexes @@ -443,10 +447,10 @@ def test_sync_indexed_set(self): """ sync_table(TestIndexSetModel) table_meta = management._get_table_metadata(TestIndexSetModel) - self.assertIsNotNone(management._get_index_name_by_column(table_meta, 'int_set')) - self.assertIsNotNone(management._get_index_name_by_column(table_meta, 'int_list')) - self.assertIsNotNone(management._get_index_name_by_column(table_meta, 'text_map')) - self.assertIsNotNone(management._get_index_name_by_column(table_meta, 'mixed_tuple')) + assert management._get_index_name_by_column(table_meta, 'int_set') is not None + assert management._get_index_name_by_column(table_meta, 'int_list') is not None + assert management._get_index_name_by_column(table_meta, 'text_map') is not None + assert management._get_index_name_by_column(table_meta, 'mixed_tuple') is not None class NonModelFailureTest(BaseCassEngTestCase): @@ -454,7 +458,7 @@ class FakeModel(object): pass def test_failure(self): - with self.assertRaises(CQLEngineException): + with pytest.raises(CQLEngineException): sync_table(self.FakeModel) @@ -475,9 +479,9 @@ class StaticModel(Model): with mock.patch.object(session, "execute", wraps=session.execute) as m: sync_table(StaticModel) - self.assertGreater(m.call_count, 0) + assert m.call_count > 0 statement = m.call_args[0][0].query_string - self.assertIn('"name" text static', statement) + assert '"name" text static' in statement # if we sync again, we should not apply an alter w/ a static sync_table(StaticModel) @@ -485,4 +489,4 @@ class StaticModel(Model): with mock.patch.object(session, "execute", wraps=session.execute) as m2: sync_table(StaticModel) - self.assertEqual(len(m2.call_args_list), 0) + assert len(m2.call_args_list) == 0 diff --git a/tests/integration/cqlengine/model/test_class_construction.py b/tests/integration/cqlengine/model/test_class_construction.py index dae97c4438..df0a57d543 100644 --- a/tests/integration/cqlengine/model/test_class_construction.py +++ b/tests/integration/cqlengine/model/test_class_construction.py @@ -20,6 +20,7 @@ from cassandra.cqlengine.query import ModelQuerySet, DMLQuery from tests.integration.cqlengine.base import BaseCassEngTestCase +import pytest class TestModelClassFunction(BaseCassEngTestCase): @@ -39,16 +40,16 @@ class TestModel(Model): text = columns.Text() # check class attibutes - self.assertHasAttr(TestModel, '_columns') - self.assertHasAttr(TestModel, 'id') - self.assertHasAttr(TestModel, 'text') + assert hasattr(TestModel, '_columns') + assert hasattr(TestModel, 'id') + assert hasattr(TestModel, 'text') # check instance attributes inst = TestModel() - self.assertHasAttr(inst, 'id') - self.assertHasAttr(inst, 'text') - self.assertIsNotNone(inst.id) - self.assertIsNone(inst.text) + assert hasattr(inst, 'id') + assert hasattr(inst, 'text') + assert inst.id is not None + assert inst.text is None def test_values_on_instantiation(self): """ @@ -61,15 +62,15 @@ class TestPerson(Model): # Check that defaults are available at instantiation. inst1 = TestPerson() - self.assertHasAttr(inst1, 'first_name') - self.assertHasAttr(inst1, 'last_name') - self.assertEqual(inst1.first_name, 'kevin') - self.assertEqual(inst1.last_name, 'deldycke') + assert hasattr(inst1, 'first_name') + assert hasattr(inst1, 'last_name') + assert inst1.first_name == 'kevin' + assert inst1.last_name == 'deldycke' # Check that values on instantiation overrides defaults. inst2 = TestPerson(first_name='bob', last_name='joe') - self.assertEqual(inst2.first_name, 'bob') - self.assertEqual(inst2.last_name, 'joe') + assert inst2.first_name == 'bob' + assert inst2.last_name == 'joe' def test_db_map(self): """ @@ -83,15 +84,15 @@ class WildDBNames(Model): numbers = columns.Integer(db_field='integers_etc') db_map = WildDBNames._db_map - self.assertEqual(db_map['words_and_whatnot'], 'content') - self.assertEqual(db_map['integers_etc'], 'numbers') + assert db_map['words_and_whatnot'] == 'content' + assert db_map['integers_etc'] == 'numbers' def test_attempting_to_make_duplicate_column_names_fails(self): """ Tests that trying to create conflicting db column names will fail """ - with self.assertRaisesRegex(ModelException, r".*more than once$"): + with pytest.raises(ModelException, match=r".*more than once$"): class BadNames(Model): words = columns.Text(primary_key=True) content = columns.Text(db_field='words') @@ -108,10 +109,10 @@ class Stuff(Model): content = columns.Text() numbers = columns.Integer() - self.assertEqual([x for x in Stuff._columns.keys()], ['id', 'words', 'content', 'numbers']) + assert [x for x in Stuff._columns.keys()] == ['id', 'words', 'content', 'numbers'] def test_exception_raised_when_creating_class_without_pk(self): - with self.assertRaises(ModelDefinitionException): + with pytest.raises(ModelDefinitionException): class TestModel(Model): count = columns.Integer() @@ -129,9 +130,9 @@ class Stuff(Model): inst1 = Stuff(num=5) inst2 = Stuff(num=7) - self.assertNotEqual(inst1.num, inst2.num) - self.assertEqual(inst1.num, 5) - self.assertEqual(inst2.num, 7) + assert inst1.num != inst2.num + assert inst1.num == 5 + assert inst2.num == 7 def test_superclass_fields_are_inherited(self): """ @@ -170,16 +171,16 @@ class ModelWithPartitionKeys(Model): cols = ModelWithPartitionKeys._columns - self.assertTrue(cols['c1'].primary_key) - self.assertFalse(cols['c1'].partition_key) + assert cols['c1'].primary_key + assert not cols['c1'].partition_key - self.assertTrue(cols['p1'].primary_key) - self.assertTrue(cols['p1'].partition_key) - self.assertTrue(cols['p2'].primary_key) - self.assertTrue(cols['p2'].partition_key) + assert cols['p1'].primary_key + assert cols['p1'].partition_key + assert cols['p2'].primary_key + assert cols['p2'].partition_key obj = ModelWithPartitionKeys(p1='a', p2='b') - self.assertEqual(obj.pk, ('a', 'b')) + assert obj.pk == ('a', 'b') def test_del_attribute_is_assigned_properly(self): """ Tests that columns that can be deleted have the del attribute """ @@ -191,7 +192,7 @@ class DelModel(Model): model = DelModel(key=4, data=5) del model.data - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): del model.key def test_does_not_exist_exceptions_are_not_shared_between_model(self): @@ -236,7 +237,7 @@ class NoKeyspace(Model): __abstract__ = True key = columns.UUID(primary_key=True) - self.assertEqual(len(warn), 0) + assert len(warn) == 0 class TestManualTableNaming(BaseCassEngTestCase): @@ -278,8 +279,8 @@ def test_proper_table_naming_case_insensitive(self): @test_category object_mapper """ - self.assertEqual(self.RenamedCaseInsensitiveTest.column_family_name(include_keyspace=False), 'manual_name') - self.assertEqual(self.RenamedCaseInsensitiveTest.column_family_name(include_keyspace=True), 'whatever.manual_name') + assert self.RenamedCaseInsensitiveTest.column_family_name(include_keyspace=False) == 'manual_name' + assert self.RenamedCaseInsensitiveTest.column_family_name(include_keyspace=True) == 'whatever.manual_name' def test_proper_table_naming_case_sensitive(self): """ @@ -292,8 +293,8 @@ def test_proper_table_naming_case_sensitive(self): @test_category object_mapper """ - self.assertEqual(self.RenamedCaseSensitiveTest.column_family_name(include_keyspace=False), '"Manual_Name"') - self.assertEqual(self.RenamedCaseSensitiveTest.column_family_name(include_keyspace=True), 'whatever."Manual_Name"') + assert self.RenamedCaseSensitiveTest.column_family_name(include_keyspace=False) == '"Manual_Name"' + assert self.RenamedCaseSensitiveTest.column_family_name(include_keyspace=True) == 'whatever."Manual_Name"' class AbstractModel(Model): @@ -339,18 +340,18 @@ def test_abstract_attribute_is_not_inherited(self): def test_attempting_to_save_abstract_model_fails(self): """ Attempting to save a model from an abstract model should fail """ - with self.assertRaises(CQLEngineException): + with pytest.raises(CQLEngineException): AbstractModelWithFullCols.create(pkey=1, data=2) def test_attempting_to_create_abstract_table_fails(self): """ Attempting to create a table from an abstract model should fail """ from cassandra.cqlengine.management import sync_table - with self.assertRaises(CQLEngineException): + with pytest.raises(CQLEngineException): sync_table(AbstractModelWithFullCols) def test_attempting_query_on_abstract_model_fails(self): """ Tests attempting to execute query with an abstract model fails """ - with self.assertRaises(CQLEngineException): + with pytest.raises(CQLEngineException): iter(AbstractModelWithFullCols.objects(pkey=5)).next() def test_abstract_columns_are_inherited(self): @@ -395,7 +396,7 @@ class CQModel(Model): part = columns.UUID(primary_key=True) data = columns.Text() - with self.assertRaises(self.TestException): + with pytest.raises(self.TestException): CQModel.create(part=uuid4(), data='s') def test_overriding_dmlqueryset(self): @@ -410,7 +411,7 @@ class CDQModel(Model): part = columns.UUID(primary_key=True) data = columns.Text() - with self.assertRaises(self.TestException): + with pytest.raises(self.TestException): CDQModel().save() @@ -422,4 +423,4 @@ def test_subclassing(self): class AlreadyLoadedTest(ConcreteModelWithCol): new_field = columns.Integer() - self.assertGreater(len(AlreadyLoadedTest()), length) + assert len(AlreadyLoadedTest()) > length diff --git a/tests/integration/cqlengine/model/test_model.py b/tests/integration/cqlengine/model/test_model.py index d5153843f5..98d71993fd 100644 --- a/tests/integration/cqlengine/model/test_model.py +++ b/tests/integration/cqlengine/model/test_model.py @@ -22,6 +22,7 @@ from uuid import uuid1 from tests.integration import pypy from tests.integration.cqlengine.base import TestQueryUpdateModel +import pytest class TestModel(unittest.TestCase): """ Tests the non-io functionality of models """ @@ -35,8 +36,8 @@ class EqualityModel(Model): m0 = EqualityModel(pk=0) m1 = EqualityModel(pk=1) - self.assertEqual(m0, m0) - self.assertNotEqual(m0, m1) + assert m0 == m0 + assert m0 != m1 def test_model_equality(self): """ tests the model equality functionality """ @@ -51,8 +52,8 @@ class EqualityModel1(Model): m0 = EqualityModel0(pk=0) m1 = EqualityModel1(kk=1) - self.assertEqual(m0, m0) - self.assertNotEqual(m0, m1) + assert m0 == m0 + assert m0 != m1 def test_keywords_as_names(self): """ @@ -87,8 +88,8 @@ class table(Model): created = table.create(select=0, table='table') selected = table.objects(select=0)[0] - self.assertEqual(created.select, selected.select) - self.assertEqual(created.table, selected.table) + assert created.select == selected.select + assert created.table == selected.table # Alter should work class table(Model): @@ -101,9 +102,9 @@ class table(Model): created = table.create(select=1, table='table') selected = table.objects(select=1)[0] - self.assertEqual(created.select, selected.select) - self.assertEqual(created.table, selected.table) - self.assertEqual(created.where, selected.where) + assert created.select == selected.select + assert created.table == selected.table + assert created.where == selected.where drop_keyspace('keyspace') @@ -112,18 +113,19 @@ class TestModel(Model): k = columns.Integer(primary_key=True) # no model keyspace uses default - self.assertEqual(TestModel.column_family_name(), "%s.test_model" % (models.DEFAULT_KEYSPACE,)) + assert TestModel.column_family_name() == "%s.test_model" % (models.DEFAULT_KEYSPACE,) # model keyspace overrides TestModel.__keyspace__ = "my_test_keyspace" - self.assertEqual(TestModel.column_family_name(), "%s.test_model" % (TestModel.__keyspace__,)) + assert TestModel.column_family_name() == "%s.test_model" % (TestModel.__keyspace__,) # neither set should raise CQLEngineException before failing or formatting an invalid name del TestModel.__keyspace__ with patch('cassandra.cqlengine.models.DEFAULT_KEYSPACE', None): - self.assertRaises(CQLEngineException, TestModel.column_family_name) + with pytest.raises(CQLEngineException): + TestModel.column_family_name() # .. but we can still get the bare CF name - self.assertEqual(TestModel.column_family_name(include_keyspace=False), "test_model") + assert TestModel.column_family_name(include_keyspace=False) == "test_model" def test_column_family_case_sensitive(self): """ @@ -141,15 +143,16 @@ class TestModel(Model): k = columns.Integer(primary_key=True) - self.assertEqual(TestModel.column_family_name(), '%s."TestModel"' % (models.DEFAULT_KEYSPACE,)) + assert TestModel.column_family_name() == '%s."TestModel"' % (models.DEFAULT_KEYSPACE,) TestModel.__keyspace__ = "my_test_keyspace" - self.assertEqual(TestModel.column_family_name(), '%s."TestModel"' % (TestModel.__keyspace__,)) + assert TestModel.column_family_name() == '%s."TestModel"' % (TestModel.__keyspace__,) del TestModel.__keyspace__ with patch('cassandra.cqlengine.models.DEFAULT_KEYSPACE', None): - self.assertRaises(CQLEngineException, TestModel.column_family_name) - self.assertEqual(TestModel.column_family_name(include_keyspace=False), '"TestModel"') + with pytest.raises(CQLEngineException): + TestModel.column_family_name() + assert TestModel.column_family_name(include_keyspace=False) == '"TestModel"' class BuiltInAttributeConflictTest(unittest.TestCase): @@ -157,7 +160,7 @@ class BuiltInAttributeConflictTest(unittest.TestCase): def test_model_with_attribute_name_conflict(self): """should raise exception when model defines column that conflicts with built-in attribute""" - with self.assertRaises(ModelDefinitionException): + with pytest.raises(ModelDefinitionException): class IllegalTimestampColumnModel(Model): my_primary_key = columns.Integer(primary_key=True) @@ -165,7 +168,7 @@ class IllegalTimestampColumnModel(Model): def test_model_with_method_name_conflict(self): """should raise exception when model defines column that conflicts with built-in method""" - with self.assertRaises(ModelDefinitionException): + with pytest.raises(ModelDefinitionException): class IllegalFilterColumnModel(Model): my_primary_key = columns.Integer(primary_key=True) @@ -216,11 +219,11 @@ def test_comparison(self): TestQueryUpdateModel.text_list.column, TestQueryUpdateModel.text_map.column] - self.assertEqual(l, sorted(l)) - self.assertNotEqual(TestQueryUpdateModel.partition.column, TestQueryUpdateModel.cluster.column) - self.assertLessEqual(TestQueryUpdateModel.partition.column, TestQueryUpdateModel.cluster.column) - self.assertGreater(TestQueryUpdateModel.cluster.column, TestQueryUpdateModel.partition.column) - self.assertGreaterEqual(TestQueryUpdateModel.cluster.column, TestQueryUpdateModel.partition.column) + assert l == sorted(l) + assert TestQueryUpdateModel.partition.column != TestQueryUpdateModel.cluster.column + assert TestQueryUpdateModel.partition.column <= TestQueryUpdateModel.cluster.column + assert TestQueryUpdateModel.cluster.column > TestQueryUpdateModel.partition.column + assert TestQueryUpdateModel.cluster.column >= TestQueryUpdateModel.partition.column class TestDeprecationWarning(unittest.TestCase): @@ -256,12 +259,8 @@ class SensitiveModel(Model): rows[-1] rows[-1:] - # ignore DeprecationWarning('The loop argument is deprecated since Python 3.8, and scheduled for removal in Python 3.10.') - relevant_warnings = [warn for warn in w if "The loop argument is deprecated" not in str(warn.message)] + warning_messages = [str(warn.message) for warn in w] - self.assertIn("__table_name_case_sensitive__ will be removed in 4.0.", str(relevant_warnings[0].message)) - self.assertIn("__table_name_case_sensitive__ will be removed in 4.0.", str(relevant_warnings[1].message)) - self.assertIn("ModelQuerySet indexing with negative indices support will be removed in 4.0.", - str(relevant_warnings[2].message)) - self.assertIn("ModelQuerySet slicing with negative indices support will be removed in 4.0.", - str(relevant_warnings[3].message)) + assert sum("__table_name_case_sensitive__ will be removed in 4.0." in message for message in warning_messages) == 2 + assert sum("ModelQuerySet indexing with negative indices support will be removed in 4.0." in message for message in warning_messages) == 1 + assert sum("ModelQuerySet slicing with negative indices support will be removed in 4.0." in message for message in warning_messages) == 1 diff --git a/tests/integration/cqlengine/model/test_model_io.py b/tests/integration/cqlengine/model/test_model_io.py index 81240e90c5..f55815310a 100644 --- a/tests/integration/cqlengine/model/test_model_io.py +++ b/tests/integration/cqlengine/model/test_model_io.py @@ -33,6 +33,7 @@ from tests.integration import PROTOCOL_VERSION, greaterthanorequalcass3_10 from tests.integration.cqlengine.base import BaseCassEngTestCase from tests.integration.cqlengine import DEFAULT_KEYSPACE +from tests.util import assertSetEqual class TestModel(Model): @@ -73,13 +74,13 @@ def test_model_save_and_load(self): Tests that models can be saved and retrieved, using the create method. """ tm = TestModel.create(count=8, text='123456789') - self.assertIsInstance(tm, TestModel) + assert isinstance(tm, TestModel) tm2 = TestModel.objects(id=tm.pk).first() - self.assertIsInstance(tm2, TestModel) + assert isinstance(tm2, TestModel) for cname in tm._columns.keys(): - self.assertEqual(getattr(tm, cname), getattr(tm2, cname)) + assert getattr(tm, cname) == getattr(tm2, cname) def test_model_instantiation_save_and_load(self): """ @@ -88,14 +89,14 @@ def test_model_instantiation_save_and_load(self): """ tm = TestModel(count=8, text='123456789') # Tests that values are available on instantiation. - self.assertIsNotNone(tm['id']) - self.assertEqual(tm.count, 8) - self.assertEqual(tm.text, '123456789') + assert tm['id'] is not None + assert tm.count == 8 + assert tm.text == '123456789' tm.save() tm2 = TestModel.objects(id=tm.id).first() for cname in tm._columns.keys(): - self.assertEqual(getattr(tm, cname), getattr(tm2, cname)) + assert getattr(tm, cname) == getattr(tm2, cname) def test_model_read_as_dict(self): """ @@ -108,18 +109,16 @@ def test_model_read_as_dict(self): 'text': tm.text, 'a_bool': tm.a_bool, } - self.assertEqual(sorted(tm.keys()), sorted(column_dict.keys())) + assert sorted(tm.keys()) == sorted(column_dict.keys()) - self.assertSetEqual(set(tm.values()), set(column_dict.values())) - self.assertEqual( - sorted(tm.items(), key=itemgetter(0)), - sorted(column_dict.items(), key=itemgetter(0))) - self.assertEqual(len(tm), len(column_dict)) + assertSetEqual(set(tm.values()), set(column_dict.values())) + assert sorted(tm.items(), key=itemgetter(0)) == sorted(column_dict.items(), key=itemgetter(0)) + assert len(tm) == len(column_dict) for column_id in column_dict.keys(): - self.assertEqual(tm[column_id], column_dict[column_id]) + assert tm[column_id] == column_dict[column_id] tm['count'] = 6 - self.assertEqual(tm.count, 6) + assert tm.count == 6 def test_model_updating_works_properly(self): """ @@ -132,8 +131,8 @@ def test_model_updating_works_properly(self): tm.save() tm2 = TestModel.objects(id=tm.pk).first() - self.assertEqual(tm.count, tm2.count) - self.assertEqual(tm.a_bool, tm2.a_bool) + assert tm.count == tm2.count + assert tm.a_bool == tm2.a_bool def test_model_deleting_works_properly(self): """ @@ -142,7 +141,7 @@ def test_model_deleting_works_properly(self): tm = TestModel.create(count=8, text='123456789') tm.delete() tm2 = TestModel.objects(id=tm.pk).first() - self.assertIsNone(tm2) + assert tm2 is None def test_column_deleting_works_properly(self): """ @@ -152,10 +151,10 @@ def test_column_deleting_works_properly(self): tm.save() tm2 = TestModel.objects(id=tm.pk).first() - self.assertIsInstance(tm2, TestModel) + assert isinstance(tm2, TestModel) - self.assertTrue(tm2.text is None) - self.assertTrue(tm2._values['text'].previous_value is None) + assert tm2.text is None + assert tm2._values['text'].previous_value is None def test_a_sensical_error_is_raised_if_you_try_to_create_a_table_twice(self): """ @@ -212,26 +211,26 @@ class AllDatatypesModel(Model): m=UUID('067e6162-3b6f-4ae2-a171-2470b63dff00'), n=int(str(2147483647) + '000'), o=Duration(2, 3, 4)) - self.assertEqual(1, AllDatatypesModel.objects.count()) + assert 1 == AllDatatypesModel.objects.count() output = AllDatatypesModel.objects.first() for i, i_char in enumerate(range(ord('a'), ord('a') + 14)): - self.assertEqual(input[i], output[chr(i_char)]) + assert input[i] == output[chr(i_char)] def test_can_specify_none_instead_of_default(self): - self.assertIsNotNone(TestModel.a_bool.column.default) + assert TestModel.a_bool.column.default is not None # override default inst = TestModel.create(a_bool=None) - self.assertIsNone(inst.a_bool) + assert inst.a_bool is None queried = TestModel.objects(id=inst.id).first() - self.assertIsNone(queried.a_bool) + assert queried.a_bool is None # letting default be set inst = TestModel.create() - self.assertEqual(inst.a_bool, TestModel.a_bool.column.default) + assert inst.a_bool == TestModel.a_bool.column.default queried = TestModel.objects(id=inst.id).first() - self.assertEqual(queried.a_bool, TestModel.a_bool.column.default) + assert queried.a_bool == TestModel.a_bool.column.default def test_can_insert_model_with_all_protocol_v4_column_types(self): """ @@ -265,11 +264,11 @@ class v4DatatypesModel(Model): v4DatatypesModel.create(id=0, a=date(1970, 1, 1), b=32523, c=time(16, 47, 25, 7), d=123) - self.assertEqual(1, v4DatatypesModel.objects.count()) + assert 1 == v4DatatypesModel.objects.count() output = v4DatatypesModel.objects.first() for i, i_char in enumerate(range(ord('a'), ord('a') + 3)): - self.assertEqual(input[i], output[chr(i_char)]) + assert input[i] == output[chr(i_char)] def test_can_insert_double_and_float(self): """ @@ -292,16 +291,16 @@ class FloatingPointModel(Model): FloatingPointModel.create(id=0, f=2.39) output = FloatingPointModel.objects.first() - self.assertEqual(2.390000104904175, output.f) # float loses precision + assert 2.390000104904175 == output.f # float loses precision FloatingPointModel.create(id=0, f=3.4028234663852886e+38, d=2.39) output = FloatingPointModel.objects.first() - self.assertEqual(3.4028234663852886e+38, output.f) - self.assertEqual(2.39, output.d) # double retains precision + assert 3.4028234663852886e+38 == output.f + assert 2.39 == output.d # double retains precision FloatingPointModel.create(id=0, d=3.4028234663852886e+38) output = FloatingPointModel.objects.first() - self.assertEqual(3.4028234663852886e+38, output.d) + assert 3.4028234663852886e+38 == output.d class TestMultiKeyModel(Model): @@ -331,11 +330,11 @@ def test_deleting_only_deletes_one_object(self): for i in range(5): TestMultiKeyModel.create(partition=partition, cluster=i, count=i, text=str(i)) - self.assertTrue(TestMultiKeyModel.filter(partition=partition).count() == 5) + assert TestMultiKeyModel.filter(partition=partition).count() == 5 TestMultiKeyModel.get(partition=partition, cluster=0).delete() - self.assertTrue(TestMultiKeyModel.filter(partition=partition).count() == 4) + assert TestMultiKeyModel.filter(partition=partition).count() == 4 TestMultiKeyModel.filter(partition=partition).delete() @@ -370,8 +369,8 @@ def test_vanilla_update(self): self.instance.save() check = TestMultiKeyModel.get(partition=self.instance.partition, cluster=self.instance.cluster) - self.assertTrue(check.count == 5) - self.assertTrue(check.text == 'happy') + assert check.count == 5 + assert check.text == 'happy' def test_deleting_only(self): self.instance.count = None @@ -379,79 +378,79 @@ def test_deleting_only(self): self.instance.save() check = TestMultiKeyModel.get(partition=self.instance.partition, cluster=self.instance.cluster) - self.assertTrue(check.count is None) - self.assertTrue(check.text is None) + assert check.count is None + assert check.text is None def test_get_changed_columns(self): - self.assertTrue(self.instance.get_changed_columns() == []) + assert self.instance.get_changed_columns() == [] self.instance.count = 1 changes = self.instance.get_changed_columns() - self.assertTrue(len(changes) == 1) - self.assertTrue(changes == ['count']) + assert len(changes) == 1 + assert changes == ['count'] self.instance.save() - self.assertTrue(self.instance.get_changed_columns() == []) + assert self.instance.get_changed_columns() == [] def test_previous_value_tracking_of_persisted_instance(self): # Check initial internal states. - self.assertTrue(self.instance.get_changed_columns() == []) - self.assertTrue(self.instance._values['count'].previous_value == 0) + assert self.instance.get_changed_columns() == [] + assert self.instance._values['count'].previous_value == 0 # Change value and check internal states. self.instance.count = 1 - self.assertTrue(self.instance.get_changed_columns() == ['count']) - self.assertTrue(self.instance._values['count'].previous_value == 0) + assert self.instance.get_changed_columns() == ['count'] + assert self.instance._values['count'].previous_value == 0 # Internal states should be updated on save. self.instance.save() - self.assertTrue(self.instance.get_changed_columns() == []) - self.assertTrue(self.instance._values['count'].previous_value == 1) + assert self.instance.get_changed_columns() == [] + assert self.instance._values['count'].previous_value == 1 # Change value twice. self.instance.count = 2 - self.assertTrue(self.instance.get_changed_columns() == ['count']) - self.assertTrue(self.instance._values['count'].previous_value == 1) + assert self.instance.get_changed_columns() == ['count'] + assert self.instance._values['count'].previous_value == 1 self.instance.count = 3 - self.assertTrue(self.instance.get_changed_columns() == ['count']) - self.assertTrue(self.instance._values['count'].previous_value == 1) + assert self.instance.get_changed_columns() == ['count'] + assert self.instance._values['count'].previous_value == 1 # Internal states updated on save. self.instance.save() - self.assertTrue(self.instance.get_changed_columns() == []) - self.assertTrue(self.instance._values['count'].previous_value == 3) + assert self.instance.get_changed_columns() == [] + assert self.instance._values['count'].previous_value == 3 # Change value and reset it. self.instance.count = 2 - self.assertTrue(self.instance.get_changed_columns() == ['count']) - self.assertTrue(self.instance._values['count'].previous_value == 3) + assert self.instance.get_changed_columns() == ['count'] + assert self.instance._values['count'].previous_value == 3 self.instance.count = 3 - self.assertTrue(self.instance.get_changed_columns() == []) - self.assertTrue(self.instance._values['count'].previous_value == 3) + assert self.instance.get_changed_columns() == [] + assert self.instance._values['count'].previous_value == 3 # Nothing to save: values in initial conditions. self.instance.save() - self.assertTrue(self.instance.get_changed_columns() == []) - self.assertTrue(self.instance._values['count'].previous_value == 3) + assert self.instance.get_changed_columns() == [] + assert self.instance._values['count'].previous_value == 3 # Change Multiple values self.instance.count = 4 self.instance.text = "changed" - self.assertTrue(len(self.instance.get_changed_columns()) == 2) - self.assertTrue('text' in self.instance.get_changed_columns()) - self.assertTrue('count' in self.instance.get_changed_columns()) + assert len(self.instance.get_changed_columns()) == 2 + assert 'text' in self.instance.get_changed_columns() + assert 'count' in self.instance.get_changed_columns() self.instance.save() - self.assertTrue(self.instance.get_changed_columns() == []) + assert self.instance.get_changed_columns() == [] # Reset Multiple Values self.instance.count = 5 self.instance.text = "changed" - self.assertTrue(self.instance.get_changed_columns() == ['count']) + assert self.instance.get_changed_columns() == ['count'] self.instance.text = "changed2" - self.assertTrue(len(self.instance.get_changed_columns()) == 2) - self.assertTrue('text' in self.instance.get_changed_columns()) - self.assertTrue('count' in self.instance.get_changed_columns()) + assert len(self.instance.get_changed_columns()) == 2 + assert 'text' in self.instance.get_changed_columns() + assert 'count' in self.instance.get_changed_columns() self.instance.count = 4 self.instance.text = "changed" - self.assertTrue(self.instance.get_changed_columns() == []) + assert self.instance.get_changed_columns() == [] def test_previous_value_tracking_on_instantiation(self): self.instance = TestMultiKeyModel( @@ -461,30 +460,30 @@ def test_previous_value_tracking_on_instantiation(self): text='happy') # Columns of instances not persisted yet should be marked as changed. - self.assertTrue(set(self.instance.get_changed_columns()) == set([ - 'partition', 'cluster', 'count', 'text'])) - self.assertTrue(self.instance._values['partition'].previous_value is None) - self.assertTrue(self.instance._values['cluster'].previous_value is None) - self.assertTrue(self.instance._values['count'].previous_value is None) - self.assertTrue(self.instance._values['text'].previous_value is None) + assert set(self.instance.get_changed_columns()) == set([ + 'partition', 'cluster', 'count', 'text']) + assert self.instance._values['partition'].previous_value is None + assert self.instance._values['cluster'].previous_value is None + assert self.instance._values['count'].previous_value is None + assert self.instance._values['text'].previous_value is None # Value changes doesn't affect internal states. self.instance.count = 1 - self.assertTrue('count' in self.instance.get_changed_columns()) - self.assertTrue(self.instance._values['count'].previous_value is None) + assert 'count' in self.instance.get_changed_columns() + assert self.instance._values['count'].previous_value is None self.instance.count = 2 - self.assertTrue('count' in self.instance.get_changed_columns()) - self.assertTrue(self.instance._values['count'].previous_value is None) + assert 'count' in self.instance.get_changed_columns() + assert self.instance._values['count'].previous_value is None # Value reset is properly tracked. self.instance.count = None - self.assertTrue('count' not in self.instance.get_changed_columns()) - self.assertTrue(self.instance._values['count'].previous_value is None) + assert 'count' not in self.instance.get_changed_columns() + assert self.instance._values['count'].previous_value is None self.instance.save() - self.assertTrue(self.instance.get_changed_columns() == []) - self.assertTrue(self.instance._values['count'].previous_value is None) - self.assertTrue(self.instance.count is None) + assert self.instance.get_changed_columns() == [] + assert self.instance._values['count'].previous_value is None + assert self.instance.count is None def test_previous_value_tracking_on_instantiation_with_default(self): @@ -503,31 +502,31 @@ class TestDefaultValueTracking(Model): int3=7777, int5=5555) - self.assertEqual(instance.id, 1) - self.assertEqual(instance.int1, 9999) - self.assertEqual(instance.int2, 456) - self.assertEqual(instance.int3, 7777) - self.assertIsNotNone(instance.int4) - self.assertIsInstance(instance.int4, int) - self.assertGreaterEqual(instance.int4, 0) - self.assertLessEqual(instance.int4, 1000) - self.assertEqual(instance.int5, 5555) - self.assertTrue(instance.int6 is None) + assert instance.id == 1 + assert instance.int1 == 9999 + assert instance.int2 == 456 + assert instance.int3 == 7777 + assert instance.int4 is not None + assert isinstance(instance.int4, int) + assert instance.int4 >= 0 + assert instance.int4 <= 1000 + assert instance.int5 == 5555 + assert instance.int6 is None # All previous values are unset as the object hasn't been persisted # yet. - self.assertTrue(instance._values['id'].previous_value is None) - self.assertTrue(instance._values['int1'].previous_value is None) - self.assertTrue(instance._values['int2'].previous_value is None) - self.assertTrue(instance._values['int3'].previous_value is None) - self.assertTrue(instance._values['int4'].previous_value is None) - self.assertTrue(instance._values['int5'].previous_value is None) - self.assertTrue(instance._values['int6'].previous_value is None) + assert instance._values['id'].previous_value is None + assert instance._values['int1'].previous_value is None + assert instance._values['int2'].previous_value is None + assert instance._values['int3'].previous_value is None + assert instance._values['int4'].previous_value is None + assert instance._values['int5'].previous_value is None + assert instance._values['int6'].previous_value is None # All explicitely set columns, and those with default values are # flagged has changed. - self.assertTrue(set(instance.get_changed_columns()) == set([ - 'id', 'int1', 'int3', 'int5'])) + assert set(instance.get_changed_columns()) == set([ + 'id', 'int1', 'int3', 'int5']) def test_save_to_none(self): """ @@ -554,20 +553,20 @@ def test_save_to_none(self): text_set=text_set, text_map=text_map) initial.save() current = TestModelSave.objects.get(partition=partition, cluster=cluster) - self.assertEqual(current.text, text) - self.assertEqual(current.text_list, text_list) - self.assertEqual(current.text_set, text_set) - self.assertEqual(current.text_map, text_map) + assert current.text == text + assert current.text_list == text_list + assert current.text_set == text_set + assert current.text_map == text_map next = TestModelSave(partition=partition, cluster=cluster, text=None, text_list=None, text_set=None, text_map=None) next.save() current = TestModelSave.objects.get(partition=partition, cluster=cluster) - self.assertEqual(current.text, None) - self.assertEqual(current.text_list, []) - self.assertEqual(current.text_set, set()) - self.assertEqual(current.text_map, {}) + assert current.text == None + assert current.text_list == [] + assert current.text_set == set() + assert current.text_map == {} def test_none_filter_fails(): @@ -602,28 +601,28 @@ def test_success_case(self): # object hasn't been saved, # shouldn't be able to update - self.assertTrue(not tm._is_persisted) - self.assertTrue(not tm._can_update()) + assert not tm._is_persisted + assert not tm._can_update() tm.save() # object has been saved, # should be able to update - self.assertTrue(tm._is_persisted) - self.assertTrue(tm._can_update()) + assert tm._is_persisted + assert tm._can_update() tm.count = 200 # primary keys haven't changed, # should still be able to update - self.assertTrue(tm._can_update()) + assert tm._can_update() tm.save() tm.id = uuid4() # primary keys have changed, # should not be able to update - self.assertTrue(not tm._can_update()) + assert not tm._can_update() class IndexDefinitionModel(Model): @@ -656,9 +655,9 @@ def test_reserved_cql_words_can_be_used_as_column_names(self): model2 = ReservedWordModel.filter(token='1') - self.assertTrue(len(model2) == 1) - self.assertTrue(model1.token == model2[0].token) - self.assertTrue(model1.insert == model2[0].insert) + assert len(model2) == 1 + assert model1.token == model2[0].token + assert model1.insert == model2[0].insert class TestQueryModel(Model): @@ -697,14 +696,14 @@ def test_query_with_date(self): day = date(2013, 11, 26) obj = TestQueryModel.create(test_id=uid, date=day, description=u'foo') - self.assertEqual(obj.description, u'foo') + assert obj.description == u'foo' inst = TestQueryModel.filter( TestQueryModel.test_id == uid, TestQueryModel.date == day).limit(1).first() - self.assertTrue(inst.test_id == uid) - self.assertTrue(inst.date == day) + assert inst.test_id == uid + assert inst.date == day class BasicModelNoRouting(Model): @@ -774,16 +773,16 @@ def test_routing_key_is_ignored(self): mrk = BasicModelNoRouting._routing_key_from_values([1], self.session.cluster.protocol_version) simple = SimpleStatement("") simple.routing_key = mrk - self.assertNotEqual(bound.routing_key, simple.routing_key) + assert bound.routing_key != simple.routing_key # Verify that basic create, update and delete work with no routing key t = BasicModelNoRouting.create(k=2, v=3) t.update(v=4).save() f = BasicModelNoRouting.objects.filter(k=2).first() - self.assertEqual(t, f) + assert t == f t.delete() - self.assertEqual(BasicModelNoRouting.objects.count(), 0) + assert BasicModelNoRouting.objects.count() == 0 def test_routing_key_generation_basic(self): @@ -806,7 +805,7 @@ def test_routing_key_generation_basic(self): mrk = BasicModel._routing_key_from_values([1], self.session.cluster.protocol_version) simple = SimpleStatement("") simple.routing_key = mrk - self.assertEqual(bound.routing_key, simple.routing_key) + assert bound.routing_key == simple.routing_key def test_routing_key_generation_multi(self): """ @@ -827,7 +826,7 @@ def test_routing_key_generation_multi(self): mrk = BasicModelMulti._routing_key_from_values([1, 2], self.session.cluster.protocol_version) simple = SimpleStatement("") simple.routing_key = mrk - self.assertEqual(bound.routing_key, simple.routing_key) + assert bound.routing_key == simple.routing_key def test_routing_key_generation_complex(self): """ @@ -853,7 +852,7 @@ def test_routing_key_generation_complex(self): mrk = ComplexModelRouting._routing_key_from_values([partition, cluster, text, float], self.session.cluster.protocol_version) simple = SimpleStatement("") simple.routing_key = mrk - self.assertEqual(bound.routing_key, simple.routing_key) + assert bound.routing_key == simple.routing_key def test_partition_key_index(self): """ @@ -899,7 +898,7 @@ def _check_partition_value_generation(self, model, state, reverse=False): # Those specified in the models partition field for indx, value in enumerate(state.partition_key_values(model._partition_key_index)): name = res.get(value) - self.assertEqual(indx, model._partition_key_index.get(name)) + assert indx == model._partition_key_index.get(name) def test_none_filter_fails(): diff --git a/tests/integration/cqlengine/model/test_polymorphism.py b/tests/integration/cqlengine/model/test_polymorphism.py index f27703367d..a37b499df6 100644 --- a/tests/integration/cqlengine/model/test_polymorphism.py +++ b/tests/integration/cqlengine/model/test_polymorphism.py @@ -20,20 +20,21 @@ from cassandra.cqlengine.connection import get_session from tests.integration.cqlengine.base import BaseCassEngTestCase from cassandra.cqlengine import management +import pytest class TestInheritanceClassConstruction(BaseCassEngTestCase): def test_multiple_discriminator_value_failure(self): """ Tests that defining a model with more than one discriminator column fails """ - with self.assertRaises(models.ModelDefinitionException): + with pytest.raises(models.ModelDefinitionException): class M(models.Model): partition = columns.Integer(primary_key=True) type1 = columns.Integer(discriminator_column=True) type2 = columns.Integer(discriminator_column=True) def test_no_discriminator_column_failure(self): - with self.assertRaises(models.ModelDefinitionException): + with pytest.raises(models.ModelDefinitionException): class M(models.Model): __discriminator_value__ = 1 @@ -86,7 +87,7 @@ class M1(Base): assert Base.column_family_name() == M1.column_family_name() def test_collection_columns_cant_be_discriminator_column(self): - with self.assertRaises(models.ModelDefinitionException): + with pytest.raises(models.ModelDefinitionException): class Base(models.Model): partition = columns.Integer(primary_key=True) @@ -124,7 +125,7 @@ def tearDownClass(cls): management.drop_table(Inherit2) def test_saving_base_model_fails(self): - with self.assertRaises(models.PolymorphicModelException): + with pytest.raises(models.PolymorphicModelException): InheritBase.create() def test_saving_subclass_saves_disc_value(self): @@ -154,7 +155,7 @@ def test_delete_on_subclass_does_not_include_disc_value(self): # not sure how we would even get here if it was in there # since the CQL would fail. - self.assertNotIn("row_type", m.call_args[0][0].query_string) + assert "row_type" not in m.call_args[0][0].query_string class UnindexedInheritBase(models.Model): @@ -210,9 +211,9 @@ def test_subclassed_model_results_work_properly(self): assert len(list(UnindexedInherit2.objects(partition=p1.partition, cluster__in=[p2.cluster, p3.cluster]))) == 2 def test_conflicting_type_results(self): - with self.assertRaises(models.PolymorphicModelException): + with pytest.raises(models.PolymorphicModelException): list(UnindexedInherit1.objects(partition=self.p1.partition)) - with self.assertRaises(models.PolymorphicModelException): + with pytest.raises(models.PolymorphicModelException): list(UnindexedInherit2.objects(partition=self.p1.partition)) @@ -251,5 +252,5 @@ def tearDownClass(cls): management.drop_table(IndexedInherit2) def test_success_case(self): - self.assertEqual(len(list(IndexedInherit1.objects(partition=self.p1.partition))), 1) - self.assertEqual(len(list(IndexedInherit2.objects(partition=self.p1.partition))), 1) + assert len(list(IndexedInherit1.objects(partition=self.p1.partition))) == 1 + assert len(list(IndexedInherit2.objects(partition=self.p1.partition))) == 1 diff --git a/tests/integration/cqlengine/model/test_udts.py b/tests/integration/cqlengine/model/test_udts.py index 7063df8caa..80f1b9693f 100644 --- a/tests/integration/cqlengine/model/test_udts.py +++ b/tests/integration/cqlengine/model/test_udts.py @@ -28,6 +28,7 @@ from tests.integration import PROTOCOL_VERSION from tests.integration.cqlengine.base import BaseCassEngTestCase from tests.integration.cqlengine import DEFAULT_KEYSPACE +import pytest class User(UserType): @@ -75,8 +76,8 @@ class User(UserType): sync_type(DEFAULT_KEYSPACE, User) user = User(age=42, name="John") - self.assertEqual(42, user.age) - self.assertEqual("John", user.name) + assert 42 == user.age + assert "John" == user.name # Add a field class User(UserType): @@ -88,9 +89,9 @@ class User(UserType): user = User(age=42) user["name"] = "John" user["gender"] = "male" - self.assertEqual(42, user.age) - self.assertEqual("John", user.name) - self.assertEqual("male", user.gender) + assert 42 == user.age + assert "John" == user.name + assert "male" == user.gender # Remove a field class User(UserType): @@ -99,7 +100,7 @@ class User(UserType): sync_type(DEFAULT_KEYSPACE, User) user = User(age=42, name="John", gender="male") - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): user.gender def test_can_insert_udts(self): @@ -110,13 +111,13 @@ def test_can_insert_udts(self): user = User(age=42, name="John") UserModel.create(id=0, info=user) - self.assertEqual(1, UserModel.objects.count()) + assert 1 == UserModel.objects.count() john = UserModel.objects.first() - self.assertEqual(0, john.id) - self.assertTrue(type(john.info) is User) - self.assertEqual(42, john.info.age) - self.assertEqual("John", john.info.name) + assert 0 == john.id + assert type(john.info) is User + assert 42 == john.info.age + assert "John" == john.info.name def test_can_update_udts(self): sync_table(UserModel) @@ -126,15 +127,15 @@ def test_can_update_udts(self): created_user = UserModel.create(id=0, info=user) john_info = UserModel.objects.first().info - self.assertEqual(42, john_info.age) - self.assertEqual("John", john_info.name) + assert 42 == john_info.age + assert "John" == john_info.name created_user.info = User(age=22, name="Mary") created_user.update() mary_info = UserModel.objects.first().info - self.assertEqual(22, mary_info["age"]) - self.assertEqual("Mary", mary_info["name"]) + assert 22 == mary_info["age"] + assert "Mary" == mary_info["name"] def test_can_update_udts_with_nones(self): sync_table(UserModel) @@ -144,14 +145,14 @@ def test_can_update_udts_with_nones(self): created_user = UserModel.create(id=0, info=user) john_info = UserModel.objects.first().info - self.assertEqual(42, john_info.age) - self.assertEqual("John", john_info.name) + assert 42 == john_info.age + assert "John" == john_info.name created_user.info = None created_user.update() john_info = UserModel.objects.first().info - self.assertIsNone(john_info) + assert john_info is None def test_can_create_same_udt_different_keyspaces(self): sync_type(DEFAULT_KEYSPACE, User) @@ -177,17 +178,17 @@ class UserModelGender(Model): UserModelGender.create(id=0, info=user) john_info = UserModelGender.objects.first().info - self.assertEqual(42, john_info.age) - self.assertEqual("John", john_info.name) - self.assertIsNone(john_info.gender) + assert 42 == john_info.age + assert "John" == john_info.name + assert john_info.gender is None user = UserGender(age=42) UserModelGender.create(id=0, info=user) john_info = UserModelGender.objects.first().info - self.assertEqual(42, john_info.age) - self.assertIsNone(john_info.name) - self.assertIsNone(john_info.gender) + assert 42 == john_info.age + assert john_info.name is None + assert john_info.gender is None def test_can_insert_nested_udts(self): class Depth_0(UserType): @@ -221,10 +222,10 @@ class DepthModel(Model): DepthModel.create(id=0, v_0=udts[0], v_1=udts[1], v_2=udts[2], v_3=udts[3]) output = DepthModel.objects.first() - self.assertEqual(udts[0], output.v_0) - self.assertEqual(udts[1], output.v_1) - self.assertEqual(udts[2], output.v_2) - self.assertEqual(udts[3], output.v_3) + assert udts[0] == output.v_0 + assert udts[1] == output.v_1 + assert udts[2] == output.v_2 + assert udts[3] == output.v_3 def test_can_insert_udts_with_nones(self): """ @@ -248,10 +249,10 @@ def test_can_insert_udts_with_nones(self): l=None, m=None, n=None) AllDatatypesModel.create(id=0, data=input) - self.assertEqual(1, AllDatatypesModel.objects.count()) + assert 1 == AllDatatypesModel.objects.count() output = AllDatatypesModel.objects.first().data - self.assertEqual(input, output) + assert input == output def test_can_insert_udts_with_all_datatypes(self): """ @@ -278,11 +279,11 @@ def test_can_insert_udts_with_all_datatypes(self): m=UUID('067e6162-3b6f-4ae2-a171-2470b63dff00'), n=int(str(2147483647) + '000')) AllDatatypesModel.create(id=0, data=input) - self.assertEqual(1, AllDatatypesModel.objects.count()) + assert 1 == AllDatatypesModel.objects.count() output = AllDatatypesModel.objects.first().data for i in range(ord('a'), ord('a') + 14): - self.assertEqual(input[chr(i)], output[chr(i)]) + assert input[chr(i)] == output[chr(i)] def test_can_insert_udts_protocol_v4_datatypes(self): """ @@ -320,11 +321,11 @@ class Allv4DatatypesModel(Model): input = Allv4Datatypes(a=Date(date(1970, 1, 1)), b=32523, c=Time(time(16, 47, 25, 7)), d=123) Allv4DatatypesModel.create(id=0, data=input) - self.assertEqual(1, Allv4DatatypesModel.objects.count()) + assert 1 == Allv4DatatypesModel.objects.count() output = Allv4DatatypesModel.objects.first().data for i in range(ord('a'), ord('a') + 3): - self.assertEqual(input[chr(i)], output[chr(i)]) + assert input[chr(i)] == output[chr(i)] def test_nested_udts_inserts(self): """ @@ -364,9 +365,9 @@ class Container(Model): Container.create(id=UUID('FE2B4360-28C6-11E2-81C1-0800200C9A66'), names=names) # Validate input and output matches - self.assertEqual(1, Container.objects.count()) + assert 1 == Container.objects.count() names_output = Container.objects.first().names - self.assertEqual(names_output, names) + assert names_output == names def test_udts_with_unicode(self): """ @@ -407,8 +408,8 @@ def test_register_default_keyspace(self): # None emulating no model and no default keyspace before connecting connection.udt_by_keyspace.clear() User.register_for_keyspace(None) - self.assertEqual(len(connection.udt_by_keyspace), 1) - self.assertIn(None, connection.udt_by_keyspace) + assert len(connection.udt_by_keyspace) == 1 + assert None in connection.udt_by_keyspace # register should be with default keyspace, not None cluster = Mock() @@ -443,9 +444,9 @@ class TheModel(Model): type_fields = (db_field_different.age.column, db_field_different.name.column) - self.assertEqual(len(type_meta.field_names), len(type_fields)) + assert len(type_meta.field_names) == len(type_fields) for f in type_fields: - self.assertIn(f.db_field_name, type_meta.field_names) + assert f.db_field_name in type_meta.field_names id = 0 age = 42 @@ -453,17 +454,17 @@ class TheModel(Model): info = db_field_different(age=age, name=name) TheModel.create(id=id, info=info) - self.assertEqual(1, TheModel.objects.count()) + assert 1 == TheModel.objects.count() john = TheModel.objects.first() - self.assertEqual(john.id, id) + assert john.id == id info = john.info - self.assertIsInstance(info, db_field_different) - self.assertEqual(info.age, age) - self.assertEqual(info.name, name) + assert isinstance(info, db_field_different) + assert info.age == age + assert info.name == name # also excercise the db_Field mapping - self.assertEqual(info.a, age) - self.assertEqual(info.n, name) + assert info.a == age + assert info.n == name def test_db_field_overload(self): """ @@ -478,12 +479,12 @@ def test_db_field_overload(self): @test_category data_types:udt """ - with self.assertRaises(UserTypeDefinitionException): + with pytest.raises(UserTypeDefinitionException): class something_silly(UserType): first_col = columns.Integer() second_col = columns.Text(db_field='first_col') - with self.assertRaises(UserTypeDefinitionException): + with pytest.raises(UserTypeDefinitionException): class something_silly_2(UserType): first_col = columns.Integer(db_field="second_col") second_col = columns.Text() @@ -493,7 +494,7 @@ def test_set_udt_fields(self): u = User() u.age = 20 - self.assertEqual(20, u.age) + assert 20 == u.age def test_default_values(self): """ @@ -526,10 +527,10 @@ class OuterModel(Model): t.nested = [NestedUdt(something='test')] t.simple = NestedUdt(something="") t.save() - self.assertIsNotNone(t.nested[0].test_id) - self.assertEqual(t.nested[0].default_text, "default text") - self.assertIsNotNone(t.simple.test_id) - self.assertEqual(t.simple.default_text, "default text") + assert t.nested[0].test_id is not None + assert t.nested[0].default_text == "default text" + assert t.simple.test_id is not None + assert t.simple.default_text == "default text" def test_udt_validate(self): """ @@ -556,7 +557,7 @@ class UserModelValidate(Model): user = UserValidate(age=1, name="Robert") item = UserModelValidate(id=1, info=user) - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): item.save() def test_udt_validate_with_default(self): @@ -584,5 +585,5 @@ class UserModelValidateDefault(Model): user = UserValidateDefault(age=1) item = UserModelValidateDefault(id=1, info=user) - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): item.save() diff --git a/tests/integration/cqlengine/model/test_updates.py b/tests/integration/cqlengine/model/test_updates.py index 718c651880..c64df8fdcc 100644 --- a/tests/integration/cqlengine/model/test_updates.py +++ b/tests/integration/cqlengine/model/test_updates.py @@ -23,6 +23,7 @@ from cassandra.cqlengine import columns from cassandra.cqlengine.management import sync_table, drop_table from cassandra.cqlengine.usertype import UserType +import pytest class TestUpdateModel(Model): __test__ = False @@ -60,18 +61,18 @@ def test_update_model(self): # database should reflect both updates m2 = TestUpdateModel.get(partition=m0.partition, cluster=m0.cluster) - self.assertEqual(m2.count, m1.count) - self.assertEqual(m2.text, m0.text) + assert m2.count == m1.count + assert m2.text == m0.text #This shouldn't raise a Validation error as the PR is not changing m0.update(partition=m0.partition, cluster=m0.cluster) #Assert a ValidationError is risen if the PR changes - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): m0.update(partition=m0.partition, cluster=20) # Assert a ValidationError is risen if the columns doesn't exist - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): m0.update(invalid_column=20) def test_update_values(self): @@ -85,12 +86,12 @@ def test_update_values(self): # update the text, and call update m0.update(text='monkey land') - self.assertEqual(m0.text, 'monkey land') + assert m0.text == 'monkey land' # database should reflect both updates m2 = TestUpdateModel.get(partition=m0.partition, cluster=m0.cluster) - self.assertEqual(m2.count, m1.count) - self.assertEqual(m2.text, m0.text) + assert m2.count == m1.count + assert m2.text == m0.text def test_noop_model_direct_update(self): """ Tests that calling update on a model with no changes will do nothing. """ @@ -139,13 +140,13 @@ def test_noop_model_assignation_update(self): def test_invalid_update_kwarg(self): """ tests that passing in a kwarg to the update method that isn't a column will fail """ m0 = TestUpdateModel.create(count=5, text='monkey') - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): m0.update(numbers=20) def test_primary_key_update_failure(self): """ tests that attempting to update the value of a primary key will fail """ m0 = TestUpdateModel.create(count=5, text='monkey') - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): m0.update(partition=uuid4()) @@ -206,15 +207,13 @@ def test_value_override_with_default(self): initial = ModelWithDefault(id=1, mf={0: 0}, dummy=0, udt=first_udt, udt_default=first_udt) initial.save() - self.assertEqual(ModelWithDefault.get()._as_dict(), - {'id': 1, 'dummy': 0, 'mf': {0: 0}, "udt": first_udt, "udt_default": first_udt}) + assert ModelWithDefault.get()._as_dict() == {'id': 1, 'dummy': 0, 'mf': {0: 0}, "udt": first_udt, "udt_default": first_udt} second_udt = UDT(age=1, mf={3: 3}, dummy_udt=12) second = ModelWithDefault(id=1) second.update(mf={0: 1}, udt=second_udt) - self.assertEqual(ModelWithDefault.get()._as_dict(), - {'id': 1, 'dummy': 0, 'mf': {0: 1}, "udt": second_udt, "udt_default": first_udt}) + assert ModelWithDefault.get()._as_dict() == {'id': 1, 'dummy': 0, 'mf': {0: 1}, "udt": second_udt, "udt_default": first_udt} def test_value_is_written_if_is_default(self): """ @@ -231,8 +230,7 @@ def test_value_is_written_if_is_default(self): initial.udt_default = self.udt_default initial.update() - self.assertEqual(ModelWithDefault.get()._as_dict(), - {'id': 1, 'dummy': 42, 'mf': {0: 0}, "udt": None, "udt_default": self.udt_default}) + assert ModelWithDefault.get()._as_dict() == {'id': 1, 'dummy': 42, 'mf': {0: 0}, "udt": None, "udt_default": self.udt_default} def test_null_update_is_respected(self): """ @@ -253,8 +251,7 @@ def test_null_update_is_respected(self): updated_udt = UDT(age=1, mf={2:2}, dummy_udt=None) obj.update(dummy=None, udt_default=updated_udt) - self.assertEqual(ModelWithDefault.get()._as_dict(), - {'id': 1, 'dummy': None, 'mf': {0: 0}, "udt": None, "udt_default": updated_udt}) + assert ModelWithDefault.get()._as_dict() == {'id': 1, 'dummy': None, 'mf': {0: 0}, "udt": None, "udt_default": updated_udt} def test_only_set_values_is_updated(self): """ @@ -276,8 +273,7 @@ def test_only_set_values_is_updated(self): item.udt, item.udt_default = udt, udt_default item.save() - self.assertEqual(ModelWithDefault.get()._as_dict(), - {'id': 1, 'dummy': None, 'mf': {1: 2}, "udt": udt, "udt_default": udt_default}) + assert ModelWithDefault.get()._as_dict() == {'id': 1, 'dummy': None, 'mf': {1: 2}, "udt": udt, "udt_default": udt_default} def test_collections(self): """ @@ -296,8 +292,7 @@ def test_collections(self): udt, udt_default = UDT(age=1, mf={2: 1}), UDT(age=1, mf={2: 1}) item.update(mf={2:1}, udt=udt, udt_default=udt_default) - self.assertEqual(ModelWithDefault.get()._as_dict(), - {'id': 1, 'dummy': 1, 'mf': {2: 1}, "udt": udt, "udt_default": udt_default}) + assert ModelWithDefault.get()._as_dict() == {'id': 1, 'dummy': 1, 'mf': {2: 1}, "udt": udt, "udt_default": udt_default} def test_collection_with_default(self): """ @@ -314,38 +309,31 @@ def test_collection_with_default(self): udt, udt_default = UDT(age=1, mf={6: 6}), UDT(age=1, mf={6: 6}) item = ModelWithDefaultCollection.create(id=1, mf={1: 1}, dummy=1, udt=udt, udt_default=udt_default).save() - self.assertEqual(ModelWithDefaultCollection.objects.get(id=1)._as_dict(), - {'id': 1, 'dummy': 1, 'mf': {1: 1}, "udt": udt, "udt_default": udt_default}) + assert ModelWithDefaultCollection.objects.get(id=1)._as_dict() == {'id': 1, 'dummy': 1, 'mf': {1: 1}, "udt": udt, "udt_default": udt_default} udt, udt_default = UDT(age=1, mf={5: 5}), UDT(age=1, mf={5: 5}) item.update(mf={2: 2}, udt=udt, udt_default=udt_default) - self.assertEqual(ModelWithDefaultCollection.objects.get(id=1)._as_dict(), - {'id': 1, 'dummy': 1, 'mf': {2: 2}, "udt": udt, "udt_default": udt_default}) + assert ModelWithDefaultCollection.objects.get(id=1)._as_dict() == {'id': 1, 'dummy': 1, 'mf': {2: 2}, "udt": udt, "udt_default": udt_default} udt, udt_default = UDT(age=1, mf=None), UDT(age=1, mf=None) expected_udt, expected_udt_default = UDT(age=1, mf={}), UDT(age=1, mf={}) item.update(mf=None, udt=udt, udt_default=udt_default) - self.assertEqual(ModelWithDefaultCollection.objects.get(id=1)._as_dict(), - {'id': 1, 'dummy': 1, 'mf': {}, "udt": expected_udt, "udt_default": expected_udt_default}) + assert ModelWithDefaultCollection.objects.get(id=1)._as_dict() == {'id': 1, 'dummy': 1, 'mf': {}, "udt": expected_udt, "udt_default": expected_udt_default} udt_default = UDT(age=1, mf={2:2}, dummy_udt=42) item = ModelWithDefaultCollection.create(id=2, dummy=2) - self.assertEqual(ModelWithDefaultCollection.objects.get(id=2)._as_dict(), - {'id': 2, 'dummy': 2, 'mf': {2: 2}, "udt": None, "udt_default": udt_default}) + assert ModelWithDefaultCollection.objects.get(id=2)._as_dict() == {'id': 2, 'dummy': 2, 'mf': {2: 2}, "udt": None, "udt_default": udt_default} udt, udt_default = UDT(age=1, mf={1: 1, 6: 6}), UDT(age=1, mf={1: 1, 6: 6}) item.update(mf={1: 1, 4: 4}, udt=udt, udt_default=udt_default) - self.assertEqual(ModelWithDefaultCollection.objects.get(id=2)._as_dict(), - {'id': 2, 'dummy': 2, 'mf': {1: 1, 4: 4}, "udt": udt, "udt_default": udt_default}) + assert ModelWithDefaultCollection.objects.get(id=2)._as_dict() == {'id': 2, 'dummy': 2, 'mf': {1: 1, 4: 4}, "udt": udt, "udt_default": udt_default} item.update(udt_default=None) - self.assertEqual(ModelWithDefaultCollection.objects.get(id=2)._as_dict(), - {'id': 2, 'dummy': 2, 'mf': {1: 1, 4: 4}, "udt": udt, "udt_default": None}) + assert ModelWithDefaultCollection.objects.get(id=2)._as_dict() == {'id': 2, 'dummy': 2, 'mf': {1: 1, 4: 4}, "udt": udt, "udt_default": None} udt_default = UDT(age=1, mf={2:2}) item.update(udt_default=udt_default) - self.assertEqual(ModelWithDefaultCollection.objects.get(id=2)._as_dict(), - {'id': 2, 'dummy': 2, 'mf': {1: 1, 4: 4}, "udt": udt, "udt_default": udt_default}) + assert ModelWithDefaultCollection.objects.get(id=2)._as_dict() == {'id': 2, 'dummy': 2, 'mf': {1: 1, 4: 4}, "udt": udt, "udt_default": udt_default} def test_udt_to_python(self): @@ -370,5 +358,4 @@ def test_udt_to_python(self): item.update(udt=user_to_update) udt, udt_default = UDT(time_col=10), UDT(age=1, mf={2:2}) - self.assertEqual(ModelWithDefault.objects.get(id=1)._as_dict(), - {'id': 1, 'dummy': 42, 'mf': {}, "udt": udt, "udt_default": udt_default}) + assert ModelWithDefault.objects.get(id=1)._as_dict() == {'id': 1, 'dummy': 42, 'mf': {}, "udt": udt, "udt_default": udt_default} diff --git a/tests/integration/cqlengine/model/test_value_lists.py b/tests/integration/cqlengine/model/test_value_lists.py index 8fd7f4b392..cdab57ea38 100644 --- a/tests/integration/cqlengine/model/test_value_lists.py +++ b/tests/integration/cqlengine/model/test_value_lists.py @@ -57,7 +57,7 @@ def test_clustering_order(self): values = list(TestModel.objects.values_list('clustering_key', flat=True)) # [19L, 18L, 17L, 16L, 15L, 14L, 13L, 12L, 11L, 10L, 9L, 8L, 7L, 6L, 5L, 4L, 3L, 2L, 1L, 0L] - self.assertEqual(values, sorted(items, reverse=True)) + assert values == sorted(items, reverse=True) def test_clustering_order_more_complex(self): """ @@ -72,6 +72,6 @@ def test_clustering_order_more_complex(self): values = list(TestClusteringComplexModel.objects.values_list('some_value', flat=True)) - self.assertEqual([2] * 20, values) + assert [2] * 20 == values drop_table(TestClusteringComplexModel) diff --git a/tests/integration/cqlengine/operators/__init__.py b/tests/integration/cqlengine/operators/__init__.py index 05a41c46fd..45690e9448 100644 --- a/tests/integration/cqlengine/operators/__init__.py +++ b/tests/integration/cqlengine/operators/__init__.py @@ -15,6 +15,6 @@ from cassandra.cqlengine.operators import BaseWhereOperator -def check_lookup(test_case, symbol, expected): +def check_lookup(symbol, expected): op = BaseWhereOperator.get_operator(symbol) - test_case.assertEqual(op, expected) + assert op == expected diff --git a/tests/integration/cqlengine/operators/test_where_operators.py b/tests/integration/cqlengine/operators/test_where_operators.py index e04a377c88..c7e30b8905 100644 --- a/tests/integration/cqlengine/operators/test_where_operators.py +++ b/tests/integration/cqlengine/operators/test_where_operators.py @@ -26,6 +26,7 @@ from tests.integration.cqlengine.base import TestQueryUpdateModel, BaseCassEngTestCase from tests.integration.cqlengine.operators import check_lookup from tests.integration import greaterthanorequalcass30 +import pytest class TestWhereOperators(unittest.TestCase): @@ -33,27 +34,27 @@ class TestWhereOperators(unittest.TestCase): def test_symbol_lookup(self): """ tests where symbols are looked up properly """ - check_lookup(self, 'EQ', EqualsOperator) - check_lookup(self, 'NE', NotEqualsOperator) - check_lookup(self, 'IN', InOperator) - check_lookup(self, 'GT', GreaterThanOperator) - check_lookup(self, 'GTE', GreaterThanOrEqualOperator) - check_lookup(self, 'LT', LessThanOperator) - check_lookup(self, 'LTE', LessThanOrEqualOperator) - check_lookup(self, 'CONTAINS', ContainsOperator) - check_lookup(self, 'LIKE', LikeOperator) + check_lookup('EQ', EqualsOperator) + check_lookup('NE', NotEqualsOperator) + check_lookup('IN', InOperator) + check_lookup('GT', GreaterThanOperator) + check_lookup('GTE', GreaterThanOrEqualOperator) + check_lookup('LT', LessThanOperator) + check_lookup('LTE', LessThanOrEqualOperator) + check_lookup('CONTAINS', ContainsOperator) + check_lookup('LIKE', LikeOperator) def test_operator_rendering(self): """ tests symbols are rendered properly """ - self.assertEqual("=", str(EqualsOperator())) - self.assertEqual("!=", str(NotEqualsOperator())) - self.assertEqual("IN", str(InOperator())) - self.assertEqual(">", str(GreaterThanOperator())) - self.assertEqual(">=", str(GreaterThanOrEqualOperator())) - self.assertEqual("<", str(LessThanOperator())) - self.assertEqual("<=", str(LessThanOrEqualOperator())) - self.assertEqual("CONTAINS", str(ContainsOperator())) - self.assertEqual("LIKE", str(LikeOperator())) + assert "=" == str(EqualsOperator()) + assert "!=" == str(NotEqualsOperator()) + assert "IN" == str(InOperator()) + assert ">" == str(GreaterThanOperator()) + assert ">=" == str(GreaterThanOrEqualOperator()) + assert "<" == str(LessThanOperator()) + assert "<=" == str(LessThanOrEqualOperator()) + assert "CONTAINS" == str(ContainsOperator()) + assert "LIKE" == str(LikeOperator()) class TestIsNotNull(BaseCassEngTestCase): @@ -68,21 +69,15 @@ def test_is_not_null_to_cql(self): @test_category cqlengine """ - check_lookup(self, 'IS NOT NULL', IsNotNullOperator) + check_lookup('IS NOT NULL', IsNotNullOperator) # The * is not expanded because there are no referred fields - self.assertEqual( - str(TestQueryUpdateModel.filter(IsNotNull("text")).limit(2)), - 'SELECT * FROM cqlengine_test.test_query_update_model WHERE "text" IS NOT NULL LIMIT 2' - ) + assert str(TestQueryUpdateModel.filter(IsNotNull("text")).limit(2)) == 'SELECT * FROM cqlengine_test.test_query_update_model WHERE "text" IS NOT NULL LIMIT 2' # We already know partition so cqlengine doesn't query for it - self.assertEqual( - str(TestQueryUpdateModel.filter(IsNotNull("text"), partition=uuid4())), - ('SELECT "cluster", "count", "text", "text_set", ' - '"text_list", "text_map", "bin_map" FROM cqlengine_test.test_query_update_model ' - 'WHERE "text" IS NOT NULL AND "partition" = %(0)s LIMIT 10000') - ) + assert str(TestQueryUpdateModel.filter(IsNotNull("text"), partition=uuid4())) == ('SELECT "cluster", "count", "text", "text_set", ' + '"text_list", "text_map", "bin_map" FROM cqlengine_test.test_query_update_model ' + 'WHERE "text" IS NOT NULL AND "partition" = %(0)s LIMIT 10000') @greaterthanorequalcass30 def test_is_not_null_execution(self): @@ -102,8 +97,8 @@ def test_is_not_null_execution(self): self.addCleanup(drop_table, TestQueryUpdateModel) # Raises InvalidRequest instead of dse.protocol.SyntaxException - with self.assertRaises(InvalidRequest): + with pytest.raises(InvalidRequest): list(TestQueryUpdateModel.filter(IsNotNull("text"))) - with self.assertRaises(InvalidRequest): + with pytest.raises(InvalidRequest): list(TestQueryUpdateModel.filter(IsNotNull("text"), partition=uuid4())) diff --git a/tests/integration/cqlengine/query/test_batch_query.py b/tests/integration/cqlengine/query/test_batch_query.py index a69f19d811..512a580154 100644 --- a/tests/integration/cqlengine/query/test_batch_query.py +++ b/tests/integration/cqlengine/query/test_batch_query.py @@ -24,6 +24,7 @@ from cassandra.cluster import Session from cassandra.query import BatchType as cassandra_BatchType from cassandra.cqlengine.query import BatchType as cqlengine_BatchType +import pytest class TestMultiKeyModel(Model): @@ -70,7 +71,7 @@ def test_insert_success_case(self): b = BatchQuery() inst = TestMultiKeyModel.batch(b).create(partition=self.pkey, cluster=2, count=3, text='4') - with self.assertRaises(TestMultiKeyModel.DoesNotExist): + with pytest.raises(TestMultiKeyModel.DoesNotExist): TestMultiKeyModel.get(partition=self.pkey, cluster=2) b.execute() @@ -88,12 +89,12 @@ def test_update_success_case(self): inst.batch(b).save() inst2 = TestMultiKeyModel.get(partition=self.pkey, cluster=2) - self.assertEqual(inst2.count, 3) + assert inst2.count == 3 b.execute() inst3 = TestMultiKeyModel.get(partition=self.pkey, cluster=2) - self.assertEqual(inst3.count, 4) + assert inst3.count == 4 @execute_count(4) def test_delete_success_case(self): @@ -108,7 +109,7 @@ def test_delete_success_case(self): b.execute() - with self.assertRaises(TestMultiKeyModel.DoesNotExist): + with pytest.raises(TestMultiKeyModel.DoesNotExist): TestMultiKeyModel.get(partition=self.pkey, cluster=2) @execute_count(11) @@ -119,7 +120,7 @@ def test_context_manager(self): TestMultiKeyModel.batch(b).create(partition=self.pkey, cluster=i, count=3, text='4') for i in range(5): - with self.assertRaises(TestMultiKeyModel.DoesNotExist): + with pytest.raises(TestMultiKeyModel.DoesNotExist): TestMultiKeyModel.get(partition=self.pkey, cluster=i) for i in range(5): @@ -134,9 +135,9 @@ def test_bulk_delete_success_case(self): with BatchQuery() as b: TestMultiKeyModel.objects.batch(b).filter(partition=0).delete() - self.assertEqual(TestMultiKeyModel.filter(partition=0).count(), 5) + assert TestMultiKeyModel.filter(partition=0).count() == 5 - self.assertEqual(TestMultiKeyModel.filter(partition=0).count(), 0) + assert TestMultiKeyModel.filter(partition=0).count() == 0 #cleanup for m in TestMultiKeyModel.all(): m.delete() @@ -147,10 +148,10 @@ def test_none_success_case(self): b = BatchQuery() q = TestMultiKeyModel.objects.batch(b) - self.assertEqual(q._batch, b) + assert q._batch == b q = q.batch(None) - self.assertIsNone(q._batch) + assert q._batch is None @execute_count(0) def test_dml_none_success_case(self): @@ -158,10 +159,10 @@ def test_dml_none_success_case(self): b = BatchQuery() q = DMLQuery(TestMultiKeyModel, batch=b) - self.assertEqual(q._batch, b) + assert q._batch == b q.batch(None) - self.assertIsNone(q._batch) + assert q._batch is None @execute_count(3) def test_batch_execute_on_exception_succeeds(self): @@ -170,7 +171,7 @@ def test_batch_execute_on_exception_succeeds(self): sync_table(BatchQueryLogModel) obj = BatchQueryLogModel.objects(k=1) - self.assertEqual(0, len(obj)) + assert 0 == len(obj) try: with BatchQuery(execute_on_exception=True) as b: @@ -181,7 +182,7 @@ def test_batch_execute_on_exception_succeeds(self): obj = BatchQueryLogModel.objects(k=1) # should be 1 because the batch should execute - self.assertEqual(1, len(obj)) + assert 1 == len(obj) @execute_count(2) def test_batch_execute_on_exception_skips_if_not_specified(self): @@ -190,7 +191,7 @@ def test_batch_execute_on_exception_skips_if_not_specified(self): sync_table(BatchQueryLogModel) obj = BatchQueryLogModel.objects(k=2) - self.assertEqual(0, len(obj)) + assert 0 == len(obj) try: with BatchQuery() as b: @@ -202,21 +203,21 @@ def test_batch_execute_on_exception_skips_if_not_specified(self): obj = BatchQueryLogModel.objects(k=2) # should be 0 because the batch should not execute - self.assertEqual(0, len(obj)) + assert 0 == len(obj) @execute_count(1) def test_batch_execute_timeout(self): with mock.patch.object(Session, 'execute') as mock_execute: with BatchQuery(timeout=1) as b: BatchQueryLogModel.batch(b).create(k=2, v=2) - self.assertEqual(mock_execute.call_args[-1]['timeout'], 1) + assert mock_execute.call_args[-1]['timeout'] == 1 @execute_count(1) def test_batch_execute_no_timeout(self): with mock.patch.object(Session, 'execute') as mock_execute: with BatchQuery() as b: BatchQueryLogModel.batch(b).create(k=2, v=2) - self.assertEqual(mock_execute.call_args[-1]['timeout'], NOT_SET) + assert mock_execute.call_args[-1]['timeout'] == NOT_SET class BatchTypeQueryTests(BaseCassEngTestCase): @@ -245,7 +246,7 @@ def test_cassandra_batch_type(self): TestMultiKeyModel.batch(b).create(partition=1, cluster=2) obj = TestMultiKeyModel.objects(partition=1) - self.assertEqual(2, len(obj)) + assert 2 == len(obj) with BatchQuery(batch_type=cassandra_BatchType.COUNTER) as b: CounterBatchQueryModel.batch(b).create(k=1, v=1) @@ -253,15 +254,15 @@ def test_cassandra_batch_type(self): CounterBatchQueryModel.batch(b).create(k=1, v=10) obj = CounterBatchQueryModel.objects(k=1) - self.assertEqual(1, len(obj)) - self.assertEqual(obj[0].v, 13) + assert 1 == len(obj) + assert obj[0].v == 13 with BatchQuery(batch_type=cassandra_BatchType.LOGGED) as b: TestMultiKeyModel.batch(b).create(partition=1, cluster=1) TestMultiKeyModel.batch(b).create(partition=1, cluster=2) obj = TestMultiKeyModel.objects(partition=1) - self.assertEqual(2, len(obj)) + assert 2 == len(obj) @execute_count(4) def test_cqlengine_batch_type(self): @@ -280,7 +281,7 @@ def test_cqlengine_batch_type(self): TestMultiKeyModel.batch(b).create(partition=1, cluster=2) obj = TestMultiKeyModel.objects(partition=1) - self.assertEqual(2, len(obj)) + assert 2 == len(obj) with BatchQuery(batch_type=cqlengine_BatchType.Counter) as b: CounterBatchQueryModel.batch(b).create(k=1, v=1) @@ -288,5 +289,5 @@ def test_cqlengine_batch_type(self): CounterBatchQueryModel.batch(b).create(k=1, v=10) obj = CounterBatchQueryModel.objects(k=1) - self.assertEqual(1, len(obj)) - self.assertEqual(obj[0].v, 13) + assert 1 == len(obj) + assert obj[0].v == 13 diff --git a/tests/integration/cqlengine/query/test_datetime_queries.py b/tests/integration/cqlengine/query/test_datetime_queries.py index ba1c90bb9e..e61e1bdd96 100644 --- a/tests/integration/cqlengine/query/test_datetime_queries.py +++ b/tests/integration/cqlengine/query/test_datetime_queries.py @@ -15,6 +15,7 @@ from datetime import datetime, timedelta from uuid import uuid4 from cassandra.cqlengine.functions import get_total_seconds +import pytest from tests.integration.cqlengine.base import BaseCassEngTestCase @@ -70,6 +71,5 @@ def test_datetime_precision(self): obj = DateTimeQueryTestModel.create(user=pk, day=now, data='energy cheese') load = DateTimeQueryTestModel.get(user=pk) - self.assertAlmostEqual(get_total_seconds(now - load.day), 0, 2) + assert get_total_seconds(now - load.day) == pytest.approx(0, abs=1e-2) obj.delete() - diff --git a/tests/integration/cqlengine/query/test_named.py b/tests/integration/cqlengine/query/test_named.py index 0d5ba38200..66ba8b973a 100644 --- a/tests/integration/cqlengine/query/test_named.py +++ b/tests/integration/cqlengine/query/test_named.py @@ -27,7 +27,8 @@ from tests.integration.cqlengine.query.test_queryset import BaseQuerySetUsage -from tests.integration import BasicSharedKeyspaceUnitTestCase, greaterthanorequalcass30, requires_collection_indexes +from tests.integration import BasicSharedKeyspaceUnitTestCase, greaterthanorequalcass30, requires_collection_indexes, get_tablets_disabled_ddl_suffix, execute_with_long_wait_retry +import pytest class TestQuerySetOperation(BaseCassEngTestCase): @@ -77,46 +78,46 @@ def test_filter_method_where_clause_generation(self): Tests the where clause creation """ query1 = self.table.objects(test_id=5) - self.assertEqual(len(query1._where), 1) + assert len(query1._where) == 1 where = query1._where[0] - self.assertEqual(where.field, 'test_id') - self.assertEqual(where.value, 5) + assert where.field == 'test_id' + assert where.value == 5 query2 = query1.filter(expected_result__gte=1) - self.assertEqual(len(query2._where), 2) + assert len(query2._where) == 2 where = query2._where[0] - self.assertEqual(where.field, 'test_id') - self.assertIsInstance(where.operator, EqualsOperator) - self.assertEqual(where.value, 5) + assert where.field == 'test_id' + assert isinstance(where.operator, EqualsOperator) + assert where.value == 5 where = query2._where[1] - self.assertEqual(where.field, 'expected_result') - self.assertIsInstance(where.operator, GreaterThanOrEqualOperator) - self.assertEqual(where.value, 1) + assert where.field == 'expected_result' + assert isinstance(where.operator, GreaterThanOrEqualOperator) + assert where.value == 1 def test_query_expression_where_clause_generation(self): """ Tests the where clause creation """ query1 = self.table.objects(self.table.column('test_id') == 5) - self.assertEqual(len(query1._where), 1) + assert len(query1._where) == 1 where = query1._where[0] - self.assertEqual(where.field, 'test_id') - self.assertEqual(where.value, 5) + assert where.field == 'test_id' + assert where.value == 5 query2 = query1.filter(self.table.column('expected_result') >= 1) - self.assertEqual(len(query2._where), 2) + assert len(query2._where) == 2 where = query2._where[0] - self.assertEqual(where.field, 'test_id') - self.assertIsInstance(where.operator, EqualsOperator) - self.assertEqual(where.value, 5) + assert where.field == 'test_id' + assert isinstance(where.operator, EqualsOperator) + assert where.value == 5 where = query2._where[1] - self.assertEqual(where.field, 'expected_result') - self.assertIsInstance(where.operator, GreaterThanOrEqualOperator) - self.assertEqual(where.value, 1) + assert where.field == 'expected_result' + assert isinstance(where.operator, GreaterThanOrEqualOperator) + assert where.value == 1 @requires_collection_indexes class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage): @@ -265,7 +266,7 @@ def test_get_doesnotexist_exception(self): """ Tests that get calls that don't return a result raises a DoesNotExist error """ - with self.assertRaises(self.table.DoesNotExist): + with pytest.raises(self.table.DoesNotExist): self.table.objects.get(test_id=100) @execute_count(1) @@ -273,12 +274,18 @@ def test_get_multipleobjects_exception(self): """ Tests that get calls that return multiple results raise a MultipleObjectsReturned error """ - with self.assertRaises(self.table.MultipleObjectsReturned): + with pytest.raises(self.table.MultipleObjectsReturned): self.table.objects.get(test_id=1) class TestNamedWithMV(BasicSharedKeyspaceUnitTestCase): + @classmethod + def create_keyspace(cls, rf): + ddl = "CREATE KEYSPACE {0} WITH replication = {{'class': 'NetworkTopologyStrategy', 'replication_factor': '{1}'}}{2}".format( + cls.ks_name, rf, get_tablets_disabled_ddl_suffix()) + execute_with_long_wait_retry(cls.session, ddl) + @classmethod def setUpClass(cls): super(TestNamedWithMV, cls).setUpClass() @@ -358,17 +365,17 @@ def test_named_table_with_mv(self): key_space = NamedKeyspace(ks) mv_monthly = key_space.table("monthlyhigh") mv_all_time = key_space.table("alltimehigh") - self.assertTrue(self.check_table_size("scores", key_space, len(parameters))) - self.assertTrue(self.check_table_size("monthlyhigh", key_space, len(parameters))) - self.assertTrue(self.check_table_size("alltimehigh", key_space, len(parameters))) + assert self.check_table_size("scores", key_space, len(parameters)) + assert self.check_table_size("monthlyhigh", key_space, len(parameters)) + assert self.check_table_size("alltimehigh", key_space, len(parameters)) filtered_mv_monthly_objects = mv_monthly.objects.filter(game='Chess', year=2015, month=6) - self.assertEqual(len(filtered_mv_monthly_objects), 1) - self.assertEqual(filtered_mv_monthly_objects[0]['score'], 3500) - self.assertEqual(filtered_mv_monthly_objects[0]['user'], 'jbellis') + assert len(filtered_mv_monthly_objects) == 1 + assert filtered_mv_monthly_objects[0]['score'] == 3500 + assert filtered_mv_monthly_objects[0]['user'] == 'jbellis' filtered_mv_alltime_objects = mv_all_time.objects.filter(game='Chess') - self.assertEqual(len(filtered_mv_alltime_objects), 2) - self.assertEqual(filtered_mv_alltime_objects[0]['score'], 3500) + assert len(filtered_mv_alltime_objects) == 2 + assert filtered_mv_alltime_objects[0]['score'] == 3500 def check_table_size(self, table_name, key_space, expected_size): table = key_space.table(table_name) diff --git a/tests/integration/cqlengine/query/test_queryoperators.py b/tests/integration/cqlengine/query/test_queryoperators.py index fbf666cf21..b9e9356b06 100644 --- a/tests/integration/cqlengine/query/test_queryoperators.py +++ b/tests/integration/cqlengine/query/test_queryoperators.py @@ -25,6 +25,7 @@ from tests.integration.cqlengine import DEFAULT_KEYSPACE from tests.integration.cqlengine.base import BaseCassEngTestCase from tests.integration.cqlengine import execute_count +import pytest class TestQuerySetOperation(BaseCassEngTestCase): @@ -37,10 +38,10 @@ def test_maxtimeuuid_function(self): where = WhereClause('time', EqualsOperator(), functions.MaxTimeUUID(now)) where.set_context_id(5) - self.assertEqual(str(where), '"time" = MaxTimeUUID(%(5)s)') + assert str(where) == '"time" = MaxTimeUUID(%(5)s)' ctx = {} where.update_context(ctx) - self.assertEqual(ctx, {'5': columns.DateTime().to_database(now)}) + assert ctx == {'5': columns.DateTime().to_database(now)} def test_mintimeuuid_function(self): """ @@ -50,10 +51,10 @@ def test_mintimeuuid_function(self): where = WhereClause('time', EqualsOperator(), functions.MinTimeUUID(now)) where.set_context_id(5) - self.assertEqual(str(where), '"time" = MinTimeUUID(%(5)s)') + assert str(where) == '"time" = MinTimeUUID(%(5)s)' ctx = {} where.update_context(ctx) - self.assertEqual(ctx, {'5': columns.DateTime().to_database(now)}) + assert ctx == {'5': columns.DateTime().to_database(now)} class TokenTestModel(Model): @@ -93,7 +94,7 @@ def test_token_function(self): # pk__token equality r = TokenTestModel.objects(pk__token=functions.Token(last_token)) - self.assertEqual(len(r), 1) + assert len(r) == 1 r.all() # Attempt to obtain queryset for results. This has thrown an exception in the past def test_compound_pk_token_function(self): @@ -108,7 +109,7 @@ class TestModel(Model): q = TestModel.objects.filter(pk__token__gt=func) where = q._where[0] where.set_context_id(1) - self.assertEqual(str(where), 'token("p1", "p2") > token(%({0})s, %({1})s)'.format(1, 2)) + assert str(where) == 'token("p1", "p2") > token(%({0})s, %({1})s)'.format(1, 2) # Verify that a SELECT query can be successfully generated str(q._select_query()) @@ -120,19 +121,22 @@ class TestModel(Model): q = TestModel.objects.filter(pk__token__gt=func) where = q._where[0] where.set_context_id(1) - self.assertEqual(str(where), 'token("p1", "p2") > token(%({0})s, %({1})s)'.format(1, 2)) + assert str(where) == 'token("p1", "p2") > token(%({0})s, %({1})s)'.format(1, 2) str(q._select_query()) # The 'pk__token' virtual column may only be compared to a Token - self.assertRaises(query.QueryException, TestModel.objects.filter, pk__token__gt=10) + with pytest.raises(query.QueryException): + TestModel.objects.filter(pk__token__gt=10) # A Token may only be compared to the `pk__token' virtual column func = functions.Token('a', 'b') - self.assertRaises(query.QueryException, TestModel.objects.filter, p1__gt=func) + with pytest.raises(query.QueryException): + TestModel.objects.filter(p1__gt=func) # The # of arguments to Token must match the # of partition keys func = functions.Token('a') - self.assertRaises(query.QueryException, TestModel.objects.filter, pk__token__gt=func) + with pytest.raises(query.QueryException): + TestModel.objects.filter(pk__token__gt=func) @execute_count(7) def test_named_table_pk_token_function(self): @@ -154,6 +158,6 @@ def test_named_table_pk_token_function(self): query = named.all().limit(1) first_page = list(query) last = first_page[-1] - self.assertTrue(len(first_page) == 1) + assert len(first_page) == 1 next_page = list(query.filter(pk__token__gt=functions.Token(last.key))) - self.assertTrue(len(next_page) == 1) + assert len(next_page) == 1 diff --git a/tests/integration/cqlengine/query/test_queryset.py b/tests/integration/cqlengine/query/test_queryset.py index d15390827f..34b4ab5964 100644 --- a/tests/integration/cqlengine/query/test_queryset.py +++ b/tests/integration/cqlengine/query/test_queryset.py @@ -41,6 +41,7 @@ from tests.integration import PROTOCOL_VERSION, CASSANDRA_VERSION, greaterthancass20, greaterthancass21, \ greaterthanorequalcass30, TestCluster, requires_collection_indexes from tests.integration.cqlengine import execute_count, DEFAULT_KEYSPACE +import pytest class TzOffset(tzinfo): @@ -131,8 +132,8 @@ def test_query_filter_parsing(self): assert len(query2._where) == 2 op = query2._where[1] - self.assertIsInstance(op, statements.WhereClause) - self.assertIsInstance(op.operator, operators.GreaterThanOrEqualOperator) + assert isinstance(op, statements.WhereClause) + assert isinstance(op.operator, operators.GreaterThanOrEqualOperator) assert op.value == 1 def test_query_expression_parsing(self): @@ -149,29 +150,29 @@ def test_query_expression_parsing(self): assert len(query2._where) == 2 op = query2._where[1] - self.assertIsInstance(op, statements.WhereClause) - self.assertIsInstance(op.operator, operators.GreaterThanOrEqualOperator) + assert isinstance(op, statements.WhereClause) + assert isinstance(op.operator, operators.GreaterThanOrEqualOperator) assert op.value == 1 def test_using_invalid_column_names_in_filter_kwargs_raises_error(self): """ Tests that using invalid or nonexistant column names for filter args raises an error """ - with self.assertRaises(query.QueryException): + with pytest.raises(query.QueryException): TestModel.objects(nonsense=5) def test_using_nonexistant_column_names_in_query_args_raises_error(self): """ Tests that using invalid or nonexistant columns for query args raises an error """ - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): TestModel.objects(TestModel.nonsense == 5) def test_using_non_query_operators_in_query_args_raises_error(self): """ Tests that providing query args that are not query operator instances raises an error """ - with self.assertRaises(query.QueryException): + with pytest.raises(query.QueryException): TestModel.objects(5) def test_queryset_is_immutable(self): @@ -218,13 +219,13 @@ def test_queryset_with_distinct(self): """ query1 = TestModel.objects.distinct() - self.assertEqual(len(query1._distinct_fields), 1) + assert len(query1._distinct_fields) == 1 query2 = TestModel.objects.distinct(['test_id']) - self.assertEqual(len(query2._distinct_fields), 1) + assert len(query2._distinct_fields) == 1 query3 = TestModel.objects.distinct(['test_id', 'attempt_id']) - self.assertEqual(len(query3._distinct_fields), 2) + assert len(query3._distinct_fields) == 2 def test_defining_only_fields(self): """ @@ -238,35 +239,35 @@ def test_defining_only_fields(self): """ # simple only definition q = TestModel.objects.only(['attempt_id', 'description']) - self.assertEqual(q._select_fields(), ['attempt_id', 'description']) + assert q._select_fields() == ['attempt_id', 'description'] - with self.assertRaises(query.QueryException): + with pytest.raises(query.QueryException): TestModel.objects.only(['nonexistent_field']) # Cannot define more than once only fields - with self.assertRaises(query.QueryException): + with pytest.raises(query.QueryException): TestModel.objects.only(['description']).only(['attempt_id']) # only with defer fields q = TestModel.objects.only(['attempt_id', 'description']) q = q.defer(['description']) - self.assertEqual(q._select_fields(), ['attempt_id']) + assert q._select_fields() == ['attempt_id'] # Eliminate all results confirm exception is thrown q = TestModel.objects.only(['description']) q = q.defer(['description']) - with self.assertRaises(query.QueryException): + with pytest.raises(query.QueryException): q._select_fields() q = TestModel.objects.filter(test_id=0).only(['test_id', 'attempt_id', 'description']) - self.assertEqual(q._select_fields(), ['attempt_id', 'description']) + assert q._select_fields() == ['attempt_id', 'description'] # no fields to select - with self.assertRaises(query.QueryException): + with pytest.raises(query.QueryException): q = TestModel.objects.only(['test_id']).defer(['test_id']) q._select_fields() - with self.assertRaises(query.QueryException): + with pytest.raises(query.QueryException): q = TestModel.objects.filter(test_id=0).only(['test_id']) q._select_fields() @@ -284,34 +285,34 @@ def test_defining_defer_fields(self): # simple defer definition q = TestModel.objects.defer(['attempt_id', 'description']) - self.assertEqual(q._select_fields(), ['test_id', 'expected_result', 'test_result']) + assert q._select_fields() == ['test_id', 'expected_result', 'test_result'] - with self.assertRaises(query.QueryException): + with pytest.raises(query.QueryException): TestModel.objects.defer(['nonexistent_field']) # defer more than one q = TestModel.objects.defer(['attempt_id', 'description']) q = q.defer(['expected_result']) - self.assertEqual(q._select_fields(), ['test_id', 'test_result']) + assert q._select_fields() == ['test_id', 'test_result'] # defer with only q = TestModel.objects.defer(['description', 'attempt_id']) q = q.only(['description', 'test_id']) - self.assertEqual(q._select_fields(), ['test_id']) + assert q._select_fields() == ['test_id'] # Eliminate all results confirm exception is thrown q = TestModel.objects.defer(['description', 'attempt_id']) q = q.only(['description']) - with self.assertRaises(query.QueryException): + with pytest.raises(query.QueryException): q._select_fields() # implicit defer q = TestModel.objects.filter(test_id=0) - self.assertEqual(q._select_fields(), ['attempt_id', 'description', 'expected_result', 'test_result']) + assert q._select_fields() == ['attempt_id', 'description', 'expected_result', 'test_result'] # when all fields are defered, it fallbacks select the partition keys q = TestModel.objects.defer(['test_id', 'attempt_id', 'description', 'expected_result', 'test_result']) - self.assertEqual(q._select_fields(), ['test_id']) + assert q._select_fields() == ['test_id'] class BaseQuerySetUsage(BaseCassEngTestCase): @@ -523,7 +524,7 @@ def test_get_doesnotexist_exception(self): """ Tests that get calls that don't return a result raises a DoesNotExist error """ - with self.assertRaises(TestModel.DoesNotExist): + with pytest.raises(TestModel.DoesNotExist): TestModel.objects.get(test_id=100) @execute_count(1) @@ -531,7 +532,7 @@ def test_get_multipleobjects_exception(self): """ Tests that get calls that return multiple results raise a MultipleObjectsReturned error """ - with self.assertRaises(TestModel.MultipleObjectsReturned): + with pytest.raises(TestModel.MultipleObjectsReturned): TestModel.objects.get(test_id=1) def test_allow_filtering_flag(self): @@ -566,37 +567,37 @@ class TestQuerySetDistinct(BaseQuerySetUsage): @execute_count(1) def test_distinct_without_parameter(self): q = TestModel.objects.distinct() - self.assertEqual(len(q), 3) + assert len(q) == 3 @execute_count(1) def test_distinct_with_parameter(self): q = TestModel.objects.distinct(['test_id']) - self.assertEqual(len(q), 3) + assert len(q) == 3 @execute_count(1) def test_distinct_with_filter(self): q = TestModel.objects.distinct(['test_id']).filter(test_id__in=[1, 2]) - self.assertEqual(len(q), 2) + assert len(q) == 2 @execute_count(1) def test_distinct_with_non_partition(self): - with self.assertRaises(InvalidRequest): + with pytest.raises(InvalidRequest): q = TestModel.objects.distinct(['description']).filter(test_id__in=[1, 2]) len(q) @execute_count(1) def test_zero_result(self): q = TestModel.objects.distinct(['test_id']).filter(test_id__in=[52]) - self.assertEqual(len(q), 0) + assert len(q) == 0 @greaterthancass21 @execute_count(2) def test_distinct_with_explicit_count(self): q = TestModel.objects.distinct(['test_id']) - self.assertEqual(q.count(), 3) + assert q.count() == 3 q = TestModel.objects.distinct(['test_id']).filter(test_id__in=[1, 2]) - self.assertEqual(q.count(), 2) + assert q.count() == 2 @requires_collection_indexes @@ -615,19 +616,19 @@ def test_order_by_success_case(self): def test_ordering_by_non_second_primary_keys_fail(self): # kwarg filtering - with self.assertRaises(query.QueryException): + with pytest.raises(query.QueryException): TestModel.objects(test_id=0).order_by('test_id') # kwarg filtering - with self.assertRaises(query.QueryException): + with pytest.raises(query.QueryException): TestModel.objects(TestModel.test_id == 0).order_by('test_id') def test_ordering_by_non_primary_keys_fails(self): - with self.assertRaises(query.QueryException): + with pytest.raises(query.QueryException): TestModel.objects(test_id=0).order_by('description') def test_ordering_on_indexed_columns_fails(self): - with self.assertRaises(query.QueryException): + with pytest.raises(query.QueryException): IndexedTestModel.objects(test_id=0).order_by('attempt_id') @execute_count(8) @@ -654,7 +655,7 @@ class TestQuerySetSlicing(BaseQuerySetUsage): @execute_count(1) def test_out_of_range_index_raises_error(self): q = TestModel.objects(test_id=0).order_by('attempt_id') - with self.assertRaises(IndexError): + with pytest.raises(IndexError): q[10] @execute_count(1) @@ -677,10 +678,10 @@ def test_slicing_works_properly(self): expected_order = [0, 1, 2, 3] for model, expect in zip(q[1:3], expected_order[1:3]): - self.assertEqual(model.attempt_id, expect) + assert model.attempt_id == expect for model, expect in zip(q[0:3:2], expected_order[0:3:2]): - self.assertEqual(model.attempt_id, expect) + assert model.attempt_id == expect @execute_count(1) def test_negative_slicing(self): @@ -688,19 +689,19 @@ def test_negative_slicing(self): expected_order = [0, 1, 2, 3] for model, expect in zip(q[-3:], expected_order[-3:]): - self.assertEqual(model.attempt_id, expect) + assert model.attempt_id == expect for model, expect in zip(q[:-1], expected_order[:-1]): - self.assertEqual(model.attempt_id, expect) + assert model.attempt_id == expect for model, expect in zip(q[1:-1], expected_order[1:-1]): - self.assertEqual(model.attempt_id, expect) + assert model.attempt_id == expect for model, expect in zip(q[-3:-1], expected_order[-3:-1]): - self.assertEqual(model.attempt_id, expect) + assert model.attempt_id == expect for model, expect in zip(q[-3:-1:2], expected_order[-3:-1:2]): - self.assertEqual(model.attempt_id, expect) + assert model.attempt_id == expect @requires_collection_indexes @@ -710,7 +711,7 @@ def test_primary_key_or_index_must_be_specified(self): """ Tests that queries that don't have an equals relation to a primary key or indexed field fail """ - with self.assertRaises(query.QueryException): + with pytest.raises(query.QueryException): q = TestModel.objects(test_result=25) list([i for i in q]) @@ -718,7 +719,7 @@ def test_primary_key_or_index_must_have_equal_relation_filter(self): """ Tests that queries that don't have non equal (>,<, etc) relation to a primary key or indexed field fail """ - with self.assertRaises(query.QueryException): + with pytest.raises(query.QueryException): q = TestModel.objects(test_id__gt=0) list([i for i in q]) @@ -729,52 +730,52 @@ def test_indexed_field_can_be_queried(self): Tests that queries on an indexed field will work without any primary key relations specified """ q = IndexedTestModel.objects(test_result=25) - self.assertEqual(q.count(), 4) + assert q.count() == 4 q = IndexedCollectionsTestModel.objects.filter(test_list__contains=42) - self.assertEqual(q.count(), 1) + assert q.count() == 1 q = IndexedCollectionsTestModel.objects.filter(test_list__contains=13) - self.assertEqual(q.count(), 0) + assert q.count() == 0 q = IndexedCollectionsTestModel.objects.filter(test_set__contains=42) - self.assertEqual(q.count(), 1) + assert q.count() == 1 q = IndexedCollectionsTestModel.objects.filter(test_set__contains=13) - self.assertEqual(q.count(), 0) + assert q.count() == 0 q = IndexedCollectionsTestModel.objects.filter(test_map__contains=42) - self.assertEqual(q.count(), 1) + assert q.count() == 1 q = IndexedCollectionsTestModel.objects.filter(test_map__contains=13) - self.assertEqual(q.count(), 0) + assert q.count() == 0 def test_custom_indexed_field_can_be_queried(self): """ Tests that queries on an custom indexed field will work without any primary key relations specified """ - with self.assertRaises(query.QueryException): + with pytest.raises(query.QueryException): list(CustomIndexedTestModel.objects.filter(data='test')) # not custom indexed # It should return InvalidRequest if target an indexed columns - with self.assertRaises(InvalidRequest): + with pytest.raises(InvalidRequest): list(CustomIndexedTestModel.objects.filter(indexed='test', data='test')) # It should return InvalidRequest if target an indexed columns - with self.assertRaises(InvalidRequest): + with pytest.raises(InvalidRequest): list(CustomIndexedTestModel.objects.filter(description='test', data='test')) # equals operator, server error since there is no real index, but it passes - with self.assertRaises(InvalidRequest): + with pytest.raises(InvalidRequest): list(CustomIndexedTestModel.objects.filter(description='test')) - with self.assertRaises(InvalidRequest): + with pytest.raises(InvalidRequest): list(CustomIndexedTestModel.objects.filter(test_id=1, description='test')) # gte operator, server error since there is no real index, but it passes # this can't work with a secondary index - with self.assertRaises(InvalidRequest): + with pytest.raises(InvalidRequest): list(CustomIndexedTestModel.objects.filter(description__gte='test')) with TestCluster().connect() as session: @@ -805,12 +806,12 @@ def test_delete(self): def test_delete_without_partition_key(self): """ Tests that attempting to delete a model without defining a partition key fails """ - with self.assertRaises(query.QueryException): + with pytest.raises(query.QueryException): TestModel.objects(attempt_id=0).delete() def test_delete_without_any_where_args(self): """ Tests that attempting to delete a whole table without any arguments will fail """ - with self.assertRaises(query.QueryException): + with pytest.raises(query.QueryException): TestModel.objects(attempt_id=0).delete() @greaterthanorequalcass30 @@ -824,16 +825,16 @@ def test_range_deletion(self): TestMultiClusteringModel.objects().create(one=1, two=i, three=i) TestMultiClusteringModel.objects(one=1, two__gte=0, two__lte=3).delete() - self.assertEqual(6, len(TestMultiClusteringModel.objects.all())) + assert 6 == len(TestMultiClusteringModel.objects.all()) TestMultiClusteringModel.objects(one=1, two__gt=3, two__lt=5).delete() - self.assertEqual(5, len(TestMultiClusteringModel.objects.all())) + assert 5 == len(TestMultiClusteringModel.objects.all()) TestMultiClusteringModel.objects(one=1, two__in=[8, 9]).delete() - self.assertEqual(3, len(TestMultiClusteringModel.objects.all())) + assert 3 == len(TestMultiClusteringModel.objects.all()) TestMultiClusteringModel.objects(one__in=[1], two__gte=0).delete() - self.assertEqual(0, len(TestMultiClusteringModel.objects.all())) + assert 0 == len(TestMultiClusteringModel.objects.all()) class TimeUUIDQueryModel(Model): @@ -912,7 +913,7 @@ def test_success_case(self): # test kwarg filtering q = TimeUUIDQueryModel.filter(partition=pk, time__lte=functions.MaxTimeUUID(midpoint)) q = [d for d in q] - self.assertEqual(len(q), 2, msg="Got: %s" % q) + assert len(q) == 2, "Got: %s" % q datas = [d.data for d in q] assert '1' in datas assert '2' in datas @@ -977,9 +978,9 @@ class bool_model(Model): bool_model.create(k=0, b=True) bool_model.create(k=0, b=False) - self.assertEqual(len(bool_model.objects.all()), 2) - self.assertEqual(len(bool_model.objects.filter(k=0, b=True)), 1) - self.assertEqual(len(bool_model.objects.filter(k=0, b=False)), 1) + assert len(bool_model.objects.all()) == 2 + assert len(bool_model.objects.filter(k=0, b=True)) == 1 + assert len(bool_model.objects.filter(k=0, b=False)) == 1 @execute_count(3) def test_bool_filter(self): @@ -1001,7 +1002,7 @@ class bool_model2(Model): bool_model2.create(k=True, b=1, v='a') bool_model2.create(k=False, b=1, v='b') - self.assertEqual(len(list(bool_model2.objects(k__in=(True, False)))), 2) + assert len(list(bool_model2.objects(k__in=(True, False)))) == 2 @greaterthancass20 @@ -1012,63 +1013,63 @@ class TestContainsOperator(BaseQuerySetUsage): def test_kwarg_success_case(self): """ Tests the CONTAINS operator works with the kwarg query method """ q = IndexedCollectionsTestModel.filter(test_list__contains=1) - self.assertEqual(q.count(), 2) + assert q.count() == 2 q = IndexedCollectionsTestModel.filter(test_list__contains=13) - self.assertEqual(q.count(), 0) + assert q.count() == 0 q = IndexedCollectionsTestModel.filter(test_set__contains=3) - self.assertEqual(q.count(), 2) + assert q.count() == 2 q = IndexedCollectionsTestModel.filter(test_set__contains=13) - self.assertEqual(q.count(), 0) + assert q.count() == 0 q = IndexedCollectionsTestModel.filter(test_map__contains=42) - self.assertEqual(q.count(), 1) + assert q.count() == 1 q = IndexedCollectionsTestModel.filter(test_map__contains=13) - self.assertEqual(q.count(), 0) + assert q.count() == 0 - with self.assertRaises(QueryException): + with pytest.raises(QueryException): q = IndexedCollectionsTestModel.filter(test_list_no_index__contains=1) - self.assertEqual(q.count(), 0) - with self.assertRaises(QueryException): + assert q.count() == 0 + with pytest.raises(QueryException): q = IndexedCollectionsTestModel.filter(test_set_no_index__contains=1) - self.assertEqual(q.count(), 0) - with self.assertRaises(QueryException): + assert q.count() == 0 + with pytest.raises(QueryException): q = IndexedCollectionsTestModel.filter(test_map_no_index__contains=1) - self.assertEqual(q.count(), 0) + assert q.count() == 0 @execute_count(6) def test_query_expression_success_case(self): """ Tests the CONTAINS operator works with the query expression query method """ q = IndexedCollectionsTestModel.filter(IndexedCollectionsTestModel.test_list.contains_(1)) - self.assertEqual(q.count(), 2) + assert q.count() == 2 q = IndexedCollectionsTestModel.filter(IndexedCollectionsTestModel.test_list.contains_(13)) - self.assertEqual(q.count(), 0) + assert q.count() == 0 q = IndexedCollectionsTestModel.filter(IndexedCollectionsTestModel.test_set.contains_(3)) - self.assertEqual(q.count(), 2) + assert q.count() == 2 q = IndexedCollectionsTestModel.filter(IndexedCollectionsTestModel.test_set.contains_(13)) - self.assertEqual(q.count(), 0) + assert q.count() == 0 q = IndexedCollectionsTestModel.filter(IndexedCollectionsTestModel.test_map.contains_(42)) - self.assertEqual(q.count(), 1) + assert q.count() == 1 q = IndexedCollectionsTestModel.filter(IndexedCollectionsTestModel.test_map.contains_(13)) - self.assertEqual(q.count(), 0) + assert q.count() == 0 - with self.assertRaises(QueryException): + with pytest.raises(QueryException): q = IndexedCollectionsTestModel.filter(IndexedCollectionsTestModel.test_map_no_index.contains_(1)) - self.assertEqual(q.count(), 0) - with self.assertRaises(QueryException): + assert q.count() == 0 + with pytest.raises(QueryException): q = IndexedCollectionsTestModel.filter(IndexedCollectionsTestModel.test_map_no_index.contains_(1)) - self.assertEqual(q.count(), 0) - with self.assertRaises(QueryException): + assert q.count() == 0 + with pytest.raises(QueryException): q = IndexedCollectionsTestModel.filter(IndexedCollectionsTestModel.test_map_no_index.contains_(1)) - self.assertEqual(q.count(), 0) + assert q.count() == 0 @requires_collection_indexes @@ -1120,17 +1121,17 @@ class ModelQuerySetTimeoutTestCase(BaseQuerySetUsage): def test_default_timeout(self): with mock.patch.object(Session, 'execute') as mock_execute: list(TestModel.objects()) - self.assertEqual(mock_execute.call_args[-1]['timeout'], NOT_SET) + assert mock_execute.call_args[-1]['timeout'] == NOT_SET def test_float_timeout(self): with mock.patch.object(Session, 'execute') as mock_execute: list(TestModel.objects().timeout(0.5)) - self.assertEqual(mock_execute.call_args[-1]['timeout'], 0.5) + assert mock_execute.call_args[-1]['timeout'] == 0.5 def test_none_timeout(self): with mock.patch.object(Session, 'execute') as mock_execute: list(TestModel.objects().timeout(None)) - self.assertEqual(mock_execute.call_args[-1]['timeout'], None) + assert mock_execute.call_args[-1]['timeout'] == None @requires_collection_indexes @@ -1142,28 +1143,28 @@ def setUp(self): def test_default_timeout(self): with mock.patch.object(Session, 'execute') as mock_execute: self.model.save() - self.assertEqual(mock_execute.call_args[-1]['timeout'], NOT_SET) + assert mock_execute.call_args[-1]['timeout'] == NOT_SET def test_float_timeout(self): with mock.patch.object(Session, 'execute') as mock_execute: self.model.timeout(0.5).save() - self.assertEqual(mock_execute.call_args[-1]['timeout'], 0.5) + assert mock_execute.call_args[-1]['timeout'] == 0.5 def test_none_timeout(self): with mock.patch.object(Session, 'execute') as mock_execute: self.model.timeout(None).save() - self.assertEqual(mock_execute.call_args[-1]['timeout'], None) + assert mock_execute.call_args[-1]['timeout'] == None def test_timeout_then_batch(self): b = query.BatchQuery() m = self.model.timeout(None) - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): m.batch(b) def test_batch_then_timeout(self): b = query.BatchQuery() m = self.model.batch(b) - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): m.timeout(0.5) @@ -1220,26 +1221,26 @@ def test_basic_crud(self): # create i = model.create(**values) i = model.objects(k0=i.k0, k1=i.k1).first() - self.assertEqual(i, model(**values)) + assert i == model(**values) # create values['v0'] = 101 i.update(v0=values['v0']) i = model.objects(k0=i.k0, k1=i.k1).first() - self.assertEqual(i, model(**values)) + assert i == model(**values) # delete model.objects(k0=i.k0, k1=i.k1).delete() i = model.objects(k0=i.k0, k1=i.k1).first() - self.assertIsNone(i) + assert i is None i = model.create(**values) i = model.objects(k0=i.k0, k1=i.k1).first() - self.assertEqual(i, model(**values)) + assert i == model(**values) i.delete() model.objects(k0=i.k0, k1=i.k1).delete() i = model.objects(k0=i.k0, k1=i.k1).first() - self.assertIsNone(i) + assert i is None @execute_count(21) def test_slice(self): @@ -1259,10 +1260,10 @@ def test_slice(self): values['c0'] = c i = model.create(**values) - self.assertEqual(model.objects(k0=i.k0, k1=i.k1).count(), len(clustering_values)) - self.assertEqual(model.objects(k0=i.k0, k1=i.k1, c0=i.c0).count(), 1) - self.assertEqual(model.objects(k0=i.k0, k1=i.k1, c0__lt=i.c0).count(), len(clustering_values[:-1])) - self.assertEqual(model.objects(k0=i.k0, k1=i.k1, c0__gt=0).count(), len(clustering_values[1:])) + assert model.objects(k0=i.k0, k1=i.k1).count() == len(clustering_values) + assert model.objects(k0=i.k0, k1=i.k1, c0=i.c0).count() == 1 + assert model.objects(k0=i.k0, k1=i.k1, c0__lt=i.c0).count() == len(clustering_values[:-1]) + assert model.objects(k0=i.k0, k1=i.k1, c0__gt=0).count() == len(clustering_values[1:]) @execute_count(15) def test_order(self): @@ -1281,8 +1282,8 @@ def test_order(self): for c in clustering_values: values['c0'] = c i = model.create(**values) - self.assertEqual(model.objects(k0=i.k0, k1=i.k1).order_by('c0').first().c0, clustering_values[0]) - self.assertEqual(model.objects(k0=i.k0, k1=i.k1).order_by('-c0').first().c0, clustering_values[-1]) + assert model.objects(k0=i.k0, k1=i.k1).order_by('c0').first().c0 == clustering_values[0] + assert model.objects(k0=i.k0, k1=i.k1).order_by('-c0').first().c0 == clustering_values[-1] @execute_count(15) def test_index(self): @@ -1302,8 +1303,8 @@ def test_index(self): values['c0'] = c values['v1'] = c i = model.create(**values) - self.assertEqual(model.objects(k0=i.k0, k1=i.k1).count(), len(clustering_values)) - self.assertEqual(model.objects(k0=i.k0, k1=i.k1, v1=0).count(), 1) + assert model.objects(k0=i.k0, k1=i.k1).count() == len(clustering_values) + assert model.objects(k0=i.k0, k1=i.k1, v1=0).count() == 1 @execute_count(1) def test_db_field_names_used(self): @@ -1325,7 +1326,7 @@ def test_db_field_names_used(self): v1=9, ) for value in values: - self.assertTrue(value not in str(b.queries[0])) + assert value not in str(b.queries[0]) # Test DML path b2 = BatchQuery() @@ -1335,15 +1336,13 @@ def test_db_field_names_used(self): v1=9, ) for value in values: - self.assertTrue(value not in str(b2.queries[0])) + assert value not in str(b2.queries[0]) def test_db_field_value_list(self): DBFieldModel.create(k0=0, k1=0, c0=0, v0=4, v1=5) - self.assertEqual(DBFieldModel.objects.filter(c0=0, k0=0, k1=0).values_list('c0', 'v0')._defer_fields, - {'a', 'c', 'b'}) - self.assertEqual(DBFieldModel.objects.filter(c0=0, k0=0, k1=0).values_list('c0', 'v0')._only_fields, - ['c', 'd']) + assert DBFieldModel.objects.filter(c0=0, k0=0, k1=0).values_list('c0', 'v0')._defer_fields == {'a', 'c', 'b'} + assert DBFieldModel.objects.filter(c0=0, k0=0, k1=0).values_list('c0', 'v0')._only_fields == ['c', 'd'] list(DBFieldModel.objects.filter(c0=0, k0=0, k1=0).values_list('c0', 'v0')) @@ -1390,18 +1389,18 @@ def test_defaultFetchSize(self): for i in range(5000, 5100): TestModelSmall.batch(b).create(test_id=i) - self.assertEqual(len(TestModelSmall.objects.fetch_size(1)), 5100) - self.assertEqual(len(TestModelSmall.objects.fetch_size(500)), 5100) - self.assertEqual(len(TestModelSmall.objects.fetch_size(4999)), 5100) - self.assertEqual(len(TestModelSmall.objects.fetch_size(5000)), 5100) - self.assertEqual(len(TestModelSmall.objects.fetch_size(5001)), 5100) - self.assertEqual(len(TestModelSmall.objects.fetch_size(5100)), 5100) - self.assertEqual(len(TestModelSmall.objects.fetch_size(5101)), 5100) - self.assertEqual(len(TestModelSmall.objects.fetch_size(1)), 5100) + assert len(TestModelSmall.objects.fetch_size(1)) == 5100 + assert len(TestModelSmall.objects.fetch_size(500)) == 5100 + assert len(TestModelSmall.objects.fetch_size(4999)) == 5100 + assert len(TestModelSmall.objects.fetch_size(5000)) == 5100 + assert len(TestModelSmall.objects.fetch_size(5001)) == 5100 + assert len(TestModelSmall.objects.fetch_size(5100)) == 5100 + assert len(TestModelSmall.objects.fetch_size(5101)) == 5100 + assert len(TestModelSmall.objects.fetch_size(1)) == 5100 - with self.assertRaises(QueryException): + with pytest.raises(QueryException): TestModelSmall.objects.fetch_size(0) - with self.assertRaises(QueryException): + with pytest.raises(QueryException): TestModelSmall.objects.fetch_size(-1) @@ -1453,11 +1452,11 @@ def test_defaultFetchSize(self): # Check query constructions expected_fields = ['first_name', 'birthday'] - self.assertEqual(People.filter(last_name="Smith")._select_fields(), expected_fields) + assert People.filter(last_name="Smith")._select_fields() == expected_fields # Validate correct fields are fetched smiths = list(People.filter(last_name="Smith")) - self.assertEqual(len(smiths), 3) - self.assertTrue(smiths[0].last_name is not None) + assert len(smiths) == 3 + assert smiths[0].last_name is not None # Modify table with new value sync_table(People2) @@ -1468,9 +1467,9 @@ def test_defaultFetchSize(self): # validate query construction expected_fields = ['first_name', 'middle_name', 'birthday'] - self.assertEqual(People2.filter(last_name="Smith")._select_fields(), expected_fields) + assert People2.filter(last_name="Smith")._select_fields() == expected_fields # validate correct items are returneds smiths = list(People2.filter(last_name="Smith")) - self.assertEqual(len(smiths), 5) - self.assertTrue(smiths[0].last_name is not None) + assert len(smiths) == 5 + assert smiths[0].last_name is not None diff --git a/tests/integration/cqlengine/query/test_updates.py b/tests/integration/cqlengine/query/test_updates.py index f92e4fc53f..cedde0cd7b 100644 --- a/tests/integration/cqlengine/query/test_updates.py +++ b/tests/integration/cqlengine/query/test_updates.py @@ -23,6 +23,7 @@ from tests.integration.cqlengine.base import BaseCassEngTestCase, TestQueryUpdateModel from tests.integration.cqlengine import execute_count from tests.integration import greaterthancass20 +import pytest class QueryUpdateTests(BaseCassEngTestCase): @@ -46,17 +47,17 @@ def test_update_values(self): # sanity check for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)): - self.assertEqual(row.cluster, i) - self.assertEqual(row.count, i) - self.assertEqual(row.text, str(i)) + assert row.cluster == i + assert row.count == i + assert row.text == str(i) # perform update TestQueryUpdateModel.objects(partition=partition, cluster=3).update(count=6) for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)): - self.assertEqual(row.cluster, i) - self.assertEqual(row.count, 6 if i == 3 else i) - self.assertEqual(row.text, str(i)) + assert row.cluster == i + assert row.count == (6 if i == 3 else i) + assert row.text == str(i) @execute_count(6) def test_update_values_validation(self): @@ -67,22 +68,22 @@ def test_update_values_validation(self): # sanity check for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)): - self.assertEqual(row.cluster, i) - self.assertEqual(row.count, i) - self.assertEqual(row.text, str(i)) + assert row.cluster == i + assert row.count == i + assert row.text == str(i) # perform update - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): TestQueryUpdateModel.objects(partition=partition, cluster=3).update(count='asdf') def test_invalid_update_kwarg(self): """ tests that passing in a kwarg to the update method that isn't a column will fail """ - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): TestQueryUpdateModel.objects(partition=uuid4(), cluster=3).update(bacon=5000) def test_primary_key_update_failure(self): """ tests that attempting to update the value of a primary key will fail """ - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): TestQueryUpdateModel.objects(partition=uuid4(), cluster=3).update(cluster=5000) @execute_count(8) @@ -94,17 +95,17 @@ def test_null_update_deletes_column(self): # sanity check for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)): - self.assertEqual(row.cluster, i) - self.assertEqual(row.count, i) - self.assertEqual(row.text, str(i)) + assert row.cluster == i + assert row.count == i + assert row.text == str(i) # perform update TestQueryUpdateModel.objects(partition=partition, cluster=3).update(text=None) for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)): - self.assertEqual(row.cluster, i) - self.assertEqual(row.count, i) - self.assertEqual(row.text, None if i == 3 else str(i)) + assert row.cluster == i + assert row.count == i + assert row.text == None if i == 3 else str(i) @execute_count(9) def test_mixed_value_and_null_update(self): @@ -115,17 +116,17 @@ def test_mixed_value_and_null_update(self): # sanity check for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)): - self.assertEqual(row.cluster, i) - self.assertEqual(row.count, i) - self.assertEqual(row.text, str(i)) + assert row.cluster == i + assert row.count == i + assert row.text == str(i) # perform update TestQueryUpdateModel.objects(partition=partition, cluster=3).update(count=6, text=None) for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)): - self.assertEqual(row.cluster, i) - self.assertEqual(row.count, 6 if i == 3 else i) - self.assertEqual(row.text, None if i == 3 else str(i)) + assert row.cluster == i + assert row.count == (6 if i == 3 else i) + assert row.text == (None if i == 3 else str(i)) @execute_count(3) def test_set_add_updates(self): @@ -136,7 +137,7 @@ def test_set_add_updates(self): TestQueryUpdateModel.objects( partition=partition, cluster=cluster).update(text_set__add=set(('bar',))) obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) - self.assertEqual(obj.text_set, set(("foo", "bar"))) + assert obj.text_set == set(("foo", "bar")) @execute_count(2) def test_set_add_updates_new_record(self): @@ -147,7 +148,7 @@ def test_set_add_updates_new_record(self): TestQueryUpdateModel.objects( partition=partition, cluster=cluster).update(text_set__add=set(('bar',))) obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) - self.assertEqual(obj.text_set, set(("bar",))) + assert obj.text_set == set(("bar",)) @execute_count(3) def test_set_remove_updates(self): @@ -159,7 +160,7 @@ def test_set_remove_updates(self): partition=partition, cluster=cluster).update( text_set__remove=set(('foo',))) obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) - self.assertEqual(obj.text_set, set(("baz",))) + assert obj.text_set == set(("baz",)) @execute_count(3) def test_set_remove_new_record(self): @@ -173,7 +174,7 @@ def test_set_remove_new_record(self): partition=partition, cluster=cluster).update( text_set__remove=set(('afsd',))) obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) - self.assertEqual(obj.text_set, set(("foo",))) + assert obj.text_set == set(("foo",)) @execute_count(3) def test_list_append_updates(self): @@ -185,7 +186,7 @@ def test_list_append_updates(self): partition=partition, cluster=cluster).update( text_list__append=['bar']) obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) - self.assertEqual(obj.text_list, ["foo", "bar"]) + assert obj.text_list == ["foo", "bar"] @execute_count(3) def test_list_prepend_updates(self): @@ -201,7 +202,7 @@ def test_list_prepend_updates(self): text_list__prepend=prepended) obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) expected = (prepended[::-1] if is_prepend_reversed() else prepended) + original - self.assertEqual(obj.text_list, expected) + assert obj.text_list == expected @execute_count(3) def test_map_update_updates(self): @@ -215,7 +216,7 @@ def test_map_update_updates(self): partition=partition, cluster=cluster).update( text_map__update={"bar": '3', "baz": '4'}) obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) - self.assertEqual(obj.text_map, {"foo": '1', "bar": '3', "baz": '4'}) + assert obj.text_map == {"foo": '1', "bar": '3', "baz": '4'} @execute_count(3) def test_map_update_none_deletes_key(self): @@ -231,7 +232,7 @@ def test_map_update_none_deletes_key(self): partition=partition, cluster=cluster).update( text_map__update={"bar": None}) obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) - self.assertEqual(obj.text_map, {"foo": '1'}) + assert obj.text_map == {"foo": '1'} @greaterthancass20 @execute_count(5) @@ -256,22 +257,16 @@ def test_map_update_remove(self): bin_map__update={456: b'4', 123: b'2'} ) obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) - self.assertEqual(obj.text_map, {"foo": '2', "foz": '4'}) - self.assertEqual(obj.bin_map, {123: b'2', 456: b'4'}) + assert obj.text_map == {"foo": '2', "foz": '4'} + assert obj.bin_map == {123: b'2', 456: b'4'} TestQueryUpdateModel.objects(partition=partition, cluster=cluster).update( text_map__remove={"foo", "foz"}, bin_map__remove={123, 456} ) rec = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster) - self.assertEqual( - rec.text_map, - {} - ) - self.assertEqual( - rec.bin_map, - {} - ) + assert rec.text_map == {} + assert rec.bin_map == {} def test_map_remove_rejects_non_sets(self): """ @@ -286,7 +281,7 @@ def test_map_remove_rejects_non_sets(self): cluster=cluster, text_map={"foo": '1', "bar": '2'} ) - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): TestQueryUpdateModel.objects(partition=partition, cluster=cluster).update( text_map__remove=["bar"] ) @@ -314,7 +309,7 @@ def test_an_extra_delete_is_not_sent(self): obj = TestQueryUpdateModel.objects( partition=partition, cluster=cluster).first() - self.assertFalse({k: v for (k, v) in obj._values.items() if v.deleted}) + assert not {k: v for (k, v) in obj._values.items() if v.deleted} obj.text = 'foo' obj.save() @@ -352,6 +347,6 @@ def test_static_deletion(self): """ StaticDeleteModel.create(example_id=5, example_clust=5, example_static2=1) sdm = StaticDeleteModel.filter(example_id=5).first() - self.assertEqual(1, sdm.example_static2) + assert 1 == sdm.example_static2 sdm.update(example_static2=None) - self.assertIsNone(sdm.example_static2) + assert sdm.example_static2 is None diff --git a/tests/integration/cqlengine/statements/test_assignment_clauses.py b/tests/integration/cqlengine/statements/test_assignment_clauses.py index 82bf067cb4..dce910fd5e 100644 --- a/tests/integration/cqlengine/statements/test_assignment_clauses.py +++ b/tests/integration/cqlengine/statements/test_assignment_clauses.py @@ -24,7 +24,7 @@ def test_rendering(self): def test_insert_tuple(self): ac = AssignmentClause('a', 'b') ac.set_context_id(10) - self.assertEqual(ac.insert_tuple(), ('a', 10)) + assert ac.insert_tuple() == ('a', 10) class SetUpdateClauseTests(unittest.TestCase): @@ -34,16 +34,16 @@ def test_update_from_none(self): c._analyze() c.set_context_id(0) - self.assertEqual(c._assignments, set((1, 2))) - self.assertIsNone(c._additions) - self.assertIsNone(c._removals) + assert c._assignments == set((1, 2)) + assert c._additions is None + assert c._removals is None - self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = %(0)s') + assert c.get_context_size() == 1 + assert str(c) == '"s" = %(0)s' ctx = {} c.update_context(ctx) - self.assertEqual(ctx, {'0': set((1, 2))}) + assert ctx == {'0': set((1, 2))} def test_null_update(self): """ tests setting a set to None creates an empty update statement """ @@ -51,16 +51,16 @@ def test_null_update(self): c._analyze() c.set_context_id(0) - self.assertIsNone(c._assignments) - self.assertIsNone(c._additions) - self.assertIsNone(c._removals) + assert c._assignments is None + assert c._additions is None + assert c._removals is None - self.assertEqual(c.get_context_size(), 0) - self.assertEqual(str(c), '') + assert c.get_context_size() == 0 + assert str(c) == '' ctx = {} c.update_context(ctx) - self.assertEqual(ctx, {}) + assert ctx == {} def test_no_update(self): """ tests an unchanged value creates an empty update statement """ @@ -68,16 +68,16 @@ def test_no_update(self): c._analyze() c.set_context_id(0) - self.assertIsNone(c._assignments) - self.assertIsNone(c._additions) - self.assertIsNone(c._removals) + assert c._assignments is None + assert c._additions is None + assert c._removals is None - self.assertEqual(c.get_context_size(), 0) - self.assertEqual(str(c), '') + assert c.get_context_size() == 0 + assert str(c) == '' ctx = {} c.update_context(ctx) - self.assertEqual(ctx, {}) + assert ctx == {} def test_update_empty_set(self): """tests assigning a set to an empty set creates a nonempty @@ -86,64 +86,64 @@ def test_update_empty_set(self): c._analyze() c.set_context_id(0) - self.assertEqual(c._assignments, set()) - self.assertIsNone(c._additions) - self.assertIsNone(c._removals) + assert c._assignments == set() + assert c._additions is None + assert c._removals is None - self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = %(0)s') + assert c.get_context_size() == 1 + assert str(c) == '"s" = %(0)s' ctx = {} c.update_context(ctx) - self.assertEqual(ctx, {'0': set()}) + assert ctx == {'0': set()} def test_additions(self): c = SetUpdateClause('s', set((1, 2, 3)), previous=set((1, 2))) c._analyze() c.set_context_id(0) - self.assertIsNone(c._assignments) - self.assertEqual(c._additions, set((3,))) - self.assertIsNone(c._removals) + assert c._assignments is None + assert c._additions == set((3,)) + assert c._removals is None - self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = "s" + %(0)s') + assert c.get_context_size() == 1 + assert str(c) == '"s" = "s" + %(0)s' ctx = {} c.update_context(ctx) - self.assertEqual(ctx, {'0': set((3,))}) + assert ctx == {'0': set((3,))} def test_removals(self): c = SetUpdateClause('s', set((1, 2)), previous=set((1, 2, 3))) c._analyze() c.set_context_id(0) - self.assertIsNone(c._assignments) - self.assertIsNone(c._additions) - self.assertEqual(c._removals, set((3,))) + assert c._assignments is None + assert c._additions is None + assert c._removals == set((3,)) - self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = "s" - %(0)s') + assert c.get_context_size() == 1 + assert str(c) == '"s" = "s" - %(0)s' ctx = {} c.update_context(ctx) - self.assertEqual(ctx, {'0': set((3,))}) + assert ctx == {'0': set((3,))} def test_additions_and_removals(self): c = SetUpdateClause('s', set((2, 3)), previous=set((1, 2))) c._analyze() c.set_context_id(0) - self.assertIsNone(c._assignments) - self.assertEqual(c._additions, set((3,))) - self.assertEqual(c._removals, set((1,))) + assert c._assignments is None + assert c._additions == set((3,)) + assert c._removals == set((1,)) - self.assertEqual(c.get_context_size(), 2) - self.assertEqual(str(c), '"s" = "s" + %(0)s, "s" = "s" - %(1)s') + assert c.get_context_size() == 2 + assert str(c) == '"s" = "s" + %(0)s, "s" = "s" - %(1)s' ctx = {} c.update_context(ctx) - self.assertEqual(ctx, {'0': set((3,)), '1': set((1,))}) + assert ctx == {'0': set((3,)), '1': set((1,))} class ListUpdateClauseTests(unittest.TestCase): @@ -153,96 +153,96 @@ def test_update_from_none(self): c._analyze() c.set_context_id(0) - self.assertEqual(c._assignments, [1, 2, 3]) - self.assertIsNone(c._append) - self.assertIsNone(c._prepend) + assert c._assignments == [1, 2, 3] + assert c._append is None + assert c._prepend is None - self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = %(0)s') + assert c.get_context_size() == 1 + assert str(c) == '"s" = %(0)s' ctx = {} c.update_context(ctx) - self.assertEqual(ctx, {'0': [1, 2, 3]}) + assert ctx == {'0': [1, 2, 3]} def test_update_from_empty(self): c = ListUpdateClause('s', [1, 2, 3], previous=[]) c._analyze() c.set_context_id(0) - self.assertEqual(c._assignments, [1, 2, 3]) - self.assertIsNone(c._append) - self.assertIsNone(c._prepend) + assert c._assignments == [1, 2, 3] + assert c._append is None + assert c._prepend is None - self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = %(0)s') + assert c.get_context_size() == 1 + assert str(c) == '"s" = %(0)s' ctx = {} c.update_context(ctx) - self.assertEqual(ctx, {'0': [1, 2, 3]}) + assert ctx == {'0': [1, 2, 3]} def test_update_from_different_list(self): c = ListUpdateClause('s', [1, 2, 3], previous=[3, 2, 1]) c._analyze() c.set_context_id(0) - self.assertEqual(c._assignments, [1, 2, 3]) - self.assertIsNone(c._append) - self.assertIsNone(c._prepend) + assert c._assignments == [1, 2, 3] + assert c._append is None + assert c._prepend is None - self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = %(0)s') + assert c.get_context_size() == 1 + assert str(c) == '"s" = %(0)s' ctx = {} c.update_context(ctx) - self.assertEqual(ctx, {'0': [1, 2, 3]}) + assert ctx == {'0': [1, 2, 3]} def test_append(self): c = ListUpdateClause('s', [1, 2, 3, 4], previous=[1, 2]) c._analyze() c.set_context_id(0) - self.assertIsNone(c._assignments) - self.assertEqual(c._append, [3, 4]) - self.assertIsNone(c._prepend) + assert c._assignments is None + assert c._append == [3, 4] + assert c._prepend is None - self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = "s" + %(0)s') + assert c.get_context_size() == 1 + assert str(c) == '"s" = "s" + %(0)s' ctx = {} c.update_context(ctx) - self.assertEqual(ctx, {'0': [3, 4]}) + assert ctx == {'0': [3, 4]} def test_prepend(self): c = ListUpdateClause('s', [1, 2, 3, 4], previous=[3, 4]) c._analyze() c.set_context_id(0) - self.assertIsNone(c._assignments) - self.assertIsNone(c._append) - self.assertEqual(c._prepend, [1, 2]) + assert c._assignments is None + assert c._append is None + assert c._prepend == [1, 2] - self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = %(0)s + "s"') + assert c.get_context_size() == 1 + assert str(c) == '"s" = %(0)s + "s"' ctx = {} c.update_context(ctx) - self.assertEqual(ctx, {'0': [1, 2]}) + assert ctx == {'0': [1, 2]} def test_append_and_prepend(self): c = ListUpdateClause('s', [1, 2, 3, 4, 5, 6], previous=[3, 4]) c._analyze() c.set_context_id(0) - self.assertIsNone(c._assignments) - self.assertEqual(c._append, [5, 6]) - self.assertEqual(c._prepend, [1, 2]) + assert c._assignments is None + assert c._append == [5, 6] + assert c._prepend == [1, 2] - self.assertEqual(c.get_context_size(), 2) - self.assertEqual(str(c), '"s" = %(0)s + "s", "s" = "s" + %(1)s') + assert c.get_context_size() == 2 + assert str(c) == '"s" = %(0)s + "s", "s" = "s" + %(1)s' ctx = {} c.update_context(ctx) - self.assertEqual(ctx, {'0': [1, 2], '1': [5, 6]}) + assert ctx == {'0': [1, 2], '1': [5, 6]} def test_shrinking_list_update(self): """ tests that updating to a smaller list results in an insert statement """ @@ -250,16 +250,16 @@ def test_shrinking_list_update(self): c._analyze() c.set_context_id(0) - self.assertEqual(c._assignments, [1, 2, 3]) - self.assertIsNone(c._append) - self.assertIsNone(c._prepend) + assert c._assignments == [1, 2, 3] + assert c._append is None + assert c._prepend is None - self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"s" = %(0)s') + assert c.get_context_size() == 1 + assert str(c) == '"s" = %(0)s' ctx = {} c.update_context(ctx) - self.assertEqual(ctx, {'0': [1, 2, 3]}) + assert ctx == {'0': [1, 2, 3]} class MapUpdateTests(unittest.TestCase): @@ -269,33 +269,33 @@ def test_update(self): c._analyze() c.set_context_id(0) - self.assertEqual(c._updates, [3, 5]) - self.assertEqual(c.get_context_size(), 4) - self.assertEqual(str(c), '"s"[%(0)s] = %(1)s, "s"[%(2)s] = %(3)s') + assert c._updates == [3, 5] + assert c.get_context_size() == 4 + assert str(c) == '"s"[%(0)s] = %(1)s, "s"[%(2)s] = %(3)s' ctx = {} c.update_context(ctx) - self.assertEqual(ctx, {'0': 3, "1": 0, '2': 5, '3': 6}) + assert ctx == {'0': 3, "1": 0, '2': 5, '3': 6} def test_update_from_null(self): c = MapUpdateClause('s', {3: 0, 5: 6}) c._analyze() c.set_context_id(0) - self.assertEqual(c._updates, [3, 5]) - self.assertEqual(c.get_context_size(), 4) - self.assertEqual(str(c), '"s"[%(0)s] = %(1)s, "s"[%(2)s] = %(3)s') + assert c._updates == [3, 5] + assert c.get_context_size() == 4 + assert str(c) == '"s"[%(0)s] = %(1)s, "s"[%(2)s] = %(3)s' ctx = {} c.update_context(ctx) - self.assertEqual(ctx, {'0': 3, "1": 0, '2': 5, '3': 6}) + assert ctx == {'0': 3, "1": 0, '2': 5, '3': 6} def test_nulled_columns_arent_included(self): c = MapUpdateClause('s', {3: 0}, {1: 2, 3: 4}) c._analyze() c.set_context_id(0) - self.assertNotIn(1, c._updates) + assert 1 not in c._updates class CounterUpdateTests(unittest.TestCase): @@ -304,34 +304,34 @@ def test_positive_update(self): c = CounterUpdateClause('a', 5, 3) c.set_context_id(5) - self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"a" = "a" + %(5)s') + assert c.get_context_size() == 1 + assert str(c) == '"a" = "a" + %(5)s' ctx = {} c.update_context(ctx) - self.assertEqual(ctx, {'5': 2}) + assert ctx == {'5': 2} def test_negative_update(self): c = CounterUpdateClause('a', 4, 7) c.set_context_id(3) - self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"a" = "a" - %(3)s') + assert c.get_context_size() == 1 + assert str(c) == '"a" = "a" - %(3)s' ctx = {} c.update_context(ctx) - self.assertEqual(ctx, {'3': 3}) + assert ctx == {'3': 3} def noop_update(self): c = CounterUpdateClause('a', 5, 5) c.set_context_id(5) - self.assertEqual(c.get_context_size(), 1) - self.assertEqual(str(c), '"a" = "a" + %(0)s') + assert c.get_context_size() == 1 + assert str(c) == '"a" = "a" + %(0)s' ctx = {} c.update_context(ctx) - self.assertEqual(ctx, {'5': 0}) + assert ctx == {'5': 0} class MapDeleteTests(unittest.TestCase): @@ -341,13 +341,13 @@ def test_update(self): c._analyze() c.set_context_id(0) - self.assertEqual(c._removals, [1, 5]) - self.assertEqual(c.get_context_size(), 2) - self.assertEqual(str(c), '"s"[%(0)s], "s"[%(1)s]') + assert c._removals == [1, 5] + assert c.get_context_size() == 2 + assert str(c) == '"s"[%(0)s], "s"[%(1)s]' ctx = {} c.update_context(ctx) - self.assertEqual(ctx, {'0': 1, '1': 5}) + assert ctx == {'0': 1, '1': 5} class FieldDeleteTests(unittest.TestCase): diff --git a/tests/integration/cqlengine/statements/test_base_statement.py b/tests/integration/cqlengine/statements/test_base_statement.py index bbcdc700e1..a2af184235 100644 --- a/tests/integration/cqlengine/statements/test_base_statement.py +++ b/tests/integration/cqlengine/statements/test_base_statement.py @@ -35,13 +35,13 @@ class BaseStatementTest(unittest.TestCase): def test_fetch_size(self): """ tests that fetch_size is correctly set """ stmt = BaseCQLStatement('table', None, fetch_size=1000) - self.assertEqual(stmt.fetch_size, 1000) + assert stmt.fetch_size == 1000 stmt = BaseCQLStatement('table', None, fetch_size=None) - self.assertEqual(stmt.fetch_size, FETCH_SIZE_UNSET) + assert stmt.fetch_size == FETCH_SIZE_UNSET stmt = BaseCQLStatement('table', None) - self.assertEqual(stmt.fetch_size, FETCH_SIZE_UNSET) + assert stmt.fetch_size == FETCH_SIZE_UNSET class ExecuteStatementTest(BaseCassEngTestCase): @@ -64,8 +64,8 @@ def _verify_statement(self, original): response = result.one() for assignment in original.assignments: - self.assertEqual(response[assignment.field], assignment.value) - self.assertEqual(len(response), 8) + assert response[assignment.field] == assignment.value + assert len(response) == 8 def test_insert_statement_execute(self): """ @@ -99,7 +99,7 @@ def test_insert_statement_execute(self): # Verifying delete statement execute(DeleteStatement(self.table_name, where=where)) - self.assertEqual(TestQueryUpdateModel.objects.count(), 0) + assert TestQueryUpdateModel.objects.count() == 0 @greaterthanorequalcass3_10 @requires_custom_indexes @@ -128,17 +128,16 @@ def test_like_operator(self): ss = SelectStatement(self.table_name) like_clause = "text_for_%" ss.add_where(Column(db_field='text'), LikeOperator(), like_clause) - self.assertEqual(str(ss), - 'SELECT * FROM {} WHERE "text" LIKE %(0)s'.format(self.table_name)) + assert str(ss) == 'SELECT * FROM {} WHERE "text" LIKE %(0)s'.format(self.table_name) result = execute(ss) - self.assertEqual(result[0]["text"], self.text) + assert result[0]["text"] == self.text q = TestQueryUpdateModel.objects.filter(text__like=like_clause).allow_filtering() - self.assertEqual(q[0].text, self.text) + assert q[0].text == self.text q = TestQueryUpdateModel.objects.filter(text__like=like_clause) - self.assertEqual(q[0].text, self.text) + assert q[0].text == self.text def _insert_statement(self, partition, cluster): # Verifying insert statement diff --git a/tests/integration/cqlengine/statements/test_delete_statement.py b/tests/integration/cqlengine/statements/test_delete_statement.py index 745881f42f..c5335e94c4 100644 --- a/tests/integration/cqlengine/statements/test_delete_statement.py +++ b/tests/integration/cqlengine/statements/test_delete_statement.py @@ -24,30 +24,30 @@ class DeleteStatementTests(TestCase): def test_single_field_is_listified(self): """ tests that passing a string field into the constructor puts it into a list """ ds = DeleteStatement('table', 'field') - self.assertEqual(len(ds.fields), 1) - self.assertEqual(ds.fields[0].field, 'field') + assert len(ds.fields) == 1 + assert ds.fields[0].field == 'field' def test_field_rendering(self): """ tests that fields are properly added to the select statement """ ds = DeleteStatement('table', ['f1', 'f2']) - self.assertTrue(str(ds).startswith('DELETE "f1", "f2"'), str(ds)) - self.assertTrue(str(ds).startswith('DELETE "f1", "f2"'), str(ds)) + assert str(ds).startswith('DELETE "f1", "f2"'), str(ds) + assert str(ds).startswith('DELETE "f1", "f2"'), str(ds) def test_none_fields_rendering(self): """ tests that a '*' is added if no fields are passed in """ ds = DeleteStatement('table', None) - self.assertTrue(str(ds).startswith('DELETE FROM'), str(ds)) - self.assertTrue(str(ds).startswith('DELETE FROM'), str(ds)) + assert str(ds).startswith('DELETE FROM'), str(ds) + assert str(ds).startswith('DELETE FROM'), str(ds) def test_table_rendering(self): ds = DeleteStatement('table', None) - self.assertTrue(str(ds).startswith('DELETE FROM table'), str(ds)) - self.assertTrue(str(ds).startswith('DELETE FROM table'), str(ds)) + assert str(ds).startswith('DELETE FROM table'), str(ds) + assert str(ds).startswith('DELETE FROM table'), str(ds) def test_where_clause_rendering(self): ds = DeleteStatement('table', None) ds.add_where(Column(db_field='a'), EqualsOperator(), 'b') - self.assertEqual(str(ds), 'DELETE FROM table WHERE "a" = %(0)s', str(ds)) + assert str(ds) == 'DELETE FROM table WHERE "a" = %(0)s', str(ds) def test_context_update(self): ds = DeleteStatement('table', None) @@ -55,36 +55,36 @@ def test_context_update(self): ds.add_where(Column(db_field='a'), EqualsOperator(), 'b') ds.update_context_id(7) - self.assertEqual(str(ds), 'DELETE "d"[%(8)s] FROM table WHERE "a" = %(7)s') - self.assertEqual(ds.get_context(), {'7': 'b', '8': 3}) + assert str(ds) == 'DELETE "d"[%(8)s] FROM table WHERE "a" = %(7)s' + assert ds.get_context() == {'7': 'b', '8': 3} def test_context(self): ds = DeleteStatement('table', None) ds.add_where(Column(db_field='a'), EqualsOperator(), 'b') - self.assertEqual(ds.get_context(), {'0': 'b'}) + assert ds.get_context() == {'0': 'b'} def test_range_deletion_rendering(self): ds = DeleteStatement('table', None) ds.add_where(Column(db_field='a'), EqualsOperator(), 'b') ds.add_where(Column(db_field='created_at'), GreaterThanOrEqualOperator(), '0') ds.add_where(Column(db_field='created_at'), LessThanOrEqualOperator(), '10') - self.assertEqual(str(ds), 'DELETE FROM table WHERE "a" = %(0)s AND "created_at" >= %(1)s AND "created_at" <= %(2)s', str(ds)) + assert str(ds) == 'DELETE FROM table WHERE "a" = %(0)s AND "created_at" >= %(1)s AND "created_at" <= %(2)s', str(ds) ds = DeleteStatement('table', None) ds.add_where(Column(db_field='a'), EqualsOperator(), 'b') ds.add_where(Column(db_field='created_at'), InOperator(), ['0', '10', '20']) - self.assertEqual(str(ds), 'DELETE FROM table WHERE "a" = %(0)s AND "created_at" IN %(1)s', str(ds)) + assert str(ds) == 'DELETE FROM table WHERE "a" = %(0)s AND "created_at" IN %(1)s', str(ds) ds = DeleteStatement('table', None) ds.add_where(Column(db_field='a'), NotEqualsOperator(), 'b') - self.assertEqual(str(ds), 'DELETE FROM table WHERE "a" != %(0)s', str(ds)) + assert str(ds) == 'DELETE FROM table WHERE "a" != %(0)s', str(ds) def test_delete_conditional(self): where = [WhereClause('id', EqualsOperator(), 1)] conditionals = [ConditionalClause('f0', 'value0'), ConditionalClause('f1', 'value1')] ds = DeleteStatement('table', where=where, conditionals=conditionals) - self.assertEqual(len(ds.conditionals), len(conditionals)) - self.assertEqual(str(ds), 'DELETE FROM table WHERE "id" = %(0)s IF "f0" = %(1)s AND "f1" = %(2)s', str(ds)) + assert len(ds.conditionals) == len(conditionals) + assert str(ds) == 'DELETE FROM table WHERE "id" = %(0)s IF "f0" = %(1)s AND "f1" = %(2)s', str(ds) fields = ['one', 'two'] ds = DeleteStatement('table', fields=fields, where=where, conditionals=conditionals) - self.assertEqual(str(ds), 'DELETE "one", "two" FROM table WHERE "id" = %(0)s IF "f0" = %(1)s AND "f1" = %(2)s', str(ds)) + assert str(ds) == 'DELETE "one", "two" FROM table WHERE "id" = %(0)s IF "f0" = %(1)s AND "f1" = %(2)s', str(ds) diff --git a/tests/integration/cqlengine/statements/test_insert_statement.py b/tests/integration/cqlengine/statements/test_insert_statement.py index 45485af912..0f70253a6c 100644 --- a/tests/integration/cqlengine/statements/test_insert_statement.py +++ b/tests/integration/cqlengine/statements/test_insert_statement.py @@ -24,10 +24,7 @@ def test_statement(self): ist.add_assignment(Column(db_field='a'), 'b') ist.add_assignment(Column(db_field='c'), 'd') - self.assertEqual( - str(ist), - 'INSERT INTO table ("a", "c") VALUES (%(0)s, %(1)s)' - ) + assert str(ist) == 'INSERT INTO table ("a", "c") VALUES (%(0)s, %(1)s)' def test_context_update(self): ist = InsertStatement('table', None) @@ -35,15 +32,12 @@ def test_context_update(self): ist.add_assignment(Column(db_field='c'), 'd') ist.update_context_id(4) - self.assertEqual( - str(ist), - 'INSERT INTO table ("a", "c") VALUES (%(4)s, %(5)s)' - ) + assert str(ist) == 'INSERT INTO table ("a", "c") VALUES (%(4)s, %(5)s)' ctx = ist.get_context() - self.assertEqual(ctx, {'4': 'b', '5': 'd'}) + assert ctx == {'4': 'b', '5': 'd'} def test_additional_rendering(self): ist = InsertStatement('table', ttl=60) ist.add_assignment(Column(db_field='a'), 'b') ist.add_assignment(Column(db_field='c'), 'd') - self.assertIn('USING TTL 60', str(ist)) + assert 'USING TTL 60' in str(ist) diff --git a/tests/integration/cqlengine/statements/test_select_statement.py b/tests/integration/cqlengine/statements/test_select_statement.py index 26c9c804cb..b4bada1eb0 100644 --- a/tests/integration/cqlengine/statements/test_select_statement.py +++ b/tests/integration/cqlengine/statements/test_select_statement.py @@ -22,63 +22,63 @@ class SelectStatementTests(unittest.TestCase): def test_single_field_is_listified(self): """ tests that passing a string field into the constructor puts it into a list """ ss = SelectStatement('table', 'field') - self.assertEqual(ss.fields, ['field']) + assert ss.fields == ['field'] def test_field_rendering(self): """ tests that fields are properly added to the select statement """ ss = SelectStatement('table', ['f1', 'f2']) - self.assertTrue(str(ss).startswith('SELECT "f1", "f2"'), str(ss)) - self.assertTrue(str(ss).startswith('SELECT "f1", "f2"'), str(ss)) + assert str(ss).startswith('SELECT "f1", "f2"'), str(ss) + assert str(ss).startswith('SELECT "f1", "f2"'), str(ss) def test_none_fields_rendering(self): """ tests that a '*' is added if no fields are passed in """ ss = SelectStatement('table') - self.assertTrue(str(ss).startswith('SELECT *'), str(ss)) - self.assertTrue(str(ss).startswith('SELECT *'), str(ss)) + assert str(ss).startswith('SELECT *'), str(ss) + assert str(ss).startswith('SELECT *'), str(ss) def test_table_rendering(self): ss = SelectStatement('table') - self.assertTrue(str(ss).startswith('SELECT * FROM table'), str(ss)) - self.assertTrue(str(ss).startswith('SELECT * FROM table'), str(ss)) + assert str(ss).startswith('SELECT * FROM table'), str(ss) + assert str(ss).startswith('SELECT * FROM table'), str(ss) def test_where_clause_rendering(self): ss = SelectStatement('table') ss.add_where(Column(db_field='a'), EqualsOperator(), 'b') - self.assertEqual(str(ss), 'SELECT * FROM table WHERE "a" = %(0)s', str(ss)) + assert str(ss) == 'SELECT * FROM table WHERE "a" = %(0)s', str(ss) def test_count(self): ss = SelectStatement('table', count=True, limit=10, order_by='d') ss.add_where(Column(db_field='a'), EqualsOperator(), 'b') - self.assertEqual(str(ss), 'SELECT COUNT(*) FROM table WHERE "a" = %(0)s LIMIT 10', str(ss)) - self.assertIn('LIMIT', str(ss)) - self.assertNotIn('ORDER', str(ss)) + assert str(ss) == 'SELECT COUNT(*) FROM table WHERE "a" = %(0)s LIMIT 10', str(ss) + assert 'LIMIT' in str(ss) + assert 'ORDER' not in str(ss) def test_distinct(self): ss = SelectStatement('table', distinct_fields=['field2']) ss.add_where(Column(db_field='field1'), EqualsOperator(), 'b') - self.assertEqual(str(ss), 'SELECT DISTINCT "field2" FROM table WHERE "field1" = %(0)s', str(ss)) + assert str(ss) == 'SELECT DISTINCT "field2" FROM table WHERE "field1" = %(0)s', str(ss) ss = SelectStatement('table', distinct_fields=['field1', 'field2']) - self.assertEqual(str(ss), 'SELECT DISTINCT "field1", "field2" FROM table') + assert str(ss) == 'SELECT DISTINCT "field1", "field2" FROM table' ss = SelectStatement('table', distinct_fields=['field1'], count=True) - self.assertEqual(str(ss), 'SELECT DISTINCT COUNT("field1") FROM table') + assert str(ss) == 'SELECT DISTINCT COUNT("field1") FROM table' def test_context(self): ss = SelectStatement('table') ss.add_where(Column(db_field='a'), EqualsOperator(), 'b') - self.assertEqual(ss.get_context(), {'0': 'b'}) + assert ss.get_context() == {'0': 'b'} def test_context_id_update(self): """ tests that the right things happen the the context id """ ss = SelectStatement('table') ss.add_where(Column(db_field='a'), EqualsOperator(), 'b') - self.assertEqual(ss.get_context(), {'0': 'b'}) - self.assertEqual(str(ss), 'SELECT * FROM table WHERE "a" = %(0)s') + assert ss.get_context() == {'0': 'b'} + assert str(ss) == 'SELECT * FROM table WHERE "a" = %(0)s' ss.update_context_id(5) - self.assertEqual(ss.get_context(), {'5': 'b'}) - self.assertEqual(str(ss), 'SELECT * FROM table WHERE "a" = %(5)s') + assert ss.get_context() == {'5': 'b'} + assert str(ss) == 'SELECT * FROM table WHERE "a" = %(5)s' def test_additional_rendering(self): ss = SelectStatement( @@ -89,19 +89,19 @@ def test_additional_rendering(self): allow_filtering=True ) qstr = str(ss) - self.assertIn('LIMIT 15', qstr) - self.assertIn('ORDER BY x, y', qstr) - self.assertIn('ALLOW FILTERING', qstr) + assert 'LIMIT 15' in qstr + assert 'ORDER BY x, y' in qstr + assert 'ALLOW FILTERING' in qstr def test_limit_rendering(self): ss = SelectStatement('table', None, limit=10) qstr = str(ss) - self.assertIn('LIMIT 10', qstr) + assert 'LIMIT 10' in qstr ss = SelectStatement('table', None, limit=0) qstr = str(ss) - self.assertNotIn('LIMIT', qstr) + assert 'LIMIT' not in qstr ss = SelectStatement('table', None, limit=None) qstr = str(ss) - self.assertNotIn('LIMIT', qstr) + assert 'LIMIT' not in qstr diff --git a/tests/integration/cqlengine/statements/test_update_statement.py b/tests/integration/cqlengine/statements/test_update_statement.py index 4429625bf4..6529b73558 100644 --- a/tests/integration/cqlengine/statements/test_update_statement.py +++ b/tests/integration/cqlengine/statements/test_update_statement.py @@ -25,25 +25,25 @@ class UpdateStatementTests(unittest.TestCase): def test_table_rendering(self): """ tests that fields are properly added to the select statement """ us = UpdateStatement('table') - self.assertTrue(str(us).startswith('UPDATE table SET'), str(us)) - self.assertTrue(str(us).startswith('UPDATE table SET'), str(us)) + assert str(us).startswith('UPDATE table SET'), str(us) + assert str(us).startswith('UPDATE table SET'), str(us) def test_rendering(self): us = UpdateStatement('table') us.add_assignment(Column(db_field='a'), 'b') us.add_assignment(Column(db_field='c'), 'd') us.add_where(Column(db_field='a'), EqualsOperator(), 'x') - self.assertEqual(str(us), 'UPDATE table SET "a" = %(0)s, "c" = %(1)s WHERE "a" = %(2)s', str(us)) + assert str(us) == 'UPDATE table SET "a" = %(0)s, "c" = %(1)s WHERE "a" = %(2)s', str(us) us.add_where(Column(db_field='a'), NotEqualsOperator(), 'y') - self.assertEqual(str(us), 'UPDATE table SET "a" = %(0)s, "c" = %(1)s WHERE "a" = %(2)s AND "a" != %(3)s', str(us)) + assert str(us) == 'UPDATE table SET "a" = %(0)s, "c" = %(1)s WHERE "a" = %(2)s AND "a" != %(3)s', str(us) def test_context(self): us = UpdateStatement('table') us.add_assignment(Column(db_field='a'), 'b') us.add_assignment(Column(db_field='c'), 'd') us.add_where(Column(db_field='a'), EqualsOperator(), 'x') - self.assertEqual(us.get_context(), {'0': 'b', '1': 'd', '2': 'x'}) + assert us.get_context() == {'0': 'b', '1': 'd', '2': 'x'} def test_context_update(self): us = UpdateStatement('table') @@ -51,36 +51,36 @@ def test_context_update(self): us.add_assignment(Column(db_field='c'), 'd') us.add_where(Column(db_field='a'), EqualsOperator(), 'x') us.update_context_id(3) - self.assertEqual(str(us), 'UPDATE table SET "a" = %(4)s, "c" = %(5)s WHERE "a" = %(3)s') - self.assertEqual(us.get_context(), {'4': 'b', '5': 'd', '3': 'x'}) + assert str(us) == 'UPDATE table SET "a" = %(4)s, "c" = %(5)s WHERE "a" = %(3)s' + assert us.get_context() == {'4': 'b', '5': 'd', '3': 'x'} def test_additional_rendering(self): us = UpdateStatement('table', ttl=60) us.add_assignment(Column(db_field='a'), 'b') us.add_where(Column(db_field='a'), EqualsOperator(), 'x') - self.assertIn('USING TTL 60', str(us)) + assert 'USING TTL 60' in str(us) def test_update_set_add(self): us = UpdateStatement('table') us.add_update(Set(Text, db_field='a'), set((1,)), 'add') - self.assertEqual(str(us), 'UPDATE table SET "a" = "a" + %(0)s') + assert str(us) == 'UPDATE table SET "a" = "a" + %(0)s' def test_update_empty_set_add_does_not_assign(self): us = UpdateStatement('table') us.add_update(Set(Text, db_field='a'), set(), 'add') - self.assertFalse(us.assignments) + assert not us.assignments def test_update_empty_set_removal_does_not_assign(self): us = UpdateStatement('table') us.add_update(Set(Text, db_field='a'), set(), 'remove') - self.assertFalse(us.assignments) + assert not us.assignments def test_update_list_prepend_with_empty_list(self): us = UpdateStatement('table') us.add_update(List(Text, db_field='a'), [], 'prepend') - self.assertFalse(us.assignments) + assert not us.assignments def test_update_list_append_with_empty_list(self): us = UpdateStatement('table') us.add_update(List(Text, db_field='a'), [], 'append') - self.assertFalse(us.assignments) + assert not us.assignments diff --git a/tests/integration/cqlengine/statements/test_where_clause.py b/tests/integration/cqlengine/statements/test_where_clause.py index 0090fa0123..8ac2536a19 100644 --- a/tests/integration/cqlengine/statements/test_where_clause.py +++ b/tests/integration/cqlengine/statements/test_where_clause.py @@ -15,13 +15,14 @@ from cassandra.cqlengine.operators import EqualsOperator from cassandra.cqlengine.statements import StatementException, WhereClause +import pytest class TestWhereClause(unittest.TestCase): def test_operator_check(self): """ tests that creating a where statement with a non BaseWhereOperator object fails """ - with self.assertRaises(StatementException): + with pytest.raises(StatementException): WhereClause('a', 'b', 'c') def test_where_clause_rendering(self): @@ -29,8 +30,8 @@ def test_where_clause_rendering(self): wc = WhereClause('a', EqualsOperator(), 'c') wc.set_context_id(5) - self.assertEqual('"a" = %(5)s', str(wc), str(wc)) - self.assertEqual('"a" = %(5)s', str(wc), type(wc)) + assert '"a" = %(5)s' == str(wc), str(wc) + assert '"a" = %(5)s' == str(wc), type(wc) def test_equality_method(self): """ tests that 2 identical where clauses evaluate as == """ diff --git a/tests/integration/cqlengine/test_batch_query.py b/tests/integration/cqlengine/test_batch_query.py index 399bee6202..cd6bf0fe93 100644 --- a/tests/integration/cqlengine/test_batch_query.py +++ b/tests/integration/cqlengine/test_batch_query.py @@ -20,6 +20,7 @@ from cassandra.cqlengine.models import Model from cassandra.cqlengine.query import BatchQuery from tests.integration.cqlengine.base import BaseCassEngTestCase +from tests.util import assertRegex from unittest.mock import patch @@ -56,7 +57,7 @@ def test_insert_success_case(self): b = BatchQuery() TestMultiKeyModel.batch(b).create(partition=self.pkey, cluster=2, count=3, text='4') - with self.assertRaises(TestMultiKeyModel.DoesNotExist): + with pytest.raises(TestMultiKeyModel.DoesNotExist): TestMultiKeyModel.get(partition=self.pkey, cluster=2) b.execute() @@ -73,12 +74,12 @@ def test_update_success_case(self): inst.batch(b).save() inst2 = TestMultiKeyModel.get(partition=self.pkey, cluster=2) - self.assertEqual(inst2.count, 3) + assert inst2.count == 3 b.execute() inst3 = TestMultiKeyModel.get(partition=self.pkey, cluster=2) - self.assertEqual(inst3.count, 4) + assert inst3.count == 4 def test_delete_success_case(self): @@ -92,7 +93,7 @@ def test_delete_success_case(self): b.execute() - with self.assertRaises(TestMultiKeyModel.DoesNotExist): + with pytest.raises(TestMultiKeyModel.DoesNotExist): TestMultiKeyModel.get(partition=self.pkey, cluster=2) def test_context_manager(self): @@ -102,7 +103,7 @@ def test_context_manager(self): TestMultiKeyModel.batch(b).create(partition=self.pkey, cluster=i, count=3, text='4') for i in range(5): - with self.assertRaises(TestMultiKeyModel.DoesNotExist): + with pytest.raises(TestMultiKeyModel.DoesNotExist): TestMultiKeyModel.get(partition=self.pkey, cluster=i) for i in range(5): @@ -116,9 +117,9 @@ def test_bulk_delete_success_case(self): with BatchQuery() as b: TestMultiKeyModel.objects.batch(b).filter(partition=0).delete() - self.assertEqual(TestMultiKeyModel.filter(partition=0).count(), 5) + assert TestMultiKeyModel.filter(partition=0).count() == 5 - self.assertEqual(TestMultiKeyModel.filter(partition=0).count(), 0) + assert TestMultiKeyModel.filter(partition=0).count() == 0 #cleanup for m in TestMultiKeyModel.all(): m.delete() @@ -146,11 +147,11 @@ def my_callback(*args, **kwargs): batch.add_callback(my_callback, 2, named_arg='value') batch.add_callback(my_callback, 1, 3) - self.assertEqual(batch._callbacks, [ + assert batch._callbacks == [ (my_callback, (), {}), (my_callback, (2,), {'named_arg':'value'}), (my_callback, (1, 3), {}) - ]) + ] def test_callbacks_properly_execute_callables_and_tuples(self): @@ -166,8 +167,8 @@ def my_callback(*args, **kwargs): batch.execute() - self.assertEqual(len(call_history), 2) - self.assertEqual([(), ('more', 'args')], call_history) + assert len(call_history) == 2 + assert [(), ('more', 'args')] == call_history def test_callbacks_tied_to_execute(self): """Batch callbacks should NOT fire if batch is not executed in context manager mode""" @@ -179,12 +180,12 @@ def my_callback(*args, **kwargs): with BatchQuery() as batch: batch.add_callback(my_callback) - self.assertEqual(len(call_history), 1) + assert len(call_history) == 1 class SomeError(Exception): pass - with self.assertRaises(SomeError): + with pytest.raises(SomeError): with BatchQuery() as batch: batch.add_callback(my_callback) # this error bubbling up through context manager @@ -192,17 +193,17 @@ class SomeError(Exception): raise SomeError # still same call history. Nothing added - self.assertEqual(len(call_history), 1) + assert len(call_history) == 1 # but if execute ran, even with an error bubbling through # the callbacks also would have fired - with self.assertRaises(SomeError): + with pytest.raises(SomeError): with BatchQuery(execute_on_exception=True) as batch: batch.add_callback(my_callback) raise SomeError # updated call history - self.assertEqual(len(call_history), 2) + assert len(call_history) == 2 def test_callbacks_work_multiple_times(self): """ @@ -224,8 +225,8 @@ def my_callback(*args, **kwargs): batch.add_callback(my_callback) batch.execute() batch.execute() - self.assertEqual(len(w), 2) # package filter setup to warn always - self.assertRegex(str(w[0].message), r"^Batch.*multiple.*") + assert len(w) == 2 # package filter setup to warn always + assertRegex(str(w[0].message), r"^Batch.*multiple.*") def test_disable_multiple_callback_warning(self): """ @@ -250,4 +251,4 @@ def my_callback(*args, **kwargs): batch.add_callback(my_callback) batch.execute() batch.execute() - self.assertFalse(w) + assert not w diff --git a/tests/integration/cqlengine/test_connections.py b/tests/integration/cqlengine/test_connections.py index 32db143088..612255bdc5 100644 --- a/tests/integration/cqlengine/test_connections.py +++ b/tests/integration/cqlengine/test_connections.py @@ -23,6 +23,7 @@ from tests.integration.cqlengine.base import BaseCassEngTestCase from tests.integration.cqlengine.query import test_queryset from tests.integration import local, CASSANDRA_IP, TestCluster +import pytest class TestModel(Model): @@ -98,7 +99,7 @@ def test_context_connection_priority(self): # ContextQuery connection should have priority over default one with ContextQuery(TestModel, connection='fake_cluster') as tm: - with self.assertRaises(NoHostAvailable): + with pytest.raises(NoHostAvailable): tm.objects.create(partition=1, cluster=1) # Explicit connection should have priority over ContextQuery one @@ -110,7 +111,7 @@ def test_context_connection_priority(self): # No model connection and an invalid default connection with ContextQuery(TestModel) as tm: - with self.assertRaises(NoHostAvailable): + with pytest.raises(NoHostAvailable): tm.objects.create(partition=1, cluster=1) def test_context_connection_with_keyspace(self): @@ -126,7 +127,7 @@ def test_context_connection_with_keyspace(self): # ks2 doesn't exist with ContextQuery(TestModel, connection='cluster', keyspace='ks2') as tm: - with self.assertRaises(InvalidRequest): + with pytest.raises(InvalidRequest): tm.objects.create(partition=1, cluster=1) @@ -166,7 +167,7 @@ def test_create_drop_keyspace(self): """ # No connection (default is fake) - with self.assertRaises(NoHostAvailable): + with pytest.raises(NoHostAvailable): create_keyspace_simple(self.keyspaces[0], 1) # Explicit connections @@ -190,7 +191,7 @@ def test_create_drop_table(self): create_keyspace_simple(ks, 1, connections=self.conns) # No connection (default is fake) - with self.assertRaises(NoHostAvailable): + with pytest.raises(NoHostAvailable): sync_table(TestModel) # Explicit connections @@ -205,7 +206,7 @@ def test_create_drop_table(self): TestModel.__connection__ = None # No connection (default is fake) - with self.assertRaises(NoHostAvailable): + with pytest.raises(NoHostAvailable): drop_table(TestModel) # Model connection @@ -230,7 +231,7 @@ def test_connection_creation_from_session(self): session = cluster.connect() connection_name = 'from_session' conn.register_connection(connection_name, session=session) - self.assertIsNotNone(conn.get_connection(connection_name).cluster.metadata.get_host(CASSANDRA_IP)) + assert conn.get_connection(connection_name).cluster.metadata.get_host(CASSANDRA_IP) is not None self.addCleanup(conn.unregister_connection, connection_name) cluster.shutdown() @@ -245,7 +246,7 @@ def test_connection_from_hosts(self): """ connection_name = 'from_hosts' conn.register_connection(connection_name, hosts=[CASSANDRA_IP]) - self.assertIsNotNone(conn.get_connection(connection_name).cluster.metadata.get_host(CASSANDRA_IP)) + assert conn.get_connection(connection_name).cluster.metadata.get_host(CASSANDRA_IP) is not None self.addCleanup(conn.unregister_connection, connection_name) def test_connection_param_validation(self): @@ -259,15 +260,15 @@ def test_connection_param_validation(self): """ cluster = TestCluster() session = cluster.connect() - with self.assertRaises(CQLEngineException): + with pytest.raises(CQLEngineException): conn.register_connection("bad_coonection1", session=session, consistency="not_null") - with self.assertRaises(CQLEngineException): + with pytest.raises(CQLEngineException): conn.register_connection("bad_coonection2", session=session, lazy_connect="not_null") - with self.assertRaises(CQLEngineException): + with pytest.raises(CQLEngineException): conn.register_connection("bad_coonection3", session=session, retry_connect="not_null") - with self.assertRaises(CQLEngineException): + with pytest.raises(CQLEngineException): conn.register_connection("bad_coonection4", session=session, cluster_options="not_null") - with self.assertRaises(CQLEngineException): + with pytest.raises(CQLEngineException): conn.register_connection("bad_coonection5", hosts="not_null", session=session) cluster.shutdown() @@ -318,7 +319,7 @@ def test_basic_batch_query(self): """ # No connection with a QuerySet (default is a fake one) - with self.assertRaises(NoHostAvailable): + with pytest.raises(NoHostAvailable): with BatchQuery() as b: TestModel.objects.batch(b).create(partition=1, cluster=1) @@ -332,7 +333,7 @@ def test_basic_batch_query(self): obj.__connection__ = None # No connection with a model (default is a fake one) - with self.assertRaises(NoHostAvailable): + with pytest.raises(NoHostAvailable): with BatchQuery() as b: obj.count = 2 obj.batch(b).save() @@ -357,7 +358,7 @@ def test_batch_query_different_connection(self): TestModel.__connection__ = 'cluster' AnotherTestModel.__connection__ = 'cluster2' - with self.assertRaises(CQLEngineException): + with pytest.raises(CQLEngineException): with BatchQuery() as b: TestModel.objects.batch(b).create(partition=1, cluster=1) AnotherTestModel.objects.batch(b).create(partition=1, cluster=1) @@ -380,7 +381,7 @@ def test_batch_query_different_connection(self): obj1.count = 4 obj2.count = 4 - with self.assertRaises(CQLEngineException): + with pytest.raises(CQLEngineException): with BatchQuery() as b: obj1.batch(b).save() obj2.batch(b).save() @@ -396,11 +397,11 @@ def test_batch_query_connection_override(self): @test_category object_mapper """ - with self.assertRaises(CQLEngineException): + with pytest.raises(CQLEngineException): with BatchQuery(connection='cluster') as b: TestModel.batch(b).using(connection='test').save() - with self.assertRaises(CQLEngineException): + with pytest.raises(CQLEngineException): with BatchQuery(connection='cluster') as b: TestModel.using(connection='test').batch(b).save() @@ -408,11 +409,11 @@ def test_batch_query_connection_override(self): obj1 = tm.objects.get(partition=1, cluster=1) obj1.__connection__ = None - with self.assertRaises(CQLEngineException): + with pytest.raises(CQLEngineException): with BatchQuery(connection='cluster') as b: obj1.using(connection='test').batch(b).save() - with self.assertRaises(CQLEngineException): + with pytest.raises(CQLEngineException): with BatchQuery(connection='cluster') as b: obj1.batch(b).using(connection='test').save() @@ -470,26 +471,26 @@ def test_keyspace(self): tm.objects.using(keyspace='ks2').create(partition=1, cluster=1) tm.objects.using(keyspace='ks2').create(partition=2, cluster=2) - with self.assertRaises(TestModel.DoesNotExist): + with pytest.raises(TestModel.DoesNotExist): tm.objects.get(partition=1, cluster=1) # default keyspace ks1 obj1 = tm.objects.using(keyspace='ks2').get(partition=1, cluster=1) obj1.count = 2 obj1.save() - with self.assertRaises(NoHostAvailable): + with pytest.raises(NoHostAvailable): TestModel.objects.using(keyspace='ks2').get(partition=1, cluster=1) obj2 = TestModel.objects.using(connection='cluster', keyspace='ks2').get(partition=1, cluster=1) - self.assertEqual(obj2.count, 2) + assert obj2.count == 2 # Update test TestModel.objects(partition=2, cluster=2).using(connection='cluster', keyspace='ks2').update(count=5) obj3 = TestModel.objects.using(connection='cluster', keyspace='ks2').get(partition=2, cluster=2) - self.assertEqual(obj3.count, 5) + assert obj3.count == 5 TestModel.objects(partition=2, cluster=2).using(connection='cluster', keyspace='ks2').delete() - with self.assertRaises(TestModel.DoesNotExist): + with pytest.raises(TestModel.DoesNotExist): TestModel.objects.using(connection='cluster', keyspace='ks2').get(partition=2, cluster=2) def test_connection(self): @@ -505,20 +506,20 @@ def test_connection(self): self._reset_data() # Model class - with self.assertRaises(NoHostAvailable): + with pytest.raises(NoHostAvailable): TestModel.objects.create(partition=1, cluster=1) TestModel.objects.using(connection='cluster').create(partition=1, cluster=1) TestModel.objects(partition=1, cluster=1).using(connection='cluster').update(count=2) obj1 = TestModel.objects.using(connection='cluster').get(partition=1, cluster=1) - self.assertEqual(obj1.count, 2) + assert obj1.count == 2 obj1.using(connection='cluster').update(count=5) obj1 = TestModel.objects.using(connection='cluster').get(partition=1, cluster=1) - self.assertEqual(obj1.count, 5) + assert obj1.count == 5 obj1.using(connection='cluster').delete() - with self.assertRaises(TestModel.DoesNotExist): + with pytest.raises(TestModel.DoesNotExist): TestModel.objects.using(connection='cluster').get(partition=1, cluster=1) diff --git a/tests/integration/cqlengine/test_consistency.py b/tests/integration/cqlengine/test_consistency.py index a93bbee1ae..dedbe01fdf 100644 --- a/tests/integration/cqlengine/test_consistency.py +++ b/tests/integration/cqlengine/test_consistency.py @@ -53,11 +53,11 @@ def test_create_uses_consistency(self): qs.create(text="i am not fault tolerant this way") args = m.call_args - self.assertEqual(CL.ALL, args[0][0].consistency_level) + assert CL.ALL == args[0][0].consistency_level def test_queryset_is_returned_on_create(self): qs = TestConsistencyModel.consistency(CL.ALL) - self.assertTrue(isinstance(qs, TestConsistencyModel.__queryset__), type(qs)) + assert isinstance(qs, TestConsistencyModel.__queryset__), type(qs) def test_update_uses_consistency(self): t = TestConsistencyModel.create(text="bacon and eggs") @@ -67,7 +67,7 @@ def test_update_uses_consistency(self): t.consistency(CL.ALL).save() args = m.call_args - self.assertEqual(CL.ALL, args[0][0].consistency_level) + assert CL.ALL == args[0][0].consistency_level def test_batch_consistency(self): @@ -77,14 +77,14 @@ def test_batch_consistency(self): args = m.call_args - self.assertEqual(CL.ALL, args[0][0].consistency_level) + assert CL.ALL == args[0][0].consistency_level with mock.patch.object(self.session, 'execute') as m: with BatchQuery() as b: TestConsistencyModel.batch(b).create(text="monkey") args = m.call_args - self.assertNotEqual(CL.ALL, args[0][0].consistency_level) + assert CL.ALL != args[0][0].consistency_level def test_blind_update(self): t = TestConsistencyModel.create(text="bacon and eggs") @@ -95,7 +95,7 @@ def test_blind_update(self): TestConsistencyModel.objects(id=uid).consistency(CL.ALL).update(text="grilled cheese") args = m.call_args - self.assertEqual(CL.ALL, args[0][0].consistency_level) + assert CL.ALL == args[0][0].consistency_level def test_delete(self): # ensures we always carry consistency through on delete statements @@ -110,13 +110,13 @@ def test_delete(self): TestConsistencyModel.objects(id=uid).consistency(CL.ALL).delete() args = m.call_args - self.assertEqual(CL.ALL, args[0][0].consistency_level) + assert CL.ALL == args[0][0].consistency_level def test_default_consistency(self): # verify global assumed default - self.assertEqual(Session._default_consistency_level, ConsistencyLevel.LOCAL_ONE) + assert Session._default_consistency_level == ConsistencyLevel.LOCAL_ONE # verify that this session default is set according to connection.setup # assumes tests/cqlengine/__init__ setup uses CL.ONE session = connection.get_session() - self.assertEqual(session.default_consistency_level, ConsistencyLevel.ONE) + assert session.default_consistency_level == ConsistencyLevel.ONE diff --git a/tests/integration/cqlengine/test_context_query.py b/tests/integration/cqlengine/test_context_query.py index 8ced5f0f49..a922806dcf 100644 --- a/tests/integration/cqlengine/test_context_query.py +++ b/tests/integration/cqlengine/test_context_query.py @@ -17,6 +17,7 @@ from cassandra.cqlengine.models import Model from cassandra.cqlengine.query import ContextQuery from tests.integration.cqlengine.base import BaseCassEngTestCase +import pytest class TestModel(Model): @@ -68,9 +69,9 @@ def test_context_manager(self): # model keyspace write/read for ks in self.KEYSPACES: with ContextQuery(TestModel, keyspace=ks) as tm: - self.assertEqual(tm.__keyspace__, ks) + assert tm.__keyspace__ == ks - self.assertEqual(TestModel._get_keyspace(), 'ks1') + assert TestModel._get_keyspace() == 'ks1' def test_default_keyspace(self): """ @@ -87,14 +88,14 @@ def test_default_keyspace(self): TestModel.objects.create(partition=i, cluster=i) with ContextQuery(TestModel) as tm: - self.assertEqual(5, len(tm.objects.all())) + assert 5 == len(tm.objects.all()) with ContextQuery(TestModel, keyspace='ks1') as tm: - self.assertEqual(5, len(tm.objects.all())) + assert 5 == len(tm.objects.all()) for ks in self.KEYSPACES[1:]: with ContextQuery(TestModel, keyspace=ks) as tm: - self.assertEqual(0, len(tm.objects.all())) + assert 0 == len(tm.objects.all()) def test_context_keyspace(self): """ @@ -111,20 +112,20 @@ def test_context_keyspace(self): tm.objects.create(partition=i, cluster=i) with ContextQuery(TestModel, keyspace='ks4') as tm: - self.assertEqual(5, len(tm.objects.all())) + assert 5 == len(tm.objects.all()) - self.assertEqual(0, len(TestModel.objects.all())) + assert 0 == len(TestModel.objects.all()) for ks in self.KEYSPACES[:2]: with ContextQuery(TestModel, keyspace=ks) as tm: - self.assertEqual(0, len(tm.objects.all())) + assert 0 == len(tm.objects.all()) # simple data update with ContextQuery(TestModel, keyspace='ks4') as tm: obj = tm.objects.get(partition=1) obj.update(count=42) - self.assertEqual(42, tm.objects.get(partition=1).count) + assert 42 == tm.objects.get(partition=1).count def test_context_multiple_models(self): """ @@ -139,9 +140,9 @@ def test_context_multiple_models(self): with ContextQuery(TestModel, TestModel, keyspace='ks4') as (tm1, tm2): - self.assertNotEqual(tm1, tm2) - self.assertEqual(tm1.__keyspace__, 'ks4') - self.assertEqual(tm2.__keyspace__, 'ks4') + assert tm1 != tm2 + assert tm1.__keyspace__ == 'ks4' + assert tm2.__keyspace__ == 'ks4' def test_context_invalid_parameters(self): """ @@ -154,22 +155,22 @@ def test_context_invalid_parameters(self): @test_category query """ - with self.assertRaises(ValueError): + with pytest.raises(ValueError): with ContextQuery(keyspace='ks2'): pass - with self.assertRaises(ValueError): + with pytest.raises(ValueError): with ContextQuery(42) as tm: pass - with self.assertRaises(ValueError): + with pytest.raises(ValueError): with ContextQuery(TestModel, 42): pass - with self.assertRaises(ValueError): + with pytest.raises(ValueError): with ContextQuery(TestModel, unknown_param=42): pass - with self.assertRaises(ValueError): + with pytest.raises(ValueError): with ContextQuery(TestModel, keyspace='ks2', unknown_param=42): pass \ No newline at end of file diff --git a/tests/integration/cqlengine/test_ifexists.py b/tests/integration/cqlengine/test_ifexists.py index 9e8e5d5424..6c2ff437ab 100644 --- a/tests/integration/cqlengine/test_ifexists.py +++ b/tests/integration/cqlengine/test_ifexists.py @@ -22,6 +22,7 @@ from tests.integration.cqlengine.base import BaseCassEngTestCase from tests.integration import PROTOCOL_VERSION +import pytest class TestIfExistsModel(Model): @@ -95,25 +96,25 @@ def test_update_if_exists(self): m.text = 'changed' m.if_exists().update() m = TestIfExistsModel.get(id=id) - self.assertEqual(m.text, 'changed') + assert m.text == 'changed' # save() m.text = 'changed_again' m.if_exists().save() m = TestIfExistsModel.get(id=id) - self.assertEqual(m.text, 'changed_again') + assert m.text == 'changed_again' m = TestIfExistsModel(id=uuid4(), count=44) # do not exists - with self.assertRaises(LWTException) as assertion: + with pytest.raises(LWTException) as assertion: m.if_exists().update() - self.assertEqual(assertion.exception.existing.get('[applied]'), False) + assert assertion.value.existing.get('[applied]') == False # queryset update - with self.assertRaises(LWTException) as assertion: + with pytest.raises(LWTException) as assertion: TestIfExistsModel.objects(id=uuid4()).if_exists().update(count=8) - self.assertEqual(assertion.exception.existing.get('[applied]'), False) + assert assertion.value.existing.get('[applied]') == False @unittest.skipUnless(PROTOCOL_VERSION >= 2, "only runs against the cql3 protocol v2.0") def test_batch_update_if_exists_success(self): @@ -135,19 +136,19 @@ def test_batch_update_if_exists_success(self): m.text = '111111111' m.batch(b).if_exists().update() - with self.assertRaises(LWTException) as assertion: + with pytest.raises(LWTException) as assertion: with BatchQuery() as b: m = TestIfExistsModel(id=uuid4(), count=42) # Doesn't exist m.batch(b).if_exists().update() - self.assertEqual(assertion.exception.existing.get('[applied]'), False) + assert assertion.value.existing.get('[applied]') == False q = TestIfExistsModel.objects(id=id) - self.assertEqual(len(q), 1) + assert len(q) == 1 tm = q.first() - self.assertEqual(tm.count, 8) - self.assertEqual(tm.text, '111111111') + assert tm.count == 8 + assert tm.text == '111111111' @unittest.skipUnless(PROTOCOL_VERSION >= 2, "only runs against the cql3 protocol v2.0") def test_batch_mixed_update_if_exists_success(self): @@ -162,14 +163,14 @@ def test_batch_mixed_update_if_exists_success(self): """ m = TestIfExistsModel2.create(id=1, count=8, text='123456789') - with self.assertRaises(LWTException) as assertion: + with pytest.raises(LWTException) as assertion: with BatchQuery() as b: m.text = '111111112' m.batch(b).if_exists().update() # Does exist n = TestIfExistsModel2(id=1, count=10, text="Failure") # Doesn't exist n.batch(b).if_exists().update() - self.assertEqual(assertion.exception.existing.get('[applied]'), False) + assert assertion.value.existing.get('[applied]') == False @unittest.skipUnless(PROTOCOL_VERSION >= 2, "only runs against the cql3 protocol v2.0") def test_delete_if_exists(self): @@ -188,19 +189,19 @@ def test_delete_if_exists(self): m = TestIfExistsModel.create(id=id, count=8, text='123456789') m.if_exists().delete() q = TestIfExistsModel.objects(id=id) - self.assertEqual(len(q), 0) + assert len(q) == 0 m = TestIfExistsModel(id=uuid4(), count=44) # do not exists - with self.assertRaises(LWTException) as assertion: + with pytest.raises(LWTException) as assertion: m.if_exists().delete() - self.assertEqual(assertion.exception.existing.get('[applied]'), False) + assert assertion.value.existing.get('[applied]') == False # queryset delete - with self.assertRaises(LWTException) as assertion: + with pytest.raises(LWTException) as assertion: TestIfExistsModel.objects(id=uuid4()).if_exists().delete() - self.assertEqual(assertion.exception.existing.get('[applied]'), False) + assert assertion.value.existing.get('[applied]') == False @unittest.skipUnless(PROTOCOL_VERSION >= 2, "only runs against the cql3 protocol v2.0") def test_batch_delete_if_exists_success(self): @@ -222,14 +223,14 @@ def test_batch_delete_if_exists_success(self): m.batch(b).if_exists().delete() q = TestIfExistsModel.objects(id=id) - self.assertEqual(len(q), 0) + assert len(q) == 0 - with self.assertRaises(LWTException) as assertion: + with pytest.raises(LWTException) as assertion: with BatchQuery() as b: m = TestIfExistsModel(id=uuid4(), count=42) # Doesn't exist m.batch(b).if_exists().delete() - self.assertEqual(assertion.exception.existing.get('[applied]'), False) + assert assertion.value.existing.get('[applied]') == False @unittest.skipUnless(PROTOCOL_VERSION >= 2, "only runs against the cql3 protocol v2.0") def test_batch_delete_mixed(self): @@ -245,15 +246,15 @@ def test_batch_delete_mixed(self): m = TestIfExistsModel2.create(id=3, count=8, text='123456789') - with self.assertRaises(LWTException) as assertion: + with pytest.raises(LWTException) as assertion: with BatchQuery() as b: m.batch(b).if_exists().delete() # Does exist n = TestIfExistsModel2(id=3, count=42, text='1111111') # Doesn't exist n.batch(b).if_exists().delete() - self.assertEqual(assertion.exception.existing.get('[applied]'), False) + assert assertion.value.existing.get('[applied]') == False q = TestIfExistsModel2.objects(id=3, count=8) - self.assertEqual(len(q), 1) + assert len(q) == 1 class IfExistsQueryTest(BaseIfExistsTest): @@ -264,7 +265,7 @@ def test_if_exists_included_on_queryset_update(self): TestIfExistsModel.objects(id=uuid4()).if_exists().update(count=42) query = m.call_args[0][0].query_string - self.assertIn("IF EXISTS", query) + assert "IF EXISTS" in query def test_if_exists_included_on_update(self): """ tests that if_exists on models update works as expected """ @@ -273,7 +274,7 @@ def test_if_exists_included_on_update(self): TestIfExistsModel(id=uuid4()).if_exists().update(count=8) query = m.call_args[0][0].query_string - self.assertIn("IF EXISTS", query) + assert "IF EXISTS" in query def test_if_exists_included_on_delete(self): """ tests that if_exists on models delete works as expected """ @@ -282,7 +283,7 @@ def test_if_exists_included_on_delete(self): TestIfExistsModel(id=uuid4()).if_exists().delete() query = m.call_args[0][0].query_string - self.assertIn("IF EXISTS", query) + assert "IF EXISTS" in query class IfExistWithCounterTest(BaseIfExistsWithCounterTest): @@ -298,6 +299,5 @@ def test_instance_raise_exception(self): @test_category object_mapper """ id = uuid4() - with self.assertRaises(IfExistsWithCounterColumn): + with pytest.raises(IfExistsWithCounterColumn): TestIfExistsWithCounterModel.if_exists() - diff --git a/tests/integration/cqlengine/test_ifnotexists.py b/tests/integration/cqlengine/test_ifnotexists.py index 013d4e245e..6a1dd9d4bc 100644 --- a/tests/integration/cqlengine/test_ifnotexists.py +++ b/tests/integration/cqlengine/test_ifnotexists.py @@ -22,6 +22,7 @@ from tests.integration.cqlengine.base import BaseCassEngTestCase from tests.integration import PROTOCOL_VERSION +import pytest class TestIfNotExistsModel(Model): __test__ = False @@ -80,25 +81,25 @@ def test_insert_if_not_exists(self): TestIfNotExistsModel.create(id=id, count=8, text='123456789') - with self.assertRaises(LWTException) as assertion: + with pytest.raises(LWTException): TestIfNotExistsModel.if_not_exists().create(id=id, count=9, text='111111111111') - with self.assertRaises(LWTException) as assertion: + with pytest.raises(LWTException) as assertion: TestIfNotExistsModel.objects(count=9, text='111111111111').if_not_exists().create(id=id) - self.assertEqual(assertion.exception.existing, { + assert assertion.value.existing == { 'count': 8, 'id': id, 'text': '123456789', '[applied]': False, - }) + } q = TestIfNotExistsModel.objects(id=id) - self.assertEqual(len(q), 1) + assert len(q) == 1 tm = q.first() - self.assertEqual(tm.count, 8) - self.assertEqual(tm.text, '123456789') + assert tm.count == 8 + assert tm.text == '123456789' @unittest.skipUnless(PROTOCOL_VERSION >= 2, "only runs against the cql3 protocol v2.0") def test_batch_insert_if_not_exists(self): @@ -111,22 +112,22 @@ def test_batch_insert_if_not_exists(self): b = BatchQuery() TestIfNotExistsModel.batch(b).if_not_exists().create(id=id, count=9, text='111111111111') - with self.assertRaises(LWTException) as assertion: + with pytest.raises(LWTException) as assertion: b.execute() - self.assertEqual(assertion.exception.existing, { + assert assertion.value.existing == { 'count': 8, 'id': id, 'text': '123456789', '[applied]': False, - }) + } q = TestIfNotExistsModel.objects(id=id) - self.assertEqual(len(q), 1) + assert len(q) == 1 tm = q.first() - self.assertEqual(tm.count, 8) - self.assertEqual(tm.text, '123456789') + assert tm.count == 8 + assert tm.text == '123456789' class IfNotExistsModelTest(BaseIfNotExistsTest): @@ -138,7 +139,7 @@ def test_if_not_exists_included_on_create(self): TestIfNotExistsModel.if_not_exists().create(count=8) query = m.call_args[0][0].query_string - self.assertIn("IF NOT EXISTS", query) + assert "IF NOT EXISTS" in query def test_if_not_exists_included_on_save(self): """ tests if we correctly put 'IF NOT EXISTS' for insert statement """ @@ -148,12 +149,12 @@ def test_if_not_exists_included_on_save(self): tm.if_not_exists().save() query = m.call_args[0][0].query_string - self.assertIn("IF NOT EXISTS", query) + assert "IF NOT EXISTS" in query def test_queryset_is_returned_on_class(self): """ ensure we get a queryset description back """ qs = TestIfNotExistsModel.if_not_exists() - self.assertTrue(isinstance(qs, TestIfNotExistsModel.__queryset__), type(qs)) + assert isinstance(qs, TestIfNotExistsModel.__queryset__), type(qs) def test_batch_if_not_exists(self): """ ensure 'IF NOT EXISTS' exists in statement when in batch """ @@ -161,7 +162,7 @@ def test_batch_if_not_exists(self): with BatchQuery() as b: TestIfNotExistsModel.batch(b).if_not_exists().create(count=8) - self.assertIn("IF NOT EXISTS", m.call_args[0][0].query_string) + assert "IF NOT EXISTS" in m.call_args[0][0].query_string class IfNotExistsInstanceTest(BaseIfNotExistsTest): @@ -174,7 +175,7 @@ def test_instance_is_returned(self): o = TestIfNotExistsModel.create(text="whatever") o.text = "new stuff" o = o.if_not_exists() - self.assertEqual(True, o._if_not_exists) + assert True == o._if_not_exists def test_if_not_exists_is_not_include_with_query_on_update(self): """ @@ -188,7 +189,7 @@ def test_if_not_exists_is_not_include_with_query_on_update(self): o.save() query = m.call_args[0][0].query_string - self.assertNotIn("IF NOT EXIST", query) + assert "IF NOT EXIST" not in query class IfNotExistWithCounterTest(BaseIfNotExistsWithCounterTest): @@ -198,6 +199,5 @@ def test_instance_raise_exception(self): if_not_exists on table with counter column """ id = uuid4() - with self.assertRaises(IfNotExistsWithCounterColumn): + with pytest.raises(IfNotExistsWithCounterColumn): TestIfNotExistsWithCounterModel.if_not_exists() - diff --git a/tests/integration/cqlengine/test_lwt_conditional.py b/tests/integration/cqlengine/test_lwt_conditional.py index f5ec89dd2e..f8d9d01035 100644 --- a/tests/integration/cqlengine/test_lwt_conditional.py +++ b/tests/integration/cqlengine/test_lwt_conditional.py @@ -23,6 +23,7 @@ from tests.integration.cqlengine.base import BaseCassEngTestCase from tests.integration import greaterthancass20 +import pytest class TestConditionalModel(Model): @@ -62,7 +63,7 @@ def test_update_using_conditional(self): t.iff(text='blah blah').save() args = m.call_args - self.assertIn('IF "text" = %(0)s', args[0][0].query_string) + assert 'IF "text" = %(0)s' in args[0][0].query_string def test_update_conditional_success(self): t = TestConditionalModel.if_not_exists().create(text='blah blah', count=5) @@ -71,21 +72,21 @@ def test_update_conditional_success(self): t.iff(text='blah blah').save() updated = TestConditionalModel.objects(id=id).first() - self.assertEqual(updated.count, 5) - self.assertEqual(updated.text, 'new blah') + assert updated.count == 5 + assert updated.text == 'new blah' def test_update_failure(self): t = TestConditionalModel.if_not_exists().create(text='blah blah') t.text = 'new blah' t = t.iff(text='something wrong') - with self.assertRaises(LWTException) as assertion: + with pytest.raises(LWTException) as assertion: t.save() - self.assertEqual(assertion.exception.existing, { + assert assertion.value.existing == { 'text': 'blah blah', '[applied]': False, - }) + } def test_blind_update(self): t = TestConditionalModel.if_not_exists().create(text='blah blah') @@ -96,27 +97,27 @@ def test_blind_update(self): TestConditionalModel.objects(id=uid).iff(text='blah blah').update(text='oh hey der') args = m.call_args - self.assertIn('IF "text" = %(1)s', args[0][0].query_string) + assert 'IF "text" = %(1)s' in args[0][0].query_string def test_blind_update_fail(self): t = TestConditionalModel.if_not_exists().create(text='blah blah') t.text = 'something else' uid = t.id qs = TestConditionalModel.objects(id=uid).iff(text='Not dis!') - with self.assertRaises(LWTException) as assertion: + with pytest.raises(LWTException) as assertion: qs.update(text='this will never work') - self.assertEqual(assertion.exception.existing, { + assert assertion.value.existing == { 'text': 'blah blah', '[applied]': False, - }) + } def test_conditional_clause(self): tc = ConditionalClause('some_value', 23) tc.set_context_id(3) - self.assertEqual('"some_value" = %(3)s', str(tc)) - self.assertEqual('"some_value" = %(3)s', str(tc)) + assert '"some_value" = %(3)s' == str(tc) + assert '"some_value" = %(3)s' == str(tc) def test_batch_update_conditional(self): t = TestConditionalModel.if_not_exists().create(text='something', count=5) @@ -125,21 +126,21 @@ def test_batch_update_conditional(self): t.batch(b).iff(count=5).update(text='something else') updated = TestConditionalModel.objects(id=id).first() - self.assertEqual(updated.text, 'something else') + assert updated.text == 'something else' b = BatchQuery() updated.batch(b).iff(count=6).update(text='and another thing') - with self.assertRaises(LWTException) as assertion: + with pytest.raises(LWTException) as assertion: b.execute() - self.assertEqual(assertion.exception.existing, { + assert assertion.value.existing == { 'id': id, 'count': 5, '[applied]': False, - }) + } updated = TestConditionalModel.objects(id=id).first() - self.assertEqual(updated.text, 'something else') + assert updated.text == 'something else' @unittest.skip("Skipping until PYTHON-943 is resolved") def test_batch_update_conditional_several_rows(self): @@ -155,7 +156,7 @@ def test_batch_update_conditional_several_rows(self): TestUpdateModel.batch(b).if_not_exists().create(partition=1, cluster=3, value=5, text='something else') # The response will be more than two rows because two of the inserts will fail - with self.assertRaises(LWTException): + with pytest.raises(LWTException): b.execute() first_row.delete() @@ -166,21 +167,21 @@ def test_batch_update_conditional_several_rows(self): def test_delete_conditional(self): # DML path t = TestConditionalModel.if_not_exists().create(text='something', count=5) - self.assertEqual(TestConditionalModel.objects(id=t.id).count(), 1) - with self.assertRaises(LWTException): + assert TestConditionalModel.objects(id=t.id).count() == 1 + with pytest.raises(LWTException): t.iff(count=9999).delete() - self.assertEqual(TestConditionalModel.objects(id=t.id).count(), 1) + assert TestConditionalModel.objects(id=t.id).count() == 1 t.iff(count=5).delete() - self.assertEqual(TestConditionalModel.objects(id=t.id).count(), 0) + assert TestConditionalModel.objects(id=t.id).count() == 0 # QuerySet path t = TestConditionalModel.if_not_exists().create(text='something', count=5) - self.assertEqual(TestConditionalModel.objects(id=t.id).count(), 1) - with self.assertRaises(LWTException): + assert TestConditionalModel.objects(id=t.id).count() == 1 + with pytest.raises(LWTException): TestConditionalModel.objects(id=t.id).iff(count=9999).delete() - self.assertEqual(TestConditionalModel.objects(id=t.id).count(), 1) + assert TestConditionalModel.objects(id=t.id).count() == 1 TestConditionalModel.objects(id=t.id).iff(count=5).delete() - self.assertEqual(TestConditionalModel.objects(id=t.id).count(), 0) + assert TestConditionalModel.objects(id=t.id).count() == 0 def test_delete_lwt_ne(self): """ @@ -195,19 +196,19 @@ def test_delete_lwt_ne(self): # DML path t = TestConditionalModel.if_not_exists().create(text='something', count=5) - self.assertEqual(TestConditionalModel.objects(id=t.id).count(), 1) - with self.assertRaises(LWTException): + assert TestConditionalModel.objects(id=t.id).count() == 1 + with pytest.raises(LWTException): t.iff(count__ne=5).delete() t.iff(count__ne=2).delete() - self.assertEqual(TestConditionalModel.objects(id=t.id).count(), 0) + assert TestConditionalModel.objects(id=t.id).count() == 0 # QuerySet path t = TestConditionalModel.if_not_exists().create(text='something', count=5) - self.assertEqual(TestConditionalModel.objects(id=t.id).count(), 1) - with self.assertRaises(LWTException): + assert TestConditionalModel.objects(id=t.id).count() == 1 + with pytest.raises(LWTException): TestConditionalModel.objects(id=t.id).iff(count__ne=5).delete() TestConditionalModel.objects(id=t.id).iff(count__ne=2).delete() - self.assertEqual(TestConditionalModel.objects(id=t.id).count(), 0) + assert TestConditionalModel.objects(id=t.id).count() == 0 def test_update_lwt_ne(self): """ @@ -222,20 +223,20 @@ def test_update_lwt_ne(self): # DML path t = TestConditionalModel.if_not_exists().create(text='something', count=5) - self.assertEqual(TestConditionalModel.objects(id=t.id).count(), 1) - with self.assertRaises(LWTException): + assert TestConditionalModel.objects(id=t.id).count() == 1 + with pytest.raises(LWTException): t.iff(count__ne=5).update(text='nothing') t.iff(count__ne=2).update(text='nothing') - self.assertEqual(TestConditionalModel.objects(id=t.id).first().text, 'nothing') + assert TestConditionalModel.objects(id=t.id).first().text == 'nothing' t.delete() # QuerySet path t = TestConditionalModel.if_not_exists().create(text='something', count=5) - self.assertEqual(TestConditionalModel.objects(id=t.id).count(), 1) - with self.assertRaises(LWTException): + assert TestConditionalModel.objects(id=t.id).count() == 1 + with pytest.raises(LWTException): TestConditionalModel.objects(id=t.id).iff(count__ne=5).update(text='nothing') TestConditionalModel.objects(id=t.id).iff(count__ne=2).update(text='nothing') - self.assertEqual(TestConditionalModel.objects(id=t.id).first().text, 'nothing') + assert TestConditionalModel.objects(id=t.id).first().text == 'nothing' t.delete() def test_update_to_none(self): @@ -245,36 +246,36 @@ def test_update_to_none(self): # DML path t = TestConditionalModel.if_not_exists().create(text='something', count=5) - self.assertEqual(TestConditionalModel.objects(id=t.id).count(), 1) - with self.assertRaises(LWTException): + assert TestConditionalModel.objects(id=t.id).count() == 1 + with pytest.raises(LWTException): t.iff(count=9999).update(text=None) - self.assertIsNotNone(TestConditionalModel.objects(id=t.id).first().text) + assert TestConditionalModel.objects(id=t.id).first().text is not None t.iff(count=5).update(text=None) - self.assertIsNone(TestConditionalModel.objects(id=t.id).first().text) + assert TestConditionalModel.objects(id=t.id).first().text is None # QuerySet path t = TestConditionalModel.if_not_exists().create(text='something', count=5) - self.assertEqual(TestConditionalModel.objects(id=t.id).count(), 1) - with self.assertRaises(LWTException): + assert TestConditionalModel.objects(id=t.id).count() == 1 + with pytest.raises(LWTException): TestConditionalModel.objects(id=t.id).iff(count=9999).update(text=None) - self.assertIsNotNone(TestConditionalModel.objects(id=t.id).first().text) + assert TestConditionalModel.objects(id=t.id).first().text is not None TestConditionalModel.objects(id=t.id).iff(count=5).update(text=None) - self.assertIsNone(TestConditionalModel.objects(id=t.id).first().text) + assert TestConditionalModel.objects(id=t.id).first().text is None def test_column_delete_after_update(self): # DML path t = TestConditionalModel.if_not_exists().create(text='something', count=5) t.iff(count=5).update(text=None, count=6) - self.assertIsNone(t.text) - self.assertEqual(t.count, 6) + assert t.text is None + assert t.count == 6 # QuerySet path t = TestConditionalModel.if_not_exists().create(text='something', count=5) TestConditionalModel.objects(id=t.id).iff(count=5).update(text=None, count=6) - self.assertIsNone(TestConditionalModel.objects(id=t.id).first().text) - self.assertEqual(TestConditionalModel.objects(id=t.id).first().count, 6) + assert TestConditionalModel.objects(id=t.id).first().text is None + assert TestConditionalModel.objects(id=t.id).first().count == 6 def test_conditional_without_instance(self): """ @@ -294,5 +295,5 @@ def test_conditional_without_instance(self): TestConditionalModel.iff(count=5).filter(id=uuid).update(text=None, count=6) t = TestConditionalModel.filter(id=uuid).first() - self.assertIsNone(t.text) - self.assertEqual(t.count, 6) + assert t.text is None + assert t.count == 6 diff --git a/tests/integration/cqlengine/test_timestamp.py b/tests/integration/cqlengine/test_timestamp.py index 3c57e20b7d..7b10ad578f 100644 --- a/tests/integration/cqlengine/test_timestamp.py +++ b/tests/integration/cqlengine/test_timestamp.py @@ -12,9 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import re from datetime import timedelta, datetime from unittest import mock -import sure from uuid import uuid4 from cassandra.cqlengine import columns @@ -22,6 +22,7 @@ from cassandra.cqlengine.models import Model from cassandra.cqlengine.query import BatchQuery from tests.integration.cqlengine.base import BaseCassEngTestCase +import pytest class TestTimestampModel(Model): @@ -46,7 +47,7 @@ def test_batch_is_included(self): with BatchQuery(timestamp=timedelta(seconds=30)) as b: TestTimestampModel.batch(b).create(count=1) - "USING TIMESTAMP".should.be.within(m.call_args[0][0].query_string) + assert "USING TIMESTAMP" in m.call_args[0][0].query_string class CreateWithTimestampTest(BaseTimestampTest): @@ -57,37 +58,34 @@ def test_batch(self): TestTimestampModel.timestamp(timedelta(seconds=10)).batch(b).create(count=1) query = m.call_args[0][0].query_string - - query.should.match(r"INSERT.*USING TIMESTAMP") - query.should_not.match(r"TIMESTAMP.*INSERT") + assert re.search(r"INSERT.*USING TIMESTAMP", query) + assert not re.search(r"TIMESTAMP.*INSERT", query) def test_timestamp_not_included_on_normal_create(self): with mock.patch.object(self.session, "execute") as m: TestTimestampModel.create(count=2) - "USING TIMESTAMP".shouldnt.be.within(m.call_args[0][0].query_string) + assert "USING TIMESTAMP" not in m.call_args[0][0].query_string def test_timestamp_is_set_on_model_queryset(self): delta = timedelta(seconds=30) tmp = TestTimestampModel.timestamp(delta) - tmp._timestamp.should.equal(delta) + assert tmp._timestamp == delta def test_non_batch_syntax_integration(self): tmp = TestTimestampModel.timestamp(timedelta(seconds=30)).create(count=1) - tmp.should.be.ok + assert tmp def test_non_batch_syntax_with_tll_integration(self): tmp = TestTimestampModel.timestamp(timedelta(seconds=30)).ttl(30).create(count=1) - tmp.should.be.ok + assert tmp def test_non_batch_syntax_unit(self): with mock.patch.object(self.session, "execute") as m: TestTimestampModel.timestamp(timedelta(seconds=30)).create(count=1) - query = m.call_args[0][0].query_string - - "USING TIMESTAMP".should.be.within(query) + assert "USING TIMESTAMP" in m.call_args[0][0].query_string def test_non_batch_syntax_with_ttl_unit(self): @@ -95,9 +93,7 @@ def test_non_batch_syntax_with_ttl_unit(self): TestTimestampModel.timestamp(timedelta(seconds=30)).ttl(30).create( count=1) - query = m.call_args[0][0].query_string - - query.should.match(r"USING TTL \d* AND TIMESTAMP") + assert re.search(r"USING TTL \d* AND TIMESTAMP", m.call_args[0][0].query_string) class UpdateWithTimestampTest(BaseTimestampTest): @@ -111,15 +107,14 @@ def test_instance_update_includes_timestamp_in_query(self): with mock.patch.object(self.session, "execute") as m: self.instance.timestamp(timedelta(seconds=30)).update(count=2) - "USING TIMESTAMP".should.be.within(m.call_args[0][0].query_string) + assert "USING TIMESTAMP" in m.call_args[0][0].query_string def test_instance_update_in_batch(self): with mock.patch.object(self.session, "execute") as m: with BatchQuery() as b: self.instance.batch(b).timestamp(timedelta(seconds=30)).update(count=2) - query = m.call_args[0][0].query_string - "USING TIMESTAMP".should.be.within(query) + assert "USING TIMESTAMP" in m.call_args[0][0].query_string class DeleteWithTimestampTest(BaseTimestampTest): @@ -131,29 +126,29 @@ def test_non_batch(self): uid = uuid4() tmp = TestTimestampModel.create(id=uid, count=1) - TestTimestampModel.get(id=uid).should.be.ok + assert TestTimestampModel.get(id=uid) tmp.timestamp(timedelta(seconds=5)).delete() - with self.assertRaises(TestTimestampModel.DoesNotExist): + with pytest.raises(TestTimestampModel.DoesNotExist): TestTimestampModel.get(id=uid) tmp = TestTimestampModel.create(id=uid, count=1) - with self.assertRaises(TestTimestampModel.DoesNotExist): + with pytest.raises(TestTimestampModel.DoesNotExist): TestTimestampModel.get(id=uid) # calling .timestamp sets the TS on the model tmp.timestamp(timedelta(seconds=5)) - tmp._timestamp.should.be.ok + assert tmp._timestamp # calling save clears the set timestamp tmp.save() - tmp._timestamp.shouldnt.be.ok + assert not tmp._timestamp tmp.timestamp(timedelta(seconds=5)) tmp.update() - tmp._timestamp.shouldnt.be.ok + assert not tmp._timestamp def test_blind_delete(self): """ @@ -162,16 +157,16 @@ def test_blind_delete(self): uid = uuid4() tmp = TestTimestampModel.create(id=uid, count=1) - TestTimestampModel.get(id=uid).should.be.ok + assert TestTimestampModel.get(id=uid) TestTimestampModel.objects(id=uid).timestamp(timedelta(seconds=5)).delete() - with self.assertRaises(TestTimestampModel.DoesNotExist): + with pytest.raises(TestTimestampModel.DoesNotExist): TestTimestampModel.get(id=uid) tmp = TestTimestampModel.create(id=uid, count=1) - with self.assertRaises(TestTimestampModel.DoesNotExist): + with pytest.raises(TestTimestampModel.DoesNotExist): TestTimestampModel.get(id=uid) def test_blind_delete_with_datetime(self): @@ -181,29 +176,27 @@ def test_blind_delete_with_datetime(self): uid = uuid4() tmp = TestTimestampModel.create(id=uid, count=1) - TestTimestampModel.get(id=uid).should.be.ok + assert TestTimestampModel.get(id=uid) plus_five_seconds = datetime.now() + timedelta(seconds=5) TestTimestampModel.objects(id=uid).timestamp(plus_five_seconds).delete() - with self.assertRaises(TestTimestampModel.DoesNotExist): + with pytest.raises(TestTimestampModel.DoesNotExist): TestTimestampModel.get(id=uid) tmp = TestTimestampModel.create(id=uid, count=1) - with self.assertRaises(TestTimestampModel.DoesNotExist): + with pytest.raises(TestTimestampModel.DoesNotExist): TestTimestampModel.get(id=uid) def test_delete_in_the_past(self): uid = uuid4() tmp = TestTimestampModel.create(id=uid, count=1) - TestTimestampModel.get(id=uid).should.be.ok + assert TestTimestampModel.get(id=uid) # delete the in past, should not affect the object created above TestTimestampModel.objects(id=uid).timestamp(timedelta(seconds=-60)).delete() TestTimestampModel.get(id=uid) - - diff --git a/tests/integration/cqlengine/test_ttl.py b/tests/integration/cqlengine/test_ttl.py index 4507e91ae7..df1afb6bf0 100644 --- a/tests/integration/cqlengine/test_ttl.py +++ b/tests/integration/cqlengine/test_ttl.py @@ -94,14 +94,14 @@ def test_ttl_included_on_create(self): TestTTLModel.ttl(60).create(text="hello blake") query = m.call_args[0][0].query_string - self.assertIn("USING TTL", query) + assert "USING TTL" in query def test_queryset_is_returned_on_class(self): """ ensures we get a queryset descriptor back """ qs = TestTTLModel.ttl(60) - self.assertTrue(isinstance(qs, TestTTLModel.__queryset__), type(qs)) + assert isinstance(qs, TestTTLModel.__queryset__), type(qs) class TTLInstanceUpdateTest(BaseTTLTest): @@ -113,7 +113,7 @@ def test_update_includes_ttl(self): model.ttl(60).update(text="goodbye forever") query = m.call_args[0][0].query_string - self.assertIn("USING TTL", query) + assert "USING TTL" in query def test_update_syntax_valid(self): # sanity test that ensures the TTL syntax is accepted by cassandra @@ -130,7 +130,7 @@ def test_instance_is_returned(self): o = TestTTLModel.create(text="whatever") o.text = "new stuff" o = o.ttl(60) - self.assertEqual(60, o._ttl) + assert 60 == o._ttl def test_ttl_is_include_with_query_on_update(self): session = get_session() @@ -143,7 +143,7 @@ def test_ttl_is_include_with_query_on_update(self): o.save() query = m.call_args[0][0].query_string - self.assertIn("USING TTL", query) + assert "USING TTL" in query class TTLBlindUpdateTest(BaseTTLTest): @@ -157,7 +157,7 @@ def test_ttl_included_with_blind_update(self): TestTTLModel.objects(id=tid).ttl(60).update(text="bacon") query = m.call_args[0][0].query_string - self.assertIn("USING TTL", query) + assert "USING TTL" in query class TTLDefaultTest(BaseDefaultTTLTest): @@ -177,16 +177,16 @@ def test_default_ttl_not_set(self): o = TestTTLModel.create(text="some text") tid = o.id - self.assertIsNone(o._ttl) + assert o._ttl is None default_ttl = self.get_default_ttl('test_ttlmodel') - self.assertEqual(default_ttl, 0) + assert default_ttl == 0 with mock.patch.object(session, 'execute') as m: TestTTLModel.objects(id=tid).update(text="aligators") query = m.call_args[0][0].query_string - self.assertNotIn("USING TTL", query) + assert "USING TTL" not in query def test_default_ttl_set(self): session = get_session() @@ -195,29 +195,29 @@ def test_default_ttl_set(self): tid = o.id # Should not be set, it's handled by Cassandra - self.assertIsNone(o._ttl) + assert o._ttl is None default_ttl = self.get_default_ttl('test_default_ttlmodel') - self.assertEqual(default_ttl, 20) + assert default_ttl == 20 with mock.patch.object(session, 'execute') as m: TestTTLModel.objects(id=tid).update(text="aligators expired") # Should not be set either query = m.call_args[0][0].query_string - self.assertNotIn("USING TTL", query) + assert "USING TTL" not in query def test_default_ttl_modify(self): session = get_session() default_ttl = self.get_default_ttl('test_default_ttlmodel') - self.assertEqual(default_ttl, 20) + assert default_ttl == 20 TestDefaultTTLModel.__options__ = {'default_time_to_live': 10} sync_table(TestDefaultTTLModel) default_ttl = self.get_default_ttl('test_default_ttlmodel') - self.assertEqual(default_ttl, 10) + assert default_ttl == 10 # Restore default TTL TestDefaultTTLModel.__options__ = {'default_time_to_live': 20} @@ -229,10 +229,10 @@ def test_override_default_ttl(self): tid = o.id o.ttl(3600) - self.assertEqual(o._ttl, 3600) + assert o._ttl == 3600 with mock.patch.object(session, 'execute') as m: TestDefaultTTLModel.objects(id=tid).ttl(None).update(text="aligators expired") query = m.call_args[0][0].query_string - self.assertNotIn("USING TTL", query) + assert "USING TTL" not in query diff --git a/tests/integration/long/test_consistency.py b/tests/integration/long/test_consistency.py index 8f5fcb6313..48d0ca4ae2 100644 --- a/tests/integration/long/test_consistency.py +++ b/tests/integration/long/test_consistency.py @@ -17,6 +17,7 @@ import sys import time import traceback +import pytest from cassandra import ConsistencyLevel, OperationTimedOut, ReadTimeout, WriteTimeout, Unavailable from cassandra.cluster import ExecutionProfile, EXEC_PROFILE_DEFAULT @@ -51,12 +52,12 @@ def setUp(self): self.coordinator_stats = CoordinatorStats() def _cl_failure(self, consistency_level, e): - self.fail('Instead of success, saw %s for CL.%s:\n\n%s' % ( + pytest.fail('Instead of success, saw %s for CL.%s:\n\n%s' % ( e, ConsistencyLevel.value_to_name[consistency_level], traceback.format_exc())) def _cl_expected_failure(self, cl): - self.fail('Test passed at ConsistencyLevel.%s:\n\n%s' % ( + pytest.fail('Test passed at ConsistencyLevel.%s:\n\n%s' % ( ConsistencyLevel.value_to_name[cl], traceback.format_exc())) def _insert(self, session, keyspace, count, consistency_level=ConsistencyLevel.ONE): @@ -101,9 +102,9 @@ def _assert_reads_succeed(self, session, keyspace, consistency_levels, expected_ self._query(session, keyspace, 1, cl) for i in range(3): if i == expected_reader: - self.coordinator_stats.assert_query_count_equals(self, i, 1) + self.coordinator_stats.assert_query_count_equals(i, 1) else: - self.coordinator_stats.assert_query_count_equals(self, i, 0) + self.coordinator_stats.assert_query_count_equals(i, 0) except Exception as e: self._cl_failure(cl, e) @@ -136,9 +137,9 @@ def _test_tokenaware_one_node_down(self, keyspace, rf, accepted): create_schema(cluster, session, keyspace, replication_factor=rf) self._insert(session, keyspace, count=1) self._query(session, keyspace, count=1) - self.coordinator_stats.assert_query_count_equals(self, 1, 0) - self.coordinator_stats.assert_query_count_equals(self, 2, 1) - self.coordinator_stats.assert_query_count_equals(self, 3, 0) + self.coordinator_stats.assert_query_count_equals(1, 0) + self.coordinator_stats.assert_query_count_equals(2, 1) + self.coordinator_stats.assert_query_count_equals(3, 0) try: force_stop(2) @@ -188,9 +189,9 @@ def test_rfthree_tokenaware_none_down(self): create_schema(cluster, session, keyspace, replication_factor=3) self._insert(session, keyspace, count=1) self._query(session, keyspace, count=1) - self.coordinator_stats.assert_query_count_equals(self, 1, 0) - self.coordinator_stats.assert_query_count_equals(self, 2, 1) - self.coordinator_stats.assert_query_count_equals(self, 3, 0) + self.coordinator_stats.assert_query_count_equals(1, 0) + self.coordinator_stats.assert_query_count_equals(2, 1) + self.coordinator_stats.assert_query_count_equals(3, 0) self.coordinator_stats.reset_counts() @@ -211,9 +212,9 @@ def _test_downgrading_cl(self, keyspace, rf, accepted): create_schema(cluster, session, keyspace, replication_factor=rf) self._insert(session, keyspace, 1) self._query(session, keyspace, 1) - self.coordinator_stats.assert_query_count_equals(self, 1, 0) - self.coordinator_stats.assert_query_count_equals(self, 2, 1) - self.coordinator_stats.assert_query_count_equals(self, 3, 0) + self.coordinator_stats.assert_query_count_equals(1, 0) + self.coordinator_stats.assert_query_count_equals(2, 1) + self.coordinator_stats.assert_query_count_equals(3, 0) try: force_stop(2) @@ -268,13 +269,13 @@ def rfthree_downgradingcl(self, cluster, keyspace, roundrobin): self._query(session, keyspace, count=12) if roundrobin: - self.coordinator_stats.assert_query_count_equals(self, 1, 4) - self.coordinator_stats.assert_query_count_equals(self, 2, 4) - self.coordinator_stats.assert_query_count_equals(self, 3, 4) + self.coordinator_stats.assert_query_count_equals(1, 4) + self.coordinator_stats.assert_query_count_equals(2, 4) + self.coordinator_stats.assert_query_count_equals(3, 4) else: - self.coordinator_stats.assert_query_count_equals(self, 1, 0) - self.coordinator_stats.assert_query_count_equals(self, 2, 12) - self.coordinator_stats.assert_query_count_equals(self, 3, 0) + self.coordinator_stats.assert_query_count_equals(1, 0) + self.coordinator_stats.assert_query_count_equals(2, 12) + self.coordinator_stats.assert_query_count_equals(3, 0) try: self.coordinator_stats.reset_counts() @@ -288,13 +289,13 @@ def rfthree_downgradingcl(self, cluster, keyspace, roundrobin): self.coordinator_stats.reset_counts() self._query(session, keyspace, 12, consistency_level=cl) if roundrobin: - self.coordinator_stats.assert_query_count_equals(self, 1, 6) - self.coordinator_stats.assert_query_count_equals(self, 2, 0) - self.coordinator_stats.assert_query_count_equals(self, 3, 6) + self.coordinator_stats.assert_query_count_equals(1, 6) + self.coordinator_stats.assert_query_count_equals(2, 0) + self.coordinator_stats.assert_query_count_equals(3, 6) else: - self.coordinator_stats.assert_query_count_equals(self, 1, 0) - self.coordinator_stats.assert_query_count_equals(self, 2, 0) - self.coordinator_stats.assert_query_count_equals(self, 3, 12) + self.coordinator_stats.assert_query_count_equals(1, 0) + self.coordinator_stats.assert_query_count_equals(2, 0) + self.coordinator_stats.assert_query_count_equals(3, 12) finally: start(2) wait_for_up(cluster, 2) @@ -360,6 +361,3 @@ def test_pool_with_host_down(self): start(node_to_stop) wait_for_up(cluster, node_to_stop) cluster.shutdown() - - - diff --git a/tests/integration/long/test_failure_types.py b/tests/integration/long/test_failure_types.py index ea8897185a..04d75555f5 100644 --- a/tests/integration/long/test_failure_types.py +++ b/tests/integration/long/test_failure_types.py @@ -32,8 +32,10 @@ get_node, start_cluster_wait_for_up, requiresmallclockgranularity, local, CASSANDRA_VERSION, TestCluster) +from tests.integration import requires_java_udf import unittest +import pytest log = logging.getLogger(__name__) @@ -157,13 +159,13 @@ def _perform_cql_statement(self, text, consistency_level, expected_exception, se if expected_exception is None: self.execute_helper(session, statement) else: - with self.assertRaises(expected_exception) as cm: + with pytest.raises(expected_exception) as cm: self.execute_helper(session, statement) if ProtocolVersion.uses_error_code_map(PROTOCOL_VERSION): - if isinstance(cm.exception, ReadFailure): - self.assertEqual(list(cm.exception.error_code_map.values())[0], 1) - if isinstance(cm.exception, WriteFailure): - self.assertEqual(list(cm.exception.error_code_map.values())[0], 0) + if isinstance(cm.value, ReadFailure): + assert list(cm.value.error_code_map.values())[0] == 1 + if isinstance(cm.value, WriteFailure): + assert list(cm.value.error_code_map.values())[0] == 0 def test_write_failures_from_coordinator(self): """ @@ -185,7 +187,7 @@ def test_write_failures_from_coordinator(self): self._perform_cql_statement( """ CREATE KEYSPACE testksfail - WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '3'} + WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '3'} """, consistency_level=ConsistencyLevel.ALL, expected_exception=None) # create table @@ -269,6 +271,7 @@ def test_tombstone_overflow_read_failure(self): DROP TABLE test3rf.test2; """, consistency_level=ConsistencyLevel.ALL, expected_exception=None) + @requires_java_udf def test_user_function_failure(self): """ Test to validate that exceptions in user defined function are correctly surfaced by the driver to us. @@ -380,13 +383,13 @@ def test_async_timeouts(self): # Test with default timeout (should be 10) start_time = time.time() future = self.session.execute_async(ss) - with self.assertRaises(OperationTimedOut): + with pytest.raises(OperationTimedOut): future.result() end_time = time.time() total_time = end_time-start_time expected_time = self.cluster.profile_manager.default.request_timeout # check timeout and ensure it's within a reasonable range - self.assertAlmostEqual(expected_time, total_time, delta=.05) + assert expected_time == pytest.approx(total_time, abs=.05) # Test with user defined timeout (Should be 1) expected_time = 1 @@ -397,11 +400,11 @@ def test_async_timeouts(self): future.add_callback(mock_callback) future.add_errback(mock_errorback) - with self.assertRaises(OperationTimedOut): + with pytest.raises(OperationTimedOut): future.result() end_time = time.time() total_time = end_time-start_time # check timeout and ensure it's within a reasonable range - self.assertAlmostEqual(expected_time, total_time, delta=.05) - self.assertTrue(mock_errorback.called) - self.assertFalse(mock_callback.called) + assert expected_time == pytest.approx(total_time, abs=.05) + assert mock_errorback.called + assert not mock_callback.called diff --git a/tests/integration/long/test_ipv6.py b/tests/integration/long/test_ipv6.py index d58bad987a..1d2c7b2874 100644 --- a/tests/integration/long/test_ipv6.py +++ b/tests/integration/long/test_ipv6.py @@ -38,6 +38,7 @@ import unittest +import pytest # If more modules do IPV6 testing, this can be moved down to integration.__init__. @@ -83,22 +84,23 @@ def test_connect(self): session = cluster.connect() future = session.execute_async("SELECT * FROM system.local WHERE key='local'") future.result() - self.assertEqual(future._current_host.address, '::1') + assert future._current_host.address == '::1' cluster.shutdown() def test_error(self): cluster = TestCluster(connection_class=self.connection_class, contact_points=['::1'], port=9043, connect_timeout=10) - self.assertRaisesRegex(NoHostAvailable, '\(\'Unable to connect.*%s.*::1\', 9043.*Connection refused.*' - % errno.ECONNREFUSED, cluster.connect) + with pytest.raises(NoHostAvailable, match='\(\'Unable to connect.*%s.*::1\', 9043.*Connection refused.*' + % errno.ECONNREFUSED): + cluster.connect() def test_error_multiple(self): if len(socket.getaddrinfo('localhost', 9043, socket.AF_UNSPEC, socket.SOCK_STREAM)) < 2: raise unittest.SkipTest('localhost only resolves one address') cluster = TestCluster(connection_class=self.connection_class, contact_points=['localhost'], port=9043, connect_timeout=10) - self.assertRaisesRegex(NoHostAvailable, '\(\'Unable to connect.*Tried connecting to \[\(.*\(.*\].*Last error', - cluster.connect) + with pytest.raises(NoHostAvailable, match='\(\'Unable to connect.*Tried connecting to \[\(.*\(.*\].*Last error'): + cluster.connect() class LibevConnectionTests(IPV6ConnectionTest, unittest.TestCase): diff --git a/tests/integration/long/test_large_data.py b/tests/integration/long/test_large_data.py index 59873204a4..0a1b368bf0 100644 --- a/tests/integration/long/test_large_data.py +++ b/tests/integration/long/test_large_data.py @@ -12,10 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -try: - from Queue import Queue, Empty -except ImportError: - from queue import Queue, Empty # noqa +from queue import Queue, Empty from struct import pack import logging, sys, traceback, time @@ -28,6 +25,7 @@ from tests.integration.long.utils import create_schema import unittest +import pytest log = logging.getLogger(__name__) @@ -115,7 +113,7 @@ def test_wide_rows(self): # Verify for i, row in enumerate(results): - self.assertAlmostEqual(row['i'], i, delta=3) + assert row['i'] == pytest.approx(i, abs=3) session.cluster.shutdown() @@ -161,11 +159,11 @@ def test_wide_batch_rows(self): lastvalue = 0 for j, row in enumerate(results): lastValue=row['i'] - self.assertEqual(lastValue, j) + assert lastValue == j #check the last value make sure it's what we expect index_value = to_insert-1 - self.assertEqual(lastValue,index_value,"Verification failed only found {0} inserted we were expecting {1}".format(j,index_value)) + assert lastValue == index_value, "Verification failed only found {0} inserted we were expecting {1}".format(j,index_value) session.cluster.shutdown() @@ -200,9 +198,9 @@ def test_wide_byte_rows(self): # Verify bb = pack('>H', 0xCAFE) for i, row in enumerate(results): - self.assertEqual(row['v'], bb) + assert row['v'] == bb - self.assertGreaterEqual(i, expected_results, "Verification failed only found {0} inserted we were expecting {1}".format(i,expected_results)) + assert i >= expected_results, "Verification failed only found {0} inserted we were expecting {1}".format(i,expected_results) session.cluster.shutdown() @@ -235,9 +233,9 @@ def test_large_text(self): # Verify found_result = False for i, row in enumerate(result): - self.assertEqual(row['txt'], text) + assert row['txt'] == text found_result = True - self.assertTrue(found_result, "No results were found") + assert found_result, "No results were found" session.cluster.shutdown() @@ -266,6 +264,6 @@ def test_wide_table(self): # Verify for row in result: for i in range(table_width): - self.assertEqual(row[create_column_name(i)], i) + assert row[create_column_name(i)] == i session.cluster.shutdown() diff --git a/tests/integration/long/test_loadbalancingpolicies.py b/tests/integration/long/test_loadbalancingpolicies.py index 7848a21b1d..072786dc23 100644 --- a/tests/integration/long/test_loadbalancingpolicies.py +++ b/tests/integration/long/test_loadbalancingpolicies.py @@ -16,6 +16,7 @@ import struct import sys import traceback +import pytest from cassandra import cqltypes from cassandra import ConsistencyLevel, Unavailable, OperationTimedOut, ReadTimeout, ReadFailure, \ @@ -30,7 +31,7 @@ ) from cassandra.query import SimpleStatement -from tests.integration import use_singledc, use_multidc, remove_cluster, TestCluster, greaterthanorequalcass40, notdse +from tests.integration import use_singledc, use_multidc, remove_cluster, TestCluster, greaterthanorequalcass40 from tests.integration.long.utils import (wait_for_up, create_schema, CoordinatorStats, force_stop, wait_for_down, decommission, start, @@ -44,7 +45,6 @@ class LoadBalancingPolicyTests(unittest.TestCase): def setUp(self): - remove_cluster() # clear ahead of test so it doesn't use one left in unknown state self.coordinator_stats = CoordinatorStats() self.prepared = None self.probe_cluster = None @@ -185,11 +185,12 @@ def test_token_aware_is_used_by_default(self): self.addCleanup(cluster.shutdown) if murmur3 is not None: - self.assertTrue(isinstance(cluster.profile_manager.default.load_balancing_policy, TokenAwarePolicy)) + assert isinstance(cluster.profile_manager.default.load_balancing_policy, TokenAwarePolicy) else: - self.assertTrue(isinstance(cluster.profile_manager.default.load_balancing_policy, DCAwareRoundRobinPolicy)) + assert isinstance(cluster.profile_manager.default.load_balancing_policy, DCAwareRoundRobinPolicy) def test_roundrobin(self): + remove_cluster() use_singledc() keyspace = 'test_roundrobin' cluster, session = self._cluster_session_with_lbp(RoundRobinPolicy()) @@ -200,9 +201,9 @@ def test_roundrobin(self): self._insert(session, keyspace) self._query(session, keyspace) - self.coordinator_stats.assert_query_count_equals(self, 1, 4) - self.coordinator_stats.assert_query_count_equals(self, 2, 4) - self.coordinator_stats.assert_query_count_equals(self, 3, 4) + self.coordinator_stats.assert_query_count_equals(1, 4) + self.coordinator_stats.assert_query_count_equals(2, 4) + self.coordinator_stats.assert_query_count_equals(3, 4) force_stop(3) self._wait_for_nodes_down([3], cluster) @@ -210,9 +211,9 @@ def test_roundrobin(self): self.coordinator_stats.reset_counts() self._query(session, keyspace) - self.coordinator_stats.assert_query_count_equals(self, 1, 6) - self.coordinator_stats.assert_query_count_equals(self, 2, 6) - self.coordinator_stats.assert_query_count_equals(self, 3, 0) + self.coordinator_stats.assert_query_count_equals(1, 6) + self.coordinator_stats.assert_query_count_equals(2, 6) + self.coordinator_stats.assert_query_count_equals(3, 0) decommission(1) start(3) @@ -222,11 +223,12 @@ def test_roundrobin(self): self.coordinator_stats.reset_counts() self._query(session, keyspace) - self.coordinator_stats.assert_query_count_equals(self, 1, 0) - self.coordinator_stats.assert_query_count_equals(self, 2, 6) - self.coordinator_stats.assert_query_count_equals(self, 3, 6) + self.coordinator_stats.assert_query_count_equals(1, 0) + self.coordinator_stats.assert_query_count_equals(2, 6) + self.coordinator_stats.assert_query_count_equals(3, 6) def test_roundrobin_two_dcs(self): + remove_cluster() use_multidc([2, 2]) keyspace = 'test_roundrobin_two_dcs' cluster, session = self._cluster_session_with_lbp(RoundRobinPolicy()) @@ -237,10 +239,10 @@ def test_roundrobin_two_dcs(self): self._insert(session, keyspace) self._query(session, keyspace) - self.coordinator_stats.assert_query_count_equals(self, 1, 3) - self.coordinator_stats.assert_query_count_equals(self, 2, 3) - self.coordinator_stats.assert_query_count_equals(self, 3, 3) - self.coordinator_stats.assert_query_count_equals(self, 4, 3) + self.coordinator_stats.assert_query_count_equals(1, 3) + self.coordinator_stats.assert_query_count_equals(2, 3) + self.coordinator_stats.assert_query_count_equals(3, 3) + self.coordinator_stats.assert_query_count_equals(4, 3) force_stop(1) bootstrap(5, 'dc3') @@ -253,13 +255,14 @@ def test_roundrobin_two_dcs(self): self.coordinator_stats.reset_counts() self._query(session, keyspace) - self.coordinator_stats.assert_query_count_equals(self, 1, 0) - self.coordinator_stats.assert_query_count_equals(self, 2, 3) - self.coordinator_stats.assert_query_count_equals(self, 3, 3) - self.coordinator_stats.assert_query_count_equals(self, 4, 3) - self.coordinator_stats.assert_query_count_equals(self, 5, 3) + self.coordinator_stats.assert_query_count_equals(1, 0) + self.coordinator_stats.assert_query_count_equals(2, 3) + self.coordinator_stats.assert_query_count_equals(3, 3) + self.coordinator_stats.assert_query_count_equals(4, 3) + self.coordinator_stats.assert_query_count_equals(5, 3) def test_roundrobin_two_dcs_2(self): + remove_cluster() use_multidc([2, 2]) keyspace = 'test_roundrobin_two_dcs_2' cluster, session = self._cluster_session_with_lbp(RoundRobinPolicy()) @@ -270,10 +273,10 @@ def test_roundrobin_two_dcs_2(self): self._insert(session, keyspace) self._query(session, keyspace) - self.coordinator_stats.assert_query_count_equals(self, 1, 3) - self.coordinator_stats.assert_query_count_equals(self, 2, 3) - self.coordinator_stats.assert_query_count_equals(self, 3, 3) - self.coordinator_stats.assert_query_count_equals(self, 4, 3) + self.coordinator_stats.assert_query_count_equals(1, 3) + self.coordinator_stats.assert_query_count_equals(2, 3) + self.coordinator_stats.assert_query_count_equals(3, 3) + self.coordinator_stats.assert_query_count_equals(4, 3) force_stop(1) bootstrap(5, 'dc1') @@ -286,13 +289,14 @@ def test_roundrobin_two_dcs_2(self): self.coordinator_stats.reset_counts() self._query(session, keyspace) - self.coordinator_stats.assert_query_count_equals(self, 1, 0) - self.coordinator_stats.assert_query_count_equals(self, 2, 3) - self.coordinator_stats.assert_query_count_equals(self, 3, 3) - self.coordinator_stats.assert_query_count_equals(self, 4, 3) - self.coordinator_stats.assert_query_count_equals(self, 5, 3) + self.coordinator_stats.assert_query_count_equals(1, 0) + self.coordinator_stats.assert_query_count_equals(2, 3) + self.coordinator_stats.assert_query_count_equals(3, 3) + self.coordinator_stats.assert_query_count_equals(4, 3) + self.coordinator_stats.assert_query_count_equals(5, 3) def test_dc_aware_roundrobin_two_dcs(self): + remove_cluster() use_multidc([3, 2]) keyspace = 'test_dc_aware_roundrobin_two_dcs' cluster, session = self._cluster_session_with_lbp(DCAwareRoundRobinPolicy('dc1')) @@ -303,13 +307,14 @@ def test_dc_aware_roundrobin_two_dcs(self): self._insert(session, keyspace) self._query(session, keyspace) - self.coordinator_stats.assert_query_count_equals(self, 1, 4) - self.coordinator_stats.assert_query_count_equals(self, 2, 4) - self.coordinator_stats.assert_query_count_equals(self, 3, 4) - self.coordinator_stats.assert_query_count_equals(self, 4, 0) - self.coordinator_stats.assert_query_count_equals(self, 5, 0) + self.coordinator_stats.assert_query_count_equals(1, 4) + self.coordinator_stats.assert_query_count_equals(2, 4) + self.coordinator_stats.assert_query_count_equals(3, 4) + self.coordinator_stats.assert_query_count_equals(4, 0) + self.coordinator_stats.assert_query_count_equals(5, 0) def test_dc_aware_roundrobin_two_dcs_2(self): + remove_cluster() use_multidc([3, 2]) keyspace = 'test_dc_aware_roundrobin_two_dcs_2' cluster, session = self._cluster_session_with_lbp(DCAwareRoundRobinPolicy('dc2')) @@ -320,13 +325,14 @@ def test_dc_aware_roundrobin_two_dcs_2(self): self._insert(session, keyspace) self._query(session, keyspace) - self.coordinator_stats.assert_query_count_equals(self, 1, 0) - self.coordinator_stats.assert_query_count_equals(self, 2, 0) - self.coordinator_stats.assert_query_count_equals(self, 3, 0) - self.coordinator_stats.assert_query_count_equals(self, 4, 6) - self.coordinator_stats.assert_query_count_equals(self, 5, 6) + self.coordinator_stats.assert_query_count_equals(1, 0) + self.coordinator_stats.assert_query_count_equals(2, 0) + self.coordinator_stats.assert_query_count_equals(3, 0) + self.coordinator_stats.assert_query_count_equals(4, 6) + self.coordinator_stats.assert_query_count_equals(5, 6) def test_dc_aware_roundrobin_one_remote_host(self): + remove_cluster() use_multidc([2, 2]) keyspace = 'test_dc_aware_roundrobin_one_remote_host' cluster, session = self._cluster_session_with_lbp(DCAwareRoundRobinPolicy('dc2', used_hosts_per_remote_dc=1)) @@ -337,10 +343,10 @@ def test_dc_aware_roundrobin_one_remote_host(self): self._insert(session, keyspace) self._query(session, keyspace) - self.coordinator_stats.assert_query_count_equals(self, 1, 0) - self.coordinator_stats.assert_query_count_equals(self, 2, 0) - self.coordinator_stats.assert_query_count_equals(self, 3, 6) - self.coordinator_stats.assert_query_count_equals(self, 4, 6) + self.coordinator_stats.assert_query_count_equals(1, 0) + self.coordinator_stats.assert_query_count_equals(2, 0) + self.coordinator_stats.assert_query_count_equals(3, 6) + self.coordinator_stats.assert_query_count_equals(4, 6) self.coordinator_stats.reset_counts() bootstrap(5, 'dc1') @@ -348,11 +354,11 @@ def test_dc_aware_roundrobin_one_remote_host(self): self._query(session, keyspace) - self.coordinator_stats.assert_query_count_equals(self, 1, 0) - self.coordinator_stats.assert_query_count_equals(self, 2, 0) - self.coordinator_stats.assert_query_count_equals(self, 3, 6) - self.coordinator_stats.assert_query_count_equals(self, 4, 6) - self.coordinator_stats.assert_query_count_equals(self, 5, 0) + self.coordinator_stats.assert_query_count_equals(1, 0) + self.coordinator_stats.assert_query_count_equals(2, 0) + self.coordinator_stats.assert_query_count_equals(3, 6) + self.coordinator_stats.assert_query_count_equals(4, 6) + self.coordinator_stats.assert_query_count_equals(5, 0) self.coordinator_stats.reset_counts() decommission(3) @@ -361,12 +367,12 @@ def test_dc_aware_roundrobin_one_remote_host(self): self._query(session, keyspace) - self.coordinator_stats.assert_query_count_equals(self, 3, 0) - self.coordinator_stats.assert_query_count_equals(self, 4, 0) + self.coordinator_stats.assert_query_count_equals(3, 0) + self.coordinator_stats.assert_query_count_equals(4, 0) responses = set() for node in [1, 2, 5]: responses.add(self.coordinator_stats.get_query_count(node)) - self.assertEqual(set([0, 0, 12]), responses) + assert set([0, 0, 12]) == responses self.coordinator_stats.reset_counts() decommission(5) @@ -374,13 +380,13 @@ def test_dc_aware_roundrobin_one_remote_host(self): self._query(session, keyspace) - self.coordinator_stats.assert_query_count_equals(self, 3, 0) - self.coordinator_stats.assert_query_count_equals(self, 4, 0) - self.coordinator_stats.assert_query_count_equals(self, 5, 0) + self.coordinator_stats.assert_query_count_equals(3, 0) + self.coordinator_stats.assert_query_count_equals(4, 0) + self.coordinator_stats.assert_query_count_equals(5, 0) responses = set() for node in [1, 2]: responses.add(self.coordinator_stats.get_query_count(node)) - self.assertEqual(set([0, 12]), responses) + assert set([0, 12]) == responses self.coordinator_stats.reset_counts() decommission(1) @@ -388,20 +394,17 @@ def test_dc_aware_roundrobin_one_remote_host(self): self._query(session, keyspace) - self.coordinator_stats.assert_query_count_equals(self, 1, 0) - self.coordinator_stats.assert_query_count_equals(self, 2, 12) - self.coordinator_stats.assert_query_count_equals(self, 3, 0) - self.coordinator_stats.assert_query_count_equals(self, 4, 0) - self.coordinator_stats.assert_query_count_equals(self, 5, 0) + self.coordinator_stats.assert_query_count_equals(1, 0) + self.coordinator_stats.assert_query_count_equals(2, 12) + self.coordinator_stats.assert_query_count_equals(3, 0) + self.coordinator_stats.assert_query_count_equals(4, 0) + self.coordinator_stats.assert_query_count_equals(5, 0) self.coordinator_stats.reset_counts() force_stop(2) - try: + with pytest.raises(NoHostAvailable): self._query(session, keyspace) - self.fail() - except NoHostAvailable: - pass def test_token_aware(self): keyspace = 'test_token_aware' @@ -412,6 +415,7 @@ def test_token_aware_prepared(self): self.token_aware(keyspace, True) def token_aware(self, keyspace, use_prepared=False): + remove_cluster() use_singledc() cluster, session = self._cluster_session_with_lbp(TokenAwarePolicy(RoundRobinPolicy())) self.addCleanup(cluster.shutdown) @@ -421,28 +425,26 @@ def token_aware(self, keyspace, use_prepared=False): self._insert(session, keyspace) self._query(session, keyspace, use_prepared=use_prepared) - self.coordinator_stats.assert_query_count_equals(self, 1, 0) - self.coordinator_stats.assert_query_count_equals(self, 2, 12) - self.coordinator_stats.assert_query_count_equals(self, 3, 0) + self.coordinator_stats.assert_query_count_equals(1, 0) + self.coordinator_stats.assert_query_count_equals(2, 12) + self.coordinator_stats.assert_query_count_equals(3, 0) self.coordinator_stats.reset_counts() self._query(session, keyspace, use_prepared=use_prepared) - self.coordinator_stats.assert_query_count_equals(self, 1, 0) - self.coordinator_stats.assert_query_count_equals(self, 2, 12) - self.coordinator_stats.assert_query_count_equals(self, 3, 0) + self.coordinator_stats.assert_query_count_equals(1, 0) + self.coordinator_stats.assert_query_count_equals(2, 12) + self.coordinator_stats.assert_query_count_equals(3, 0) self.coordinator_stats.reset_counts() force_stop(2) self._wait_for_nodes_down([2], cluster) - try: + with pytest.raises(Unavailable) as e: self._query(session, keyspace, use_prepared=use_prepared) - self.fail() - except Unavailable as e: - self.assertEqual(e.consistency, 1) - self.assertEqual(e.required_replicas, 1) - self.assertEqual(e.alive_replicas, 0) + assert e.value.consistency == 1 + assert e.value.required_replicas == 1 + assert e.value.alive_replicas == 0 self.coordinator_stats.reset_counts() start(2) @@ -450,19 +452,16 @@ def token_aware(self, keyspace, use_prepared=False): self._query(session, keyspace, use_prepared=use_prepared) - self.coordinator_stats.assert_query_count_equals(self, 1, 0) - self.coordinator_stats.assert_query_count_equals(self, 2, 12) - self.coordinator_stats.assert_query_count_equals(self, 3, 0) + self.coordinator_stats.assert_query_count_equals(1, 0) + self.coordinator_stats.assert_query_count_equals(2, 12) + self.coordinator_stats.assert_query_count_equals(3, 0) self.coordinator_stats.reset_counts() stop(2) self._wait_for_nodes_down([2], cluster) - try: + with pytest.raises(Unavailable): self._query(session, keyspace, use_prepared=use_prepared) - self.fail() - except Unavailable: - pass self.coordinator_stats.reset_counts() start(2) @@ -476,8 +475,8 @@ def token_aware(self, keyspace, use_prepared=False): self.coordinator_stats.get_query_count(1), self.coordinator_stats.get_query_count(3) ]) - self.assertEqual(results, set([0, 12])) - self.coordinator_stats.assert_query_count_equals(self, 2, 0) + assert results == set([0, 12]) + self.coordinator_stats.assert_query_count_equals(2, 0) def test_token_aware_composite_key(self): use_singledc() @@ -500,20 +499,19 @@ def test_token_aware_composite_key(self): '(?, ?, ?)' % table) bound = prepared.bind((1, 2, 3)) result = session.execute(bound) - self.assertIn(result.response_future.attempted_hosts[0], - cluster.metadata.get_replicas(keyspace, bound.routing_key)) + assert result.response_future.attempted_hosts[0] in cluster.metadata.get_replicas(keyspace, bound.routing_key) # There could be race condition with querying a node # which doesn't yet have the data so we query one of # the replicas results = session.execute(SimpleStatement('SELECT * FROM %s WHERE k1 = 1 AND k2 = 2' % table, routing_key=bound.routing_key)) - self.assertIn(results.response_future.attempted_hosts[0], - cluster.metadata.get_replicas(keyspace, bound.routing_key)) + assert results.response_future.attempted_hosts[0] in cluster.metadata.get_replicas(keyspace, bound.routing_key) - self.assertTrue(results[0].i) + assert results[0].i def test_token_aware_with_rf_2(self, use_prepared=False): + remove_cluster() use_singledc() keyspace = 'test_token_aware_with_rf_2' cluster, session = self._cluster_session_with_lbp(TokenAwarePolicy(RoundRobinPolicy())) @@ -524,9 +522,9 @@ def test_token_aware_with_rf_2(self, use_prepared=False): self._insert(session, keyspace) self._query(session, keyspace) - self.coordinator_stats.assert_query_count_equals(self, 1, 0) - self.coordinator_stats.assert_query_count_equals(self, 2, 12) - self.coordinator_stats.assert_query_count_equals(self, 3, 0) + self.coordinator_stats.assert_query_count_equals(1, 0) + self.coordinator_stats.assert_query_count_equals(2, 12) + self.coordinator_stats.assert_query_count_equals(3, 0) self.coordinator_stats.reset_counts() stop(2) @@ -534,9 +532,9 @@ def test_token_aware_with_rf_2(self, use_prepared=False): self._query(session, keyspace) - self.coordinator_stats.assert_query_count_equals(self, 1, 0) - self.coordinator_stats.assert_query_count_equals(self, 2, 0) - self.coordinator_stats.assert_query_count_equals(self, 3, 12) + self.coordinator_stats.assert_query_count_equals(1, 0) + self.coordinator_stats.assert_query_count_equals(2, 0) + self.coordinator_stats.assert_query_count_equals(3, 12) def test_token_aware_with_local_table(self): use_singledc() @@ -547,7 +545,7 @@ def test_token_aware_with_local_table(self): p = session.prepare("SELECT * FROM system.local WHERE key=?") # this would blow up prior to 61b4fad r = session.execute(p, ('local',)) - self.assertEqual(r[0].key, 'local') + assert r[0].key == 'local' def test_token_aware_with_shuffle_rf2(self): """ @@ -572,9 +570,9 @@ def test_token_aware_with_shuffle_rf2(self): self._query(session, keyspace) - self.coordinator_stats.assert_query_count_equals(self, 1, 0) - self.coordinator_stats.assert_query_count_equals(self, 2, 0) - self.coordinator_stats.assert_query_count_equals(self, 3, 12) + self.coordinator_stats.assert_query_count_equals(1, 0) + self.coordinator_stats.assert_query_count_equals(2, 0) + self.coordinator_stats.assert_query_count_equals(3, 12) def test_token_aware_with_shuffle_rf3(self): """ @@ -599,10 +597,10 @@ def test_token_aware_with_shuffle_rf3(self): self._query(session, keyspace) - self.coordinator_stats.assert_query_count_equals(self, 1, 0) + self.coordinator_stats.assert_query_count_equals(1, 0) query_count_two = self.coordinator_stats.get_query_count(2) query_count_three = self.coordinator_stats.get_query_count(3) - self.assertEqual(query_count_two + query_count_three, 12) + assert query_count_two + query_count_three == 12 self.coordinator_stats.reset_counts() stop(2) @@ -610,11 +608,10 @@ def test_token_aware_with_shuffle_rf3(self): self._query(session, keyspace) - self.coordinator_stats.assert_query_count_equals(self, 1, 0) - self.coordinator_stats.assert_query_count_equals(self, 2, 0) - self.coordinator_stats.assert_query_count_equals(self, 3, 12) + self.coordinator_stats.assert_query_count_equals(1, 0) + self.coordinator_stats.assert_query_count_equals(2, 0) + self.coordinator_stats.assert_query_count_equals(3, 12) - @notdse @greaterthanorequalcass40 def test_token_aware_with_transient_replication(self): """ @@ -627,6 +624,7 @@ def test_token_aware_with_transient_replication(self): @test_category policy """ + remove_cluster() # We can test this with a single dc when CASSANDRA-15670 is fixed use_multidc([3, 3]) @@ -645,18 +643,19 @@ def test_token_aware_with_transient_replication(self): f = session.execute_async(query, (i,), trace=True) full_dc1_replicas = [h for h in cluster.metadata.get_replicas('test_tr', cqltypes.Int32Type.serialize(i, cluster.protocol_version)) if h.datacenter == 'dc1'] - self.assertEqual(len(full_dc1_replicas), 2) + assert len(full_dc1_replicas) == 2 f.result() trace_hosts = [cluster.metadata.get_host(e.source) for e in f.get_query_trace().events] for h in f.attempted_hosts: - self.assertIn(h, full_dc1_replicas) + assert h in full_dc1_replicas for h in trace_hosts: - self.assertIn(h, full_dc1_replicas) + assert h in full_dc1_replicas def _set_up_shuffle_test(self, keyspace, replication_factor): + remove_cluster() use_singledc() cluster, session = self._cluster_session_with_lbp( TokenAwarePolicy(RoundRobinPolicy(), shuffle_replicas=True) @@ -682,12 +681,13 @@ def _check_query_order_changes(self, session, keyspace): self.coordinator_stats.get_query_count(3)) query_counts.add(loop_qcs) - self.assertEqual(sum(loop_qcs), 12) + assert sum(loop_qcs) == 12 # end the loop if we get more than one query ordering self.coordinator_stats.reset_counts() def test_white_list(self): + remove_cluster() use_singledc() keyspace = 'test_white_list' @@ -707,24 +707,21 @@ def test_white_list(self): self._insert(session, keyspace) self._query(session, keyspace) - self.coordinator_stats.assert_query_count_equals(self, 1, 0) - self.coordinator_stats.assert_query_count_equals(self, 2, 12) - self.coordinator_stats.assert_query_count_equals(self, 3, 0) + self.coordinator_stats.assert_query_count_equals(1, 0) + self.coordinator_stats.assert_query_count_equals(2, 12) + self.coordinator_stats.assert_query_count_equals(3, 0) # white list policy should not allow reconnecting to ignored hosts force_stop(3) self._wait_for_nodes_down([3]) - self.assertFalse(cluster.metadata.get_host(IP_FORMAT % 3).is_currently_reconnecting()) + assert not cluster.metadata.get_host(IP_FORMAT % 3).is_currently_reconnecting() self.coordinator_stats.reset_counts() force_stop(2) self._wait_for_nodes_down([2]) - try: + with pytest.raises(NoHostAvailable): self._query(session, keyspace) - self.fail() - except NoHostAvailable: - pass def test_black_list_with_host_filter_policy(self): """ @@ -736,6 +733,7 @@ def test_black_list_with_host_filter_policy(self): @test_category policy """ + remove_cluster() use_singledc() keyspace = 'test_black_list_with_hfp' ignored_address = (IP_FORMAT % 2) @@ -753,7 +751,7 @@ def test_black_list_with_host_filter_policy(self): session = cluster.connect() self._wait_for_nodes_up([1, 2, 3]) - self.assertNotIn(ignored_address, [h.address for h in hfp.make_query_plan()]) + assert ignored_address not in [h.address for h in hfp.make_query_plan()] create_schema(cluster, session, keyspace) self._insert(session, keyspace) @@ -764,12 +762,12 @@ def test_black_list_with_host_filter_policy(self): # will be 4 and for the other 8 first_node_count = self.coordinator_stats.get_query_count(1) third_node_count = self.coordinator_stats.get_query_count(3) - self.assertEqual(first_node_count + third_node_count, 12) - self.assertTrue(first_node_count == 8 or first_node_count == 4) + assert first_node_count + third_node_count == 12 + assert first_node_count == 8 or first_node_count == 4 - self.coordinator_stats.assert_query_count_equals(self, 2, 0) + self.coordinator_stats.assert_query_count_equals(2, 0) # policy should not allow reconnecting to ignored host force_stop(2) self._wait_for_nodes_down([2]) - self.assertFalse(cluster.metadata.get_host(ignored_address).is_currently_reconnecting()) + assert not cluster.metadata.get_host(ignored_address).is_currently_reconnecting() diff --git a/tests/integration/long/test_policies.py b/tests/integration/long/test_policies.py index 33f35ced0d..5cada34d8b 100644 --- a/tests/integration/long/test_policies.py +++ b/tests/integration/long/test_policies.py @@ -18,6 +18,7 @@ from cassandra.cluster import ExecutionProfile, EXEC_PROFILE_DEFAULT from tests.integration import use_cluster, get_cluster, get_node, TestCluster +import pytest def setup_module(): @@ -47,7 +48,7 @@ def test_should_rethrow_on_unvailable_with_default_policy_if_cas(self): cluster = TestCluster(execution_profiles={EXEC_PROFILE_DEFAULT: ep}) session = cluster.connect() - session.execute("CREATE KEYSPACE test_retry_policy_cas WITH replication = {'class':'SimpleStrategy','replication_factor': 3};") + session.execute("CREATE KEYSPACE test_retry_policy_cas WITH replication = {'class':'NetworkTopologyStrategy','replication_factor': 3};") session.execute("CREATE TABLE test_retry_policy_cas.t (id int PRIMARY KEY, data text);") session.execute('INSERT INTO test_retry_policy_cas.t ("id", "data") VALUES (%(0)s, %(1)s)', {'0': 42, '1': 'testing'}) @@ -58,10 +59,10 @@ def test_should_rethrow_on_unvailable_with_default_policy_if_cas(self): # supported as conditional update commit consistency. ...."" # after fix: cassandra.Unavailable (expected since replicas are down) - with self.assertRaises(Unavailable) as cm: + with pytest.raises(Unavailable) as cm: session.execute("update test_retry_policy_cas.t set data = 'staging' where id = 42 if data ='testing'") - exception = cm.exception - self.assertEqual(exception.consistency, ConsistencyLevel.SERIAL) - self.assertEqual(exception.required_replicas, 2) - self.assertEqual(exception.alive_replicas, 1) + exception = cm.value + assert exception.consistency == ConsistencyLevel.SERIAL + assert exception.required_replicas == 2 + assert exception.alive_replicas == 1 diff --git a/tests/integration/long/test_schema.py b/tests/integration/long/test_schema.py index f1cc80a17a..d60ff775c4 100644 --- a/tests/integration/long/test_schema.py +++ b/tests/integration/long/test_schema.py @@ -57,7 +57,7 @@ def test_recreates(self): log.debug(drop) execute_until_pass(session, drop) - create = "CREATE KEYSPACE {0} WITH replication = {{'class': 'SimpleStrategy', 'replication_factor': 3}}".format(keyspace) + create = "CREATE KEYSPACE {0} WITH replication = {{'class': 'NetworkTopologyStrategy', 'replication_factor': 3}}".format(keyspace) log.debug(create) execute_until_pass(session, create) @@ -82,7 +82,7 @@ def test_for_schema_disagreements_different_keyspaces(self): session = self.session for i in range(30): - execute_until_pass(session, "CREATE KEYSPACE test_{0} WITH replication = {{'class': 'SimpleStrategy', 'replication_factor': 1}}".format(i)) + execute_until_pass(session, "CREATE KEYSPACE test_{0} WITH replication = {{'class': 'NetworkTopologyStrategy', 'replication_factor': 1}}".format(i)) execute_until_pass(session, "CREATE TABLE test_{0}.cf (key int PRIMARY KEY, value int)".format(i)) for j in range(100): @@ -100,10 +100,10 @@ def test_for_schema_disagreements_same_keyspace(self): for i in range(30): try: - execute_until_pass(session, "CREATE KEYSPACE test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}") + execute_until_pass(session, "CREATE KEYSPACE test WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': 1}") except AlreadyExists: execute_until_pass(session, "DROP KEYSPACE test") - execute_until_pass(session, "CREATE KEYSPACE test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}") + execute_until_pass(session, "CREATE KEYSPACE test WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': 1}") execute_until_pass(session, "CREATE TABLE test.cf (key int PRIMARY KEY, value int)") @@ -132,7 +132,7 @@ def test_for_schema_disagreement_attribute(self): cluster = TestCluster(max_schema_agreement_wait=0.001) session = cluster.connect(wait_for_all_pools=True) - rs = session.execute("CREATE KEYSPACE test_schema_disagreement WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3}") + rs = session.execute("CREATE KEYSPACE test_schema_disagreement WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': 3}") self.check_and_wait_for_agreement(session, rs, False) rs = session.execute(SimpleStatement("CREATE TABLE test_schema_disagreement.cf (key int PRIMARY KEY, value int)", consistency_level=ConsistencyLevel.ALL)) @@ -144,7 +144,7 @@ def test_for_schema_disagreement_attribute(self): # These should have schema agreement cluster = TestCluster(max_schema_agreement_wait=100) session = cluster.connect() - rs = session.execute("CREATE KEYSPACE test_schema_disagreement WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3}") + rs = session.execute("CREATE KEYSPACE test_schema_disagreement WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': 3}") self.check_and_wait_for_agreement(session, rs, True) rs = session.execute(SimpleStatement("CREATE TABLE test_schema_disagreement.cf (key int PRIMARY KEY, value int)", consistency_level=ConsistencyLevel.ALL)) @@ -156,6 +156,6 @@ def test_for_schema_disagreement_attribute(self): def check_and_wait_for_agreement(self, session, rs, exepected): # Wait for RESULT_KIND_SCHEMA_CHANGE message to arrive time.sleep(1) - self.assertEqual(rs.response_future.is_schema_agreed, exepected) + assert rs.response_future.is_schema_agreed == exepected if not rs.response_future.is_schema_agreed: - session.cluster.control_connection.wait_for_schema_agreement(wait_time=1000) + session.wait_for_schema_agreement(wait_time=1000) diff --git a/tests/integration/long/test_ssl.py b/tests/integration/long/test_ssl.py index 070e2fe268..0170f56fa1 100644 --- a/tests/integration/long/test_ssl.py +++ b/tests/integration/long/test_ssl.py @@ -25,6 +25,7 @@ from tests.integration import ( get_cluster, remove_cluster, use_single_node, start_cluster_wait_for_up, EVENT_LOOP_MANAGER, TestCluster ) +import pytest if not hasattr(ssl, 'match_hostname'): try: @@ -115,7 +116,7 @@ def validate_ssl_options(**kwargs): # attempt a few simple commands. insert_keyspace = """CREATE KEYSPACE ssltest - WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '3'} + WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '3'} """ statement = SimpleStatement(insert_keyspace) statement.consistency_level = 3 @@ -290,7 +291,7 @@ def test_cannot_connect_without_client_auth(self): cluster = TestCluster(ssl_options={'ca_certs': CLIENT_CA_CERTS, 'ssl_version': ssl_version}) - with self.assertRaises(NoHostAvailable) as _: + with pytest.raises(NoHostAvailable): cluster.connect() cluster.shutdown() @@ -322,7 +323,7 @@ def test_cannot_connect_with_bad_client_auth(self): 'keyfile': DRIVER_KEYFILE} ) - with self.assertRaises(NoHostAvailable) as _: + with pytest.raises(NoHostAvailable): cluster.connect() cluster.shutdown() @@ -333,7 +334,7 @@ def test_cannot_connect_with_invalid_hostname(self): 'certfile': DRIVER_CERTFILE} ssl_options.update(verify_certs) - with self.assertRaises(Exception): + with pytest.raises(Exception): validate_ssl_options(ssl_options=ssl_options, hostname='localhost') @@ -368,7 +369,7 @@ def test_ssl_want_write_errors_are_retried(self): except: pass session.execute( - "CREATE KEYSPACE ssl_error_test WITH replication = {'class':'SimpleStrategy','replication_factor':1};") + "CREATE KEYSPACE ssl_error_test WITH replication = {'class':'NetworkTopologyStrategy','replication_factor':1};") session.execute("CREATE TABLE ssl_error_test.big_text (id uuid PRIMARY KEY, data text);") params = { @@ -487,7 +488,7 @@ def test_cannot_connect_ssl_context_with_invalid_hostname(self): ) ssl_context.verify_mode = ssl.CERT_REQUIRED ssl_options["check_hostname"] = True - with self.assertRaises(Exception): + with pytest.raises(Exception): validate_ssl_options(ssl_context=ssl_context, ssl_options=ssl_options, hostname="localhost") @unittest.skipIf(USES_PYOPENSSL, "This test is for the built-in ssl.Context") diff --git a/tests/integration/long/test_topology_change.py b/tests/integration/long/test_topology_change.py index 5b12eef28c..80540cfb2f 100644 --- a/tests/integration/long/test_topology_change.py +++ b/tests/integration/long/test_topology_change.py @@ -39,10 +39,10 @@ def test_removed_node_stops_reconnecting(self): get_node(3).nodetool("disablebinary") wait_until(condition=lambda: state_listener.downed_host is not None, delay=2, max_attempts=50) - self.assertTrue(state_listener.downed_host.is_currently_reconnecting()) + assert state_listener.downed_host.is_currently_reconnecting() decommission(3) wait_until(condition=lambda: state_listener.removed_host is not None, delay=2, max_attempts=50) - self.assertIs(state_listener.downed_host, state_listener.removed_host) # Just a sanity check - self.assertFalse(state_listener.removed_host.is_currently_reconnecting()) + assert state_listener.downed_host is state_listener.removed_host # Just a sanity check + assert not state_listener.removed_host.is_currently_reconnecting() diff --git a/tests/integration/long/utils.py b/tests/integration/long/utils.py index 58c3241a42..ba9351828e 100644 --- a/tests/integration/long/utils.py +++ b/tests/integration/long/utils.py @@ -14,12 +14,13 @@ import logging import time +import pytest from collections import defaultdict from packaging.version import Version from tests.integration import (get_node, get_cluster, wait_for_node_socket, - DSE_VERSION, CASSANDRA_VERSION) + CASSANDRA_VERSION) IP_FORMAT = '127.0.0.%s' @@ -48,10 +49,10 @@ def get_query_count(self, node): ip = '127.0.0.%d' % node return self.coordinator_counts[ip] - def assert_query_count_equals(self, testcase, node, expected): + def assert_query_count_equals(self, node, expected): ip = '127.0.0.%d' % node if self.get_query_count(node) != expected: - testcase.fail('Expected %d queries to %s, but got %d. Query counts: %s' % ( + pytest.fail('Expected %d queries to %s, but got %d. Query counts: %s' % ( expected, ip, self.coordinator_counts[ip], dict(self.coordinator_counts))) @@ -62,7 +63,7 @@ def create_schema(cluster, session, keyspace, simple_strategy=True, if simple_strategy: ddl = "CREATE KEYSPACE %s WITH replication" \ - " = {'class': 'SimpleStrategy', 'replication_factor': '%s'}" + " = {'class': 'NetworkTopologyStrategy', 'replication_factor': '%s'}" session.execute(ddl % (keyspace, replication_factor), timeout=10) else: if not replication_strategy: @@ -92,7 +93,7 @@ def force_stop(node): def decommission(node): - if (DSE_VERSION and DSE_VERSION >= Version("5.1")) or CASSANDRA_VERSION >= Version("4.0-a"): + if CASSANDRA_VERSION >= Version("4.0-a"): # CASSANDRA-12510 get_node(node).decommission(force=True) else: diff --git a/tests/integration/simulacron/__init__.py b/tests/integration/simulacron/__init__.py index c959fd6e08..b75b67c540 100644 --- a/tests/integration/simulacron/__init__.py +++ b/tests/integration/simulacron/__init__.py @@ -13,7 +13,7 @@ # limitations under the License import unittest -from tests.integration import requiredse, CASSANDRA_VERSION, DSE_VERSION, SIMULACRON_JAR, PROTOCOL_VERSION +from tests.integration import CASSANDRA_VERSION, SIMULACRON_JAR, PROTOCOL_VERSION from tests.integration.simulacron.utils import ( clear_queries, start_and_prime_singledc, @@ -26,7 +26,7 @@ from packaging.version import Version -PROTOCOL_VERSION = min(4, PROTOCOL_VERSION if (DSE_VERSION is None or DSE_VERSION >= Version('5.0')) else 3) +PROTOCOL_VERSION = min(4, PROTOCOL_VERSION) def teardown_package(): @@ -61,22 +61,3 @@ def tearDownClass(cls): if cls.cluster: cls.cluster.shutdown() stop_simulacron() - - -@requiredse -class DseSimulacronCluster(SimulacronBase): - - simulacron_cluster = None - cluster, connect = None, True - nodes_per_dc = 1 - - @classmethod - def setUpClass(cls): - if DSE_VERSION is None and SIMULACRON_JAR is None or CASSANDRA_VERSION < Version("2.1"): - return - - cls.simulacron_cluster = start_and_prime_cluster_defaults(dse_version=DSE_VERSION, - nodes_per_dc=cls.nodes_per_dc) - if cls.connect: - cls.cluster = Cluster(protocol_version=PROTOCOL_VERSION, compression=False) - cls.session = cls.cluster.connect(wait_for_all_pools=True) diff --git a/tests/integration/simulacron/advanced/__init__.py b/tests/integration/simulacron/advanced/__init__.py deleted file mode 100644 index 2c9ca172f8..0000000000 --- a/tests/integration/simulacron/advanced/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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. diff --git a/tests/integration/simulacron/advanced/test_insights.py b/tests/integration/simulacron/advanced/test_insights.py deleted file mode 100644 index 5ddae4ec7c..0000000000 --- a/tests/integration/simulacron/advanced/test_insights.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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 unittest - -import time -import json -import re - -from cassandra.cluster import Cluster -from cassandra.datastax.insights.util import version_supports_insights - -from tests.integration import requiressimulacron, requiredse, DSE_VERSION -from tests.integration.simulacron import DseSimulacronCluster, PROTOCOL_VERSION -from tests.integration.simulacron.utils import SimulacronClient, GetLogsQuery, ClearLogsQuery - - -@requiredse -@requiressimulacron -@unittest.skipUnless(DSE_VERSION and version_supports_insights(str(DSE_VERSION)), 'DSE {} does not support insights'.format(DSE_VERSION)) -class InsightsTests(DseSimulacronCluster): - """ - Tests insights integration - - @since 3.18 - @jira_ticket PYTHON-1047 - @expected_result startup and status messages are sent - """ - - connect = False - - def tearDown(self): - if self.cluster: - self.cluster.shutdown() - - @staticmethod - def _get_node_logs(raw_data): - return list(filter(lambda q: q['type'] == 'QUERY' and q['query'].startswith('CALL InsightsRpc.reportInsight'), - json.loads(raw_data)['data_centers'][0]['nodes'][0]['queries'])) - - @staticmethod - def _parse_data(data, index=0): - return json.loads(re.match( - r"CALL InsightsRpc.reportInsight\('(.+)'\)", - data[index]['frame']['message']['query']).group(1)) - - def test_startup_message(self): - self.cluster = Cluster(protocol_version=PROTOCOL_VERSION, compression=False) - self.session = self.cluster.connect(wait_for_all_pools=True) - - time.sleep(1) # wait the monitor thread is started - response = SimulacronClient().submit_request(GetLogsQuery()) - self.assertTrue('CALL InsightsRpc.reportInsight' in response) - - node_queries = self._get_node_logs(response) - self.assertEqual(1, len(node_queries)) - self.assertTrue(node_queries, "RPC query not found") - - message = self._parse_data(node_queries) - - self.assertEqual(message['metadata']['name'], 'driver.startup') - self.assertEqual(message['data']['initialControlConnection'], - self.cluster.control_connection._connection.host) - self.assertEqual(message['data']['sessionId'], str(self.session.session_id)) - self.assertEqual(message['data']['clientId'], str(self.cluster.client_id)) - self.assertEqual(message['data']['compression'], 'NONE') - - def test_status_message(self): - SimulacronClient().submit_request(ClearLogsQuery()) - - self.cluster = Cluster(protocol_version=PROTOCOL_VERSION, compression=False, monitor_reporting_interval=1) - self.session = self.cluster.connect(wait_for_all_pools=True) - - time.sleep(1.1) - response = SimulacronClient().submit_request(GetLogsQuery()) - self.assertTrue('CALL InsightsRpc.reportInsight' in response) - - node_queries = self._get_node_logs(response) - self.assertEqual(2, len(node_queries)) - self.assertTrue(node_queries, "RPC query not found") - - message = self._parse_data(node_queries, 1) - - self.assertEqual(message['metadata']['name'], 'driver.status') - self.assertEqual(message['data']['controlConnection'], - self.cluster.control_connection._connection.host) - self.assertEqual(message['data']['sessionId'], str(self.session.session_id)) - self.assertEqual(message['data']['clientId'], str(self.cluster.client_id)) - self.assertEqual(message['metadata']['insightType'], 'EVENT') - - def test_monitor_disabled(self): - SimulacronClient().submit_request(ClearLogsQuery()) - - self.cluster = Cluster(protocol_version=PROTOCOL_VERSION, compression=False, monitor_reporting_enabled=False) - self.session = self.cluster.connect(wait_for_all_pools=True) - - response = SimulacronClient().submit_request(GetLogsQuery()) - self.assertFalse('CALL InsightsRpc.reportInsight' in response) diff --git a/tests/integration/simulacron/test_backpressure.py b/tests/integration/simulacron/test_backpressure.py index 69c38da8fe..0b84f73e29 100644 --- a/tests/integration/simulacron/test_backpressure.py +++ b/tests/integration/simulacron/test_backpressure.py @@ -19,6 +19,7 @@ from tests.integration import requiressimulacron, libevtest from tests.integration.simulacron import SimulacronBase, PROTOCOL_VERSION from tests.integration.simulacron.utils import ResumeReads, PauseReads, prime_request, start_and_prime_singledc +import pytest @requiressimulacron @@ -70,10 +71,10 @@ def test_paused_connections(self): # Make sure we actually have some stuck in-flight requests for in_flight in [pool._connection.in_flight for pool in session.get_pools()]: - self.assertGreater(in_flight, 100) + assert in_flight > 100 time.sleep(.5) for in_flight in [pool._connection.in_flight for pool in session.get_pools()]: - self.assertGreater(in_flight, 100) + assert in_flight > 100 prime_request(ResumeReads()) @@ -83,7 +84,7 @@ def test_paused_connections(self): except NoHostAvailable as e: # We shouldn't have any timeouts here, but all of the queries beyond what can fit # in the tcp buffer will have returned with a ConnectionBusy exception - self.assertIn("ConnectionBusy", str(e)) + assert "ConnectionBusy" in str(e) # Verify that we can continue sending queries without any problems for host in session.cluster.metadata.all_hosts(): @@ -121,9 +122,9 @@ def test_queued_requests_timeout(self): # Simulacron will respond to a couple queries before cutting off reads, so we'll just verify # that only "a few" successes happened here - self.assertLess(successes, 50) - self.assertLess(self.callback_successes, 50) - self.assertEqual(self.callback_errors, len(futures) - self.callback_successes) + assert successes < 50 + assert self.callback_successes < 50 + assert self.callback_errors == len(futures) - self.callback_successes def test_cluster_busy(self): """ Verify that once TCP buffer is full we get busy exceptions rather than timeouts """ @@ -146,9 +147,9 @@ def test_cluster_busy(self): # Now that our send buffer is completely full, verify we immediately get busy exceptions rather than timing out for i in range(1000): - with self.assertRaises(NoHostAvailable) as e: + with pytest.raises(NoHostAvailable) as e: session.execute(query, [str(i)]) - self.assertIn("ConnectionBusy", str(e.exception)) + assert "ConnectionBusy" in str(e.value) def test_node_busy(self): """ Verify that once TCP buffer is full, queries continue to get re-routed to other nodes """ @@ -176,4 +177,3 @@ def test_node_busy(self): # verify queries get re-routed to other nodes and queries complete successfully for i in range(1000): session.execute(query, [str(i)]) - diff --git a/tests/integration/simulacron/test_cluster.py b/tests/integration/simulacron/test_cluster.py index dfbf6c0ec6..898734c416 100644 --- a/tests/integration/simulacron/test_cluster.py +++ b/tests/integration/simulacron/test_cluster.py @@ -18,15 +18,16 @@ import cassandra from tests.integration.simulacron import SimulacronCluster, SimulacronBase -from tests.integration import (requiressimulacron, PROTOCOL_VERSION, DSE_VERSION, MockLoggingHandler) +from tests.integration import (requiressimulacron, PROTOCOL_VERSION, MockLoggingHandler) from tests.integration.simulacron.utils import prime_query, start_and_prime_singledc from cassandra import (WriteTimeout, WriteType, ConsistencyLevel, UnresolvableContactPoints) from cassandra.cluster import Cluster, ControlConnection +import pytest -PROTOCOL_VERSION = min(4, PROTOCOL_VERSION if (DSE_VERSION is None or DSE_VERSION >= Version('5.0')) else 3) +PROTOCOL_VERSION = min(4, PROTOCOL_VERSION) @requiressimulacron class ClusterTests(SimulacronCluster): @@ -48,17 +49,17 @@ def test_writetimeout(self): } prime_query(query_to_prime_simple, then=then, rows=None, column_types=None) - with self.assertRaises(WriteTimeout) as assert_raised_context: + with pytest.raises(WriteTimeout) as assert_raised_context: self.session.execute(query_to_prime_simple) - wt = assert_raised_context.exception - self.assertEqual(wt.write_type, WriteType.name_to_value[write_type]) - self.assertEqual(wt.consistency, ConsistencyLevel.name_to_value[consistency]) - self.assertEqual(wt.received_responses, received_responses) - self.assertEqual(wt.required_responses, required_responses) - self.assertIn(write_type, str(wt)) - self.assertIn(consistency, str(wt)) - self.assertIn(str(received_responses), str(wt)) - self.assertIn(str(required_responses), str(wt)) + wt = assert_raised_context.value + assert wt.write_type == WriteType.name_to_value[write_type] + assert wt.consistency == ConsistencyLevel.name_to_value[consistency] + assert wt.received_responses == received_responses + assert wt.required_responses == required_responses + assert write_type in str(wt) + assert consistency in str(wt) + assert str(received_responses) in str(wt) + assert str(required_responses) in str(wt) @requiressimulacron @@ -77,7 +78,7 @@ def test_connection_with_one_unresolvable_contact_point(self): compression=False) def test_connection_with_only_unresolvable_contact_points(self): - with self.assertRaises(UnresolvableContactPoints): + with pytest.raises(UnresolvableContactPoints): self.cluster = Cluster(['dns.invalid'], protocol_version=PROTOCOL_VERSION, compression=False) @@ -89,7 +90,7 @@ class DuplicateRpcTest(SimulacronCluster): def test_duplicate(self): with MockLoggingHandler().set_module_name(cassandra.cluster.__name__) as mock_handler: - address_column = "native_transport_address" if DSE_VERSION and DSE_VERSION > Version("6.0") else "rpc_address" + address_column = "rpc_address" rows = [ {"peer": "127.0.0.1", "data_center": "dc", "host_id": "dontcare1", "rack": "rack1", "release_version": "3.11.4", address_column: "127.0.0.1", "schema_version": "dontcare", "tokens": "1"}, @@ -102,6 +103,6 @@ def test_duplicate(self): session = cluster.connect(wait_for_all_pools=True) warnings = mock_handler.messages.get("warning") - self.assertEqual(len(warnings), 1) - self.assertTrue('multiple hosts with the same endpoint' in warnings[0]) + assert len(warnings) == 1 + assert 'multiple hosts with the same endpoint' in warnings[0] cluster.shutdown() diff --git a/tests/integration/simulacron/test_connection.py b/tests/integration/simulacron/test_connection.py index 95df69e44c..ceceea814f 100644 --- a/tests/integration/simulacron/test_connection.py +++ b/tests/integration/simulacron/test_connection.py @@ -23,7 +23,7 @@ from cassandra.policies import HostStateListener, RoundRobinPolicy, WhiteListRoundRobinPolicy from tests import connection_class, thread_pool_executor_class -from tests.util import late +from tests.util import late, wait_until_not_raised from tests.integration import requiressimulacron, libevtest from tests.integration.util import assert_quiescent_pool_state # important to import the patch PROTOCOL_VERSION from the simulacron module @@ -36,6 +36,7 @@ start_and_prime_singledc, clear_queries, RejectConnections, RejectType, AcceptConnections, PauseReads, ResumeReads) +import pytest class TrackDownListener(HostStateListener): @@ -141,7 +142,7 @@ def test_heart_beat_timeout(self): for f in futures: f._event.wait() - self.assertIsInstance(f._final_exception, OperationTimedOut) + assert isinstance(f._final_exception, OperationTimedOut) prime_request(PrimeOptions(then=NO_THEN)) @@ -150,10 +151,10 @@ def test_heart_beat_timeout(self): time.sleep((idle_heartbeat_timeout + idle_heartbeat_interval) * 2.5) for host in cluster.metadata.all_hosts(): - self.assertIn(host, listener.hosts_marked_down) + assert host in listener.hosts_marked_down # In this case HostConnection._replace shouldn't be called - self.assertNotIn("_replace", executor.called_functions) + assert "_replace" not in executor.called_functions def test_callbacks_and_pool_when_oto(self): """ @@ -181,9 +182,10 @@ def test_callbacks_and_pool_when_oto(self): future = session.execute_async(query_to_prime, timeout=1) callback, errback = Mock(name='callback'), Mock(name='errback') future.add_callbacks(callback, errback) - self.assertRaises(OperationTimedOut, future.result) + with pytest.raises(OperationTimedOut): + future.result() - assert_quiescent_pool_state(self, cluster) + assert_quiescent_pool_state(cluster) time.sleep(server_delay + 1) # PYTHON-630 -- only the errback should be called @@ -261,7 +263,8 @@ def connection_factory(self, *args, **kwargs): prime_request(PrimeOptions(then={"result": "no_result", "delay_in_ms": never})) prime_request(RejectConnections("unbind")) - self.assertRaisesRegex(OperationTimedOut, "Connection defunct by heartbeat", future.result) + with pytest.raises(OperationTimedOut, match="Connection defunct by heartbeat"): + future.result() def test_close_when_query(self): """ @@ -289,7 +292,8 @@ def test_close_when_query(self): } prime_query(query_to_prime, rows=None, column_types=None, then=then) - self.assertRaises(NoHostAvailable, session.execute, query_to_prime) + with pytest.raises(NoHostAvailable): + session.execute(query_to_prime) def test_retry_after_defunct(self): """ @@ -345,20 +349,22 @@ def test_retry_after_defunct(self): response_future = session.execute_async(query_to_prime, timeout=4 * idle_heartbeat_interval + idle_heartbeat_timeout) response_future.result() - self.assertGreater(len(response_future.attempted_hosts), 1) + assert len(response_future.attempted_hosts) > 1 # No error should be raised here since the hosts have been marked # as down and there's still 1 DC available for _ in range(10): session.execute(query_to_prime) - # Might take some time to close the previous connections and reconnect - time.sleep(10) - assert_quiescent_pool_state(self, cluster) + # Wait for previous connections to close and pool to stabilize + wait_until_not_raised( + lambda: assert_quiescent_pool_state(cluster), + delay=1, max_attempts=30) clear_queries() - time.sleep(10) - assert_quiescent_pool_state(self, cluster) + wait_until_not_raised( + lambda: assert_quiescent_pool_state(cluster), + delay=1, max_attempts=30) def test_idle_connection_is_not_closed(self): """ @@ -386,7 +392,7 @@ def test_idle_connection_is_not_closed(self): time.sleep(20) - self.assertEqual(listener.hosts_marked_down, []) + assert listener.hosts_marked_down == [] def test_host_is_not_set_to_down_after_query_oto(self): """ @@ -418,10 +424,10 @@ def test_host_is_not_set_to_down_after_query_oto(self): for f in futures: f._event.wait() - self.assertIsInstance(f._final_exception, OperationTimedOut) + assert isinstance(f._final_exception, OperationTimedOut) - self.assertEqual(listener.hosts_marked_down, []) - assert_quiescent_pool_state(self, cluster) + assert listener.hosts_marked_down == [] + assert_quiescent_pool_state(cluster) def test_can_shutdown_connection_subclass(self): start_and_prime_singledc() @@ -461,16 +467,17 @@ def test_driver_recovers_nework_isolation(self): time.sleep((idle_heartbeat_timeout + idle_heartbeat_interval) * 2) for host in cluster.metadata.all_hosts(): - self.assertIn(host, listener.hosts_marked_down) + assert host in listener.hosts_marked_down - self.assertRaises(NoHostAvailable, session.execute, "SELECT * from system.local WHERE key='local'") + with pytest.raises(NoHostAvailable): + session.execute("SELECT * from system.local WHERE key='local'") clear_queries() prime_request(AcceptConnections()) time.sleep(idle_heartbeat_timeout + idle_heartbeat_interval + 2) - self.assertIsNotNone(session.execute("SELECT * from system.local WHERE key='local'")) + assert session.execute("SELECT * from system.local WHERE key='local'") is not None def test_max_in_flight(self): """ Verify we don't exceed max_in_flight when borrowing connections or sending heartbeats """ diff --git a/tests/integration/simulacron/test_empty_column.py b/tests/integration/simulacron/test_empty_column.py index 046aaacf79..daa9f20fa8 100644 --- a/tests/integration/simulacron/test_empty_column.py +++ b/tests/integration/simulacron/test_empty_column.py @@ -29,10 +29,6 @@ from tests.integration.simulacron.utils import PrimeQuery, prime_request -PROTOCOL_VERSION = 4 if PROTOCOL_VERSION in \ - (ProtocolVersion.DSE_V1, ProtocolVersion.DSE_V2) else PROTOCOL_VERSION - - @requiressimulacron class EmptyColumnTests(SimulacronCluster): """ @@ -81,28 +77,16 @@ def test_empty_columns_with_all_row_factories(self): # Test all row factories self.cluster.profile_manager.profiles[EXEC_PROFILE_DEFAULT].row_factory = named_tuple_factory - self.assertEqual( - list(self.session.execute(query)), - [namedtuple('Row', ['field_0_', 'field_1_'])('testval', 'testval1')] - ) + assert list(self.session.execute(query)) == [namedtuple('Row', ['field_0_', 'field_1_'])('testval', 'testval1')] self.cluster.profile_manager.profiles[EXEC_PROFILE_DEFAULT].row_factory = tuple_factory - self.assertEqual( - list(self.session.execute(query)), - [('testval', 'testval1')] - ) + assert list(self.session.execute(query)) == [('testval', 'testval1')] self.cluster.profile_manager.profiles[EXEC_PROFILE_DEFAULT].row_factory = dict_factory - self.assertEqual( - list(self.session.execute(query)), - [{'': 'testval', ' ': 'testval1'}] - ) + assert list(self.session.execute(query)) == [{'': 'testval', ' ': 'testval1'}] self.cluster.profile_manager.profiles[EXEC_PROFILE_DEFAULT].row_factory = ordered_dict_factory - self.assertEqual( - list(self.session.execute(query)), - [OrderedDict((('', 'testval'), (' ', 'testval1')))] - ) + assert list(self.session.execute(query)) == [OrderedDict((('', 'testval'), (' ', 'testval1')))] def test_empty_columns_in_system_schema(self): queries = [ @@ -156,9 +140,9 @@ def test_empty_columns_in_system_schema(self): 'delay_in_ms': 0, 'rows': [ { - "strategy_class": "SimpleStrategy", # C* 2.2 + "strategy_class": "NetworkTopologyStrategy", # C* 2.2 "strategy_options": '{}', # C* 2.2 - "replication": {'strategy': 'SimpleStrategy', 'replication_factor': 1}, + "replication": {'strategy': 'NetworkTopologyStrategy', 'replication_factor': 1}, "durable_writes": True, "keyspace_name": "testks" } @@ -232,9 +216,9 @@ def test_empty_columns_in_system_schema(self): self.session = self.cluster.connect(wait_for_all_pools=True) table_metadata = self.cluster.metadata.keyspaces['testks'].tables['testtable'] - self.assertEqual(len(table_metadata.columns), 2) - self.assertIn('', table_metadata.columns) - self.assertIn(' ', table_metadata.columns) + assert len(table_metadata.columns) == 2 + assert '' in table_metadata.columns + assert ' ' in table_metadata.columns def test_empty_columns_with_cqlengine(self): self._prime_testtable_query() @@ -249,7 +233,4 @@ class TestModel(Model): empty = columns.Text(db_field='', primary_key=True) space = columns.Text(db_field=' ') - self.assertEqual( - [TestModel(empty='testval', space='testval1')], - list(TestModel.objects.only(['empty', 'space']).all()) - ) + assert [TestModel(empty='testval', space='testval1')] == list(TestModel.objects.only(['empty', 'space']).all()) diff --git a/tests/integration/simulacron/test_endpoint.py b/tests/integration/simulacron/test_endpoint.py index 9e2d91b6d3..5af38a9f6b 100644 --- a/tests/integration/simulacron/test_endpoint.py +++ b/tests/integration/simulacron/test_endpoint.py @@ -76,17 +76,17 @@ class EndPointTests(SimulacronCluster): def test_default_endpoint(self): hosts = self.cluster.metadata.all_hosts() - self.assertEqual(len(hosts), 3) + assert len(hosts) == 3 for host in hosts: - self.assertIsNotNone(host.endpoint) - self.assertIsInstance(host.endpoint, DefaultEndPoint) - self.assertEqual(host.address, host.endpoint.address) - self.assertEqual(host.broadcast_rpc_address, host.endpoint.address) + assert host.endpoint is not None + assert isinstance(host.endpoint, DefaultEndPoint) + assert host.address == host.endpoint.address + assert host.broadcast_rpc_address == host.endpoint.address - self.assertIsInstance(self.cluster.control_connection._connection.endpoint, DefaultEndPoint) - self.assertIsNotNone(self.cluster.control_connection._connection.endpoint) + assert isinstance(self.cluster.control_connection._connection.endpoint, DefaultEndPoint) + assert self.cluster.control_connection._connection.endpoint is not None endpoints = [host.endpoint for host in hosts] - self.assertIn(self.cluster.control_connection._connection.endpoint, endpoints) + assert self.cluster.control_connection._connection.endpoint in endpoints def test_custom_endpoint(self): cluster = Cluster( @@ -98,17 +98,17 @@ def test_custom_endpoint(self): cluster.connect(wait_for_all_pools=True) hosts = cluster.metadata.all_hosts() - self.assertEqual(len(hosts), 3) + assert len(hosts) == 3 for host in hosts: - self.assertIsNotNone(host.endpoint) - self.assertIsInstance(host.endpoint, AddressEndPoint) - self.assertEqual(str(host.endpoint), host.endpoint.address) - self.assertEqual(host.address, host.endpoint.address) - self.assertEqual(host.broadcast_rpc_address, host.endpoint.address) - - self.assertIsInstance(cluster.control_connection._connection.endpoint, AddressEndPoint) - self.assertIsNotNone(cluster.control_connection._connection.endpoint) + assert host.endpoint is not None + assert isinstance(host.endpoint, AddressEndPoint) + assert str(host.endpoint) == host.endpoint.address + assert host.address == host.endpoint.address + assert host.broadcast_rpc_address == host.endpoint.address + + assert isinstance(cluster.control_connection._connection.endpoint, AddressEndPoint) + assert cluster.control_connection._connection.endpoint is not None endpoints = [host.endpoint for host in hosts] - self.assertIn(cluster.control_connection._connection.endpoint, endpoints) + assert cluster.control_connection._connection.endpoint in endpoints cluster.shutdown() diff --git a/tests/integration/simulacron/test_policies.py b/tests/integration/simulacron/test_policies.py index 6d0d081889..3f94a41222 100644 --- a/tests/integration/simulacron/test_policies.py +++ b/tests/integration/simulacron/test_policies.py @@ -27,6 +27,7 @@ from itertools import count from packaging.version import Version +import pytest class BadRoundRobinPolicy(RoundRobinPolicy): @@ -101,30 +102,30 @@ def test_speculative_execution(self): # This LBP should repeat hosts up to around 30 result = self.session.execute(statement, execution_profile='spec_ep_brr') - self.assertEqual(7, len(result.response_future.attempted_hosts)) + assert 7 == len(result.response_future.attempted_hosts) # This LBP should keep host list to 3 result = self.session.execute(statement, execution_profile='spec_ep_rr') - self.assertEqual(3, len(result.response_future.attempted_hosts)) + assert 3 == len(result.response_future.attempted_hosts) # Spec_execution policy should limit retries to 1 result = self.session.execute(statement, execution_profile='spec_ep_rr_lim') - self.assertEqual(2, len(result.response_future.attempted_hosts)) + assert 2 == len(result.response_future.attempted_hosts) # Spec_execution policy should not be used if the query is not idempotent result = self.session.execute(statement_non_idem, execution_profile='spec_ep_brr') - self.assertEqual(1, len(result.response_future.attempted_hosts)) + assert 1 == len(result.response_future.attempted_hosts) # Default policy with non_idem query result = self.session.execute(statement_non_idem, timeout=12) - self.assertEqual(1, len(result.response_future.attempted_hosts)) + assert 1 == len(result.response_future.attempted_hosts) # Should be able to run an idempotent query against default execution policy with no speculative_execution_policy result = self.session.execute(statement, timeout=12) - self.assertEqual(1, len(result.response_future.attempted_hosts)) + assert 1 == len(result.response_future.attempted_hosts) # Test timeout with spec_ex - with self.assertRaises(OperationTimedOut): + with pytest.raises(OperationTimedOut): self.session.execute(statement, execution_profile='spec_ep_rr', timeout=.5) prepared_query_to_prime = "SELECT * FROM test3rf.test where k = ?" @@ -135,11 +136,11 @@ def test_speculative_execution(self): prepared_statement = self.session.prepare(prepared_query_to_prime) # non-idempotent result = self.session.execute(prepared_statement, ("0",), execution_profile='spec_ep_brr') - self.assertEqual(1, len(result.response_future.attempted_hosts)) + assert 1 == len(result.response_future.attempted_hosts) # idempotent prepared_statement.is_idempotent = True result = self.session.execute(prepared_statement, ("0",), execution_profile='spec_ep_brr') - self.assertLess(1, len(result.response_future.attempted_hosts)) + assert 1 < len(result.response_future.attempted_hosts) def test_speculative_and_timeout(self): """ @@ -162,10 +163,10 @@ def test_speculative_and_timeout(self): response_future = self.session.execute_async(statement, execution_profile='spec_ep_brr_lim', timeout=14) response_future._event.wait(16) - self.assertIsInstance(response_future._final_exception, OperationTimedOut) + assert isinstance(response_future._final_exception, OperationTimedOut) # This is because 14 / 4 + 1 = 4 - self.assertEqual(len(response_future.attempted_hosts), 4) + assert len(response_future.attempted_hosts) == 4 def test_delay_can_be_0(self): """ @@ -199,11 +200,11 @@ def patched(*args, **kwargs): stmt = SimpleStatement(query_to_prime) stmt.is_idempotent = True results = session.execute(stmt, execution_profile="spec") - self.assertEqual(len(results.response_future.attempted_hosts), 3) + assert len(results.response_future.attempted_hosts) == 3 # send_request is called number_of_requests times for the speculative request # plus one for the call from the main thread. - self.assertEqual(next(counter), number_of_requests + 1) + assert next(counter) == number_of_requests + 1 class CustomRetryPolicy(RetryPolicy): @@ -306,7 +307,7 @@ def test_retry_policy_ignores_and_rethrows(self): then["write_type"] = "CDC" prime_query(query_to_prime_cdc, rows=None, column_types=None, then=then) - with self.assertRaises(WriteTimeout): + with pytest.raises(WriteTimeout): self.session.execute(query_to_prime_simple) #CDC should be ignored @@ -337,7 +338,7 @@ def test_retry_policy_with_prepared(self): } prime_query(query_to_prime, then=then, rows=None, column_types=None) self.session.execute(query_to_prime) - self.assertEqual(next(counter_policy.write_timeout), 1) + assert next(counter_policy.write_timeout) == 1 counter_policy.reset_counters() query_to_prime_prepared = "SELECT * from simulacron_keyspace.simulacron_table WHERE key = :key" @@ -349,11 +350,11 @@ def test_retry_policy_with_prepared(self): bound_stm = prepared_stmt.bind({"key": "0"}) self.session.execute(bound_stm) - self.assertEqual(next(counter_policy.write_timeout), 1) + assert next(counter_policy.write_timeout) == 1 counter_policy.reset_counters() self.session.execute(prepared_stmt, ("0",)) - self.assertEqual(next(counter_policy.write_timeout), 1) + assert next(counter_policy.write_timeout) == 1 def test_setting_retry_policy_to_statement(self): """ @@ -385,13 +386,13 @@ def test_setting_retry_policy_to_statement(self): prepared_stmt = self.session.prepare(query_to_prime_prepared) prepared_stmt.retry_policy = counter_policy self.session.execute(prepared_stmt, ("0",)) - self.assertEqual(next(counter_policy.write_timeout), 1) + assert next(counter_policy.write_timeout) == 1 counter_policy.reset_counters() bound_stmt = prepared_stmt.bind({"key": "0"}) bound_stmt.retry_policy = counter_policy self.session.execute(bound_stmt) - self.assertEqual(next(counter_policy.write_timeout), 1) + assert next(counter_policy.write_timeout) == 1 def test_retry_policy_on_request_error(self): """ @@ -438,12 +439,12 @@ def test_retry_policy_on_request_error(self): prime_query(query_to_prime, then=prime_error, rows=None, column_types=None) rf = self.session.execute_async(query_to_prime) - with self.assertRaises(exc): + with pytest.raises(exc): rf.result() - self.assertEqual(len(rf.attempted_hosts), 1) # no retry + assert len(rf.attempted_hosts) == 1 # no retry - self.assertEqual(next(retry_policy.request_error), 4) + assert next(retry_policy.request_error) == 4 # Test that by default, retry on next host retry_policy = RetryPolicy() @@ -455,7 +456,7 @@ def test_retry_policy_on_request_error(self): prime_query(query_to_prime, then=e, rows=None, column_types=None) rf = self.session.execute_async(query_to_prime) - with self.assertRaises(NoHostAvailable): + with pytest.raises(NoHostAvailable): rf.result() - self.assertEqual(len(rf.attempted_hosts), 3) # all 3 nodes failed + assert len(rf.attempted_hosts) == 3 # all 3 nodes failed diff --git a/tests/integration/simulacron/utils.py b/tests/integration/simulacron/utils.py index 37e259dfd7..2322319234 100644 --- a/tests/integration/simulacron/utils.py +++ b/tests/integration/simulacron/utils.py @@ -20,7 +20,7 @@ from cassandra.metadata import SchemaParserV4, SchemaParserDSE68 from tests.util import wait_until_not_raised -from tests.integration import CASSANDRA_VERSION, SIMULACRON_JAR, DSE_VERSION +from tests.integration import CASSANDRA_VERSION, SIMULACRON_JAR DEFAULT_CLUSTER = "python_simulacron_cluster" @@ -89,8 +89,13 @@ def start_simulacron(): SERVER_SIMULACRON.start() - # TODO improve this sleep, maybe check the logs like ccm - time.sleep(5) + # Poll the admin endpoint until simulacron is ready + def _check_simulacron_ready(): + opener = build_opener(HTTPHandler) + request = Request("http://127.0.0.1:8187/cluster") + opener.open(request, timeout=2) + + wait_until_not_raised(_check_simulacron_ready, delay=0.5, max_attempts=30) def stop_simulacron(): @@ -122,12 +127,6 @@ def prime_server_versions(self): system_local_row = {} system_local_row["cql_version"] = CASSANDRA_VERSION.base_version system_local_row["release_version"] = CASSANDRA_VERSION.base_version + "-SNAPSHOT" - if DSE_VERSION: - system_local_row["dse_version"] = DSE_VERSION.base_version - column_types = {"cql_version": "ascii", "release_version": "ascii"} - system_local = PrimeQuery("SELECT cql_version, release_version FROM system.local WHERE key='local'", - rows=[system_local_row], - column_types=column_types) self.submit_request(system_local) diff --git a/tests/integration/standard/column_encryption/test_policies.py b/tests/integration/standard/column_encryption/test_policies.py index 36376d689b..4b12fa135a 100644 --- a/tests/integration/standard/column_encryption/test_policies.py +++ b/tests/integration/standard/column_encryption/test_policies.py @@ -30,7 +30,7 @@ class ColumnEncryptionPolicyTest(unittest.TestCase): def _recreate_keyspace(self, session): session.execute("drop keyspace if exists foo") - session.execute("CREATE KEYSPACE foo WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}") + session.execute("CREATE KEYSPACE foo WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '1'}") session.execute("CREATE TABLE foo.bar(encrypted blob, unencrypted int, primary key(unencrypted))") def _create_policy(self, key, iv = None): @@ -59,14 +59,14 @@ def test_end_to_end_prepared(self): # A straight select from the database will now return the decrypted bits. We select both encrypted and unencrypted # values here to confirm that we don't interfere with regular processing of unencrypted vals. (encrypted,unencrypted) = session.execute("select encrypted, unencrypted from foo.bar where unencrypted = %s allow filtering", (expected,)).one() - self.assertEqual(expected, encrypted) - self.assertEqual(expected, unencrypted) + assert expected == encrypted + assert expected == unencrypted # Confirm the same behaviour from a subsequent prepared statement as well prepared = session.prepare("select encrypted, unencrypted from foo.bar where unencrypted = ? allow filtering") (encrypted,unencrypted) = session.execute(prepared, [expected]).one() - self.assertEqual(expected, encrypted) - self.assertEqual(expected, unencrypted) + assert expected == encrypted + assert expected == unencrypted def test_end_to_end_simple(self): @@ -80,21 +80,21 @@ def test_end_to_end_simple(self): # Use encode_and_encrypt helper function to populate date for i in range(1,100): - self.assertIsNotNone(i) + assert i is not None encrypted = cl_policy.encode_and_encrypt(col_desc, i) session.execute("insert into foo.bar (encrypted, unencrypted) values (%s,%s)", (encrypted, i)) # A straight select from the database will now return the decrypted bits. We select both encrypted and unencrypted # values here to confirm that we don't interfere with regular processing of unencrypted vals. (encrypted,unencrypted) = session.execute("select encrypted, unencrypted from foo.bar where unencrypted = %s allow filtering", (expected,)).one() - self.assertEqual(expected, encrypted) - self.assertEqual(expected, unencrypted) + assert expected == encrypted + assert expected == unencrypted # Confirm the same behaviour from a subsequent prepared statement as well prepared = session.prepare("select encrypted, unencrypted from foo.bar where unencrypted = ? allow filtering") (encrypted,unencrypted) = session.execute(prepared, [expected]).one() - self.assertEqual(expected, encrypted) - self.assertEqual(expected, unencrypted) + assert expected == encrypted + assert expected == unencrypted def test_end_to_end_different_cle_contexts_different_ivs(self): """ @@ -119,7 +119,7 @@ def test_end_to_end_different_cle_contexts_different_ivs(self): # Use encode_and_encrypt helper function to populate date for i in range(1,100): - self.assertIsNotNone(i) + assert i is not None encrypted = cl_policy1.encode_and_encrypt(col_desc1, i) session1.execute("insert into foo.bar (encrypted, unencrypted) values (%s,%s)", (encrypted, i)) session1.shutdown() @@ -129,15 +129,15 @@ def test_end_to_end_different_cle_contexts_different_ivs(self): # that would entail not re-using any cached ciphers AES256ColumnEncryptionPolicy._build_cipher.cache_clear() cache_info = cl_policy1.cache_info() - self.assertEqual(cache_info.currsize, 0) + assert cache_info.currsize == 0 iv2 = os.urandom(AES256_BLOCK_SIZE_BYTES) (_, cl_policy2) = self._create_policy(key, iv=iv2) cluster2 = TestCluster(column_encryption_policy=cl_policy2) session2 = cluster2.connect() (encrypted,unencrypted) = session2.execute("select encrypted, unencrypted from foo.bar where unencrypted = %s allow filtering", (expected,)).one() - self.assertEqual(expected, encrypted) - self.assertEqual(expected, unencrypted) + assert expected == encrypted + assert expected == unencrypted def test_end_to_end_different_cle_contexts_different_policies(self): """ @@ -162,10 +162,10 @@ def test_end_to_end_different_cle_contexts_different_policies(self): # A straight select from the database will now return the decrypted bits. We select both encrypted and unencrypted # values here to confirm that we don't interfere with regular processing of unencrypted vals. (encrypted,unencrypted) = session2.execute("select encrypted, unencrypted from foo.bar where unencrypted = %s allow filtering", (expected,)).one() - self.assertEqual(cl_policy.encode_and_encrypt(col_desc, expected), encrypted) - self.assertEqual(expected, unencrypted) + assert cl_policy.encode_and_encrypt(col_desc, expected) == encrypted + assert expected == unencrypted # Confirm the same behaviour from a subsequent prepared statement as well prepared = session2.prepare("select encrypted, unencrypted from foo.bar where unencrypted = ? allow filtering") (encrypted,unencrypted) = session2.execute(prepared, [expected]).one() - self.assertEqual(cl_policy.encode_and_encrypt(col_desc, expected), encrypted) + assert cl_policy.encode_and_encrypt(col_desc, expected) == encrypted diff --git a/tests/integration/standard/conftest.py b/tests/integration/standard/conftest.py index 6028c2a06d..9934cfcbbb 100644 --- a/tests/integration/standard/conftest.py +++ b/tests/integration/standard/conftest.py @@ -1,6 +1,70 @@ import pytest import logging +# Cluster topology groups for test ordering. +# Tests are sorted so that modules sharing the same CCM cluster run +# together, minimising expensive cluster teardown/restart cycles. +# Lower number = runs first. Modules not listed get a high default. +_MODULE_CLUSTER_ORDER = { + # Group 0: default 3-node singledc (CLUSTER_NAME = 'test_cluster') + "test_metadata": 0, + "test_policies": 0, + "test_control_connection": 0, + "test_routing": 0, + "test_prepared_statements": 0, + "test_metrics": 0, + "test_connection": 0, + "test_concurrent": 0, + "test_custom_payload": 0, + "test_query_paging": 0, + "test_single_interface": 0, + "test_rate_limit_exceeded": 0, + # Group 1: 'cluster_tests' (--smp 2, 3 nodes) + "test_cluster": 1, + "test_shard_aware": 1, + # Group 2: 'shared_aware' (--smp 2 --memory 2048M, 3 nodes) + "test_use_keyspace": 2, + "test_client_routes": 2, + # Group 3: single-node cluster + "test_types": 3, + "test_cython_protocol_handlers": 3, + "test_custom_protocol_handler": 3, + "test_row_factories": 3, + "test_udts": 3, + "test_client_warnings": 3, + "test_application_info": 3, + # Group 4: destructive / special clusters (run last) + "test_ip_change": 4, + "test_authentication": 4, + "test_authentication_misconfiguration": 4, + "test_control_connection_query_fallback": 4, + "test_custom_cluster": 4, + "test_query": 4, + # Group 5: tablets (destructive — decommissions a node) + "test_tablets": 5, + # Group 6: schema change + node kill (destructive — kills node2) + "test_concurrent_schema_change_and_node_kill": 6, + # Group 7: multi-dc (7 nodes — most expensive to create) + "test_rack_aware_policy": 7, +} + + +def pytest_collection_modifyitems(items): + """Sort tests so modules with the same cluster topology are adjacent. + + Uses the original collection index as tie-breaker so that the + definition order inside each file is preserved (important for tests + that depend on running order, e.g. destructive tablet tests). + """ + orig_order = {id(item): idx for idx, item in enumerate(items)} + + def _sort_key(item): + module_name = item.module.__name__.rsplit(".", 1)[-1] + return (_MODULE_CLUSTER_ORDER.get(module_name, 99), item.fspath, orig_order[id(item)]) + + items[:] = sorted(items, key=_sort_key) + + # from https://github.com/streamlit/streamlit/pull/5047/files def pytest_sessionfinish(): # We're not waiting for scriptrunner threads to cleanly close before ending the PyTest, @@ -10,4 +74,4 @@ def pytest_sessionfinish(): # * https://github.com/pytest-dev/pytest/issues/5282 # To prevent the exception from being raised on pytest_sessionfinish # we disable exception raising in logging module - logging.raiseExceptions = False \ No newline at end of file + logging.raiseExceptions = False diff --git a/tests/integration/standard/test_application_info.py b/tests/integration/standard/test_application_info.py index e1e0cb2ee4..5d4b679fc8 100644 --- a/tests/integration/standard/test_application_info.py +++ b/tests/integration/standard/test_application_info.py @@ -15,7 +15,7 @@ import unittest from cassandra.application_info import ApplicationInfo -from tests.integration import TestCluster, use_single_node, remove_cluster, xfail_scylla +from tests.integration import TestCluster, use_single_node, remove_cluster, xfail_scylla_version_lt def setup_module(): @@ -26,7 +26,8 @@ def teardown_module(): remove_cluster() -@xfail_scylla("#scylladb/scylla-enterprise#5467 - not released yet") +@xfail_scylla_version_lt(reason='scylladb/scylla-enterprise#5467 - system.client_options is not yet supported', + scylla_version="2026.1.0") class ApplicationInfoTest(unittest.TestCase): attribute_to_startup_key = { 'application_name': 'APPLICATION_NAME', @@ -74,7 +75,14 @@ def test_create_session_and_check_system_views_clients(self): )) found = False - for row in cluster.connect().execute("select client_options from system_views.clients"): + session = cluster.connect() + + try: + rows = list(session.execute("SELECT client_options FROM system.clients")) + except Exception: + rows = list(session.execute("SELECT client_options FROM system_views.clients")) + + for row in rows: if not row[0]: continue for attribute_key, startup_key in self.attribute_to_startup_key.items(): diff --git a/tests/integration/standard/test_authentication.py b/tests/integration/standard/test_authentication.py index 122df55a02..f172707fff 100644 --- a/tests/integration/standard/test_authentication.py +++ b/tests/integration/standard/test_authentication.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os + from packaging.version import Version import logging import time @@ -24,6 +26,7 @@ from tests.integration.util import assert_quiescent_pool_state import unittest +import pytest log = logging.getLogger(__name__) @@ -31,27 +34,51 @@ #This can be tested for remote hosts, but the cluster has to be configured accordingly #@local +_saved_scylla_ext_opts = None + def setup_module(): + global _saved_scylla_ext_opts + _saved_scylla_ext_opts = os.environ.get('SCYLLA_EXT_OPTS') if CASSANDRA_IP.startswith("127.0.0.") and not USE_CASS_EXTERNAL: use_singledc(start=False) ccm_cluster = get_cluster() ccm_cluster.stop() - config_options = {'authenticator': 'PasswordAuthenticator', - 'authorizer': 'CassandraAuthorizer'} + config_options = { + 'authenticator': 'PasswordAuthenticator', + 'authorizer': 'CassandraAuthorizer', + 'auth_superuser_name': 'cassandra', + 'auth_superuser_salted_password': '$6$x7IFjiX5VCpvNiFk$2IfjTvSyGL7zerpV.wbY7mJjaRCrJ/68dtT3UpT.sSmNYz1bPjtn3mH.kJKFvaZ2T4SbVeBijjmwGjcb83LlV/' + } ccm_cluster.set_configuration_options(config_options) log.debug("Starting ccm test cluster with %s", config_options) start_cluster_wait_for_up(ccm_cluster) # PYTHON-1328 # - # Give the cluster enough time to startup (and perform necessary initialization) - # before executing the test. + # Wait for PasswordAuthenticator to finish initializing (creating the + # default superuser). Poll by attempting to authenticate rather than + # using a fixed sleep. if CASSANDRA_VERSION > Version('4.0-a'): - time.sleep(10) + from tests.util import wait_until_not_raised + + def _check_auth_ready(): + cluster = TestCluster(protocol_version=PROTOCOL_VERSION, + auth_provider=PlainTextAuthProvider('cassandra', 'cassandra')) + try: + session = cluster.connect() + session.execute("SELECT * FROM system.local WHERE key='local'") + finally: + cluster.shutdown() + + wait_until_not_raised(_check_auth_ready, delay=1, max_attempts=30) def teardown_module(): remove_cluster() # this test messes with config + if _saved_scylla_ext_opts is None: + os.environ.pop('SCYLLA_EXT_OPTS', None) + else: + os.environ['SCYLLA_EXT_OPTS'] = _saved_scylla_ext_opts class AuthenticationTests(unittest.TestCase): @@ -105,56 +132,52 @@ def test_auth_connect(self): cluster = self.cluster_as(user, passwd) session = cluster.connect(wait_for_all_pools=True) try: - self.assertTrue(session.execute("SELECT release_version FROM system.local WHERE key='local'")) - assert_quiescent_pool_state(self, cluster, wait=1) + assert session.execute("SELECT release_version FROM system.local WHERE key='local'") + assert_quiescent_pool_state(cluster, wait=1) for pool in session.get_pools(): connection, _ = pool.borrow_connection(timeout=0) - self.assertEqual(connection.authenticator.server_authenticator_class, 'org.apache.cassandra.auth.PasswordAuthenticator') + assert connection.authenticator.server_authenticator_class == 'org.apache.cassandra.auth.PasswordAuthenticator' pool.return_connection(connection) finally: cluster.shutdown() finally: root_session.execute('DROP USER %s', user) - assert_quiescent_pool_state(self, root_session.cluster, wait=1) + assert_quiescent_pool_state(root_session.cluster, wait=1) root_session.cluster.shutdown() def test_connect_wrong_pwd(self): cluster = self.cluster_as('cassandra', 'wrong_pass') try: - self.assertRaisesRegex(NoHostAvailable, - '.*AuthenticationFailed.', - cluster.connect) - assert_quiescent_pool_state(self, cluster) + with pytest.raises(NoHostAvailable, match='.*AuthenticationFailed.'): + cluster.connect() + assert_quiescent_pool_state(cluster) finally: cluster.shutdown() def test_connect_wrong_username(self): cluster = self.cluster_as('wrong_user', 'cassandra') try: - self.assertRaisesRegex(NoHostAvailable, - '.*AuthenticationFailed.*', - cluster.connect) - assert_quiescent_pool_state(self, cluster) + with pytest.raises(NoHostAvailable, match='.*AuthenticationFailed.*'): + cluster.connect() + assert_quiescent_pool_state(cluster) finally: cluster.shutdown() def test_connect_empty_pwd(self): cluster = self.cluster_as('Cassandra', '') try: - self.assertRaisesRegex(NoHostAvailable, - '.*AuthenticationFailed.*', - cluster.connect) - assert_quiescent_pool_state(self, cluster) + with pytest.raises(NoHostAvailable, match='.*AuthenticationFailed.*'): + cluster.connect() + assert_quiescent_pool_state(cluster) finally: cluster.shutdown() def test_connect_no_auth_provider(self): cluster = TestCluster() try: - self.assertRaisesRegex(NoHostAvailable, - '.*AuthenticationFailed.*', - cluster.connect) - assert_quiescent_pool_state(self, cluster) + with pytest.raises(NoHostAvailable, match='.*AuthenticationFailed.*'): + cluster.connect() + assert_quiescent_pool_state(cluster) finally: cluster.shutdown() @@ -184,8 +207,9 @@ def test_host_passthrough(self): provider = SaslAuthProvider(**sasl_kwargs) host = 'thehostname' authenticator = provider.new_authenticator(host) - self.assertEqual(authenticator.sasl.host, host) + assert authenticator.sasl.host == host def test_host_rejected(self): sasl_kwargs = {'host': 'something'} - self.assertRaises(ValueError, SaslAuthProvider, **sasl_kwargs) + with pytest.raises(ValueError): + SaslAuthProvider(**sasl_kwargs) diff --git a/tests/integration/standard/test_authentication_misconfiguration.py b/tests/integration/standard/test_authentication_misconfiguration.py index 2b02664c3f..9ad4ad997d 100644 --- a/tests/integration/standard/test_authentication_misconfiguration.py +++ b/tests/integration/standard/test_authentication_misconfiguration.py @@ -40,7 +40,7 @@ def test_connect_no_auth_provider(self): cluster.connect() cluster.refresh_nodes() down_hosts = [host for host in cluster.metadata.all_hosts() if not host.is_up] - self.assertEqual(len(down_hosts), 1) + assert len(down_hosts) == 1 cluster.shutdown() @classmethod diff --git a/tests/integration/standard/test_client_routes.py b/tests/integration/standard/test_client_routes.py new file mode 100644 index 0000000000..292eabca30 --- /dev/null +++ b/tests/integration/standard/test_client_routes.py @@ -0,0 +1,1336 @@ +# Copyright 2026 ScyllaDB, Inc. +# +# 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 +# +# http://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. + +""" +Comprehensive integration tests for Client Routes (Private Link) support. + +Includes: +- TCP proxy and NLB emulator for simulating private link infrastructure +- Tests verifying all connections go exclusively through the proxy +- Tests for dynamic route updates and topology changes +- Tests for query_routes filtering +""" + +import logging +import os +import select +import shutil +import socket +import ssl +import subprocess +import tempfile +import threading +import time +import unittest +import uuid + +import json as _json +import urllib.request + +from cassandra.cluster import Cluster +from cassandra.client_routes import ClientRoutesConfig, ClientRouteProxy +from cassandra.connection import ClientRoutesEndPoint +from cassandra.policies import RoundRobinPolicy +from tests.integration import ( + TestCluster, + get_cluster, + get_node, + use_cluster, + wait_for_node_socket, + skip_scylla_version_lt, +) +from tests.util import wait_until_not_raised + +log = logging.getLogger(__name__) + +class TcpProxy: + """ + A simple TCP proxy that forwards connections from a local listen port + to a target (host, port). Tracks active connections so tests can + verify that traffic flows through the proxy. + """ + + BUF_SIZE = 65536 + + def __init__(self, listen_host, listen_port, target_host, target_port): + self.listen_host = listen_host + self.listen_port = listen_port + self.target_host = target_host + self.target_port = target_port + + self._server_sock = None + self._running = False + self._thread = None + self._lock = threading.Lock() + self._connections = set() + self.total_connections = 0 + + def start(self): + self._server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._server_sock.bind((self.listen_host, self.listen_port)) + self.listen_port = self._server_sock.getsockname()[1] + self._server_sock.listen(128) + self._server_sock.setblocking(False) + self._running = True + self._thread = threading.Thread(target=self._run, daemon=True, + name="proxy-%s:%d" % (self.listen_host, self.listen_port)) + self._thread.start() + log.info("TcpProxy started %s:%d -> %s:%d", + self.listen_host, self.listen_port, + self.target_host, self.target_port) + + def stop(self): + self._running = False + if self._server_sock: + try: + self._server_sock.close() + except Exception: + pass + with self._lock: + for csock, tsock in list(self._connections): + self._close_pair(csock, tsock) + self._connections.clear() + if self._thread: + self._thread.join(timeout=5) + log.info("TcpProxy stopped %s:%d", self.listen_host, self.listen_port) + + @property + def active_connections(self): + with self._lock: + return len(self._connections) + + def retarget(self, new_host, new_port): + """Change the backend target for new connections (existing ones keep the old target).""" + self.target_host = new_host + self.target_port = new_port + log.info("TcpProxy %s:%d retargeted to %s:%d", + self.listen_host, self.listen_port, new_host, new_port) + + def drop_connections(self): + """Forcibly close all active connections.""" + with self._lock: + for csock, tsock in list(self._connections): + self._close_pair(csock, tsock) + self._connections.clear() + log.info("TcpProxy %s:%d dropped all connections", self.listen_host, self.listen_port) + + def _run(self): + while self._running: + try: + readable, _, _ = select.select([self._server_sock], [], [], 0.2) + except (ValueError, OSError): + break + for sock in readable: + if sock is self._server_sock: + try: + client_sock, _ = self._server_sock.accept() + except OSError: + continue + self._handle_new_connection(client_sock) + + def _handle_new_connection(self, client_sock, target_host=None, target_port=None): + target_host = target_host or self.target_host + target_port = target_port or self.target_port + try: + target_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + target_sock.connect((target_host, target_port)) + except Exception as e: + log.warning("TcpProxy %s:%d failed to connect to target %s:%d: %s", + self.listen_host, self.listen_port, + target_host, target_port, e) + client_sock.close() + return + + with self._lock: + self._connections.add((client_sock, target_sock)) + self.total_connections += 1 + + t = threading.Thread(target=self._forward_loop, + args=(client_sock, target_sock), + daemon=True) + t.start() + + def _forward_loop(self, client_sock, target_sock): + try: + while self._running: + readable, _, _ = select.select([client_sock, target_sock], [], [], 0.5) + for sock in readable: + data = sock.recv(self.BUF_SIZE) + if not data: + return + if sock is client_sock: + target_sock.sendall(data) + else: + client_sock.sendall(data) + except (OSError, ConnectionResetError, BrokenPipeError): + pass + finally: + with self._lock: + self._connections.discard((client_sock, target_sock)) + self._close_pair(client_sock, target_sock) + + @staticmethod + def _close_pair(csock, tsock): + for s in (csock, tsock): + try: + s.close() + except Exception: + pass + + +class NLBEmulator: + """ + Emulates a Network Load Balancer for a CCM cluster. + + Provides: + - One *discovery port* (round-robin across all live nodes, used as the + driver's ``contact_points``). + - One *per-node port* for each node (dedicated proxy to that node's + native transport port). + + All proxies listen on ``LISTEN_HOST`` (127.254.254.101), an address + outside the CCM node range, simulating a real NLB endpoint. + + Port layout (all ports are OS-assigned by default): + LISTEN_HOST:discovery_port -> round-robin to all live nodes + LISTEN_HOST: -> node1 (127.0.0.1:9042) + LISTEN_HOST: -> node2 (127.0.0.2:9042) + ... + + Automatically creates/removes per-node proxies when nodes are + added/removed so CCM cluster operations are reflected seamlessly. + """ + + LISTEN_HOST = "127.254.254.101" + + def __init__(self, discovery_port=0, + per_node_base=0, + native_port=9042, + node_addresses=None): + self.discovery_port = discovery_port + self.per_node_base = per_node_base + self.native_port = native_port + self._deferred_node_addresses = node_addresses + + self._node_proxies = {} + self._discovery_proxy = None + self._rr_index = 0 + self._lock = threading.Lock() + self._running = False + + def start(self, node_addresses): + """ + Start the NLB with an initial set of node addresses. + + :param node_addresses: dict of node_id -> ip_address, e.g. + {1: "127.0.0.1", 2: "127.0.0.2"} + """ + self._running = True + try: + for node_id, addr in node_addresses.items(): + self._add_node_proxy(node_id, addr) + + first_addr = list(node_addresses.values())[0] + self._discovery_proxy = TcpProxy( + self.LISTEN_HOST, self.discovery_port, + first_addr, self.native_port, + ) + self._discovery_proxy.start() + self.discovery_port = self._discovery_proxy.listen_port + except Exception: + self.stop() + raise + original_handler = self._discovery_proxy._handle_new_connection + + def rr_handler(client_sock): + addrs = self._live_addresses() + if not addrs: + client_sock.close() + return + idx = self._rr_index % len(addrs) + self._rr_index += 1 + addr = addrs[idx] + original_handler(client_sock, target_host=addr, target_port=self.native_port) + + self._discovery_proxy._handle_new_connection = rr_handler + + log.info("NLB started: discovery=%s:%d, %d node proxies", + self.LISTEN_HOST, self.discovery_port, len(self._node_proxies)) + return self + + def __enter__(self): + if not self._running and self._deferred_node_addresses is not None: + self.start(self._deferred_node_addresses) + return self + + def __exit__(self, *args): + self.stop() + + def stop(self): + self._running = False + if self._discovery_proxy: + self._discovery_proxy.stop() + for proxy in self._node_proxies.values(): + proxy.stop() + self._node_proxies.clear() + log.info("NLB stopped") + + def add_node(self, node_id, addr): + self._add_node_proxy(node_id, addr) + + def remove_node(self, node_id): + with self._lock: + proxy = self._node_proxies.pop(node_id, None) + if proxy: + proxy.stop() + log.info("NLB removed node %d", node_id) + + def node_port(self, node_id): + proxy = self._node_proxies.get(node_id) + if proxy: + return proxy.listen_port + return self.per_node_base + node_id + + def get_node_proxy(self, node_id): + return self._node_proxies.get(node_id) + + def total_proxy_connections(self): + return sum(p.total_connections for p in self._node_proxies.values()) + + def active_proxy_connections(self): + return sum(p.active_connections for p in self._node_proxies.values()) + + def drop_all_connections(self): + for proxy in self._node_proxies.values(): + proxy.drop_connections() + if self._discovery_proxy: + self._discovery_proxy.drop_connections() + + def _add_node_proxy(self, node_id, addr): + port = 0 + proxy = TcpProxy(self.LISTEN_HOST, port, addr, self.native_port) + proxy.start() + with self._lock: + self._node_proxies[node_id] = proxy + log.info("NLB added node %d: %s:%d -> %s:%d", + node_id, self.LISTEN_HOST, port, addr, self.native_port) + + def _live_addresses(self): + """IPs of nodes with active proxies.""" + return [p.target_host for p in self._node_proxies.values()] + +def post_client_routes(contact_point, routes): + """ + Post client routes to Scylla's REST API. + + :param contact_point: IP/hostname of a Scylla node (e.g. "127.0.0.1") + :param routes: List of route dicts with keys: connection_id, host_id, address, port + and optionally tls_port + """ + payload = [] + for route in routes: + entry = { + "connection_id": str(route["connection_id"]), + "host_id": str(route["host_id"]), + "address": route["address"], + "port": route["port"], + } + if route.get("tls_port") is not None: + entry["tls_port"] = route["tls_port"] + payload.append(entry) + + url = "http://%s:10000/v2/client-routes" % contact_point + log.info("Posting %d routes to %s", len(payload), url) + data = _json.dumps(payload).encode("utf-8") + req = urllib.request.Request( + url, + data=data, + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + }, + method="POST", + ) + response = urllib.request.urlopen(req) + log.info("Routes posted successfully (status %d)", response.status) + + +def get_host_ids_from_cluster(session): + """ + Build a mapping of rpc_address -> host_id for all nodes in the cluster. + + Uses the driver's metadata rather than querying system.local / system.peers + directly, because those queries can be routed to different coordinators + (system.local returns the coordinator's own info while system.peers omits + the coordinator), leading to a node being missing from the map. + """ + host_id_map = {} + for host in session.cluster.metadata.all_hosts(): + host_id_map[host.address] = host.host_id + return host_id_map + + +def build_routes_for_nlb(connection_id, host_id_map, nlb): + """ + Build routes that direct each host_id through the NLB per-node proxy. + + :param connection_id: Connection ID string + :param host_id_map: dict ip -> uuid host_id (from get_host_ids_from_cluster) + :param nlb: NLBEmulator instance + :return: list of route dicts + """ + routes = [] + for ip, host_id in host_id_map.items(): + node_id = int(ip.split(".")[-1]) + port = nlb.node_port(node_id) + routes.append({ + "connection_id": connection_id, + "host_id": host_id, + "address": NLBEmulator.LISTEN_HOST, + "port": port, + }) + return routes + + +def post_routes_for_nlb(contact_point, connection_id, host_id_map, nlb): + """Build routes for the NLB and POST them via the REST API.""" + routes = build_routes_for_nlb(connection_id, host_id_map, nlb) + post_client_routes(contact_point, routes) + return routes + +def wait_for_routes_visible(session, connection_id, expected_count, timeout=10, poll_interval=0.1): + """ + Poll system.client_routes on **every** node until each one sees at + least *expected_count* rows for *connection_id*. + + ``system.client_routes`` is a node-local table, so routes posted via + the REST API to one node are not guaranteed to be visible on the + others at the same time. This helper ensures they have propagated + everywhere before the test proceeds. + + :param session: an active driver Session (direct, not through NLB) + :param connection_id: the connection_id string to filter on + :param expected_count: how many rows we expect to see per node + :param timeout: maximum seconds to wait + :param poll_interval: seconds between polls + """ + all_hosts = list(session.cluster.metadata.all_hosts()) + deadline = time.time() + timeout + while True: + pending_hosts = [] + for host in all_hosts: + rows = list(session.execute( + "SELECT * FROM system.client_routes WHERE connection_id = %s", + (connection_id,), + host=host, + )) + if len(rows) < expected_count: + pending_hosts.append((host, len(rows))) + if not pending_hosts: + return + if time.time() >= deadline: + details = ", ".join( + "%s: %d" % (h.address, count) for h, count in pending_hosts + ) + raise RuntimeError( + "Timed out waiting for %d routes (connection_id=%s) to appear " + "in system.client_routes on all nodes; pending: %s" + % (expected_count, connection_id, details) + ) + time.sleep(poll_interval) + + +def node_id_from_ip(ip): + """Extract node_id from an IP like '127.0.0.3' -> 3.""" + return int(ip.split(".")[-1]) + + +def assert_routes_via_nlb(test, cluster, nlb, expected_node_ids): + """ + Assert that every host in *expected_node_ids* has its endpoint + resolving through the NLB (correct address and per-node port). + """ + nlb_listen_host = NLBEmulator.LISTEN_HOST + expected_node_ids = set(expected_node_ids) + + seen_node_ids = set() + for host in cluster.metadata.all_hosts(): + ep = host.endpoint + if not isinstance(ep, ClientRoutesEndPoint): + continue + node_id = node_id_from_ip(ep.address) + if node_id not in expected_node_ids: + continue + resolved_addr, resolved_port = ep.resolve() + test.assertEqual( + resolved_addr, nlb_listen_host, + "Node %d endpoint should resolve to NLB address %s, got %s" + % (node_id, nlb_listen_host, resolved_addr), + ) + test.assertEqual( + resolved_port, nlb.node_port(node_id), + "Node %d endpoint should resolve to NLB port %d, got %d" + % (node_id, nlb.node_port(node_id), resolved_port), + ) + seen_node_ids.add(node_id) + test.assertEqual( + seen_node_ids, expected_node_ids, + "Not all expected nodes found in metadata endpoints", + ) + + +def assert_routes_direct(test, cluster, expected_node_ids, direct_port=9042): + """ + Assert that every host in *expected_node_ids* has its endpoint + resolving to the node's own IP on *direct_port*. + """ + expected_node_ids = set(expected_node_ids) + + for host in cluster.metadata.all_hosts(): + ep = host.endpoint + if not isinstance(ep, ClientRoutesEndPoint): + continue + node_id = node_id_from_ip(ep.address) + if node_id not in expected_node_ids: + continue + resolved_addr, resolved_port = ep.resolve() + expected_ip = "127.0.0.%d" % node_id + test.assertEqual( + resolved_addr, expected_ip, + "Node %d endpoint should resolve to direct address %s, got %s" + % (node_id, expected_ip, resolved_addr), + ) + test.assertEqual( + resolved_port, direct_port, + "Node %d endpoint should resolve to direct port %d, got %d" + % (node_id, direct_port, resolved_port), + ) + + +_saved_scylla_ext_opts = None + + +def setup_module(): + global _saved_scylla_ext_opts + _saved_scylla_ext_opts = os.environ.get('SCYLLA_EXT_OPTS') + os.environ['SCYLLA_EXT_OPTS'] = "--smp 2 --memory 2048M" + use_cluster('shared_aware', [3], start=True) + + +def teardown_module(): + if _saved_scylla_ext_opts is None: + os.environ.pop('SCYLLA_EXT_OPTS', None) + else: + os.environ['SCYLLA_EXT_OPTS'] = _saved_scylla_ext_opts + +@skip_scylla_version_lt(reason='scylladb/scylladb#26992 - system.client_routes is not yet supported', + scylla_version="2026.1.0") +class TestGetHostPortMapping(unittest.TestCase): + """ + Test _query_all_routes_for_connections and _query_routes_for_change_event + methods with different filtering scenarios. + """ + + @classmethod + def setUpClass(cls): + cls.cluster = TestCluster(client_routes_config=ClientRoutesConfig( + proxies=[ClientRouteProxy("conn_id", "127.0.0.1")])) + cls.session = cls.cluster.connect() + + cls.host_ids = [uuid.uuid4() for _ in range(3)] + cls.connection_ids = [str(uuid.uuid4()) for _ in range(3)] + cls.expected = [] + + for idx, host_id in enumerate(cls.host_ids): + ip = f"127.0.0.{idx + 1}" + for connection_id in cls.connection_ids: + cls.expected.append({ + 'connection_id': connection_id, + 'host_id': host_id, + 'address': ip, + 'port': 9042, + 'tls_port': 9142, + }) + + cls._sort_routes(cls.expected) + post_client_routes(cls.cluster.contact_points[0], cls.expected) + + @classmethod + def tearDownClass(cls): + cls.cluster.shutdown() + + @staticmethod + def _sort_routes(routes): + routes.sort(key=lambda r: (str(r['connection_id']), str(r['host_id']))) + + def _routes_to_dicts(self, routes): + """Convert _Route objects to comparable dicts, adjusting port for ssl_enabled.""" + return [ + { + 'connection_id': route.connection_id, + 'host_id': route.host_id, + 'address': route.address, + 'port': route.port, + } + for route in routes + ] + + def _expected_dicts(self, expected): + """Build expected dicts with tls_port or port based on ssl_enabled.""" + port_key = 'tls_port' if self.cluster._client_routes_handler.ssl_enabled else 'port' + return [ + { + 'connection_id': e['connection_id'], + 'host_id': e['host_id'], + 'address': e['address'], + 'port': e[port_key], + } + for e in expected + ] + + def test_get_all_routes_for_all_connections(self): + """Querying all connection IDs returns every route.""" + cc = self.cluster.control_connection + routes = self.cluster._client_routes_handler._query_all_routes_for_connections( + cc._connection, cc._timeout, self.connection_ids, + ) + got = self._routes_to_dicts(routes) + self._sort_routes(got) + expected = self._expected_dicts(self.expected) + self._sort_routes(expected) + self.assertEqual(got, expected) + + def test_get_routes_for_single_connection(self): + """Querying a single connection ID returns only its routes.""" + cc = self.cluster.control_connection + routes = self.cluster._client_routes_handler._query_all_routes_for_connections( + cc._connection, cc._timeout, [self.connection_ids[0]], + ) + got = self._routes_to_dicts(routes) + self._sort_routes(got) + filtered = [r for r in self.expected + if r['connection_id'] == self.connection_ids[0]] + expected = self._expected_dicts(filtered) + self._sort_routes(expected) + self.assertEqual(got, expected) + + def test_get_routes_for_change_event_all_pairs(self): + """Querying all (connection_id, host_id) pairs returns every route.""" + cc = self.cluster.control_connection + pairs = [(r['connection_id'], r['host_id']) for r in self.expected] + routes = self.cluster._client_routes_handler._query_routes_for_change_event( + cc._connection, cc._timeout, pairs, + ) + got = self._routes_to_dicts(routes) + self._sort_routes(got) + expected = self._expected_dicts(self.expected) + self._sort_routes(expected) + self.assertEqual(got, expected) + + def test_get_routes_for_change_event_single_pair(self): + """Querying a single (connection_id, host_id) pair returns one route.""" + cc = self.cluster.control_connection + target_conn_id = self.connection_ids[0] + target_host_id = self.host_ids[0] + routes = self.cluster._client_routes_handler._query_routes_for_change_event( + cc._connection, cc._timeout, [(target_conn_id, target_host_id)], + ) + got = self._routes_to_dicts(routes) + self._sort_routes(got) + filtered = [r for r in self.expected + if r['connection_id'] == target_conn_id + and r['host_id'] == target_host_id] + expected = self._expected_dicts(filtered) + self._sort_routes(expected) + self.assertEqual(got, expected) + +@skip_scylla_version_lt(reason='scylladb/scylladb#26992 - system.client_routes is not yet supported', + scylla_version="2026.1.0") +class TestPrivateLinkConnectivity(unittest.TestCase): + """ + Verifies the driver connects to all cluster nodes exclusively through + the NLB proxy, never directly. + + Setup: + 1. Start a 3-node CCM cluster (done by setup_module). + 2. Start an NLB emulator with per-node proxies. + 3. Use a direct session to read host_ids, then POST client routes + pointing each host_id at the NLB proxy port. + 4. Create a client-routes-enabled session using the NLB discovery + port as the contact point. + 5. Verify all driver connections go through proxy ports. + """ + + @classmethod + def setUpClass(cls): + cls.direct_cluster = TestCluster() + cls.direct_session = cls.direct_cluster.connect() + cls.host_id_map = get_host_ids_from_cluster(cls.direct_session) + log.info("Host ID map: %s", cls.host_id_map) + + cls.node_addrs = {} + for ip in cls.host_id_map: + node_id = int(ip.split(".")[-1]) + cls.node_addrs[node_id] = ip + + cls.nlb = NLBEmulator() + cls.nlb.start(cls.node_addrs) + + cls.connection_id = str(uuid.uuid4()) + post_routes_for_nlb("127.0.0.1", cls.connection_id, cls.host_id_map, cls.nlb) + wait_for_routes_visible(cls.direct_session, cls.connection_id, len(cls.host_id_map)) + + @classmethod + def tearDownClass(cls): + cls.direct_cluster.shutdown() + cls.nlb.stop() + + def _make_client_routes_cluster(self, **extra_kwargs): + """Create a Cluster configured with client-routes pointing at the NLB.""" + return Cluster( + contact_points=[NLBEmulator.LISTEN_HOST], + port=self.nlb.discovery_port, + client_routes_config=ClientRoutesConfig( + proxies=[ClientRouteProxy(self.connection_id, NLBEmulator.LISTEN_HOST)], + ), + load_balancing_policy=RoundRobinPolicy(), + **extra_kwargs, + ) + + def test_all_connections_through_proxy(self): + """Every pool connection must go through the NLB proxy, not directly.""" + with self._make_client_routes_cluster() as cluster: + session = cluster.connect(wait_for_all_pools=True) + + for _ in range(50): + session.execute("SELECT key FROM system.local") + + pool_state = session.get_pool_state() + self.assertEqual(len(pool_state), len(self.node_addrs), + "Driver should have pools for all nodes") + + for host, state in pool_state.items(): + node_id = node_id_from_ip(host.address) + proxy = self.nlb.get_node_proxy(node_id) + self.assertIsNotNone(proxy, f"No proxy for node {node_id}") + open_count = state['open_count'] + self.assertGreaterEqual( + proxy.total_connections, open_count, + f"Node {node_id} proxy saw {proxy.total_connections} " + f"connections but pool has {open_count} open — " + f"some connections bypassed the proxy") + + assert_routes_via_nlb(self, cluster, self.nlb, + self.node_addrs.keys()) + + def test_queries_succeed_through_proxy(self): + """Queries should work normally through the proxy.""" + with self._make_client_routes_cluster() as cluster: + session = cluster.connect() + session.execute( + "CREATE KEYSPACE IF NOT EXISTS test_cr_ks " + "WITH replication = {'class':'NetworkTopologyStrategy', 'replication_factor': 3}" + ) + session.execute( + "CREATE TABLE IF NOT EXISTS test_cr_ks.t (k int PRIMARY KEY, v text)" + ) + session.execute("INSERT INTO test_cr_ks.t (k, v) VALUES (1, 'hello')") + row = session.execute("SELECT v FROM test_cr_ks.t WHERE k = 1").one() + self.assertEqual(row.v, "hello") + + assert_routes_via_nlb(self, cluster, self.nlb, + self.node_addrs.keys()) + + def test_connection_recovery_after_proxy_drop(self): + """ + After the proxy drops all connections, the driver should reconnect + (still through the proxy). + """ + with self._make_client_routes_cluster() as cluster: + session = cluster.connect(wait_for_all_pools=True) + session.execute("SELECT key FROM system.local") + + assert_routes_via_nlb(self, cluster, self.nlb, + self.node_addrs.keys()) + + self.nlb.drop_all_connections() + + def query_ok(): + session.execute("SELECT key FROM system.local") + + wait_until_not_raised(query_ok, 1, 30) + + assert_routes_via_nlb(self, cluster, self.nlb, + self.node_addrs.keys()) + +@skip_scylla_version_lt(reason='scylladb/scylladb#26992 - system.client_routes is not yet supported', + scylla_version="2026.1.0") +class TestDynamicRouteUpdates(unittest.TestCase): + """ + Verify that when routes are updated (e.g. port changes), the driver + picks up the new routes and reconnects through the new proxy ports + after existing connections are dropped. + """ + + @classmethod + def setUpClass(cls): + cls.direct_cluster = TestCluster() + cls.direct_session = cls.direct_cluster.connect() + cls.host_id_map = get_host_ids_from_cluster(cls.direct_session) + + cls.node_addrs = {} + for ip in cls.host_id_map: + node_id = int(ip.split(".")[-1]) + cls.node_addrs[node_id] = ip + + cls.connection_id = str(uuid.uuid4()) + + @classmethod + def tearDownClass(cls): + cls.direct_cluster.shutdown() + + def test_route_update_causes_reconnect_to_new_port(self): + """ + 1. Start NLB v1, post routes -> driver connects through v1 ports. + 2. Start NLB v2 on different ports, post new routes. + 3. Drop v1 connections. + 4. Driver should reconnect through v2 ports. + """ + with NLBEmulator( + node_addresses=self.node_addrs, + ) as nlb_v1, NLBEmulator( + node_addresses=self.node_addrs, + ) as nlb_v2: + post_routes_for_nlb("127.0.0.1", self.connection_id, + self.host_id_map, nlb_v1) + wait_for_routes_visible(self.direct_session, self.connection_id, len(self.host_id_map)) + + with Cluster( + contact_points=[NLBEmulator.LISTEN_HOST], + port=nlb_v1.discovery_port, + client_routes_config=ClientRoutesConfig( + proxies=[ClientRouteProxy(self.connection_id, NLBEmulator.LISTEN_HOST)], + ), + load_balancing_policy=RoundRobinPolicy(), + ) as cluster: + session = cluster.connect(wait_for_all_pools=True) + session.execute("SELECT key FROM system.local") + + for node_id in self.node_addrs: + self.assertGreater( + nlb_v1.get_node_proxy(node_id).total_connections, 0) + assert_routes_via_nlb(self, cluster, nlb_v1, + self.node_addrs.keys()) + + post_routes_for_nlb("127.0.0.1", self.connection_id, + self.host_id_map, nlb_v2) + time.sleep(2) # let CLIENT_ROUTES_CHANGE propagate + + # Stop v1 per-node proxies entirely so v1 ports become + # unreachable, forcing the driver to reconnect through v2. + # (Merely dropping connections is insufficient because v1 + # proxies would still accept new connections before the + # route update propagates.) + for node_id in list(self.node_addrs.keys()): + nlb_v1.remove_node(node_id) + + def all_nodes_via_v2(): + session.execute("SELECT key FROM system.local") + for nid in self.node_addrs: + assert nlb_v2.get_node_proxy(nid).total_connections > 0, \ + "NLB v2 node %d proxy has no connections yet" % nid + + wait_until_not_raised(all_nodes_via_v2, 1, 30) + + assert_routes_via_nlb(self, cluster, nlb_v2, + self.node_addrs.keys()) + + +def _generate_ssl_certs(cert_dir, node_ips): + """ + Generate test SSL certificates with SANs covering the given node IPs. + + File names follow CCM's ``ScyllaCluster.enable_ssl()`` convention so the + resulting directory can be passed directly to ``enable_ssl(cert_dir, ...)``. + + Creates: + - ca.key / ca.crt: self-signed CA + - ccm_node.key / ccm_node.pem: server cert signed by CA with SANs for all node_ips + + :param cert_dir: directory to write files into (must exist) + :param node_ips: list of IP strings to include as SANs (e.g. ["127.0.0.1", "127.0.0.2"]) + """ + if shutil.which("openssl") is None: + raise unittest.SkipTest("openssl not found on PATH; skipping SSL cert generation") + + san_cnf = os.path.join(cert_dir, "san.cnf") + san_value = ",".join("IP:%s" % ip for ip in node_ips) + with open(san_cnf, "w") as f: + f.write("subjectAltName=%s\n" % san_value) + + def _run(cmd): + result = subprocess.run(cmd, cwd=cert_dir, capture_output=True, text=True) + if result.returncode != 0: + raise RuntimeError("Command failed: %s\n%s" % (" ".join(cmd), result.stderr)) + + _run(["openssl", "req", "-x509", "-newkey", "rsa:2048", + "-keyout", "ca.key", "-out", "ca.crt", + "-days", "1", "-nodes", "-subj", "/CN=Test CA"]) + + _run(["openssl", "req", "-newkey", "rsa:2048", + "-keyout", "ccm_node.key", "-out", "ccm_node.csr", + "-nodes", "-subj", "/CN=Test Server"]) + + _run(["openssl", "x509", "-req", + "-in", "ccm_node.csr", "-CA", "ca.crt", "-CAkey", "ca.key", + "-CAcreateserial", "-out", "ccm_node.pem", + "-days", "1", "-extfile", "san.cnf"]) + + log.info("Generated SSL certs in %s with SANs: %s", cert_dir, san_value) + + +@skip_scylla_version_lt(reason='scylladb/scylladb#26992 - system.client_routes is not yet supported', + scylla_version="2026.1.0") +class TestMixedDirectAndNlbConnections(unittest.TestCase): + """ + Verify the cluster works when some nodes are accessed through the NLB + proxy and others are accessed directly (no route posted, falls back + to the default endpoint). + """ + + @classmethod + def setUpClass(cls): + cls.direct_cluster = TestCluster() + cls.direct_session = cls.direct_cluster.connect() + cls.host_id_map = get_host_ids_from_cluster(cls.direct_session) + + cls.node_addrs = {} + for ip in cls.host_id_map: + node_id = int(ip.split(".")[-1]) + cls.node_addrs[node_id] = ip + + cls.connection_id = str(uuid.uuid4()) + + @classmethod + def tearDownClass(cls): + cls.direct_cluster.shutdown() + + def test_mixed_direct_and_nlb_connections(self): + """ + Post routes for only a subset of nodes (through NLB proxy). + Remaining nodes have no route and fall back to direct connections. + Queries should work through both paths. + """ + proxied_node_id = min(self.node_addrs.keys()) + proxied_ip = self.node_addrs[proxied_node_id] + + with NLBEmulator( + node_addresses={proxied_node_id: proxied_ip}, + ) as nlb: + proxied_host_id = self.host_id_map[proxied_ip] + routes = [{ + "connection_id": self.connection_id, + "host_id": proxied_host_id, + "address": NLBEmulator.LISTEN_HOST, + "port": nlb.node_port(proxied_node_id), + }] + post_client_routes("127.0.0.1", routes) + time.sleep(1) + + with Cluster( + contact_points=["127.0.0.1"], + client_routes_config=ClientRoutesConfig( + proxies=[ClientRouteProxy(self.connection_id, NLBEmulator.LISTEN_HOST)], + ), + load_balancing_policy=RoundRobinPolicy(), + ) as cluster: + session = cluster.connect(wait_for_all_pools=True) + + for _ in range(50): + session.execute("SELECT key FROM system.local") + + assert_routes_via_nlb(self, cluster, nlb, + [proxied_node_id]) + + direct_node_ids = set(self.node_addrs.keys()) - {proxied_node_id} + assert_routes_direct(self, cluster, direct_node_ids) + + proxy = nlb.get_node_proxy(proxied_node_id) + self.assertGreater(proxy.total_connections, 0, + "Proxied node should have connections through NLB") + + +@skip_scylla_version_lt(reason='scylladb/scylladb#26992 - system.client_routes is not yet supported', + scylla_version="2026.1.0") +class TestSslThroughNlb(unittest.TestCase): + """ + Verify SSL with check_hostname=False works through the NLB proxy. + + When using client routes, connections go through NLB proxies whose + addresses won't match server certificates, so hostname verification + must be disabled. Certificate chain validation (verify_mode=CERT_REQUIRED) + is still active — only hostname matching is skipped. + + The driver raises ValueError at Cluster init time if check_hostname=True + is used with client_routes_config. + """ + + @classmethod + def setUpClass(cls): + cls.direct_cluster = TestCluster() + cls.direct_session = cls.direct_cluster.connect() + cls.host_id_map = get_host_ids_from_cluster(cls.direct_session) + cls.direct_cluster.shutdown() + + cls.node_addrs = {} + for ip in cls.host_id_map: + node_id = int(ip.split(".")[-1]) + cls.node_addrs[node_id] = ip + + cls.connection_id = str(uuid.uuid4()) + + cls.cert_dir = tempfile.mkdtemp(prefix="client-routes-ssl-") + cert_ips = list(cls.node_addrs.values()) + _generate_ssl_certs(cls.cert_dir, cert_ips) + + cls.ccm_cluster = get_cluster() + cls.ccm_cluster.stop() + cls.ccm_cluster.set_configuration_options({ + 'client_encryption_options': { + 'enabled': True, + 'certificate': os.path.join(cls.cert_dir, "ccm_node.pem"), + 'keyfile': os.path.join(cls.cert_dir, "ccm_node.key"), + } + }) + cls.ccm_cluster.start(wait_for_binary_proto=True) + + @classmethod + def tearDownClass(cls): + cls.ccm_cluster.stop() + cls.ccm_cluster.set_configuration_options({ + 'client_encryption_options': { + 'enabled': False, + } + }) + cls.ccm_cluster.start(wait_for_binary_proto=True) + + shutil.rmtree(cls.cert_dir, ignore_errors=True) + + def test_ssl_without_hostname_verification_through_nlb(self): + """ + Connect through NLB with SSL but check_hostname=False. + + When using client routes, connections go through NLB proxies + whose addresses won't match server certificates, so hostname + verification must be disabled. Certificate chain validation + (verify_mode=CERT_REQUIRED) is still active. + """ + with NLBEmulator( + node_addresses=self.node_addrs, + ) as nlb: + routes = build_routes_for_nlb( + self.connection_id, self.host_id_map, nlb, + ) + for route in routes: + route["tls_port"] = route["port"] + post_client_routes("127.0.0.1", routes) + + ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ssl_ctx.check_hostname = False + ssl_ctx.load_verify_locations(os.path.join(self.cert_dir, 'ca.crt')) + + self.assertFalse(ssl_ctx.check_hostname, + "check_hostname must be False for this test") + self.assertEqual(ssl_ctx.verify_mode, ssl.CERT_REQUIRED, + "verify_mode must be CERT_REQUIRED") + + def routes_visible(): + with TestCluster( + contact_points=["127.0.0.1"], + ssl_context=ssl_ctx, connect_timeout=30, + ) as c: + session = c.connect() + rs = session.execute( + "SELECT * FROM system.client_routes " + "WHERE connection_id = %s ALLOW FILTERING", + (self.connection_id,) + ) + return len(list(rs)) >= len(self.host_id_map) + + wait_until_not_raised( + lambda: self.assertTrue(routes_visible()), + 1, 30, + ) + + with Cluster( + contact_points=[NLBEmulator.LISTEN_HOST], + port=nlb.discovery_port, + ssl_context=ssl_ctx, + client_routes_config=ClientRoutesConfig( + proxies=[ClientRouteProxy(self.connection_id, NLBEmulator.LISTEN_HOST)], + ), + load_balancing_policy=RoundRobinPolicy(), + ) as cluster: + session = cluster.connect(wait_for_all_pools=True) + + for _ in range(20): + row = session.execute( + "SELECT release_version FROM system.local" + ).one() + self.assertIsNotNone(row) + + assert_routes_via_nlb(self, cluster, nlb, + self.node_addrs.keys()) + + def test_ssl_with_hostname_verification_raises_error(self): + """ + Verify that Cluster raises ValueError when client_routes_config + is used with SSL hostname verification enabled. + """ + ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ssl_ctx.load_verify_locations(os.path.join(self.cert_dir, 'ca.crt')) + self.assertTrue(ssl_ctx.check_hostname) + + with self.assertRaises(ValueError) as cm: + Cluster( + contact_points=[NLBEmulator.LISTEN_HOST], + ssl_context=ssl_ctx, + client_routes_config=ClientRoutesConfig( + proxies=[ClientRouteProxy("test-id", NLBEmulator.LISTEN_HOST)], + ), + ) + self.assertIn("check_hostname", str(cm.exception)) + +@skip_scylla_version_lt(reason='scylladb/scylladb#26992 - system.client_routes is not yet supported', + scylla_version="2026.1.0") +class TestFullNodeReplacementThroughNlb(unittest.TestCase): + """ + End-to-end test: creates a session through an NLB proxy with client routes, + scales the cluster up, then decommissions original nodes, verifying the + session survives the full node replacement. + + This test is destructive — it modifies the CCM cluster topology by + bootstrapping new nodes and decommissioning original ones. It uses + its own CCM cluster so it cannot interfere with other tests. + """ + + @classmethod + def setUpClass(cls): + cls._saved_scylla_ext_opts = os.environ.get('SCYLLA_EXT_OPTS') + os.environ['SCYLLA_EXT_OPTS'] = "--smp 2 --memory 2048M" + use_cluster('test_client_routes_replacement', [3], start=True) + + cls.direct_cluster = TestCluster() + cls.direct_session = cls.direct_cluster.connect() + cls.host_id_map = get_host_ids_from_cluster(cls.direct_session) + + cls.node_addrs = {} + for ip in cls.host_id_map: + node_id = int(ip.split(".")[-1]) + cls.node_addrs[node_id] = ip + + cls.connection_id = str(uuid.uuid4()) + + @classmethod + def tearDownClass(cls): + cls.direct_cluster.shutdown() + if cls._saved_scylla_ext_opts is None: + os.environ.pop('SCYLLA_EXT_OPTS', None) + else: + os.environ['SCYLLA_EXT_OPTS'] = cls._saved_scylla_ext_opts + + def test_should_survive_full_node_replacement_through_nlb(self): + """ + 1. Start with 3 nodes behind the NLB + 2. Bootstrap 3 new nodes, add to NLB, update routes + 3. Decommission the original 3 nodes one-by-one, updating NLB/routes + 4. Verify the session survives with only new nodes + """ + original_node_ids = sorted(self.node_addrs.keys()) + with NLBEmulator( + node_addresses=self.node_addrs, + ) as nlb: + # ---- Stage 1: Set up NLB for initial nodes ---- + log.info("Stage 1: Setting up NLB for %d initial nodes", len(original_node_ids)) + + post_routes_for_nlb("127.0.0.1", self.connection_id, self.host_id_map, nlb) + wait_for_routes_visible(self.direct_session, self.connection_id, len(self.host_id_map)) + + # ---- Stage 2: Create session through NLB ---- + log.info("Stage 2: Creating session through NLB") + with Cluster( + contact_points=[NLBEmulator.LISTEN_HOST], + port=nlb.discovery_port, + client_routes_config=ClientRoutesConfig( + proxies=[ClientRouteProxy(self.connection_id, NLBEmulator.LISTEN_HOST)], + ), + load_balancing_policy=RoundRobinPolicy(), + ) as cluster: + session = cluster.connect(wait_for_all_pools=True) + self._assert_query_works(session) + + handler = cluster._client_routes_handler + self.assertIsNotNone(handler) + + assert_routes_via_nlb(self, cluster, nlb, + original_node_ids) + log.info("Stage 2: Session created, all %d nodes via NLB", + len(original_node_ids)) + + # ---- Stage 3: Bootstrap new nodes ---- + new_node_ids = [max(original_node_ids) + 1, max(original_node_ids) + 2, max(original_node_ids) + 3] + log.info("Stage 3: Adding nodes %s", new_node_ids) + ccm_cluster = get_cluster() + + for node_id in new_node_ids: + self._bootstrap_node(ccm_cluster, node_id, data_center='dc1') + + expected_total = len(original_node_ids) + len(new_node_ids) + self._wait_for_condition( + lambda: len(cluster.metadata.all_hosts()) >= expected_total, + timeout_seconds=60, + description="%d nodes in metadata" % expected_total, + ) + + for node_id in new_node_ids: + nlb.add_node(node_id, "127.0.0.%d" % node_id) + + all_host_ids = get_host_ids_from_cluster(session) + log.info("All host IDs after expansion: %s", all_host_ids) + post_routes_for_nlb("127.0.0.1", self.connection_id, all_host_ids, nlb) + + handler.initialize( + cluster.control_connection._connection, + cluster.control_connection._timeout) + + self._wait_for_condition( + lambda: sum(1 for h in cluster.metadata.all_hosts() if h.is_up) >= expected_total, + timeout_seconds=60, + description="all %d nodes up" % expected_total, + ) + + self._assert_query_works(session) + + all_node_ids = set(original_node_ids) | set(new_node_ids) + assert_routes_via_nlb(self, cluster, nlb, all_node_ids) + log.info("Stage 3: All %d nodes via NLB after expansion", + len(all_node_ids)) + + # ---- Stage 4: Decommission original nodes ---- + log.info("Stage 4: Decommissioning original nodes %s", original_node_ids) + + remaining_node_ids = set(all_node_ids) + remaining_host_ids = dict(all_host_ids) + for node_id in original_node_ids: + log.info("Decommissioning node %d", node_id) + get_node(node_id).decommission() + nlb.remove_node(node_id) + remaining_node_ids.discard(node_id) + + ip = "127.0.0.%d" % node_id + remaining_host_ids.pop(ip, None) + + surviving_ips = list(remaining_host_ids.keys()) + if surviving_ips: + post_routes_for_nlb( + surviving_ips[0], self.connection_id, + remaining_host_ids, nlb, + ) + + expected_remaining = expected_total - (original_node_ids.index(node_id) + 1) + self._wait_for_condition( + lambda er=expected_remaining: ( + len(cluster.metadata.all_hosts()) <= er + and self._query_succeeds(session) + ), + timeout_seconds=60, + description="node %d decommissioned" % node_id, + ) + + # Reload routes after the control connection has + # re-established itself (the decommission may have + # killed the old control connection). + handler.initialize( + cluster.control_connection._connection, + cluster.control_connection._timeout) + + assert_routes_via_nlb(self, cluster, nlb, + remaining_node_ids) + log.info("Node %d decommissioned, %d nodes still via NLB", + node_id, len(remaining_node_ids)) + + # ---- Stage 5: Verify with only new nodes ---- + log.info("Stage 5: Verifying session works with only new nodes %s", new_node_ids) + self._assert_query_works(session) + + hosts = cluster.metadata.all_hosts() + self.assertEqual( + len(hosts), len(new_node_ids), + "Expected %d hosts, got %d" % (len(new_node_ids), len(hosts)) + ) + + for _ in range(10): + self._assert_query_works(session) + + assert_routes_via_nlb(self, cluster, nlb, new_node_ids) + log.info("PASS: Full node replacement, all %d new nodes via NLB", + len(new_node_ids)) + + def _assert_query_works(self, session): + rs = session.execute("SELECT release_version FROM system.local WHERE key='local'") + row = rs.one() + self.assertIsNotNone(row, "Query via NLB should return a result") + + def _query_succeeds(self, session): + try: + self._assert_query_works(session) + return True + except Exception: + return False + + def _bootstrap_node(self, ccm_cluster, node_id, data_center=None, rack=None): + node_type = type(next(iter(ccm_cluster.nodes.values()))) + ip = "127.0.0.%d" % node_id + node_instance = node_type( + 'node%s' % node_id, + ccm_cluster, + auto_bootstrap=True, + thrift_interface=(ip, 9160), + storage_interface=(ip, 7000), + binary_interface=(ip, 9042), + jmx_port=str(7000 + 100 * node_id), + remote_debug_port=0, + initial_token=None, + ) + # CCM requires explicit data_center/rack when adding a node so that + # cassandra-rackdc.properties is written correctly. Without this the + # snitch fails to parse the empty properties file and the node crashes + # on startup. + ccm_cluster.add(node_instance, is_seed=False, + data_center=data_center, rack=rack) + node_instance.start(wait_for_binary_proto=True, wait_other_notice=True) + wait_for_node_socket(node_instance, 120) + log.info("Node %d bootstrapped successfully", node_id) + + @staticmethod + def _wait_for_condition(predicate, timeout_seconds, poll_interval=2, description="condition"): + deadline = time.time() + timeout_seconds + while time.time() < deadline: + if predicate(): + return True + time.sleep(poll_interval) + raise AssertionError( + "Timed out waiting for %s after %d seconds" % (description, timeout_seconds) + ) diff --git a/tests/integration/standard/test_client_warnings.py b/tests/integration/standard/test_client_warnings.py index ce5332a59f..c18fa8cb1f 100644 --- a/tests/integration/standard/test_client_warnings.py +++ b/tests/integration/standard/test_client_warnings.py @@ -17,12 +17,13 @@ from cassandra.query import BatchStatement -from tests.integration import (use_singledc, PROTOCOL_VERSION, local, TestCluster, +from tests.integration import (use_single_node, PROTOCOL_VERSION, local, TestCluster, requires_custom_payload, xfail_scylla) +from tests.util import assertRegex, assertDictEqual def setup_module(): - use_singledc() + use_single_node() @xfail_scylla('scylladb/scylladb#10196 - scylla does not report warnings') class ClientWarningTests(unittest.TestCase): @@ -70,8 +71,8 @@ def test_warning_basic(self): """ future = self.session.execute_async(self.warn_batch) future.result() - self.assertEqual(len(future.warnings), 1) - self.assertRegex(future.warnings[0], 'Batch.*exceeding.*') + assert len(future.warnings) == 1 + assertRegex(future.warnings[0], 'Batch.*exceeding.*') def test_warning_with_trace(self): """ @@ -86,9 +87,9 @@ def test_warning_with_trace(self): """ future = self.session.execute_async(self.warn_batch, trace=True) future.result() - self.assertEqual(len(future.warnings), 1) - self.assertRegex(future.warnings[0], 'Batch.*exceeding.*') - self.assertIsNotNone(future.get_query_trace()) + assert len(future.warnings) == 1 + assertRegex(future.warnings[0], 'Batch.*exceeding.*') + assert future.get_query_trace() is not None @local @requires_custom_payload @@ -106,9 +107,9 @@ def test_warning_with_custom_payload(self): payload = {'key': b'value'} future = self.session.execute_async(self.warn_batch, custom_payload=payload) future.result() - self.assertEqual(len(future.warnings), 1) - self.assertRegex(future.warnings[0], 'Batch.*exceeding.*') - self.assertDictEqual(future.custom_payload, payload) + assert len(future.warnings) == 1 + assertRegex(future.warnings[0], 'Batch.*exceeding.*') + assertDictEqual(future.custom_payload, payload) @local @requires_custom_payload @@ -126,7 +127,7 @@ def test_warning_with_trace_and_custom_payload(self): payload = {'key': b'value'} future = self.session.execute_async(self.warn_batch, trace=True, custom_payload=payload) future.result() - self.assertEqual(len(future.warnings), 1) - self.assertRegex(future.warnings[0], 'Batch.*exceeding.*') - self.assertIsNotNone(future.get_query_trace()) - self.assertDictEqual(future.custom_payload, payload) + assert len(future.warnings) == 1 + assertRegex(future.warnings[0], 'Batch.*exceeding.*') + assert future.get_query_trace() is not None + assertDictEqual(future.custom_payload, payload) diff --git a/tests/integration/standard/test_cluster.py b/tests/integration/standard/test_cluster.py index cdfc7c1b82..00ea11ea27 100644 --- a/tests/integration/standard/test_cluster.py +++ b/tests/integration/standard/test_cluster.py @@ -24,6 +24,7 @@ import warnings from packaging.version import Version import os +import pytest import cassandra from cassandra.cluster import NoHostAvailable, ExecutionProfile, EXEC_PROFILE_DEFAULT, ControlConnection, Cluster @@ -41,20 +42,33 @@ from tests import notwindows, notasyncio from tests.integration import use_cluster, get_server_versions, CASSANDRA_VERSION, \ execute_until_pass, execute_with_long_wait_retry, get_node, MockLoggingHandler, get_unsupported_lower_protocol, \ - get_unsupported_upper_protocol, lessthanprotocolv3, protocolv6, local, CASSANDRA_IP, greaterthanorequalcass30, \ - lessthanorequalcass40, DSE_VERSION, TestCluster, PROTOCOL_VERSION, xfail_scylla, incorrect_test + get_unsupported_upper_protocol, local, CASSANDRA_IP, greaterthanorequalcass30, \ + lessthanorequalcass40, TestCluster, PROTOCOL_VERSION, xfail_scylla, incorrect_test from tests.integration.util import assert_quiescent_pool_state +from tests.util import assertListEqual import sys log = logging.getLogger(__name__) +_saved_scylla_ext_opts = None + + def setup_module(): - os.environ['SCYLLA_EXT_OPTS'] = "--smp 1" + global _saved_scylla_ext_opts + _saved_scylla_ext_opts = os.environ.get('SCYLLA_EXT_OPTS') + os.environ['SCYLLA_EXT_OPTS'] = "--smp 2" use_cluster("cluster_tests", [3], start=True, workloads=None) warnings.simplefilter("always") +def teardown_module(): + if _saved_scylla_ext_opts is None: + os.environ.pop('SCYLLA_EXT_OPTS', None) + else: + os.environ['SCYLLA_EXT_OPTS'] = _saved_scylla_ext_opts + + class IgnoredHostPolicy(RoundRobinPolicy): def __init__(self, ignored_hosts): @@ -87,9 +101,9 @@ def test_ignored_host_up(self): cluster.connect() for host in cluster.metadata.all_hosts(): if str(host) == "127.0.0.1:9042": - self.assertTrue(host.is_up) + assert host.is_up else: - self.assertIsNone(host.is_up) + assert host.is_up is None cluster.shutdown() @local @@ -104,7 +118,7 @@ def test_host_resolution(self): @test_category connection """ cluster = TestCluster(contact_points=["localhost"], connect_timeout=1) - self.assertTrue(DefaultEndPoint('127.0.0.1') in cluster.endpoints_resolved) + assert DefaultEndPoint('127.0.0.1') in cluster.endpoints_resolved @local def test_host_duplication(self): @@ -122,11 +136,11 @@ def test_host_duplication(self): connect_timeout=1 ) cluster.connect(wait_for_all_pools=True) - self.assertEqual(len(cluster.metadata.all_hosts()), 3) + assert len(cluster.metadata.all_hosts()) == 3 cluster.shutdown() cluster = TestCluster(contact_points=["127.0.0.1", "localhost"], connect_timeout=1) cluster.connect(wait_for_all_pools=True) - self.assertEqual(len(cluster.metadata.all_hosts()), 3) + assert len(cluster.metadata.all_hosts()) == 3 cluster.shutdown() @local @@ -150,7 +164,7 @@ def test_raise_error_on_control_connection_timeout(self): get_node(1).pause() cluster = TestCluster(contact_points=['127.0.0.1'], connect_timeout=1) - with self.assertRaisesRegex(NoHostAvailable, r"OperationTimedOut\('errors=Timed out creating connection \(1 seconds\)"): + with pytest.raises(NoHostAvailable, match=r"OperationTimedOut\('errors=Timed out creating connection \(1 seconds\)"): cluster.connect() cluster.shutdown() @@ -166,9 +180,9 @@ def test_basic(self): result = execute_until_pass(session, """ CREATE KEYSPACE clustertests - WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} + WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '1'} """) - self.assertFalse(result) + assert not result result = execute_with_long_wait_retry(session, """ @@ -179,16 +193,16 @@ def test_basic(self): PRIMARY KEY (a, b) ) """) - self.assertFalse(result) + assert not result result = session.execute( """ INSERT INTO clustertests.cf0 (a, b, c) VALUES ('a', 'b', 'c') """) - self.assertFalse(result) + assert not result result = session.execute("SELECT * FROM clustertests.cf0") - self.assertEqual([('a', 'b', 'c')], result) + assert [('a', 'b', 'c')] == result execute_with_long_wait_retry(session, "DROP KEYSPACE clustertests") @@ -218,13 +232,13 @@ def cleanup(): # Test with empty list self.cluster_to_shutdown = TestCluster(contact_points=[]) - with self.assertRaises(NoHostAvailable): + with pytest.raises(NoHostAvailable): self.cluster_to_shutdown.connect() self.cluster_to_shutdown.shutdown() # Test with only invalid self.cluster_to_shutdown = TestCluster(contact_points=('1.2.3.4',)) - with self.assertRaises(NoHostAvailable): + with pytest.raises(NoHostAvailable): self.cluster_to_shutdown.connect() self.cluster_to_shutdown.shutdown() @@ -250,41 +264,35 @@ def test_protocol_negotiation(self): """ cluster = Cluster() - self.assertLessEqual(cluster.protocol_version, cassandra.ProtocolVersion.MAX_SUPPORTED) + assert cluster.protocol_version <= cassandra.ProtocolVersion.MAX_SUPPORTED session = cluster.connect() updated_protocol_version = session._protocol_version updated_cluster_version = cluster.protocol_version # Make sure the correct protocol was selected by default - if DSE_VERSION and DSE_VERSION >= Version("6.0"): - self.assertEqual(updated_protocol_version, cassandra.ProtocolVersion.DSE_V2) - self.assertEqual(updated_cluster_version, cassandra.ProtocolVersion.DSE_V2) - elif DSE_VERSION and DSE_VERSION >= Version("5.1"): - self.assertEqual(updated_protocol_version, cassandra.ProtocolVersion.DSE_V1) - self.assertEqual(updated_cluster_version, cassandra.ProtocolVersion.DSE_V1) - elif CASSANDRA_VERSION >= Version('4.0-beta5'): - self.assertEqual(updated_protocol_version, cassandra.ProtocolVersion.V5) - self.assertEqual(updated_cluster_version, cassandra.ProtocolVersion.V5) + if CASSANDRA_VERSION >= Version('4.0-beta5'): + assert updated_protocol_version == cassandra.ProtocolVersion.V5 + assert updated_cluster_version == cassandra.ProtocolVersion.V5 elif CASSANDRA_VERSION >= Version('4.0-a'): - self.assertEqual(updated_protocol_version, cassandra.ProtocolVersion.V4) - self.assertEqual(updated_cluster_version, cassandra.ProtocolVersion.V4) + assert updated_protocol_version == cassandra.ProtocolVersion.V4 + assert updated_cluster_version == cassandra.ProtocolVersion.V4 elif CASSANDRA_VERSION >= Version('3.11'): - self.assertEqual(updated_protocol_version, cassandra.ProtocolVersion.V4) - self.assertEqual(updated_cluster_version, cassandra.ProtocolVersion.V4) + assert updated_protocol_version == cassandra.ProtocolVersion.V4 + assert updated_cluster_version == cassandra.ProtocolVersion.V4 elif CASSANDRA_VERSION >= Version('3.0'): - self.assertEqual(updated_protocol_version, cassandra.ProtocolVersion.V4) - self.assertEqual(updated_cluster_version, cassandra.ProtocolVersion.V4) + assert updated_protocol_version == cassandra.ProtocolVersion.V4 + assert updated_cluster_version == cassandra.ProtocolVersion.V4 elif CASSANDRA_VERSION >= Version('2.2'): - self.assertEqual(updated_protocol_version, 4) - self.assertEqual(updated_cluster_version, 4) + assert updated_protocol_version == 4 + assert updated_cluster_version == 4 elif CASSANDRA_VERSION >= Version('2.1'): - self.assertEqual(updated_protocol_version, 3) - self.assertEqual(updated_cluster_version, 3) + assert updated_protocol_version == 3 + assert updated_cluster_version == 3 elif CASSANDRA_VERSION >= Version('2.0'): - self.assertEqual(updated_protocol_version, 2) - self.assertEqual(updated_cluster_version, 2) + assert updated_protocol_version == 2 + assert updated_cluster_version == 2 else: - self.assertEqual(updated_protocol_version, 1) - self.assertEqual(updated_cluster_version, 1) + assert updated_protocol_version == 1 + assert updated_cluster_version == 1 cluster.shutdown() @@ -314,7 +322,7 @@ def test_invalid_protocol_negotation(self): log.debug('got upper_bound of {}'.format(upper_bound)) if upper_bound is not None: cluster = TestCluster(protocol_version=upper_bound) - with self.assertRaises(NoHostAvailable): + with pytest.raises(NoHostAvailable): cluster.connect() cluster.shutdown() @@ -322,7 +330,7 @@ def test_invalid_protocol_negotation(self): log.debug('got lower_bound of {}'.format(lower_bound)) if lower_bound is not None: cluster = TestCluster(protocol_version=lower_bound) - with self.assertRaises(NoHostAvailable): + with pytest.raises(NoHostAvailable): cluster.connect() cluster.shutdown() @@ -337,15 +345,15 @@ def test_connect_on_keyspace(self): """ INSERT INTO test1rf.test (k, v) VALUES (8889, 8889) """) - self.assertFalse(result) + assert not result result = session.execute("SELECT * FROM test1rf.test") - self.assertEqual([(8889, 8889)], result, "Rows in ResultSet are {0}".format(result.current_rows)) + assert [(8889, 8889)] == result, "Rows in ResultSet are {0}".format(result.current_rows) # test_connect_on_keyspace session2 = cluster.connect('test1rf') result2 = session2.execute("SELECT * FROM test") - self.assertEqual(result, result2) + assert result == result2 cluster.shutdown() def test_set_keyspace_twice(self): @@ -372,31 +380,37 @@ def test_connect_to_already_shutdown_cluster(self): """ cluster = TestCluster() cluster.shutdown() - self.assertRaises(Exception, cluster.connect) + with pytest.raises(Exception): + cluster.connect() def test_auth_provider_is_callable(self): """ Ensure that auth_providers are always callable """ - self.assertRaises(TypeError, Cluster, auth_provider=1, protocol_version=1) + with pytest.raises(TypeError): + Cluster(auth_provider=1, protocol_version=1) c = TestCluster(protocol_version=1) - self.assertRaises(TypeError, setattr, c, 'auth_provider', 1) + with pytest.raises(TypeError): + setattr(c, 'auth_provider', 1) def test_v2_auth_provider(self): """ Check for v2 auth_provider compliance """ bad_auth_provider = lambda x: {'username': 'foo', 'password': 'bar'} - self.assertRaises(TypeError, Cluster, auth_provider=bad_auth_provider, protocol_version=2) + with pytest.raises(TypeError): + Cluster(auth_provider=bad_auth_provider, protocol_version=2) c = TestCluster(protocol_version=2) - self.assertRaises(TypeError, setattr, c, 'auth_provider', bad_auth_provider) + with pytest.raises(TypeError): + setattr(c, 'auth_provider', bad_auth_provider) def test_conviction_policy_factory_is_callable(self): """ Ensure that conviction_policy_factory are always callable """ - self.assertRaises(ValueError, Cluster, conviction_policy_factory=1) + with pytest.raises(ValueError): + Cluster(conviction_policy_factory=1) def test_connect_to_bad_hosts(self): """ @@ -406,35 +420,8 @@ def test_connect_to_bad_hosts(self): cluster = TestCluster(contact_points=['127.1.2.9', '127.1.2.10'], protocol_version=PROTOCOL_VERSION) - self.assertRaises(NoHostAvailable, cluster.connect) - - @lessthanprotocolv3 - def test_cluster_settings(self): - """ - Test connection setting getters and setters - """ - - cluster = TestCluster() - - min_requests_per_connection = cluster.get_min_requests_per_connection(HostDistance.LOCAL) - self.assertEqual(cassandra.cluster.DEFAULT_MIN_REQUESTS, min_requests_per_connection) - cluster.set_min_requests_per_connection(HostDistance.LOCAL, min_requests_per_connection + 1) - self.assertEqual(cluster.get_min_requests_per_connection(HostDistance.LOCAL), min_requests_per_connection + 1) - - max_requests_per_connection = cluster.get_max_requests_per_connection(HostDistance.LOCAL) - self.assertEqual(cassandra.cluster.DEFAULT_MAX_REQUESTS, max_requests_per_connection) - cluster.set_max_requests_per_connection(HostDistance.LOCAL, max_requests_per_connection + 1) - self.assertEqual(cluster.get_max_requests_per_connection(HostDistance.LOCAL), max_requests_per_connection + 1) - - core_connections_per_host = cluster.get_core_connections_per_host(HostDistance.LOCAL) - self.assertEqual(cassandra.cluster.DEFAULT_MIN_CONNECTIONS_PER_LOCAL_HOST, core_connections_per_host) - cluster.set_core_connections_per_host(HostDistance.LOCAL, core_connections_per_host + 1) - self.assertEqual(cluster.get_core_connections_per_host(HostDistance.LOCAL), core_connections_per_host + 1) - - max_connections_per_host = cluster.get_max_connections_per_host(HostDistance.LOCAL) - self.assertEqual(cassandra.cluster.DEFAULT_MAX_CONNECTIONS_PER_LOCAL_HOST, max_connections_per_host) - cluster.set_max_connections_per_host(HostDistance.LOCAL, max_connections_per_host + 1) - self.assertEqual(cluster.get_max_connections_per_host(HostDistance.LOCAL), max_connections_per_host + 1) + with pytest.raises(NoHostAvailable): + cluster.connect() def test_refresh_schema(self): cluster = TestCluster() @@ -443,8 +430,8 @@ def test_refresh_schema(self): original_meta = cluster.metadata.keyspaces # full schema refresh, with wait cluster.refresh_schema_metadata() - self.assertIsNot(original_meta, cluster.metadata.keyspaces) - self.assertEqual(original_meta, cluster.metadata.keyspaces) + assert original_meta is not cluster.metadata.keyspaces + assert original_meta == cluster.metadata.keyspaces cluster.shutdown() @@ -458,10 +445,10 @@ def test_refresh_schema_keyspace(self): # only refresh one keyspace cluster.refresh_keyspace_metadata('system') current_meta = cluster.metadata.keyspaces - self.assertIs(original_meta, current_meta) + assert original_meta is current_meta current_system_meta = current_meta['system'] - self.assertIsNot(original_system_meta, current_system_meta) - self.assertEqual(original_system_meta.as_cql_query(), current_system_meta.as_cql_query()) + assert original_system_meta is not current_system_meta + assert original_system_meta.as_cql_query() == current_system_meta.as_cql_query() cluster.shutdown() def test_refresh_schema_table(self): @@ -477,10 +464,10 @@ def test_refresh_schema_table(self): current_meta = cluster.metadata.keyspaces current_system_meta = current_meta['system'] current_system_schema_meta = current_system_meta.tables['local'] - self.assertIs(original_meta, current_meta) - self.assertIs(original_system_meta, current_system_meta) - self.assertIsNot(original_system_schema_meta, current_system_schema_meta) - self.assertEqual(original_system_schema_meta.as_cql_query(), current_system_schema_meta.as_cql_query()) + assert original_meta is current_meta + assert original_system_meta is current_system_meta + assert original_system_schema_meta is not current_system_schema_meta + assert original_system_schema_meta.as_cql_query() == current_system_schema_meta.as_cql_query() cluster.shutdown() def test_refresh_schema_type(self): @@ -507,10 +494,10 @@ def test_refresh_schema_type(self): current_meta = cluster.metadata.keyspaces current_test1rf_meta = current_meta[keyspace_name] current_type_meta = current_test1rf_meta.user_types[type_name] - self.assertIs(original_meta, current_meta) - self.assertEqual(original_test1rf_meta.export_as_string(), current_test1rf_meta.export_as_string()) - self.assertIsNot(original_type_meta, current_type_meta) - self.assertEqual(original_type_meta.as_cql_query(), current_type_meta.as_cql_query()) + assert original_meta is current_meta + assert original_test1rf_meta.export_as_string() == current_test1rf_meta.export_as_string() + assert original_type_meta is not current_type_meta + assert original_type_meta.as_cql_query() == current_type_meta.as_cql_query() cluster.shutdown() @local @@ -532,24 +519,25 @@ def patched_wait_for_responses(*args, **kwargs): # cluster agreement wait exceeded c = TestCluster(max_schema_agreement_wait=agreement_timeout) c.connect() - self.assertTrue(c.metadata.keyspaces) + assert c.metadata.keyspaces # cluster agreement wait used for refresh original_meta = c.metadata.keyspaces start_time = time.time() - self.assertRaisesRegex(Exception, r"Schema metadata was not refreshed.*", c.refresh_schema_metadata) + with pytest.raises(Exception, match=r"Schema metadata was not refreshed.*"): + c.refresh_schema_metadata() end_time = time.time() - self.assertGreaterEqual(end_time - start_time, agreement_timeout) - self.assertIs(original_meta, c.metadata.keyspaces) + assert end_time - start_time >= agreement_timeout + assert original_meta is c.metadata.keyspaces # refresh wait overrides cluster value original_meta = c.metadata.keyspaces start_time = time.time() c.refresh_schema_metadata(max_schema_agreement_wait=0) end_time = time.time() - self.assertLess(end_time - start_time, agreement_timeout) - self.assertIsNot(original_meta, c.metadata.keyspaces) - self.assertEqual(original_meta, c.metadata.keyspaces) + assert end_time - start_time < agreement_timeout + assert original_meta is not c.metadata.keyspaces + assert original_meta == c.metadata.keyspaces c.shutdown() @@ -559,26 +547,26 @@ def patched_wait_for_responses(*args, **kwargs): start_time = time.time() s = c.connect() end_time = time.time() - self.assertLess(end_time - start_time, refresh_threshold) - self.assertTrue(c.metadata.keyspaces) + assert end_time - start_time < refresh_threshold + assert c.metadata.keyspaces # cluster agreement wait used for refresh original_meta = c.metadata.keyspaces start_time = time.time() c.refresh_schema_metadata() end_time = time.time() - self.assertLess(end_time - start_time, refresh_threshold) - self.assertIsNot(original_meta, c.metadata.keyspaces) - self.assertEqual(original_meta, c.metadata.keyspaces) + assert end_time - start_time < refresh_threshold + assert original_meta is not c.metadata.keyspaces + assert original_meta == c.metadata.keyspaces # refresh wait overrides cluster value original_meta = c.metadata.keyspaces start_time = time.time() - self.assertRaisesRegex(Exception, r"Schema metadata was not refreshed.*", c.refresh_schema_metadata, - max_schema_agreement_wait=agreement_timeout) + with pytest.raises(Exception, match=r"Schema metadata was not refreshed.*"): + c.refresh_schema_metadata(max_schema_agreement_wait=agreement_timeout) end_time = time.time() - self.assertGreaterEqual(end_time - start_time, agreement_timeout) - self.assertIs(original_meta, c.metadata.keyspaces) + assert end_time - start_time >= agreement_timeout + assert original_meta is c.metadata.keyspaces c.shutdown() def test_trace(self): @@ -600,7 +588,7 @@ def test_trace(self): query = "SELECT * FROM system.local WHERE key='local'" statement = SimpleStatement(query) result = session.execute(statement) - self.assertIsNone(result.get_query_trace()) + assert result.get_query_trace() is None statement2 = SimpleStatement(query) future = session.execute_async(statement2, trace=True) @@ -610,7 +598,7 @@ def test_trace(self): statement2 = SimpleStatement(query) future = session.execute_async(statement2) future.result() - self.assertIsNone(future.get_query_trace()) + assert future.get_query_trace() is None prepared = session.prepare("SELECT * FROM system.local WHERE key='local'") future = session.execute_async(prepared, parameters=(), trace=True) @@ -676,7 +664,7 @@ def test_one_returns_none(self): """ with TestCluster() as cluster: session = cluster.connect() - self.assertIsNone(session.execute("SELECT * from system.local WHERE key='madeup_key'").one()) + assert session.execute("SELECT * from system.local WHERE key='madeup_key'").one() is None def test_string_coverage(self): """ @@ -690,11 +678,11 @@ def test_string_coverage(self): statement = SimpleStatement(query) future = session.execute_async(statement) - self.assertIn(query, str(future)) + assert query in str(future) future.result() - self.assertIn(query, str(future)) - self.assertIn('result', str(future)) + assert query in str(future) + assert 'result' in str(future) cluster.shutdown() def test_can_connect_with_plainauth(self): @@ -742,31 +730,51 @@ def _warning_are_issued_when_auth(self, auth_provider): with MockLoggingHandler().set_module_name(connection.__name__) as mock_handler: with TestCluster(auth_provider=auth_provider) as cluster: session = cluster.connect() - self.assertIsNotNone(session.execute("SELECT * from system.local WHERE key='local'")) + assert session.execute("SELECT * from system.local WHERE key='local'") is not None - # Three conenctions to nodes plus the control connection + # Verify that auth warnings are issued for connections where + # auth is configured but the server does not send a challenge. + # At minimum one warning per node connection (3 for a 3-node + # cluster). The control connection and shard-aware connections + # may add more, so we only assert a lower bound. auth_warning = mock_handler.get_message_count('warning', "An authentication challenge was not sent") - self.assertGreaterEqual(auth_warning, 4) - self.assertEqual( - auth_warning, - mock_handler.get_message_count("debug", "Got ReadyMessage on new connection") - ) + assert auth_warning >= 3 + + def _wait_for_all_shard_connections(self, cluster, timeout=30): + """Wait until all shard-aware connections are fully established.""" + from cassandra.pool import HostConnection + deadline = time.time() + timeout + while time.time() < deadline: + all_connected = True + for holder in cluster.get_connection_holders(): + if not isinstance(holder, HostConnection): + continue + if holder.host.sharding_info and len(holder._connections) < holder.host.sharding_info.shards_count: + all_connected = False + break + if all_connected: + return + time.sleep(0.1) + raise RuntimeError("Timed out waiting for all shard connections to be established") def test_idle_heartbeat(self): interval = 2 cluster = TestCluster(idle_heartbeat_interval=interval, monitor_reporting_enabled=False) - if PROTOCOL_VERSION < 3: - cluster.set_core_connections_per_host(HostDistance.LOCAL, 1) session = cluster.connect(wait_for_all_pools=True) + # wait_for_all_pools only waits for the first connection per host; + # shard-aware connections to remaining shards are opened in background. + # Wait for them to stabilize so they don't get replaced during the test. + self._wait_for_all_shard_connections(cluster) + # This test relies on impl details of connection req id management to see if heartbeats # are being sent. May need update if impl is changed connection_request_ids = {} for h in cluster.get_connection_holders(): for c in h.get_connections(): # make sure none are idle (should have startup messages - self.assertFalse(c.is_idle) + assert not c.is_idle with c.lock: connection_request_ids[id(c)] = deque(c.request_ids) # copy of request ids @@ -780,45 +788,50 @@ def test_idle_heartbeat(self): expected_ids = connection_request_ids[id(c)] expected_ids.rotate(-1) with c.lock: - self.assertListEqual(list(c.request_ids), list(expected_ids)) + assertListEqual(list(c.request_ids), list(expected_ids)) # assert idle status - self.assertTrue(all(c.is_idle for c in connections)) + assert all(c.is_idle for c in connections) - # send messages on all connections - statements_and_params = [("SELECT release_version FROM system.local WHERE key='local'", ())] * len(cluster.metadata.all_hosts()) + # send enough messages to ensure all connections are used + # (with shard-aware routing, each query only hits one shard per host, + # so we need more queries than just len(hosts) to cover all connections) + num_connections = len([c for c in connections if not c.is_control_connection]) + statements_and_params = [("SELECT release_version FROM system.local WHERE key='local'", ())] * max(num_connections * 2, len(cluster.metadata.all_hosts())) results = execute_concurrent(session, statements_and_params) for success, result in results: - self.assertTrue(success) + assert success - # assert not idle status - self.assertFalse(any(c.is_idle if not c.is_control_connection else False for c in connections)) + # assert at least some non-control connections are no longer idle + # (shard-aware routing may not distribute queries to every connection) + non_idle = [c for c in connections if not c.is_control_connection and not c.is_idle] + assert len(non_idle) > 0 # holders include session pools and cc holders = cluster.get_connection_holders() - self.assertIn(cluster.control_connection, holders) - self.assertEqual(len(holders), len(cluster.metadata.all_hosts()) + 1) # hosts pools, 1 for cc + assert cluster.control_connection in holders + assert len(holders) == len(cluster.metadata.all_hosts()) + 1 # hosts pools, 1 for cc # include additional sessions session2 = cluster.connect(wait_for_all_pools=True) holders = cluster.get_connection_holders() - self.assertIn(cluster.control_connection, holders) - self.assertEqual(len(holders), 2 * len(cluster.metadata.all_hosts()) + 1) # 2 sessions' hosts pools, 1 for cc + assert cluster.control_connection in holders + assert len(holders) == 2 * len(cluster.metadata.all_hosts()) + 1 # 2 sessions' hosts pools, 1 for cc cluster._idle_heartbeat.stop() cluster._idle_heartbeat.join() - assert_quiescent_pool_state(self, cluster) + assert_quiescent_pool_state(cluster) cluster.shutdown() @patch('cassandra.cluster.Cluster.idle_heartbeat_interval', new=0.1) def test_idle_heartbeat_disabled(self): - self.assertTrue(Cluster.idle_heartbeat_interval) + assert Cluster.idle_heartbeat_interval # heartbeat disabled with '0' cluster = TestCluster(idle_heartbeat_interval=0) - self.assertEqual(cluster.idle_heartbeat_interval, 0) + assert cluster.idle_heartbeat_interval == 0 session = cluster.connect() # let two heatbeat intervals pass (first one had startup messages in it) @@ -827,7 +840,7 @@ def test_idle_heartbeat_disabled(self): connections = [c for holders in cluster.get_connection_holders() for c in holders.get_connections()] # assert not idle status (should never get reset because there is not heartbeat) - self.assertFalse(any(c.is_idle for c in connections)) + assert not any(c.is_idle for c in connections) cluster.shutdown() @@ -839,10 +852,10 @@ def test_pool_management(self): # prepare p = session.prepare("SELECT * FROM system.local WHERE key=?") - self.assertTrue(session.execute(p, ('local',))) + assert session.execute(p, ('local',)) # simple - self.assertTrue(session.execute("SELECT * FROM system.local WHERE key='local'")) + assert session.execute("SELECT * FROM system.local WHERE key='local'") # set keyspace session.set_keyspace('system') @@ -856,7 +869,7 @@ def test_pool_management(self): cluster.refresh_schema_metadata() cluster.refresh_schema_metadata(max_schema_agreement_wait=0) - assert_quiescent_pool_state(self, cluster) + assert_quiescent_pool_state(cluster) cluster.shutdown() @@ -886,7 +899,7 @@ def test_profile_load_balancing(self): for _ in expected_hosts: rs = session.execute(query) queried_hosts.add(rs.response_future._current_host) - self.assertEqual(queried_hosts, expected_hosts) + assert queried_hosts == expected_hosts # by name we should only hit the one expected_hosts = set(h for h in cluster.metadata.all_hosts() if h.address == CASSANDRA_IP) @@ -894,13 +907,13 @@ def test_profile_load_balancing(self): for _ in cluster.metadata.all_hosts(): rs = session.execute(query, execution_profile='node1') queried_hosts.add(rs.response_future._current_host) - self.assertEqual(queried_hosts, expected_hosts) + assert queried_hosts == expected_hosts # use a copied instance and override the row factory # assert last returned value can be accessed as a namedtuple so we can prove something different named_tuple_row = rs.one() - self.assertIsInstance(named_tuple_row, tuple) - self.assertTrue(named_tuple_row.release_version) + assert isinstance(named_tuple_row, tuple) + assert named_tuple_row.release_version tmp_profile = copy(node1) tmp_profile.row_factory = tuple_factory @@ -908,33 +921,30 @@ def test_profile_load_balancing(self): for _ in cluster.metadata.all_hosts(): rs = session.execute(query, execution_profile=tmp_profile) queried_hosts.add(rs.response_future._current_host) - self.assertEqual(queried_hosts, expected_hosts) + assert queried_hosts == expected_hosts tuple_row = rs.one() - self.assertIsInstance(tuple_row, tuple) - with self.assertRaises(AttributeError): + assert isinstance(tuple_row, tuple) + with pytest.raises(AttributeError): tuple_row.release_version # make sure original profile is not impacted - self.assertTrue(session.execute(query, execution_profile='node1').one().release_version) + assert session.execute(query, execution_profile='node1').one().release_version def test_setting_lbp_legacy(self): cluster = TestCluster() self.addCleanup(cluster.shutdown) cluster.load_balancing_policy = RoundRobinPolicy() - self.assertEqual( - list(cluster.load_balancing_policy.make_query_plan()), [] - ) + assert list(cluster.load_balancing_policy.make_query_plan()) == [] cluster.connect() - self.assertNotEqual( - list(cluster.load_balancing_policy.make_query_plan()), [] - ) + assert list(cluster.load_balancing_policy.make_query_plan()) != [] def test_profile_lb_swap(self): """ Tests that profile load balancing policies are not shared - Creates two LBP, runs a few queries, and validates that each LBP is execised - seperately between EP's + Creates two LBP, runs a few queries, and validates that each LBP is exercised + separately between EP's. Each RoundRobinPolicy starts from its own random + position and maintains independent round-robin ordering. @since 3.5 @jira_ticket PYTHON-569 @@ -949,17 +959,28 @@ def test_profile_lb_swap(self): with TestCluster(execution_profiles=exec_profiles) as cluster: session = cluster.connect(wait_for_all_pools=True) - # default is DCA RR for all hosts expected_hosts = set(cluster.metadata.all_hosts()) - rr1_queried_hosts = set() - rr2_queried_hosts = set() + num_hosts = len(expected_hosts) + assert num_hosts > 1, "Need at least 2 hosts for this test" - rs = session.execute(query, execution_profile='rr1') - rr1_queried_hosts.add(rs.response_future._current_host) - rs = session.execute(query, execution_profile='rr2') - rr2_queried_hosts.add(rs.response_future._current_host) + rr1_queried_hosts = [] + rr2_queried_hosts = [] + + for _ in range(num_hosts * 2): + rs = session.execute(query, execution_profile='rr1') + rr1_queried_hosts.append(rs.response_future._current_host) + rs = session.execute(query, execution_profile='rr2') + rr2_queried_hosts.append(rs.response_future._current_host) + + # Both policies should have queried all hosts + assert set(rr1_queried_hosts) == expected_hosts + assert set(rr2_queried_hosts) == expected_hosts - self.assertEqual(rr2_queried_hosts, rr1_queried_hosts) + # The order of hosts should demonstrate round-robin behavior + # After num_hosts queries, the pattern should repeat + for i in range(num_hosts): + assert rr1_queried_hosts[i] == rr1_queried_hosts[i + num_hosts] + assert rr2_queried_hosts[i] == rr2_queried_hosts[i + num_hosts] def test_ta_lbp(self): """ @@ -996,7 +1017,7 @@ def test_clone_shared_lbp(self): exec_profiles = {'rr1': rr1} with TestCluster(execution_profiles=exec_profiles) as cluster: session = cluster.connect(wait_for_all_pools=True) - self.assertGreater(len(cluster.metadata.all_hosts()), 1, "We only have one host connected at this point") + assert len(cluster.metadata.all_hosts()) > 1, "We only have one host connected at this point" rr1_clone = session.execution_profile_clone_update('rr1', row_factory=tuple_factory) cluster.add_execution_profile("rr1_clone", rr1_clone) @@ -1006,7 +1027,7 @@ def test_clone_shared_lbp(self): rr1_queried_hosts.add(rs.response_future._current_host) rs = session.execute(query, execution_profile='rr1_clone') rr1_clone_queried_hosts.add(rs.response_future._current_host) - self.assertNotEqual(rr1_clone_queried_hosts, rr1_queried_hosts) + assert rr1_clone_queried_hosts != rr1_queried_hosts def test_missing_exec_prof(self): """ @@ -1024,7 +1045,7 @@ def test_missing_exec_prof(self): exec_profiles = {'rr1': rr1, 'rr2': rr2} with TestCluster(execution_profiles=exec_profiles) as cluster: session = cluster.connect() - with self.assertRaises(ValueError): + with pytest.raises(ValueError): session.execute(query, execution_profile='rr3') @local @@ -1053,8 +1074,8 @@ def test_profile_pool_management(self): session = cluster.connect(wait_for_all_pools=True) pools = session.get_pool_state() # there are more hosts, but we connected to the ones in the lbp aggregate - self.assertGreater(len(cluster.metadata.all_hosts()), 2) - self.assertEqual(set(h.address for h in pools), set(('127.0.0.1', '127.0.0.2'))) + assert len(cluster.metadata.all_hosts()) > 2 + assert set(h.address for h in pools) == set(('127.0.0.1', '127.0.0.2')) # dynamically update pools on add node3 = ExecutionProfile( @@ -1064,7 +1085,7 @@ def test_profile_pool_management(self): ) cluster.add_execution_profile('node3', node3) pools = session.get_pool_state() - self.assertEqual(set(h.address for h in pools), set(('127.0.0.1', '127.0.0.2', '127.0.0.3'))) + assert set(h.address for h in pools) == set(('127.0.0.1', '127.0.0.2', '127.0.0.3')) @local def test_add_profile_timeout(self): @@ -1087,8 +1108,8 @@ def test_add_profile_timeout(self): with TestCluster(execution_profiles={EXEC_PROFILE_DEFAULT: node1}) as cluster: session = cluster.connect(wait_for_all_pools=True) pools = session.get_pool_state() - self.assertGreater(len(cluster.metadata.all_hosts()), 2) - self.assertEqual(set(h.address for h in pools), set(('127.0.0.1',))) + assert len(cluster.metadata.all_hosts()) > 2 + assert set(h.address for h in pools) == set(('127.0.0.1',)) node2 = ExecutionProfile( load_balancing_policy=HostFilterPolicy( @@ -1098,13 +1119,13 @@ def test_add_profile_timeout(self): start = time.time() try: - self.assertRaises(cassandra.OperationTimedOut, cluster.add_execution_profile, - 'profile_{0}'.format(i), + with pytest.raises(cassandra.OperationTimedOut): + cluster.add_execution_profile('profile_{0}'.format(i), node2, pool_wait_timeout=sys.float_info.min) break except AssertionError: end = time.time() - self.assertAlmostEqual(start, end, 1) + assert start == pytest.approx(end, abs=1e-1) else: raise Exception("add_execution_profile didn't timeout after {0} retries".format(max_retry_count)) @@ -1115,8 +1136,7 @@ def test_stale_connections_after_shutdown(self): """ for _ in range(10): with TestCluster(protocol_version=3) as cluster: - cluster.connect().execute("SELECT * FROM system_schema.keyspaces") - time.sleep(1) + cluster.connect(wait_for_all_pools=True).execute("SELECT * FROM system_schema.keyspaces") with TestCluster(protocol_version=3) as cluster: session = cluster.connect() @@ -1146,7 +1166,7 @@ def test_execute_query_timeout(self): # default is passed down default_profile = cluster.profile_manager.profiles[EXEC_PROFILE_DEFAULT] rs = session.execute(query) - self.assertEqual(rs.response_future.timeout, default_profile.request_timeout) + assert rs.response_future.timeout == default_profile.request_timeout # tiny timeout times out as expected tmp_profile = copy(default_profile) @@ -1156,14 +1176,14 @@ def test_execute_query_timeout(self): for _ in range(max_retry_count): start = time.time() try: - with self.assertRaises(cassandra.OperationTimedOut): + with pytest.raises(cassandra.OperationTimedOut): session.execute(query, execution_profile=tmp_profile) break except: import traceback traceback.print_exc() end = time.time() - self.assertAlmostEqual(start, end, 1) + assert start == pytest.approx(end, abs=1e-1) else: raise Exception("session.execute didn't time out in {0} tries".format(max_retry_count)) @@ -1175,27 +1195,35 @@ def test_replicas_are_queried(self): Then using HostFilterPolicy the replica is excluded from the considered hosts. By checking the trace we verify that there are no more replicas. + Requires tablets feature disabled. + @since 3.5 @jira_ticket PYTHON-653 @expected_result the replicas are queried for HostFilterPolicy @test_category metadata """ + ks_name = 'test_replicas_queried_ks' queried_hosts = set() tap_profile = ExecutionProfile( load_balancing_policy=TokenAwarePolicy(RoundRobinPolicy()) ) with TestCluster(execution_profiles={EXEC_PROFILE_DEFAULT: tap_profile}) as cluster: session = cluster.connect(wait_for_all_pools=True) + session.execute("DROP KEYSPACE IF EXISTS {}".format(ks_name)) + session.execute( + "CREATE KEYSPACE {} WITH replication = {{'class': 'NetworkTopologyStrategy', " + "'replication_factor': '1'}} AND tablets = {{'enabled': false}}".format(ks_name) + ) session.execute(''' - CREATE TABLE test1rf.table_with_big_key ( + CREATE TABLE {}.table_with_big_key ( k1 int, k2 int, k3 int, k4 int, - PRIMARY KEY((k1, k2, k3), k4))''') - prepared = session.prepare("""SELECT * from test1rf.table_with_big_key - WHERE k1 = ? AND k2 = ? AND k3 = ? AND k4 = ?""") + PRIMARY KEY((k1, k2, k3), k4))'''.format(ks_name)) + prepared = session.prepare("""SELECT * from {}.table_with_big_key + WHERE k1 = ? AND k2 = ? AND k3 = ? AND k4 = ?""".format(ks_name)) for i in range(10): result = session.execute(prepared, (i, i, i, i), trace=True) trace = result.response_future.get_query_trace(query_cl=ConsistencyLevel.ALL) @@ -1214,14 +1242,14 @@ def test_replicas_are_queried(self): execution_profiles={EXEC_PROFILE_DEFAULT: hfp_profile}) as cluster: session = cluster.connect(wait_for_all_pools=True) - prepared = session.prepare("""SELECT * from test1rf.table_with_big_key - WHERE k1 = ? AND k2 = ? AND k3 = ? AND k4 = ?""") + prepared = session.prepare("""SELECT * from {}.table_with_big_key + WHERE k1 = ? AND k2 = ? AND k3 = ? AND k4 = ?""".format(ks_name)) for _ in range(10): result = session.execute(prepared, (last_i, last_i, last_i, last_i), trace=True) trace = result.response_future.get_query_trace(query_cl=ConsistencyLevel.ALL) self._assert_replica_queried(trace, only_replicas=False) - session.execute('''DROP TABLE test1rf.table_with_big_key''') + session.execute('DROP KEYSPACE {}'.format(ks_name)) @greaterthanorequalcass30 @lessthanorequalcass40 @@ -1261,41 +1289,37 @@ def test_compact_option(self): "({i}, 'a{i}{i}', {i}{i}, {i}{i}, textAsBlob('b{i}{i}'))".format(i=i)) nc_results = nc_session.execute("SELECT * FROM compact_table") - self.assertEqual( - set(nc_results.current_rows), - {(1, u'a1', 11, 11, 'b1'), - (1, u'a11', 11, 11, 'b11'), - (2, u'a2', 22, 22, 'b2'), - (2, u'a22', 22, 22, 'b22'), - (3, u'a3', 33, 33, 'b3'), - (3, u'a33', 33, 33, 'b33'), - (4, u'a4', 44, 44, 'b4'), - (4, u'a44', 44, 44, 'b44')}) + assert set(nc_results.current_rows) == {(1, u'a1', 11, 11, 'b1'), + (1, u'a11', 11, 11, 'b11'), + (2, u'a2', 22, 22, 'b2'), + (2, u'a22', 22, 22, 'b22'), + (3, u'a3', 33, 33, 'b3'), + (3, u'a33', 33, 33, 'b33'), + (4, u'a4', 44, 44, 'b4'), + (4, u'a44', 44, 44, 'b44')} results = session.execute("SELECT * FROM compact_table") - self.assertEqual( - set(results.current_rows), - {(1, 11, 11), - (2, 22, 22), - (3, 33, 33), - (4, 44, 44)}) + assert set(results.current_rows) == {(1, 11, 11), + (2, 22, 22), + (3, 33, 33), + (4, 44, 44)} def _assert_replica_queried(self, trace, only_replicas=True): queried_hosts = set() for row in trace.events: queried_hosts.add(row.source) if only_replicas: - self.assertEqual(len(queried_hosts), 1, "The hosts queried where {}".format(queried_hosts)) + assert len(queried_hosts) == 1, "The hosts queried where {}".format(queried_hosts) else: - self.assertGreater(len(queried_hosts), 1, "The host queried was {}".format(queried_hosts)) + assert len(queried_hosts) > 1, "The host queried was {}".format(queried_hosts) return queried_hosts def _check_trace(self, trace): - self.assertIsNotNone(trace.request_type) - self.assertIsNotNone(trace.duration) - self.assertIsNotNone(trace.started_at) - self.assertIsNotNone(trace.coordinator) - self.assertIsNotNone(trace.events) + assert trace.request_type is not None + assert trace.duration is not None + assert trace.started_at is not None + assert trace.coordinator is not None + assert trace.events is not None class LocalHostAdressTranslator(AddressTranslator): @@ -1327,7 +1351,7 @@ def test_address_translator_basic(self): lh_ad = LocalHostAdressTranslator({'127.0.0.1': '127.0.0.1', '127.0.0.2': '127.0.0.1', '127.0.0.3': '127.0.0.1'}) c = TestCluster(address_translator=lh_ad) c.connect() - self.assertEqual(len(c.metadata.all_hosts()), 1) + assert len(c.metadata.all_hosts()) == 1 c.shutdown() def test_address_translator_with_mixed_nodes(self): @@ -1348,7 +1372,7 @@ def test_address_translator_with_mixed_nodes(self): c = TestCluster(address_translator=lh_ad) c.connect() for host in c.metadata.all_hosts(): - self.assertEqual(adder_map.get(host.address), host.broadcast_address) + assert adder_map.get(host.address) == host.broadcast_address c.shutdown() @local @@ -1372,8 +1396,8 @@ def test_no_connect(self): @test_category configuration """ with TestCluster() as cluster: - self.assertFalse(cluster.is_shutdown) - self.assertTrue(cluster.is_shutdown) + assert not cluster.is_shutdown + assert cluster.is_shutdown def test_simple_nested(self): """ @@ -1387,11 +1411,11 @@ def test_simple_nested(self): """ with TestCluster(**self.cluster_kwargs) as cluster: with cluster.connect() as session: - self.assertFalse(cluster.is_shutdown) - self.assertFalse(session.is_shutdown) - self.assertTrue(session.execute('select release_version from system.local').one()) - self.assertTrue(session.is_shutdown) - self.assertTrue(cluster.is_shutdown) + assert not cluster.is_shutdown + assert not session.is_shutdown + assert session.execute('select release_version from system.local').one() + assert session.is_shutdown + assert cluster.is_shutdown def test_cluster_no_session(self): """ @@ -1405,11 +1429,11 @@ def test_cluster_no_session(self): """ with TestCluster(**self.cluster_kwargs) as cluster: session = cluster.connect() - self.assertFalse(cluster.is_shutdown) - self.assertFalse(session.is_shutdown) - self.assertTrue(session.execute('select release_version from system.local').one()) - self.assertTrue(session.is_shutdown) - self.assertTrue(cluster.is_shutdown) + assert not cluster.is_shutdown + assert not session.is_shutdown + assert session.execute('select release_version from system.local').one() + assert session.is_shutdown + assert cluster.is_shutdown def test_session_no_cluster(self): """ @@ -1424,18 +1448,18 @@ def test_session_no_cluster(self): cluster = TestCluster(**self.cluster_kwargs) unmanaged_session = cluster.connect() with cluster.connect() as session: - self.assertFalse(cluster.is_shutdown) - self.assertFalse(session.is_shutdown) - self.assertFalse(unmanaged_session.is_shutdown) - self.assertTrue(session.execute('select release_version from system.local').one()) - self.assertTrue(session.is_shutdown) - self.assertFalse(cluster.is_shutdown) - self.assertFalse(unmanaged_session.is_shutdown) + assert not cluster.is_shutdown + assert not session.is_shutdown + assert not unmanaged_session.is_shutdown + assert session.execute('select release_version from system.local').one() + assert session.is_shutdown + assert not cluster.is_shutdown + assert not unmanaged_session.is_shutdown unmanaged_session.shutdown() - self.assertTrue(unmanaged_session.is_shutdown) - self.assertFalse(cluster.is_shutdown) + assert unmanaged_session.is_shutdown + assert not cluster.is_shutdown cluster.shutdown() - self.assertTrue(cluster.is_shutdown) + assert cluster.is_shutdown class HostStateTest(unittest.TestCase): @@ -1458,7 +1482,7 @@ def test_down_event_with_active_connection(self): cluster.on_down(random_host, False) for _ in range(10): new_host = cluster.metadata.all_hosts()[0] - self.assertTrue(new_host.is_up, "Host was not up on iteration {0}".format(_)) + assert new_host.is_up, "Host was not up on iteration {0}".format(_) time.sleep(.01) pool = session._pools.get(random_host) @@ -1471,7 +1495,7 @@ def test_down_event_with_active_connection(self): was_marked_down = True break time.sleep(.01) - self.assertTrue(was_marked_down) + assert was_marked_down @local @@ -1490,7 +1514,7 @@ def test_prepare_on_ignored_hosts(self): hosts = cluster.metadata.all_hosts() session.execute("CREATE KEYSPACE clustertests " "WITH replication = " - "{'class': 'SimpleStrategy', 'replication_factor': '1'}") + "{'class': 'NetworkTopologyStrategy', 'replication_factor': '1'}") session.execute("CREATE TABLE clustertests.tab (a text, PRIMARY KEY (a))") # assign to an unused variable so cluster._prepared_statements retains # reference @@ -1509,48 +1533,7 @@ def test_prepare_on_ignored_hosts(self): # the length of mock_calls will vary, but all should use the unignored # address for c in cluster.connection_factory.mock_calls: - self.assertEqual(unignored_address, c.args[0].address) - cluster.shutdown() - - -@protocolv6 -class BetaProtocolTest(unittest.TestCase): - - @protocolv6 - def test_invalid_protocol_version_beta_option(self): - """ - Test cluster connection with protocol v6 and beta flag not set - - @since 3.7.0 - @jira_ticket PYTHON-614, PYTHON-1232 - @expected_result client shouldn't connect with V6 and no beta flag set - - @test_category connection - """ - - - cluster = TestCluster(protocol_version=cassandra.ProtocolVersion.V6, allow_beta_protocol_version=False) - try: - with self.assertRaises(NoHostAvailable): - cluster.connect() - except Exception as e: - self.fail("Unexpected error encountered {0}".format(e.message)) - - @protocolv6 - def test_valid_protocol_version_beta_options_connect(self): - """ - Test cluster connection with protocol version 5 and beta flag set - - @since 3.7.0 - @jira_ticket PYTHON-614, PYTHON-1232 - @expected_result client should connect with protocol v6 and beta flag set. - - @test_category connection - """ - cluster = Cluster(protocol_version=cassandra.ProtocolVersion.V6, allow_beta_protocol_version=True) - session = cluster.connect() - self.assertEqual(cluster.protocol_version, cassandra.ProtocolVersion.V6) - self.assertTrue(session.execute("select release_version from system.local").one()) + assert unignored_address == c.args[0].address cluster.shutdown() @@ -1569,10 +1552,10 @@ def test_deprecation_warnings_legacy_parameters(self): with warnings.catch_warnings(record=True) as w: TestCluster(load_balancing_policy=RoundRobinPolicy()) logging.info(w) - self.assertGreaterEqual(len(w), 1) - self.assertTrue(any(["Legacy execution parameters will be removed in 4.0. " + assert len(w) >= 1 + assert any(["Legacy execution parameters will be removed in 4.0. " "Consider using execution profiles." in - str(wa.message) for wa in w])) + str(wa.message) for wa in w]) def test_deprecation_warnings_meta_refreshed(self): """ @@ -1589,9 +1572,9 @@ def test_deprecation_warnings_meta_refreshed(self): cluster = TestCluster() cluster.set_meta_refresh_enabled(True) logging.info(w) - self.assertGreaterEqual(len(w), 1) - self.assertTrue(any(["Cluster.set_meta_refresh_enabled is deprecated and will be removed in 4.0." in - str(wa.message) for wa in w])) + assert len(w) >= 1 + assert any(["Cluster.set_meta_refresh_enabled is deprecated and will be removed in 4.0." in + str(wa.message) for wa in w]) def test_deprecation_warning_default_consistency_level(self): """ @@ -1608,6 +1591,6 @@ def test_deprecation_warning_default_consistency_level(self): cluster = TestCluster() session = cluster.connect() session.default_consistency_level = ConsistencyLevel.ONE - self.assertGreaterEqual(len(w), 1) - self.assertTrue(any(["Setting the consistency level at the session level will be removed in 4.0" in - str(wa.message) for wa in w])) + assert len(w) >= 1 + assert any(["Setting the consistency level at the session level will be removed in 4.0" in + str(wa.message) for wa in w]) diff --git a/tests/integration/standard/test_concurrent.py b/tests/integration/standard/test_concurrent.py index ba891b4bd0..5e6b1ffd59 100644 --- a/tests/integration/standard/test_concurrent.py +++ b/tests/integration/standard/test_concurrent.py @@ -25,6 +25,7 @@ from tests.integration import use_singledc, PROTOCOL_VERSION, TestCluster import unittest +import pytest log = logging.getLogger(__name__) @@ -45,8 +46,6 @@ def setUpClass(cls): EXEC_PROFILE_DICT: ExecutionProfile(row_factory=dict_factory) } ) - if PROTOCOL_VERSION < 3: - cls.cluster.set_core_connections_per_host(HostDistance.LOCAL, 1) cls.session = cls.cluster.connect() @classmethod @@ -90,10 +89,10 @@ def execute_concurrent_base(self, test_fn, validate_fn, zip_args=True): results = \ test_fn(self.session, list(zip(statements, parameters))) if zip_args else \ test_fn(self.session, statement, parameters) - self.assertEqual(num_statements, len(results)) + assert num_statements == len(results) for success, result in results: - self.assertTrue(success) - self.assertFalse(result) + assert success + assert not result # read statement = SimpleStatement( @@ -108,12 +107,12 @@ def execute_concurrent_base(self, test_fn, validate_fn, zip_args=True): validate_fn(num_statements, results) def execute_concurrent_valiate_tuple(self, num_statements, results): - self.assertEqual(num_statements, len(results)) - self.assertEqual([(True, [(i,)]) for i in range(num_statements)], results) + assert num_statements == len(results) + assert [(True, [(i,)]) for i in range(num_statements)] == results def execute_concurrent_valiate_dict(self, num_statements, results): - self.assertEqual(num_statements, len(results)) - self.assertEqual([(True, [{"v":i}]) for i in range(num_statements)], results) + assert num_statements == len(results) + assert [(True, [{"v":i}]) for i in range(num_statements)] == results def test_execute_concurrent(self): self.execute_concurrent_base(self.execute_concurrent_helper, \ @@ -155,14 +154,14 @@ def test_execute_concurrent_with_args_generator(self): results = self.execute_concurrent_args_helper(self.session, statement, parameters, results_generator=True) for success, result in results: - self.assertTrue(success) - self.assertFalse(result) + assert success + assert not result results = self.execute_concurrent_args_helper(self.session, statement, parameters, results_generator=True) for result in results: - self.assertTrue(isinstance(result, ExecutionResult)) - self.assertTrue(result.success) - self.assertFalse(result.result_or_exc) + assert isinstance(result, ExecutionResult) + assert result.success + assert not result.result_or_exc # read statement = SimpleStatement( @@ -174,8 +173,9 @@ def test_execute_concurrent_with_args_generator(self): for i in range(num_statements): result = next(results) - self.assertEqual((True, [(i,)]), result) - self.assertRaises(StopIteration, next, results) + assert (True, [(i,)]) == result + with pytest.raises(StopIteration): + next(results) def test_execute_concurrent_paged_result(self): if PROTOCOL_VERSION < 2: @@ -190,10 +190,10 @@ def test_execute_concurrent_paged_result(self): parameters = [(i, i) for i in range(num_statements)] results = self.execute_concurrent_args_helper(self.session, statement, parameters) - self.assertEqual(num_statements, len(results)) + assert num_statements == len(results) for success, result in results: - self.assertTrue(success) - self.assertFalse(result) + assert success + assert not result # read statement = SimpleStatement( @@ -202,11 +202,11 @@ def test_execute_concurrent_paged_result(self): fetch_size=int(num_statements / 2)) results = self.execute_concurrent_args_helper(self.session, statement, [(num_statements,)]) - self.assertEqual(1, len(results)) - self.assertTrue(results[0][0]) + assert 1 == len(results) + assert results[0][0] result = results[0][1] - self.assertTrue(result.has_more_pages) - self.assertEqual(num_statements, sum(1 for _ in result)) + assert result.has_more_pages + assert num_statements == sum(1 for _ in result) def test_execute_concurrent_paged_result_generator(self): """ @@ -233,7 +233,7 @@ def test_execute_concurrent_paged_result_generator(self): parameters = [(i, i) for i in range(num_statements)] results = self.execute_concurrent_args_helper(self.session, statement, parameters, results_generator=True) - self.assertEqual(num_statements, sum(1 for _ in results)) + assert num_statements == sum(1 for _ in results) # read statement = SimpleStatement( @@ -250,7 +250,7 @@ def test_execute_concurrent_paged_result_generator(self): for _ in paged_result: found_results += 1 - self.assertEqual(found_results, num_statements) + assert found_results == num_statements def test_first_failure(self): statements = cycle(("INSERT INTO test3rf.test (k, v) VALUES (%s, %s)", )) @@ -259,9 +259,8 @@ def test_first_failure(self): # we'll get an error back from the server parameters[57] = ('efefef', 'awefawefawef') - self.assertRaises( - InvalidRequest, - execute_concurrent, self.session, list(zip(statements, parameters)), raise_on_first_error=True) + with pytest.raises(InvalidRequest): + execute_concurrent(self.session, list(zip(statements, parameters)), raise_on_first_error=True) def test_first_failure_client_side(self): statement = SimpleStatement( @@ -273,9 +272,8 @@ def test_first_failure_client_side(self): # the driver will raise an error when binding the params parameters[57] = 1 - self.assertRaises( - TypeError, - execute_concurrent, self.session, list(zip(statements, parameters)), raise_on_first_error=True) + with pytest.raises(TypeError): + execute_concurrent(self.session, list(zip(statements, parameters)), raise_on_first_error=True) def test_no_raise_on_first_failure(self): statement = SimpleStatement( @@ -290,11 +288,11 @@ def test_no_raise_on_first_failure(self): results = execute_concurrent(self.session, list(zip(statements, parameters)), raise_on_first_error=False) for i, (success, result) in enumerate(results): if i == 57: - self.assertFalse(success) - self.assertIsInstance(result, InvalidRequest) + assert not success + assert isinstance(result, InvalidRequest) else: - self.assertTrue(success) - self.assertFalse(result) + assert success + assert not result def test_no_raise_on_first_failure_client_side(self): statement = SimpleStatement( @@ -309,8 +307,8 @@ def test_no_raise_on_first_failure_client_side(self): results = execute_concurrent(self.session, list(zip(statements, parameters)), raise_on_first_error=False) for i, (success, result) in enumerate(results): if i == 57: - self.assertFalse(success) - self.assertIsInstance(result, TypeError) + assert not success + assert isinstance(result, TypeError) else: - self.assertTrue(success) - self.assertFalse(result) + assert success + assert not result diff --git a/tests/integration/standard/test_concurrent_schema_change_and_node_kill.py b/tests/integration/standard/test_concurrent_schema_change_and_node_kill.py index aeda381c0d..9a9a3d325f 100644 --- a/tests/integration/standard/test_concurrent_schema_change_and_node_kill.py +++ b/tests/integration/standard/test_concurrent_schema_change_and_node_kill.py @@ -8,7 +8,7 @@ def setup_module(): - use_cluster('test_concurrent_schema_change_and_node_kill', [3], start=True) + use_cluster('test_schema_kill', [3], start=True) @local class TestConcurrentSchemaChangeAndNodeKill(unittest.TestCase): @@ -27,7 +27,7 @@ def test_schema_change_after_node_kill(self): "DROP KEYSPACE IF EXISTS ks_deadlock;") self.session.execute( "CREATE KEYSPACE IF NOT EXISTS ks_deadlock " - "WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '2' };") + "WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '2' };") self.session.set_keyspace('ks_deadlock') self.session.execute("CREATE TABLE IF NOT EXISTS some_table(k int, c int, v int, PRIMARY KEY (k, v));") self.session.execute("INSERT INTO some_table (k, c, v) VALUES (1, 2, 3);") diff --git a/tests/integration/standard/test_connection.py b/tests/integration/standard/test_connection.py index b86a4445af..df0f568c2c 100644 --- a/tests/integration/standard/test_connection.py +++ b/tests/integration/standard/test_connection.py @@ -22,16 +22,17 @@ from threading import Thread, Event import time from unittest import SkipTest +import pytest from cassandra import ConsistencyLevel, OperationTimedOut, DependencyException from cassandra.cluster import NoHostAvailable, ConnectionShutdown, ExecutionProfile, EXEC_PROFILE_DEFAULT from cassandra.protocol import QueryMessage from cassandra.policies import HostFilterPolicy, RoundRobinPolicy, HostStateListener -from cassandra.pool import HostConnectionPool from tests import is_monkey_patched from tests.integration import use_singledc, get_node, CASSANDRA_IP, local, \ requiresmallclockgranularity, greaterthancass20, TestCluster +from tests.util import wait_until try: import cassandra.io.asyncorereactor @@ -132,7 +133,7 @@ def test_heart_beat_timeout(self): host = "127.0.0.1:9042" node = get_node(1) initial_connections = self.fetch_connections(host, self.cluster) - self.assertNotEqual(len(initial_connections), 0) + assert len(initial_connections) != 0 self.cluster.register_listener(test_listener) # Pause the node try: @@ -140,9 +141,10 @@ def test_heart_beat_timeout(self): # Wait for connections associated with this host go away self.wait_for_no_connections(host, self.cluster) - # Wait to seconds for the driver to be notified - time.sleep(2) - self.assertTrue(test_listener.host_down) + # Wait for the driver to detect the host is down + wait_until( + lambda: test_listener.host_down, + delay=0.5, max_attempts=20) # Resume paused node finally: node.resume() @@ -155,12 +157,12 @@ def test_heart_beat_timeout(self): current_host = str(rs._current_host) count += 1 time.sleep(.1) - self.assertLess(count, 100, "Never connected to the first node") + assert count < 100, "Never connected to the first node" new_connections = self.wait_for_connections(host, self.cluster) - self.assertFalse(test_listener.host_down) + assert not test_listener.host_down # Make sure underlying new connections don't match previous ones for connection in initial_connections: - self.assertFalse(connection in new_connections) + assert not connection in new_connections def fetch_connections(self, host, cluster): # Given a cluster object and host grab all connection associated with that host @@ -168,12 +170,8 @@ def fetch_connections(self, host, cluster): holders = cluster.get_connection_holders() for conn in holders: if host == str(getattr(conn, 'host', '')): - if isinstance(conn, HostConnectionPool): - if conn._connections is not None and (conn._connections): - connections.extend(conn._connections) - else: - if conn._connections and conn._connections: - connections.extend(conn._connections.values()) + if conn._connections and conn._connections: + connections.extend(conn._connections.values()) return connections def wait_for_connections(self, host, cluster): @@ -184,7 +182,7 @@ def wait_for_connections(self, host, cluster): if connections: return connections time.sleep(.1) - self.fail("No new connections found") + pytest.fail("No new connections found") def wait_for_no_connections(self, host, cluster): retry = 0 @@ -194,7 +192,7 @@ def wait_for_no_connections(self, host, cluster): if not connections: return time.sleep(.5) - self.fail("Connections never cleared") + pytest.fail("Connections never cleared") class ConnectionTests(object): @@ -401,10 +399,10 @@ def test_connect_timeout(self): conn.close() except Exception as e: end = time.time() - self.assertAlmostEqual(start, end, 1) + assert start == pytest.approx(end, abs=1e-1) exception_thrown = True break - self.assertTrue(exception_thrown) + assert exception_thrown def test_subclasses_share_loop(self): @@ -425,7 +423,7 @@ class C2(self.klass): self.addCleanup(clusterC1.shutdown) self.addCleanup(clusterC2.shutdown) - self.assertEqual(len(get_eventloop_threads(self.event_loop_name)), 1) + assert len(get_eventloop_threads(self.event_loop_name)) == 1 def get_eventloop_threads(name): diff --git a/tests/integration/standard/test_control_connection.py b/tests/integration/standard/test_control_connection.py index b6e0d3ccd3..f0c41dde14 100644 --- a/tests/integration/standard/test_control_connection.py +++ b/tests/integration/standard/test_control_connection.py @@ -14,13 +14,17 @@ # # # +from threading import Event + from cassandra import InvalidRequest import unittest +import requests from cassandra.protocol import ConfigurationException -from tests.integration import use_singledc, PROTOCOL_VERSION, TestCluster, greaterthanorequalcass40, notdse +from tests.integration import use_singledc, PROTOCOL_VERSION, TestCluster, greaterthanorequalcass40, \ + xfail_scylla_version_lt from tests.integration.datatype_utils import update_datatypes @@ -64,7 +68,7 @@ def test_drop_keyspace(self): self.session = self.cluster.connect() self.session.execute(""" CREATE KEYSPACE keyspacetodrop - WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1' } + WITH replication = { 'class' : 'NetworkTopologyStrategy', 'replication_factor': '1' } """) self.session.set_keyspace("keyspacetodrop") self.session.execute("CREATE TYPE user (age int, name text)") @@ -72,7 +76,7 @@ def test_drop_keyspace(self): cc_id_pre_drop = id(self.cluster.control_connection._connection) self.session.execute("DROP KEYSPACE keyspacetodrop") cc_id_post_drop = id(self.cluster.control_connection._connection) - self.assertEqual(cc_id_post_drop, cc_id_pre_drop) + assert cc_id_post_drop == cc_id_pre_drop def test_get_control_connection_host(self): """ @@ -86,23 +90,24 @@ def test_get_control_connection_host(self): """ host = self.cluster.get_control_connection_host() - self.assertEqual(host, None) + assert host is None self.session = self.cluster.connect() cc_host = self.cluster.control_connection._connection.host host = self.cluster.get_control_connection_host() - self.assertEqual(host.address, cc_host) - self.assertEqual(host.is_up, True) + assert host.address == cc_host + assert host.is_up # reconnect and make sure that the new host is reflected correctly self.cluster.control_connection._reconnect() - new_host = self.cluster.get_control_connection_host() - self.assertNotEqual(host, new_host) + new_host1 = self.cluster.get_control_connection_host() + + self.cluster.control_connection._reconnect() + new_host2 = self.cluster.get_control_connection_host() + + assert new_host1 != new_host2 - # TODO: enable after https://github.com/scylladb/python-driver/issues/121 is fixed - @unittest.skip('Fails on scylla due to the broadcast_rpc_port is None') - @notdse @greaterthanorequalcass40 def test_control_connection_port_discovery(self): """ @@ -114,17 +119,73 @@ def test_control_connection_port_discovery(self): self.cluster = TestCluster() host = self.cluster.get_control_connection_host() - self.assertEqual(host, None) + assert host is None self.session = self.cluster.connect() cc_endpoint = self.cluster.control_connection._connection.endpoint host = self.cluster.get_control_connection_host() - self.assertEqual(host.endpoint, cc_endpoint) - self.assertEqual(host.is_up, True) + assert host.endpoint == cc_endpoint + assert host.is_up hosts = self.cluster.metadata.all_hosts() - self.assertEqual(3, len(hosts)) + assert len(hosts) == 3 for host in hosts: - self.assertEqual(9042, host.broadcast_rpc_port) - self.assertEqual(7000, host.broadcast_port) + assert 9042 == host.broadcast_rpc_port + assert 7000 == host.broadcast_port + + @xfail_scylla_version_lt(reason='scylladb/scylladb#26992 - system.client_routes is not yet supported', + scylla_version="2026.1.0") + def test_client_routes_change_event(self): + cluster = TestCluster() + + # Establish control connection + self.session = self.cluster.connect() + + flag = Event() + + connection_ids = ["anytext", "11510f50-f906-4844-8c74-49ddab9ac6a9"] + host_ids = ["1a13fa42-c45b-410f-8ba5-58b42ada9c12", "aa13fa42-c45b-410f-8ba5-58b42ada9c12"] + got_connection_ids = [] + got_host_ids = [] + + def on_event(event): + nonlocal got_connection_ids + nonlocal got_host_ids + try: + assert event.get("change_type") == "UPDATE_NODES" + got_connection_ids = event.get("connection_ids") + got_host_ids = event.get("host_ids") + finally: + flag.set() + + self.session.cluster.control_connection._connection.register_watchers({"CLIENT_ROUTES_CHANGE": on_event}) + + try: + payload = [ + { + "connection_id": connection_ids[0], # Should be a UUID if API requires that + "host_id": host_ids[0], + "address": "localhost", + "port": 9042, + }, + { + "connection_id": connection_ids[1], + "host_id": host_ids[1], + "address": "localhost", + "port": 9042, + } + ] + response = requests.post( + "http://" + cluster.contact_points[0] + ":10000/v2/client-routes", + json=payload, + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + }) + assert response.status_code == 200 + assert flag.wait(20), "Schema change event was not received after registering watchers" + assert set(got_connection_ids) == set(connection_ids) + assert set(got_host_ids) == set(host_ids) + finally: + cluster.shutdown() diff --git a/tests/integration/standard/test_control_connection_query_fallback.py b/tests/integration/standard/test_control_connection_query_fallback.py new file mode 100644 index 0000000000..e64763a72c --- /dev/null +++ b/tests/integration/standard/test_control_connection_query_fallback.py @@ -0,0 +1,115 @@ +# Copyright DataStax, Inc. +# +# 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 +# +# http://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 unittest + +import pytest + +from cassandra.cluster import ControlConnectionQueryFallback, NoHostAvailable + +from tests.integration import USE_CASS_EXTERNAL, TestCluster, local, remove_cluster, use_cluster + + +_CLUSTER_NAME = "control_connection_query_fallback" +_UNREACHABLE_BROADCAST_RPC_ADDRESS = "127.255.255.1" + + +def setup_module(): + if USE_CASS_EXTERNAL: + return + + remove_cluster() + + ccm_cluster = use_cluster(_CLUSTER_NAME, [1], start=False) + ccm_cluster.nodes["node1"].set_configuration_options(values={ + "broadcast_rpc_address": _UNREACHABLE_BROADCAST_RPC_ADDRESS, + }) + ccm_cluster.start(wait_for_binary_proto=True, wait_other_notice=True) + + +def teardown_module(): + if USE_CASS_EXTERNAL: + return + + remove_cluster() + + +@local +class ControlConnectionQueryFallbackIntegrationTests(unittest.TestCase): + + def setUp(self): + self.cluster = None + + def tearDown(self): + if self.cluster is not None: + self.cluster.shutdown() + + def _assert_unreachable_broadcast_rpc_metadata(self): + hosts = self.cluster.metadata.all_hosts() + assert len(hosts) == 1 + + host = hosts[0] + assert host.broadcast_rpc_address == _UNREACHABLE_BROADCAST_RPC_ADDRESS + assert host.endpoint.address == _UNREACHABLE_BROADCAST_RPC_ADDRESS + return host + + def test_disabled_raises_when_broadcast_rpc_address_is_unreachable(self): + self.cluster = TestCluster( + allow_control_connection_query_fallback=ControlConnectionQueryFallback.Disabled, + connect_timeout=1, + monitor_reporting_enabled=False, + ) + + with pytest.raises(NoHostAvailable): + self.cluster.connect() + + self._assert_unreachable_broadcast_rpc_metadata() + assert self.cluster.control_connection._connection is not None + assert self.cluster.get_all_pools() == [] + + def test_fallback_executes_queries_when_broadcast_rpc_address_is_unreachable(self): + self.cluster = TestCluster( + allow_control_connection_query_fallback=ControlConnectionQueryFallback.Fallback, + connect_timeout=1, + monitor_reporting_enabled=False, + ) + + session = self.cluster.connect() + + self._assert_unreachable_broadcast_rpc_metadata() + assert session._initial_connect_futures + assert list(session.get_pools()) == [] + + row = session.execute( + "SELECT release_version, rpc_address FROM system.local WHERE key='local'").one() + assert str(row.rpc_address) == _UNREACHABLE_BROADCAST_RPC_ADDRESS + assert row.release_version + + def test_no_node_pool_fallback_executes_queries_without_creating_pools(self): + self.cluster = TestCluster( + allow_control_connection_query_fallback=ControlConnectionQueryFallback.SkipPoolCreation, + connect_timeout=1, + monitor_reporting_enabled=False, + ) + + session = self.cluster.connect() + + self._assert_unreachable_broadcast_rpc_metadata() + assert session._initial_connect_futures == set() + assert list(session.get_pools()) == [] + + row = session.execute( + "SELECT release_version, rpc_address FROM system.local WHERE key='local'").one() + assert str(row.rpc_address) == _UNREACHABLE_BROADCAST_RPC_ADDRESS + assert row.release_version diff --git a/tests/integration/standard/test_custom_cluster.py b/tests/integration/standard/test_custom_cluster.py index c1eabbfd1f..4eb62e43bc 100644 --- a/tests/integration/standard/test_custom_cluster.py +++ b/tests/integration/standard/test_custom_cluster.py @@ -17,6 +17,7 @@ from tests.util import wait_until, wait_until_not_raised import unittest +import pytest def setup_module(): @@ -44,7 +45,7 @@ def test_connection_honor_cluster_port(self): All hosts should be marked as up and we should be able to execute queries on it. """ cluster = TestCluster() - with self.assertRaises(NoHostAvailable): + with pytest.raises(NoHostAvailable): cluster.connect() # should fail on port 9042 cluster = TestCluster(port=9046) @@ -52,5 +53,5 @@ def test_connection_honor_cluster_port(self): wait_until(lambda: len(cluster.metadata.all_hosts()) == 3, 1, 5) for host in cluster.metadata.all_hosts(): - self.assertTrue(host.is_up) + assert host.is_up session.execute("select * from system.local where key='local'", host=host) diff --git a/tests/integration/standard/test_custom_payload.py b/tests/integration/standard/test_custom_payload.py index 92372972c6..fc58081070 100644 --- a/tests/integration/standard/test_custom_payload.py +++ b/tests/integration/standard/test_custom_payload.py @@ -19,6 +19,7 @@ from tests.integration import (use_singledc, PROTOCOL_VERSION, local, TestCluster, requires_custom_payload) +import pytest def setup_module(): @@ -148,7 +149,7 @@ def validate_various_custom_payloads(self, statement): # Add one custom payload to this is too many key value pairs and should fail custom_payload[str(65535)] = b'x' - with self.assertRaises(ValueError): + with pytest.raises(ValueError): self.execute_async_validate_custom_payload(statement=statement, custom_payload=custom_payload) def execute_async_validate_custom_payload(self, statement, custom_payload): @@ -164,4 +165,4 @@ def execute_async_validate_custom_payload(self, statement, custom_payload): response_future = self.session.execute_async(statement, custom_payload=custom_payload) response_future.result() returned_custom_payload = response_future.custom_payload - self.assertEqual(custom_payload, returned_custom_payload) + assert custom_payload == returned_custom_payload diff --git a/tests/integration/standard/test_custom_protocol_handler.py b/tests/integration/standard/test_custom_protocol_handler.py index 35dba6c1b5..e7d336014f 100644 --- a/tests/integration/standard/test_custom_protocol_handler.py +++ b/tests/integration/standard/test_custom_protocol_handler.py @@ -20,18 +20,19 @@ ContinuousPagingOptions, NoHostAvailable) from cassandra import ProtocolVersion, ConsistencyLevel -from tests.integration import use_singledc, drop_keyspace_shutdown_cluster, \ - greaterthanorequalcass30, execute_with_long_wait_retry, greaterthanorequaldse51, greaterthanorequalcass3_10, \ - TestCluster, greaterthanorequalcass40, requirecassandra +from tests.integration import use_single_node, drop_keyspace_shutdown_cluster, \ + greaterthanorequalcass30, execute_with_long_wait_retry, greaterthanorequalcass3_10, \ + TestCluster, greaterthanorequalcass40 from tests.integration.datatype_utils import update_datatypes, PRIMITIVE_DATATYPES from tests.integration.standard.utils import create_table_with_all_types, get_all_primitive_params import uuid from unittest import mock +import pytest def setup_module(): - use_singledc() + use_single_node() update_datatypes() @@ -41,8 +42,9 @@ class CustomProtocolHandlerTest(unittest.TestCase): def setUpClass(cls): cls.cluster = TestCluster() cls.session = cls.cluster.connect() - cls.session.execute("CREATE KEYSPACE custserdes WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1'}") + cls.session.execute("CREATE KEYSPACE custserdes WITH replication = { 'class' : 'NetworkTopologyStrategy', 'replication_factor': '1'}") cls.session.set_keyspace("custserdes") + cls.session.execute("CREATE TABLE IF NOT EXISTS custserdes.test (k int PRIMARY KEY, v int)") @classmethod def tearDownClass(cls): @@ -71,20 +73,20 @@ def test_custom_raw_uuid_row_results(self): result = session.execute("SELECT schema_version FROM system.local WHERE key='local'") uuid_type = result.one()[0] - self.assertEqual(type(uuid_type), uuid.UUID) + assert type(uuid_type) == uuid.UUID # use our custom protocol handlder session.client_protocol_handler = CustomTestRawRowType result_set = session.execute("SELECT schema_version FROM system.local WHERE key='local'") raw_value = result_set.one()[0] - self.assertTrue(isinstance(raw_value, bytes)) - self.assertEqual(len(raw_value), 16) + assert isinstance(raw_value, bytes) + assert len(raw_value) == 16 # Ensure that we get normal uuid back when we re-connect session.client_protocol_handler = ProtocolHandler result_set = session.execute("SELECT schema_version FROM system.local WHERE key='local'") uuid_type = result_set.one()[0] - self.assertEqual(type(uuid_type), uuid.UUID) + assert type(uuid_type) == uuid.UUID cluster.shutdown() def test_custom_raw_row_results_all_types(self): @@ -115,42 +117,9 @@ def test_custom_raw_row_results_all_types(self): params = get_all_primitive_params(0) results = session.execute("SELECT {0} FROM alltypes WHERE primkey=0".format(columns_string)).one() for expected, actual in zip(params, results): - self.assertEqual(actual, expected) + assert actual == expected # Ensure we have covered the various primitive types - self.assertEqual(len(CustomResultMessageTracked.checked_rev_row_set), len(PRIMITIVE_DATATYPES)-1) - cluster.shutdown() - - @unittest.expectedFailure - @requirecassandra - @greaterthanorequalcass40 - def test_protocol_divergence_v5_fail_by_continuous_paging(self): - """ - Test to validate that V5 and DSE_V1 diverge. ContinuousPagingOptions is not supported by V5 - - @since DSE 2.0b3 GRAPH 1.0b1 - @jira_ticket PYTHON-694 - @expected_result NoHostAvailable will be risen when the continuous_paging_options parameter is set - - @test_category connection - """ - cluster = TestCluster(protocol_version=ProtocolVersion.V5, allow_beta_protocol_version=True) - session = cluster.connect() - - max_pages = 4 - max_pages_per_second = 3 - continuous_paging_options = ContinuousPagingOptions(max_pages=max_pages, - max_pages_per_second=max_pages_per_second) - - future = self._send_query_message(session, timeout=session.default_timeout, - consistency_level=ConsistencyLevel.ONE, - continuous_paging_options=continuous_paging_options) - - # This should raise NoHostAvailable because continuous paging is not supported under ProtocolVersion.DSE_V1 - with self.assertRaises(NoHostAvailable) as context: - future.result() - self.assertIn("Continuous paging may only be used with protocol version ProtocolVersion.DSE_V1 or higher", - str(context.exception)) - + assert len(CustomResultMessageTracked.checked_rev_row_set) == len(PRIMITIVE_DATATYPES)-1 cluster.shutdown() @greaterthanorequalcass30 @@ -169,7 +138,6 @@ def test_protocol_divergence_v4_fail_by_flag_uses_int(self): int_flag=True) @unittest.expectedFailure - @requirecassandra @greaterthanorequalcass40 def test_protocol_v5_uses_flag_int(self): """ @@ -183,21 +151,7 @@ def test_protocol_v5_uses_flag_int(self): self._protocol_divergence_fail_by_flag_uses_int(ProtocolVersion.V5, uses_int_query_flag=True, beta=True, int_flag=True) - @greaterthanorequaldse51 - def test_protocol_dsev1_uses_flag_int(self): - """ - Test to validate that the _PAGE_SIZE_FLAG is treated correctly using write_uint for DSE_V1 - - @jira_ticket PYTHON-694 - @expected_result the fetch_size=1 parameter will be honored - - @test_category connection - """ - self._protocol_divergence_fail_by_flag_uses_int(ProtocolVersion.DSE_V1, uses_int_query_flag=True, - int_flag=True) - @unittest.expectedFailure - @requirecassandra @greaterthanorequalcass40 def test_protocol_divergence_v5_fail_by_flag_uses_int(self): """ @@ -211,21 +165,8 @@ def test_protocol_divergence_v5_fail_by_flag_uses_int(self): self._protocol_divergence_fail_by_flag_uses_int(ProtocolVersion.V5, uses_int_query_flag=False, beta=True, int_flag=False) - @greaterthanorequaldse51 - def test_protocol_divergence_dsev1_fail_by_flag_uses_int(self): - """ - Test to validate that the _PAGE_SIZE_FLAG is treated correctly using write_uint for DSE_V1 - - @jira_ticket PYTHON-694 - @expected_result the fetch_size=1 parameter will be honored - - @test_category connection - """ - self._protocol_divergence_fail_by_flag_uses_int(ProtocolVersion.DSE_V1, uses_int_query_flag=False, - int_flag=False) - def _send_query_message(self, session, timeout, **kwargs): - query = "SELECT * FROM test3rf.test" + query = "SELECT * FROM custserdes.test" message = QueryMessage(query=query, **kwargs) future = ResponseFuture(session, message, query=None, timeout=timeout) future.send_request() @@ -235,8 +176,8 @@ def _protocol_divergence_fail_by_flag_uses_int(self, version, uses_int_query_fla cluster = TestCluster(protocol_version=version, allow_beta_protocol_version=beta) session = cluster.connect() - query_one = SimpleStatement("INSERT INTO test3rf.test (k, v) VALUES (1, 1)") - query_two = SimpleStatement("INSERT INTO test3rf.test (k, v) VALUES (2, 2)") + query_one = SimpleStatement("INSERT INTO custserdes.test (k, v) VALUES (1, 1)") + query_two = SimpleStatement("INSERT INTO custserdes.test (k, v) VALUES (2, 2)") execute_with_long_wait_retry(session, query_one) execute_with_long_wait_retry(session, query_two) @@ -248,9 +189,9 @@ def _protocol_divergence_fail_by_flag_uses_int(self, version, uses_int_query_fla response = future.result() # This means the flag are not handled as they are meant by the server if uses_int=False - self.assertEqual(response.has_more_pages, uses_int_query_flag) + assert response.has_more_pages == uses_int_query_flag - execute_with_long_wait_retry(session, SimpleStatement("TRUNCATE test3rf.test")) + execute_with_long_wait_retry(session, SimpleStatement("TRUNCATE custserdes.test")) cluster.shutdown() diff --git a/tests/integration/standard/test_cython_protocol_handlers.py b/tests/integration/standard/test_cython_protocol_handlers.py index 9e85edb914..49a13ac23a 100644 --- a/tests/integration/standard/test_cython_protocol_handlers.py +++ b/tests/integration/standard/test_cython_protocol_handlers.py @@ -12,7 +12,7 @@ from cassandra.protocol import ProtocolHandler, LazyProtocolHandler, NumpyProtocolHandler from cassandra.query import tuple_factory from tests import VERIFY_CYTHON -from tests.integration import use_singledc, notprotocolv1, \ +from tests.integration import use_single_node, notprotocolv1, \ drop_keyspace_shutdown_cluster, BasicSharedKeyspaceUnitTestCase, greaterthancass21, TestCluster from tests.integration.datatype_utils import update_datatypes from tests.integration.standard.utils import ( @@ -21,7 +21,7 @@ def setup_module(): - use_singledc() + use_single_node() update_datatypes() @@ -34,7 +34,7 @@ def setUpClass(cls): cls.cluster = TestCluster() cls.session = cls.cluster.connect() cls.session.execute("CREATE KEYSPACE testspace WITH replication = " - "{ 'class' : 'SimpleStrategy', 'replication_factor': '1'}") + "{ 'class' : 'NetworkTopologyStrategy', 'replication_factor': '1'}") cls.session.set_keyspace("testspace") cls.colnames = create_table_with_all_types("test_table", cls.session, cls.N_ITEMS) @@ -47,14 +47,14 @@ def test_cython_parser(self): """ Test Cython-based parser that returns a list of tuples """ - verify_iterator_data(self.assertEqual, get_data(ProtocolHandler)) + verify_iterator_data(get_data(ProtocolHandler)) @cythontest def test_cython_lazy_parser(self): """ Test Cython-based parser that returns an iterator of tuples """ - verify_iterator_data(self.assertEqual, get_data(LazyProtocolHandler)) + verify_iterator_data(get_data(LazyProtocolHandler)) @numpytest def test_cython_lazy_results_paged(self): @@ -69,12 +69,12 @@ def test_cython_lazy_results_paged(self): session.client_protocol_handler = LazyProtocolHandler session.default_fetch_size = 2 - self.assertLess(session.default_fetch_size, self.N_ITEMS) + assert session.default_fetch_size < self.N_ITEMS results = session.execute("SELECT * FROM test_table") - self.assertTrue(results.has_more_pages) - self.assertEqual(verify_iterator_data(self.assertEqual, results), self.N_ITEMS) # make sure we see all rows + assert results.has_more_pages + assert verify_iterator_data(results) == self.N_ITEMS # make sure we see all rows cluster.shutdown() @@ -86,7 +86,7 @@ def test_numpy_parser(self): """ # arrays = { 'a': arr1, 'b': arr2, ... } result = get_data(NumpyProtocolHandler) - self.assertFalse(result.has_more_pages) + assert not result.has_more_pages self._verify_numpy_page(result[0]) @notprotocolv1 @@ -105,23 +105,24 @@ def test_numpy_results_paged(self): expected_pages = (self.N_ITEMS + session.default_fetch_size - 1) // session.default_fetch_size - self.assertLess(session.default_fetch_size, self.N_ITEMS) + assert session.default_fetch_size < self.N_ITEMS results = session.execute("SELECT * FROM test_table") - self.assertTrue(results.has_more_pages) + assert results.has_more_pages + count = 0 for count, page in enumerate(results, 1): - self.assertIsInstance(page, dict) + assert isinstance(page, dict) for colname, arr in page.items(): if count <= expected_pages: - self.assertGreater(len(arr), 0, "page count: %d" % (count,)) - self.assertLessEqual(len(arr), session.default_fetch_size) + assert len(arr) > 0, "page count: %d" % (count,) + assert len(arr) <= session.default_fetch_size else: # we get one extra item out of this iteration because of the way NumpyParser returns results # The last page is returned as a dict with zero-length arrays - self.assertEqual(len(arr), 0) - self.assertEqual(self._verify_numpy_page(page), len(arr)) - self.assertEqual(count, expected_pages + 1) # see note about extra 'page' above + assert len(arr) == 0 + assert self._verify_numpy_page(page) == len(arr) + assert count == expected_pages + 1 # see note about extra 'page' above cluster.shutdown() @@ -136,8 +137,8 @@ def test_cython_numpy_are_installed_valid(self): @test_category configuration """ if VERIFY_CYTHON: - self.assertTrue(HAVE_CYTHON) - self.assertTrue(HAVE_NUMPY) + assert HAVE_CYTHON + assert HAVE_NUMPY def _verify_numpy_page(self, page): colnames = self.colnames @@ -146,7 +147,7 @@ def _verify_numpy_page(self, page): arr = page[colname] self.match_dtype(datatype, arr.dtype) - return verify_iterator_data(self.assertEqual, arrays_to_list_of_tuples(page, colnames)) + return verify_iterator_data(arrays_to_list_of_tuples(page, colnames)) def match_dtype(self, datatype, dtype): """Match a string cqltype (e.g. 'int' or 'blob') with a numpy dtype""" @@ -161,11 +162,11 @@ def match_dtype(self, datatype, dtype): elif datatype == 'double': self.match_dtype_props(dtype, 'f', 8) else: - self.assertEqual(dtype.kind, 'O', msg=(dtype, datatype)) + assert dtype.kind == 'O', (dtype, datatype) def match_dtype_props(self, dtype, kind, size, signed=None): - self.assertEqual(dtype.kind, kind, msg=dtype) - self.assertEqual(dtype.itemsize, size, msg=dtype) + assert dtype.kind == kind, dtype + assert dtype.itemsize == size, dtype def arrays_to_list_of_tuples(arrays, colnames): @@ -192,7 +193,7 @@ def get_data(protocol_handler): return results -def verify_iterator_data(assertEqual, results): +def verify_iterator_data(results): """ Check the result of get_data() when this is a list or iterator of tuples @@ -200,13 +201,126 @@ def verify_iterator_data(assertEqual, results): count = 0 for count, result in enumerate(results, 1): params = get_all_primitive_params(result[0]) - assertEqual(len(params), len(result), - msg="Not the right number of columns?") + assert len(params) == len(result), "Not the right number of columns?" for expected, actual in zip(params, result): - assertEqual(actual, expected) + assert actual == expected return count +class NumpyWideTableTest(unittest.TestCase): + """ + Test NumpyProtocolHandler with wide tables (many columns). + + ScyllaDB has a built-in 1MB page size limit that can cause fewer rows + per page than requested when working with wide tables. + + See: https://github.com/scylladb/python-driver/issues/65 + """ + + N_COLUMNS = 200 # Number of int columns (plus primary key columns) + N_ROWS = 100 + + @classmethod + def setUpClass(cls): + cls.cluster = TestCluster() + cls.session = cls.cluster.connect() + cls.session.execute("CREATE KEYSPACE IF NOT EXISTS test_wide_table WITH replication = " + "{ 'class' : 'NetworkTopologyStrategy', 'replication_factor': '1'}") + cls.session.set_keyspace("test_wide_table") + + # Create a wide table with many int columns + columns = ["pk int", "ck int"] + columns += ["col{0} int".format(i) for i in range(cls.N_COLUMNS)] + cls.session.execute( + "CREATE TABLE wide_table ({0}, PRIMARY KEY (pk, ck))".format(", ".join(columns)), + timeout=120 + ) + + # Insert test data + col_names = ["pk", "ck"] + ["col{0}".format(i) for i in range(cls.N_COLUMNS)] + placeholders = ", ".join(["%s"] * len(col_names)) + insert_cql = "INSERT INTO wide_table ({0}) VALUES ({1})".format( + ", ".join(col_names), placeholders + ) + + for row_idx in range(cls.N_ROWS): + values = [0, row_idx] + [row_idx * 1000 + i for i in range(cls.N_COLUMNS)] + cls.session.execute(insert_cql, values, timeout=120) + + @classmethod + def tearDownClass(cls): + drop_keyspace_shutdown_cluster("test_wide_table", cls.session, cls.cluster) + + @notprotocolv1 + @numpytest + def test_numpy_wide_table_paging(self): + """ + Test that NumpyProtocolHandler works with wide tables. + + With ScyllaDB's 1MB page size limit, wide tables may return fewer + rows per page than the fetch_size requests. This test verifies + that all data is still returned correctly across multiple pages. + """ + cluster = TestCluster( + execution_profiles={EXEC_PROFILE_DEFAULT: ExecutionProfile(row_factory=tuple_factory)} + ) + session = cluster.connect(keyspace="test_wide_table") + session.client_protocol_handler = NumpyProtocolHandler + session.default_fetch_size = 1000 # Request many rows per page + + results = session.execute("SELECT * FROM wide_table") + + # Count total rows across all pages + total_rows = 0 + page_count = 0 + for page in results: + page_count += 1 + # Get row count from first column array + arr = page.get('pk') + if arr is not None: + total_rows += len(arr) + + # Verify all rows were returned + self.assertEqual(total_rows, self.N_ROWS, + "Expected {0} rows total, got {1} across {2} pages".format( + self.N_ROWS, total_rows, page_count)) + + cluster.shutdown() + + @notprotocolv1 + @numpytest + def test_numpy_wide_table_no_fetch_size(self): + """ + Test that setting fetch_size=None allows ScyllaDB to control page sizes. + + This is the recommended workaround for getting larger pages with wide tables. + """ + cluster = TestCluster( + execution_profiles={EXEC_PROFILE_DEFAULT: ExecutionProfile(row_factory=tuple_factory)} + ) + session = cluster.connect(keyspace="test_wide_table") + session.client_protocol_handler = NumpyProtocolHandler + session.default_fetch_size = None # Let server control page sizes + + results = session.execute("SELECT * FROM wide_table") + + # Count total rows across all pages + total_rows = 0 + page_count = 0 + for page in results: + page_count += 1 + arr = page.get('pk') + if arr is not None: + total_rows += len(arr) + + # Verify all rows were returned + self.assertEqual(total_rows, self.N_ROWS, + "Expected {0} rows total, got {1} across {2} pages".format( + self.N_ROWS, total_rows, page_count)) + + cluster.shutdown() + + class NumpyNullTest(BasicSharedKeyspaceUnitTestCase): @classmethod @@ -250,11 +364,15 @@ def test_null_types(self): # because None and `masked` have different identity and equals semantics if isinstance(col_array, MaskedArray): had_masked = True - [self.assertIsNot(col_array[i], masked) for i in mapped_index[:begin_unset]] - [self.assertIs(col_array[i], masked) for i in mapped_index[begin_unset:]] + for i in mapped_index[:begin_unset]: + assert col_array[i] is not masked + for i in mapped_index[begin_unset:]: + assert col_array[i] is masked else: had_none = True - [self.assertIsNotNone(col_array[i]) for i in mapped_index[:begin_unset]] - [self.assertIsNone(col_array[i]) for i in mapped_index[begin_unset:]] - self.assertTrue(had_masked) - self.assertTrue(had_none) + for i in mapped_index[:begin_unset]: + assert col_array[i] is not None + for i in mapped_index[begin_unset:]: + assert col_array[i] is None + assert had_masked + assert had_none diff --git a/tests/integration/standard/test_dse.py b/tests/integration/standard/test_dse.py deleted file mode 100644 index 7b96094b3f..0000000000 --- a/tests/integration/standard/test_dse.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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 - -from packaging.version import Version - -from tests import notwindows -from tests.unit.cython.utils import notcython -from tests.integration import (execute_until_pass, - execute_with_long_wait_retry, use_cluster, TestCluster) - -import unittest - - -CCM_IS_DSE = (os.environ.get('CCM_IS_DSE', None) == 'true') - - -@unittest.skipIf(os.environ.get('CCM_ARGS', None), 'environment has custom CCM_ARGS; skipping') -@notwindows -@notcython # no need to double up on this test; also __default__ setting doesn't work -class DseCCMClusterTest(unittest.TestCase): - """ - This class can be executed setting the DSE_VERSION variable, for example: - DSE_VERSION=5.1.4 python2.7 -m nose tests/integration/standard/test_dse.py - If CASSANDRA_VERSION is set instead, it will be converted to the corresponding DSE_VERSION - """ - - def test_dse_5x(self): - self._test_basic(Version('5.1.10')) - - def test_dse_60(self): - self._test_basic(Version('6.0.2')) - - @unittest.skipUnless(CCM_IS_DSE, 'DSE version unavailable') - def test_dse_67(self): - self._test_basic(Version('6.7.0')) - - def _test_basic(self, dse_version): - """ - Test basic connection and usage - """ - cluster_name = '{}-{}'.format( - self.__class__.__name__, dse_version.base_version.replace('.', '_') - ) - use_cluster(cluster_name=cluster_name, nodes=[3], dse_options={}) - - cluster = TestCluster() - session = cluster.connect() - result = execute_until_pass( - session, - """ - CREATE KEYSPACE clustertests - WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} - """) - self.assertFalse(result) - - result = execute_with_long_wait_retry( - session, - """ - CREATE TABLE clustertests.cf0 ( - a text, - b text, - c text, - PRIMARY KEY (a, b) - ) - """) - self.assertFalse(result) - - result = session.execute( - """ - INSERT INTO clustertests.cf0 (a, b, c) VALUES ('a', 'b', 'c') - """) - self.assertFalse(result) - - result = session.execute("SELECT * FROM clustertests.cf0") - self.assertEqual([('a', 'b', 'c')], result) - - execute_with_long_wait_retry(session, "DROP KEYSPACE clustertests") - - cluster.shutdown() diff --git a/tests/integration/standard/test_ip_change.py b/tests/integration/standard/test_ip_change.py index 6d23d30e04..53debfa1f5 100644 --- a/tests/integration/standard/test_ip_change.py +++ b/tests/integration/standard/test_ip_change.py @@ -10,11 +10,22 @@ LOGGER = logging.getLogger(__name__) +_saved_scylla_ext_opts = None + def setup_module(): + global _saved_scylla_ext_opts + _saved_scylla_ext_opts = os.environ.get('SCYLLA_EXT_OPTS') os.environ['SCYLLA_EXT_OPTS'] = "--smp 2 --memory 2048M" use_cluster('test_ip_change', [3], start=True) + +def teardown_module(): + if _saved_scylla_ext_opts is None: + os.environ.pop('SCYLLA_EXT_OPTS', None) + else: + os.environ['SCYLLA_EXT_OPTS'] = _saved_scylla_ext_opts + @local class TestIpAddressChange(unittest.TestCase): @classmethod diff --git a/tests/integration/standard/test_metadata.py b/tests/integration/standard/test_metadata.py index c76ffa22e9..f5a11dd5fe 100644 --- a/tests/integration/standard/test_metadata.py +++ b/tests/integration/standard/test_metadata.py @@ -20,6 +20,8 @@ import sys import time import os +from typing import Optional + from packaging.version import Version from unittest.mock import Mock, patch import pytest @@ -37,14 +39,15 @@ from tests.integration import (get_cluster, use_singledc, PROTOCOL_VERSION, execute_until_pass, BasicSegregatedKeyspaceUnitTestCase, BasicSharedKeyspaceUnitTestCase, BasicExistingKeyspaceUnitTestCase, drop_keyspace_shutdown_cluster, CASSANDRA_VERSION, - greaterthanorequaldse51, greaterthanorequalcass30, lessthancass30, local, + greaterthanorequalcass30, lessthancass30, local, get_supported_protocol_versions, greaterthancass20, - greaterthancass21, assert_startswith, greaterthanorequalcass40, - greaterthanorequaldse67, lessthancass40, - TestCluster, DSE_VERSION, requires_java_udf, requires_composite_type, - requires_collection_indexes, SCYLLA_VERSION, xfail_scylla, xfail_scylla_version_lt) + greaterthancass21, greaterthanorequalcass40, + lessthancass40, + TestCluster, requires_java_udf, requires_composite_type, + requires_collection_indexes, SCYLLA_VERSION, xfail_scylla, xfail_scylla_version_lt, + requirescompactstorage, get_tablets_disabled_ddl_suffix, execute_with_long_wait_retry) -from tests.util import wait_until +from tests.util import wait_until, assertRegex, assertDictEqual, assertListEqual, assert_startswith_diff log = logging.getLogger(__name__) @@ -54,8 +57,6 @@ def setup_module(): class HostMetaDataTests(BasicExistingKeyspaceUnitTestCase): - # TODO: enable after https://github.com/scylladb/python-driver/issues/121 is fixed - @unittest.skip('Fails on scylla due to the broadcast_rpc_port is None') @local def test_host_addresses(self): """ @@ -70,24 +71,26 @@ def test_host_addresses(self): """ # All nodes should have the broadcast_address, rpc_address and host_id set for host in self.cluster.metadata.all_hosts(): - self.assertIsNotNone(host.broadcast_address) - self.assertIsNotNone(host.broadcast_rpc_address) - self.assertIsNotNone(host.host_id) + assert host.broadcast_address is not None + assert host.broadcast_rpc_address is not None + assert host.host_id is not None - if not DSE_VERSION and CASSANDRA_VERSION >= Version('4-a'): - self.assertIsNotNone(host.broadcast_port) - self.assertIsNotNone(host.broadcast_rpc_port) + if CASSANDRA_VERSION >= Version('4-a'): + assert host.broadcast_port is not None + assert host.broadcast_rpc_port is not None con = self.cluster.control_connection.get_connections()[0] local_host = con.host # The control connection node should have the listen address set. - listen_addrs = [host.listen_address for host in self.cluster.metadata.all_hosts()] - self.assertTrue(local_host in listen_addrs) + # Note: Scylla does not populate listen_address in system.local + if SCYLLA_VERSION is None: + listen_addrs = [host.listen_address for host in self.cluster.metadata.all_hosts()] + assert local_host in listen_addrs # The control connection node should have the broadcast_rpc_address set. rpc_addrs = [host.broadcast_rpc_address for host in self.cluster.metadata.all_hosts()] - self.assertTrue(local_host in rpc_addrs) + assert local_host in rpc_addrs @unittest.skipUnless( os.getenv('MAPPED_CASSANDRA_VERSION', None) is not None, @@ -104,7 +107,7 @@ def test_host_release_version(self): @test_category metadata """ for host in self.cluster.metadata.all_hosts(): - assert_startswith(host.release_version, CASSANDRA_VERSION.base_version) + assert host.release_version.startswith(CASSANDRA_VERSION.base_version) @@ -133,11 +136,17 @@ def test_bad_contact_point(self): # verify the un-existing host was filtered for host in self.cluster.metadata.all_hosts(): - self.assertNotEqual(host.endpoint.address, '126.0.0.186') + assert host.endpoint.address != '126.0.0.186' class SchemaMetadataTests(BasicSegregatedKeyspaceUnitTestCase): + @classmethod + def create_keyspace(cls, rf): + ddl = "CREATE KEYSPACE {0} WITH replication = {{'class': 'NetworkTopologyStrategy', 'replication_factor': '{1}'}}{2}".format( + cls.ks_name, rf, get_tablets_disabled_ddl_suffix()) + execute_with_long_wait_retry(cls.session, ddl) + def test_schema_metadata_disable(self): """ Checks to ensure that schema metadata_enabled, and token_metadata_enabled @@ -153,18 +162,18 @@ def test_schema_metadata_disable(self): # Validate metadata is missing where appropriate no_schema = TestCluster(schema_metadata_enabled=False) no_schema_session = no_schema.connect() - self.assertEqual(len(no_schema.metadata.keyspaces), 0) - self.assertEqual(no_schema.metadata.export_schema_as_string(), '') + assert len(no_schema.metadata.keyspaces) == 0 + assert no_schema.metadata.export_schema_as_string() == '' no_token = TestCluster(token_metadata_enabled=False) no_token_session = no_token.connect() - self.assertEqual(len(no_token.metadata.token_map.token_to_host_owner), 0) + assert len(no_token.metadata.token_map.token_to_host_owner) == 0 # Do a simple query to ensure queries are working query = "SELECT * FROM system.local WHERE key='local'" no_schema_rs = no_schema_session.execute(query) no_token_rs = no_token_session.execute(query) - self.assertIsNotNone(no_schema_rs.one()) - self.assertIsNotNone(no_token_rs.one()) + assert no_schema_rs.one() is not None + assert no_token_rs.one() is not None no_schema.shutdown() no_token.shutdown() @@ -201,7 +210,7 @@ def make_create_statement(self, partition_cols, clustering_cols=None, other_cols def check_create_statement(self, tablemeta, original): recreate = tablemeta.as_cql_query(formatted=False) - self.assertEqual(original, recreate[:len(original)]) + assert original == recreate[:len(original)] execute_until_pass(self.session, "DROP TABLE {0}.{1}".format(self.keyspace_name, self.function_table_name)) execute_until_pass(self.session, recreate) @@ -221,24 +230,24 @@ def test_basic_table_meta_properties(self): self.cluster.refresh_schema_metadata() meta = self.cluster.metadata - self.assertNotEqual(meta.cluster_name, None) - self.assertTrue(self.keyspace_name in meta.keyspaces) + assert meta.cluster_name != None + assert self.keyspace_name in meta.keyspaces ksmeta = meta.keyspaces[self.keyspace_name] - self.assertEqual(ksmeta.name, self.keyspace_name) - self.assertTrue(ksmeta.durable_writes) - self.assertEqual(ksmeta.replication_strategy.name, 'SimpleStrategy') - self.assertEqual(ksmeta.replication_strategy.replication_factor, 1) + assert ksmeta.name == self.keyspace_name + assert ksmeta.durable_writes + assert ksmeta.replication_strategy.name == 'NetworkTopologyStrategy' + assert ksmeta.replication_strategy.dc_replication_factors["dc1"] == 1 - self.assertTrue(self.function_table_name in ksmeta.tables) + assert self.function_table_name in ksmeta.tables tablemeta = ksmeta.tables[self.function_table_name] - self.assertEqual(tablemeta.keyspace_name, ksmeta.name) - self.assertEqual(tablemeta.name, self.function_table_name) - self.assertEqual(tablemeta.name, self.function_table_name) + assert tablemeta.keyspace_name == ksmeta.name + assert tablemeta.name == self.function_table_name + assert tablemeta.name == self.function_table_name - self.assertEqual([u'a'], [c.name for c in tablemeta.partition_key]) - self.assertEqual([], tablemeta.clustering_key) - self.assertEqual([u'a', u'b', u'c'], sorted(tablemeta.columns.keys())) + assert [u'a'] == [c.name for c in tablemeta.partition_key] + assert [] == tablemeta.clustering_key + assert [u'a', u'b', u'c'] == sorted(tablemeta.columns.keys()) cc = self.cluster.control_connection._connection parser = get_schema_parser( @@ -250,7 +259,7 @@ def test_basic_table_meta_properties(self): ) for option in tablemeta.options: - self.assertIn(option, parser.recognized_table_options) + assert option in parser.recognized_table_options self.check_create_statement(tablemeta, create_statement) @@ -260,9 +269,9 @@ def test_compound_primary_keys(self): self.session.execute(create_statement) tablemeta = self.get_table_metadata() - self.assertEqual([u'a'], [c.name for c in tablemeta.partition_key]) - self.assertEqual([u'b'], [c.name for c in tablemeta.clustering_key]) - self.assertEqual([u'a', u'b', u'c'], sorted(tablemeta.columns.keys())) + assert [u'a'] == [c.name for c in tablemeta.partition_key] + assert [u'b'] == [c.name for c in tablemeta.clustering_key] + assert [u'a', u'b', u'c'] == sorted(tablemeta.columns.keys()) self.check_create_statement(tablemeta, create_statement) @@ -272,9 +281,9 @@ def test_compound_primary_keys_protected(self): self.session.execute(create_statement) tablemeta = self.get_table_metadata() - self.assertEqual([u'Aa'], [c.name for c in tablemeta.partition_key]) - self.assertEqual([u'Bb'], [c.name for c in tablemeta.clustering_key]) - self.assertEqual([u'Aa', u'Bb', u'Cc'], sorted(tablemeta.columns.keys())) + assert [u'Aa'] == [c.name for c in tablemeta.partition_key] + assert [u'Bb'] == [c.name for c in tablemeta.clustering_key] + assert [u'Aa', u'Bb', u'Cc'] == sorted(tablemeta.columns.keys()) self.check_create_statement(tablemeta, create_statement) @@ -284,11 +293,9 @@ def test_compound_primary_keys_more_columns(self): self.session.execute(create_statement) tablemeta = self.get_table_metadata() - self.assertEqual([u'a'], [c.name for c in tablemeta.partition_key]) - self.assertEqual([u'b', u'c'], [c.name for c in tablemeta.clustering_key]) - self.assertEqual( - [u'a', u'b', u'c', u'd', u'e', u'f'], - sorted(tablemeta.columns.keys())) + assert [u'a'] == [c.name for c in tablemeta.partition_key] + assert [u'b', u'c'] == [c.name for c in tablemeta.clustering_key] + assert [u'a', u'b', u'c', u'd', u'e', u'f'] == sorted(tablemeta.columns.keys()) self.check_create_statement(tablemeta, create_statement) @@ -297,9 +304,9 @@ def test_composite_primary_key(self): self.session.execute(create_statement) tablemeta = self.get_table_metadata() - self.assertEqual([u'a', u'b'], [c.name for c in tablemeta.partition_key]) - self.assertEqual([], tablemeta.clustering_key) - self.assertEqual([u'a', u'b', u'c'], sorted(tablemeta.columns.keys())) + assert [u'a', u'b'] == [c.name for c in tablemeta.partition_key] + assert [] == tablemeta.clustering_key + assert [u'a', u'b', u'c'] == sorted(tablemeta.columns.keys()) self.check_create_statement(tablemeta, create_statement) @@ -309,9 +316,9 @@ def test_composite_in_compound_primary_key(self): self.session.execute(create_statement) tablemeta = self.get_table_metadata() - self.assertEqual([u'a', u'b'], [c.name for c in tablemeta.partition_key]) - self.assertEqual([u'c'], [c.name for c in tablemeta.clustering_key]) - self.assertEqual([u'a', u'b', u'c', u'd', u'e'], sorted(tablemeta.columns.keys())) + assert [u'a', u'b'] == [c.name for c in tablemeta.partition_key] + assert [u'c'] == [c.name for c in tablemeta.clustering_key] + assert [u'a', u'b', u'c', u'd', u'e'] == sorted(tablemeta.columns.keys()) self.check_create_statement(tablemeta, create_statement) @@ -321,9 +328,9 @@ def test_compound_primary_keys_compact(self): self.session.execute(create_statement) tablemeta = self.get_table_metadata() - self.assertEqual([u'a'], [c.name for c in tablemeta.partition_key]) - self.assertEqual([u'b'], [c.name for c in tablemeta.clustering_key]) - self.assertEqual([u'a', u'b', u'c'], sorted(tablemeta.columns.keys())) + assert [u'a'] == [c.name for c in tablemeta.partition_key] + assert [u'b'] == [c.name for c in tablemeta.clustering_key] + assert [u'a', u'b', u'c'] == sorted(tablemeta.columns.keys()) self.check_create_statement(tablemeta, create_statement) @@ -345,9 +352,9 @@ def test_cluster_column_ordering_reversed_metadata(self): self.session.execute(create_statement) tablemeta = self.get_table_metadata() b_column = tablemeta.columns['b'] - self.assertFalse(b_column.is_reversed) + assert not b_column.is_reversed c_column = tablemeta.columns['c'] - self.assertTrue(c_column.is_reversed) + assert c_column.is_reversed def test_compound_primary_keys_more_columns_compact(self): create_statement = self.make_create_statement(["a"], ["b", "c"], ["d"]) @@ -355,9 +362,9 @@ def test_compound_primary_keys_more_columns_compact(self): self.session.execute(create_statement) tablemeta = self.get_table_metadata() - self.assertEqual([u'a'], [c.name for c in tablemeta.partition_key]) - self.assertEqual([u'b', u'c'], [c.name for c in tablemeta.clustering_key]) - self.assertEqual([u'a', u'b', u'c', u'd'], sorted(tablemeta.columns.keys())) + assert [u'a'] == [c.name for c in tablemeta.partition_key] + assert [u'b', u'c'] == [c.name for c in tablemeta.clustering_key] + assert [u'a', u'b', u'c', u'd'] == sorted(tablemeta.columns.keys()) self.check_create_statement(tablemeta, create_statement) @@ -366,9 +373,9 @@ def test_composite_primary_key_compact(self): self.session.execute(create_statement) tablemeta = self.get_table_metadata() - self.assertEqual([u'a', u'b'], [c.name for c in tablemeta.partition_key]) - self.assertEqual([], tablemeta.clustering_key) - self.assertEqual([u'a', u'b', u'c'], sorted(tablemeta.columns.keys())) + assert [u'a', u'b'] == [c.name for c in tablemeta.partition_key] + assert [] == tablemeta.clustering_key + assert [u'a', u'b', u'c'] == sorted(tablemeta.columns.keys()) self.check_create_statement(tablemeta, create_statement) @@ -378,9 +385,9 @@ def test_composite_in_compound_primary_key_compact(self): self.session.execute(create_statement) tablemeta = self.get_table_metadata() - self.assertEqual([u'a', u'b'], [c.name for c in tablemeta.partition_key]) - self.assertEqual([u'c'], [c.name for c in tablemeta.clustering_key]) - self.assertEqual([u'a', u'b', u'c', u'd'], sorted(tablemeta.columns.keys())) + assert [u'a', u'b'] == [c.name for c in tablemeta.partition_key] + assert [u'c'] == [c.name for c in tablemeta.clustering_key] + assert [u'a', u'b', u'c', u'd'] == sorted(tablemeta.columns.keys()) self.check_create_statement(tablemeta, create_statement) @@ -393,18 +400,18 @@ def test_cql_compatibility(self): self.session.execute(create_statement) tablemeta = self.get_table_metadata() - self.assertEqual([u'a'], [c.name for c in tablemeta.partition_key]) - self.assertEqual([], tablemeta.clustering_key) - self.assertEqual([u'a', u'b', u'c', u'd'], sorted(tablemeta.columns.keys())) + assert [u'a'] == [c.name for c in tablemeta.partition_key] + assert [] == tablemeta.clustering_key + assert [u'a', u'b', u'c', u'd'] == sorted(tablemeta.columns.keys()) - self.assertTrue(tablemeta.is_cql_compatible) + assert tablemeta.is_cql_compatible # It will be cql compatible after CASSANDRA-10857 # since compact storage is being dropped tablemeta.clustering_key = ["foo", "bar"] tablemeta.columns["foo"] = None tablemeta.columns["bar"] = None - self.assertTrue(tablemeta.is_cql_compatible) + assert tablemeta.is_cql_compatible def test_compound_primary_keys_ordering(self): create_statement = self.make_create_statement(["a"], ["b"], ["c"]) @@ -428,6 +435,7 @@ def test_composite_in_compound_primary_key_ordering(self): self.check_create_statement(tablemeta, create_statement) @lessthancass40 + @requirescompactstorage def test_compact_storage(self): create_statement = self.make_create_statement(["a"], [], ["b"]) create_statement += " WITH COMPACT STORAGE" @@ -437,6 +445,7 @@ def test_compact_storage(self): self.check_create_statement(tablemeta, create_statement) @lessthancass40 + @requirescompactstorage def test_dense_compact_storage(self): create_statement = self.make_create_statement(["a"], ["b"], ["c"]) create_statement += " WITH COMPACT STORAGE" @@ -456,6 +465,7 @@ def test_counter(self): self.check_create_statement(tablemeta, create_statement) @lessthancass40 + @requirescompactstorage def test_counter_with_compact_storage(self): """ PYTHON-1100 """ create_statement = ( @@ -468,6 +478,7 @@ def test_counter_with_compact_storage(self): self.check_create_statement(tablemeta, create_statement) @lessthancass40 + @requirescompactstorage def test_counter_with_dense_compact_storage(self): create_statement = ( "CREATE TABLE {keyspace}.{table} (" @@ -493,15 +504,15 @@ def test_indexes(self): statements = tablemeta.export_as_string().strip() statements = [s.strip() for s in statements.split(';')] statements = list(filter(bool, statements)) - self.assertEqual(3, len(statements)) - self.assertIn(d_index, statements) - self.assertIn(e_index, statements) + assert 3 == len(statements) + assert d_index in statements + assert e_index in statements # make sure indexes are included in KeyspaceMetadata.export_as_string() ksmeta = self.cluster.metadata.keyspaces[self.keyspace_name] statement = ksmeta.export_as_string() - self.assertIn('CREATE INDEX d_index', statement) - self.assertIn('CREATE INDEX e_index', statement) + assert 'CREATE INDEX d_index' in statement + assert 'CREATE INDEX e_index' in statement @greaterthancass21 @requires_collection_indexes @@ -514,7 +525,7 @@ def test_collection_indexes(self): % (self.keyspace_name, self.function_table_name)) tablemeta = self.get_table_metadata() - self.assertIn('(keys(b))', tablemeta.export_as_string()) + assert '(keys(b))' in tablemeta.export_as_string() self.session.execute("DROP INDEX %s.index1" % (self.keyspace_name,)) self.session.execute("CREATE INDEX index2 ON %s.%s (b)" @@ -522,7 +533,7 @@ def test_collection_indexes(self): tablemeta = self.get_table_metadata() target = ' (b)' if CASSANDRA_VERSION < Version("3.0") else 'values(b))' # explicit values in C* 3+ - self.assertIn(target, tablemeta.export_as_string()) + assert target in tablemeta.export_as_string() # test full indexes on frozen collections, if available if CASSANDRA_VERSION >= Version("2.1.3"): @@ -533,7 +544,7 @@ def test_collection_indexes(self): % (self.keyspace_name, self.function_table_name)) tablemeta = self.get_table_metadata() - self.assertIn('(full(b))', tablemeta.export_as_string()) + assert '(full(b))' in tablemeta.export_as_string() def test_compression_disabled(self): create_statement = self.make_create_statement(["a"], ["b"], ["c"]) @@ -543,7 +554,7 @@ def test_compression_disabled(self): expected = "compression = {'enabled': 'false'}" if SCYLLA_VERSION is not None or CASSANDRA_VERSION < Version("3.0"): expected = "compression = {}" - self.assertIn(expected, tablemeta.export_as_string()) + assert expected in tablemeta.export_as_string() def test_non_size_tiered_compaction(self): """ @@ -564,12 +575,12 @@ def test_non_size_tiered_compaction(self): table_meta = self.get_table_metadata() cql = table_meta.export_as_string() - self.assertIn("'tombstone_threshold': '0.3'", cql) - self.assertIn("LeveledCompactionStrategy", cql) + assert "'tombstone_threshold': '0.3'" in cql + assert "LeveledCompactionStrategy" in cql # formerly legacy options; reintroduced in 4.0 if CASSANDRA_VERSION < Version('4.0-a'): - self.assertNotIn("min_threshold", cql) - self.assertNotIn("max_threshold", cql) + assert "min_threshold" not in cql + assert "max_threshold" not in cql @requires_java_udf def test_refresh_schema_metadata(self): @@ -593,20 +604,20 @@ def test_refresh_schema_metadata(self): cluster2 = TestCluster(schema_event_refresh_window=-1) cluster2.connect() - self.assertNotIn("new_keyspace", cluster2.metadata.keyspaces) + assert "new_keyspace" not in cluster2.metadata.keyspaces # Cluster metadata modification - self.session.execute("CREATE KEYSPACE new_keyspace WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}") - self.assertNotIn("new_keyspace", cluster2.metadata.keyspaces) + self.session.execute("CREATE KEYSPACE new_keyspace WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '1'}") + assert "new_keyspace" not in cluster2.metadata.keyspaces cluster2.refresh_schema_metadata() - self.assertIn("new_keyspace", cluster2.metadata.keyspaces) + assert "new_keyspace" in cluster2.metadata.keyspaces # Keyspace metadata modification self.session.execute("ALTER KEYSPACE {0} WITH durable_writes = false".format(self.keyspace_name)) - self.assertTrue(cluster2.metadata.keyspaces[self.keyspace_name].durable_writes) + assert cluster2.metadata.keyspaces[self.keyspace_name].durable_writes cluster2.refresh_schema_metadata() - self.assertFalse(cluster2.metadata.keyspaces[self.keyspace_name].durable_writes) + assert not cluster2.metadata.keyspaces[self.keyspace_name].durable_writes # Table metadata modification table_name = "test" @@ -614,16 +625,16 @@ def test_refresh_schema_metadata(self): cluster2.refresh_schema_metadata() self.session.execute("ALTER TABLE {0}.{1} ADD c double".format(self.keyspace_name, table_name)) - self.assertNotIn("c", cluster2.metadata.keyspaces[self.keyspace_name].tables[table_name].columns) + assert "c" not in cluster2.metadata.keyspaces[self.keyspace_name].tables[table_name].columns cluster2.refresh_schema_metadata() - self.assertIn("c", cluster2.metadata.keyspaces[self.keyspace_name].tables[table_name].columns) + assert "c" in cluster2.metadata.keyspaces[self.keyspace_name].tables[table_name].columns if PROTOCOL_VERSION >= 3: # UDT metadata modification self.session.execute("CREATE TYPE {0}.user (age int, name text)".format(self.keyspace_name)) - self.assertEqual(cluster2.metadata.keyspaces[self.keyspace_name].user_types, {}) + assert cluster2.metadata.keyspaces[self.keyspace_name].user_types == {} cluster2.refresh_schema_metadata() - self.assertIn("user", cluster2.metadata.keyspaces[self.keyspace_name].user_types) + assert "user" in cluster2.metadata.keyspaces[self.keyspace_name].user_types if PROTOCOL_VERSION >= 4: # UDF metadata modification @@ -632,9 +643,9 @@ def test_refresh_schema_metadata(self): RETURNS int LANGUAGE java AS 'return key+val;';""".format(self.keyspace_name)) - self.assertEqual(cluster2.metadata.keyspaces[self.keyspace_name].functions, {}) + assert cluster2.metadata.keyspaces[self.keyspace_name].functions == {} cluster2.refresh_schema_metadata() - self.assertIn("sum_int(int,int)", cluster2.metadata.keyspaces[self.keyspace_name].functions) + assert "sum_int(int,int)" in cluster2.metadata.keyspaces[self.keyspace_name].functions # UDA metadata modification self.session.execute("""CREATE AGGREGATE {0}.sum_agg(int) @@ -643,16 +654,16 @@ def test_refresh_schema_metadata(self): INITCOND 0""" .format(self.keyspace_name)) - self.assertEqual(cluster2.metadata.keyspaces[self.keyspace_name].aggregates, {}) + assert cluster2.metadata.keyspaces[self.keyspace_name].aggregates == {} cluster2.refresh_schema_metadata() - self.assertIn("sum_agg(int)", cluster2.metadata.keyspaces[self.keyspace_name].aggregates) + assert "sum_agg(int)" in cluster2.metadata.keyspaces[self.keyspace_name].aggregates # Cluster metadata modification self.session.execute("DROP KEYSPACE new_keyspace") - self.assertIn("new_keyspace", cluster2.metadata.keyspaces) + assert "new_keyspace" in cluster2.metadata.keyspaces cluster2.refresh_schema_metadata() - self.assertNotIn("new_keyspace", cluster2.metadata.keyspaces) + assert "new_keyspace" not in cluster2.metadata.keyspaces cluster2.shutdown() @@ -676,11 +687,11 @@ def test_refresh_keyspace_metadata(self): cluster2 = TestCluster(schema_event_refresh_window=-1) cluster2.connect() - self.assertTrue(cluster2.metadata.keyspaces[self.keyspace_name].durable_writes) + assert cluster2.metadata.keyspaces[self.keyspace_name].durable_writes self.session.execute("ALTER KEYSPACE {0} WITH durable_writes = false".format(self.keyspace_name)) - self.assertTrue(cluster2.metadata.keyspaces[self.keyspace_name].durable_writes) + assert cluster2.metadata.keyspaces[self.keyspace_name].durable_writes cluster2.refresh_keyspace_metadata(self.keyspace_name) - self.assertFalse(cluster2.metadata.keyspaces[self.keyspace_name].durable_writes) + assert not cluster2.metadata.keyspaces[self.keyspace_name].durable_writes cluster2.shutdown() @@ -707,12 +718,12 @@ def test_refresh_table_metadata(self): cluster2 = TestCluster(schema_event_refresh_window=-1) cluster2.connect() - self.assertNotIn("c", cluster2.metadata.keyspaces[self.keyspace_name].tables[table_name].columns) + assert "c" not in cluster2.metadata.keyspaces[self.keyspace_name].tables[table_name].columns self.session.execute("ALTER TABLE {0}.{1} ADD c double".format(self.keyspace_name, table_name)) - self.assertNotIn("c", cluster2.metadata.keyspaces[self.keyspace_name].tables[table_name].columns) + assert "c" not in cluster2.metadata.keyspaces[self.keyspace_name].tables[table_name].columns cluster2.refresh_table_metadata(self.keyspace_name, table_name) - self.assertIn("c", cluster2.metadata.keyspaces[self.keyspace_name].tables[table_name].columns) + assert "c" in cluster2.metadata.keyspaces[self.keyspace_name].tables[table_name].columns cluster2.shutdown() @@ -742,38 +753,38 @@ def test_refresh_metadata_for_mv(self): cluster2.connect() try: - self.assertNotIn("mv1", cluster2.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views) + assert "mv1" not in cluster2.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views self.session.execute("CREATE MATERIALIZED VIEW {0}.mv1 AS SELECT a, b FROM {0}.{1} " "WHERE a IS NOT NULL AND b IS NOT NULL PRIMARY KEY (a, b)" .format(self.keyspace_name, self.function_table_name)) - self.assertNotIn("mv1", cluster2.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views) + assert "mv1" not in cluster2.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views cluster2.refresh_table_metadata(self.keyspace_name, "mv1") - self.assertIn("mv1", cluster2.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views) + assert "mv1" in cluster2.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views finally: cluster2.shutdown() original_meta = self.cluster.metadata.keyspaces[self.keyspace_name].views['mv1'] - self.assertIs(original_meta, self.session.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views['mv1']) + assert original_meta is self.session.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views['mv1'] self.cluster.refresh_materialized_view_metadata(self.keyspace_name, 'mv1') current_meta = self.cluster.metadata.keyspaces[self.keyspace_name].views['mv1'] - self.assertIsNot(current_meta, original_meta) - self.assertIsNot(original_meta, self.session.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views['mv1']) - self.assertEqual(original_meta.as_cql_query(), current_meta.as_cql_query()) + assert current_meta is not original_meta + assert original_meta is not self.session.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views['mv1'] + assert original_meta.as_cql_query() == current_meta.as_cql_query() cluster3 = TestCluster(schema_event_refresh_window=-1) cluster3.connect() try: - self.assertNotIn("mv2", cluster3.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views) + assert "mv2" not in cluster3.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views self.session.execute( "CREATE MATERIALIZED VIEW {0}.mv2 AS SELECT a, b FROM {0}.{1} " "WHERE a IS NOT NULL AND b IS NOT NULL PRIMARY KEY (a, b)".format( self.keyspace_name, self.function_table_name) ) - self.assertNotIn("mv2", cluster3.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views) + assert "mv2" not in cluster3.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views cluster3.refresh_materialized_view_metadata(self.keyspace_name, 'mv2') - self.assertIn("mv2", cluster3.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views) + assert "mv2" in cluster3.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views finally: cluster3.shutdown() @@ -800,12 +811,12 @@ def test_refresh_user_type_metadata(self): cluster2 = TestCluster(schema_event_refresh_window=-1) cluster2.connect() - self.assertEqual(cluster2.metadata.keyspaces[self.keyspace_name].user_types, {}) + assert cluster2.metadata.keyspaces[self.keyspace_name].user_types == {} self.session.execute("CREATE TYPE {0}.user (age int, name text)".format(self.keyspace_name)) - self.assertEqual(cluster2.metadata.keyspaces[self.keyspace_name].user_types, {}) + assert cluster2.metadata.keyspaces[self.keyspace_name].user_types == {} cluster2.refresh_user_type_metadata(self.keyspace_name, "user") - self.assertIn("user", cluster2.metadata.keyspaces[self.keyspace_name].user_types) + assert "user" in cluster2.metadata.keyspaces[self.keyspace_name].user_types cluster2.shutdown() @@ -827,21 +838,21 @@ def test_refresh_user_type_metadata_proto_2(self): for protocol_version in (1, 2): cluster = TestCluster() session = cluster.connect() - self.assertEqual(cluster.metadata.keyspaces[self.keyspace_name].user_types, {}) + assert cluster.metadata.keyspaces[self.keyspace_name].user_types == {} session.execute("CREATE TYPE {0}.user (age int, name text)".format(self.keyspace_name)) - self.assertIn("user", cluster.metadata.keyspaces[self.keyspace_name].user_types) - self.assertIn("age", cluster.metadata.keyspaces[self.keyspace_name].user_types["user"].field_names) - self.assertIn("name", cluster.metadata.keyspaces[self.keyspace_name].user_types["user"].field_names) + assert "user" in cluster.metadata.keyspaces[self.keyspace_name].user_types + assert "age" in cluster.metadata.keyspaces[self.keyspace_name].user_types["user"].field_names + assert "name" in cluster.metadata.keyspaces[self.keyspace_name].user_types["user"].field_names session.execute("ALTER TYPE {0}.user ADD flag boolean".format(self.keyspace_name)) - self.assertIn("flag", cluster.metadata.keyspaces[self.keyspace_name].user_types["user"].field_names) + assert "flag" in cluster.metadata.keyspaces[self.keyspace_name].user_types["user"].field_names session.execute("ALTER TYPE {0}.user RENAME flag TO something".format(self.keyspace_name)) - self.assertIn("something", cluster.metadata.keyspaces[self.keyspace_name].user_types["user"].field_names) + assert "something" in cluster.metadata.keyspaces[self.keyspace_name].user_types["user"].field_names session.execute("DROP TYPE {0}.user".format(self.keyspace_name)) - self.assertEqual(cluster.metadata.keyspaces[self.keyspace_name].user_types, {}) + assert cluster.metadata.keyspaces[self.keyspace_name].user_types == {} cluster.shutdown() @requires_java_udf @@ -869,15 +880,15 @@ def test_refresh_user_function_metadata(self): cluster2 = TestCluster(schema_event_refresh_window=-1) cluster2.connect() - self.assertEqual(cluster2.metadata.keyspaces[self.keyspace_name].functions, {}) + assert cluster2.metadata.keyspaces[self.keyspace_name].functions == {} self.session.execute("""CREATE FUNCTION {0}.sum_int(key int, val int) RETURNS NULL ON NULL INPUT RETURNS int LANGUAGE java AS ' return key + val;';""".format(self.keyspace_name)) - self.assertEqual(cluster2.metadata.keyspaces[self.keyspace_name].functions, {}) + assert cluster2.metadata.keyspaces[self.keyspace_name].functions == {} cluster2.refresh_user_function_metadata(self.keyspace_name, UserFunctionDescriptor("sum_int", ["int", "int"])) - self.assertIn("sum_int(int,int)", cluster2.metadata.keyspaces[self.keyspace_name].functions) + assert "sum_int(int,int)" in cluster2.metadata.keyspaces[self.keyspace_name].functions cluster2.shutdown() @@ -906,7 +917,7 @@ def test_refresh_user_aggregate_metadata(self): cluster2 = TestCluster(schema_event_refresh_window=-1) cluster2.connect() - self.assertEqual(cluster2.metadata.keyspaces[self.keyspace_name].aggregates, {}) + assert cluster2.metadata.keyspaces[self.keyspace_name].aggregates == {} self.session.execute("""CREATE FUNCTION {0}.sum_int(key int, val int) RETURNS NULL ON NULL INPUT RETURNS int @@ -918,9 +929,9 @@ def test_refresh_user_aggregate_metadata(self): INITCOND 0""" .format(self.keyspace_name)) - self.assertEqual(cluster2.metadata.keyspaces[self.keyspace_name].aggregates, {}) + assert cluster2.metadata.keyspaces[self.keyspace_name].aggregates == {} cluster2.refresh_user_aggregate_metadata(self.keyspace_name, UserAggregateDescriptor("sum_agg", ["int"])) - self.assertIn("sum_agg(int)", cluster2.metadata.keyspaces[self.keyspace_name].aggregates) + assert "sum_agg(int)" in cluster2.metadata.keyspaces[self.keyspace_name].aggregates cluster2.shutdown() @@ -944,19 +955,19 @@ def test_multiple_indices(self): self.session.execute("CREATE INDEX index_2 ON {0}.{1}(keys(b))".format(self.keyspace_name, self.function_table_name)) indices = self.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].indexes - self.assertEqual(len(indices), 2) + assert len(indices) == 2 index_1 = indices["index_1"] index_2 = indices['index_2'] - self.assertEqual(index_1.table_name, "test_multiple_indices") - self.assertEqual(index_1.name, "index_1") - self.assertEqual(index_1.kind, "COMPOSITES") - self.assertEqual(index_1.index_options["target"], "values(b)") - self.assertEqual(index_1.keyspace_name, "schemametadatatests") - self.assertEqual(index_2.table_name, "test_multiple_indices") - self.assertEqual(index_2.name, "index_2") - self.assertEqual(index_2.kind, "COMPOSITES") - self.assertEqual(index_2.index_options["target"], "keys(b)") - self.assertEqual(index_2.keyspace_name, "schemametadatatests") + assert index_1.table_name == "test_multiple_indices" + assert index_1.name == "index_1" + assert index_1.kind == "COMPOSITES" + assert index_1.index_options["target"] == "values(b)" + assert index_1.keyspace_name == "schemametadatatests" + assert index_2.table_name == "test_multiple_indices" + assert index_2.name == "index_2" + assert index_2.kind == "COMPOSITES" + assert index_2.index_options["target"] == "keys(b)" + assert index_2.keyspace_name == "schemametadatatests" @greaterthanorequalcass30 def test_table_extensions(self): @@ -990,17 +1001,17 @@ def after_table_cql(cls, table_meta, ext_key, ext_blob): class Ext1(Ext0): name = t + '##' - self.assertIn(Ext0.name, _RegisteredExtensionType._extension_registry) - self.assertIn(Ext1.name, _RegisteredExtensionType._extension_registry) + assert Ext0.name in _RegisteredExtensionType._extension_registry + assert Ext1.name in _RegisteredExtensionType._extension_registry # There will bee the RLAC extension here. - self.assertEqual(len(_RegisteredExtensionType._extension_registry), 3) + assert len(_RegisteredExtensionType._extension_registry) == 3 self.cluster.refresh_table_metadata(ks, t) table_meta = ks_meta.tables[t] view_meta = table_meta.views[v] - self.assertEqual(table_meta.export_as_string(), original_table_cql) - self.assertEqual(view_meta.export_as_string(), original_view_cql) + assert table_meta.export_as_string() == original_table_cql + assert view_meta.export_as_string() == original_view_cql update_t = s.prepare('UPDATE system_schema.tables SET extensions=? WHERE keyspace_name=? AND table_name=?') # for blob type coercing update_v = s.prepare('UPDATE system_schema.views SET extensions=? WHERE keyspace_name=? AND view_name=?') @@ -1014,17 +1025,17 @@ class Ext1(Ext0): table_meta = ks_meta.tables[t] view_meta = table_meta.views[v] - self.assertIn(Ext0.name, table_meta.extensions) + assert Ext0.name in table_meta.extensions new_cql = table_meta.export_as_string() - self.assertNotEqual(new_cql, original_table_cql) - self.assertIn(Ext0.after_table_cql(table_meta, Ext0.name, ext_map[Ext0.name]), new_cql) - self.assertNotIn(Ext1.name, new_cql) + assert new_cql != original_table_cql + assert Ext0.after_table_cql(table_meta, Ext0.name, ext_map[Ext0.name]) in new_cql + assert Ext1.name not in new_cql - self.assertIn(Ext0.name, view_meta.extensions) + assert Ext0.name in view_meta.extensions new_cql = view_meta.export_as_string() - self.assertNotEqual(new_cql, original_view_cql) - self.assertIn(Ext0.after_table_cql(view_meta, Ext0.name, ext_map[Ext0.name]), new_cql) - self.assertNotIn(Ext1.name, new_cql) + assert new_cql != original_view_cql + assert Ext0.after_table_cql(view_meta, Ext0.name, ext_map[Ext0.name]) in new_cql + assert Ext1.name not in new_cql # extensions registered, one present # -------------------------------------- @@ -1037,19 +1048,19 @@ class Ext1(Ext0): table_meta = ks_meta.tables[t] view_meta = table_meta.views[v] - self.assertIn(Ext0.name, table_meta.extensions) - self.assertIn(Ext1.name, table_meta.extensions) + assert Ext0.name in table_meta.extensions + assert Ext1.name in table_meta.extensions new_cql = table_meta.export_as_string() - self.assertNotEqual(new_cql, original_table_cql) - self.assertIn(Ext0.after_table_cql(table_meta, Ext0.name, ext_map[Ext0.name]), new_cql) - self.assertIn(Ext1.after_table_cql(table_meta, Ext1.name, ext_map[Ext1.name]), new_cql) + assert new_cql != original_table_cql + assert Ext0.after_table_cql(table_meta, Ext0.name, ext_map[Ext0.name]) in new_cql + assert Ext1.after_table_cql(table_meta, Ext1.name, ext_map[Ext1.name]) in new_cql - self.assertIn(Ext0.name, view_meta.extensions) - self.assertIn(Ext1.name, view_meta.extensions) + assert Ext0.name in view_meta.extensions + assert Ext1.name in view_meta.extensions new_cql = view_meta.export_as_string() - self.assertNotEqual(new_cql, original_view_cql) - self.assertIn(Ext0.after_table_cql(view_meta, Ext0.name, ext_map[Ext0.name]), new_cql) - self.assertIn(Ext1.after_table_cql(view_meta, Ext1.name, ext_map[Ext1.name]), new_cql) + assert new_cql != original_view_cql + assert Ext0.after_table_cql(view_meta, Ext0.name, ext_map[Ext0.name]) in new_cql + assert Ext1.after_table_cql(view_meta, Ext1.name, ext_map[Ext1.name]) in new_cql def test_metadata_pagination(self): self.cluster.refresh_schema_metadata() @@ -1059,20 +1070,20 @@ def test_metadata_pagination(self): self.cluster.schema_metadata_page_size = 5 self.cluster.refresh_schema_metadata() - self.assertEqual(len(self.cluster.metadata.keyspaces[self.keyspace_name].tables), 12) + assert len(self.cluster.metadata.keyspaces[self.keyspace_name].tables) == 12 def test_metadata_pagination_keyspaces(self): """ test for covering https://github.com/scylladb/python-driver/issues/174 """ - + self.cluster.refresh_schema_metadata() keyspaces = [f"keyspace{idx}" for idx in range(15)] for ks in keyspaces: self.session.execute( - f"CREATE KEYSPACE IF NOT EXISTS {ks} WITH REPLICATION = {{ 'class' : 'SimpleStrategy', 'replication_factor' : 3 }}" + f"CREATE KEYSPACE IF NOT EXISTS {ks} WITH REPLICATION = {{ 'class' : 'NetworkTopologyStrategy', 'replication_factor' : 3 }}" ) self.cluster.schema_metadata_page_size = 2000 @@ -1084,7 +1095,7 @@ def test_metadata_pagination_keyspaces(self): after_ks_num = len(self.cluster.metadata.keyspaces) - self.assertEqual(before_ks_num, after_ks_num) + assert before_ks_num == after_ks_num class TestCodeCoverage(unittest.TestCase): @@ -1097,7 +1108,7 @@ def test_export_schema(self): cluster = TestCluster() cluster.connect() - self.assertIsInstance(cluster.metadata.export_schema_as_string(), str) + assert isinstance(cluster.metadata.export_schema_as_string(), str) cluster.shutdown() def test_export_keyspace_schema(self): @@ -1110,27 +1121,10 @@ def test_export_keyspace_schema(self): for keyspace in cluster.metadata.keyspaces: keyspace_metadata = cluster.metadata.keyspaces[keyspace] - self.assertIsInstance(keyspace_metadata.export_as_string(), str) - self.assertIsInstance(keyspace_metadata.as_cql_query(), str) + assert isinstance(keyspace_metadata.export_as_string(), str) + assert isinstance(keyspace_metadata.as_cql_query(), str) cluster.shutdown() - def assert_equal_diff(self, received, expected): - if received != expected: - diff_string = '\n'.join(difflib.unified_diff(expected.split('\n'), - received.split('\n'), - 'EXPECTED', 'RECEIVED', - lineterm='')) - self.fail(diff_string) - - def assert_startswith_diff(self, received, prefix): - if not received.startswith(prefix): - prefix_lines = prefix.split('\n') - diff_string = '\n'.join(difflib.unified_diff(prefix_lines, - received.split('\n')[:len(prefix_lines)], - 'EXPECTED', 'RECEIVED', - lineterm='')) - self.fail(diff_string) - @greaterthancass20 def test_export_keyspace_schema_udts(self): """ @@ -1150,7 +1144,7 @@ def test_export_keyspace_schema_udts(self): session.execute(""" CREATE KEYSPACE export_udts - WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} + WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '1'} AND durable_writes = true; """) session.execute(""" @@ -1174,7 +1168,7 @@ def test_export_keyspace_schema_udts(self): addresses map>) """) - expected_prefix = """CREATE KEYSPACE export_udts WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true; + expected_prefix = """CREATE KEYSPACE export_udts WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '1'} AND durable_writes = true; CREATE TYPE export_udts.street ( street_number int, @@ -1195,7 +1189,7 @@ def test_export_keyspace_schema_udts(self): user text PRIMARY KEY, addresses map>""" - self.assert_startswith_diff(cluster.metadata.keyspaces['export_udts'].export_as_string(), expected_prefix) + assert_startswith_diff(cluster.metadata.keyspaces['export_udts'].export_as_string(), expected_prefix) table_meta = cluster.metadata.keyspaces['export_udts'].tables['users'] @@ -1203,13 +1197,11 @@ def test_export_keyspace_schema_udts(self): user text PRIMARY KEY, addresses map>""" - self.assert_startswith_diff(table_meta.export_as_string(), expected_prefix) + assert_startswith_diff(table_meta.export_as_string(), expected_prefix) cluster.shutdown() @greaterthancass21 - @xfail_scylla_version_lt(reason='scylladb/scylladb#10707 - Column name in CREATE INDEX is not quoted', - oss_scylla_version="5.2", ent_scylla_version="2023.1.1") def test_case_sensitivity(self): """ Test that names that need to be escaped in CREATE statements are @@ -1222,10 +1214,9 @@ def test_case_sensitivity(self): cfname = 'AnInterestingTable' session.execute("DROP KEYSPACE IF EXISTS {0}".format(ksname)) - session.execute(""" - CREATE KEYSPACE "%s" - WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} - """ % (ksname,)) + session.execute( + ("CREATE KEYSPACE \"%s\" WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '1'}" + + get_tablets_disabled_ddl_suffix()) % (ksname,)) session.execute(""" CREATE TABLE "%s"."%s" ( k int, @@ -1244,15 +1235,15 @@ def test_case_sensitivity(self): ksmeta = cluster.metadata.keyspaces[ksname] schema = ksmeta.export_as_string() - self.assertIn('CREATE KEYSPACE "AnInterestingKeyspace"', schema) - self.assertIn('CREATE TABLE "AnInterestingKeyspace"."AnInterestingTable"', schema) - self.assertIn('"A" int', schema) - self.assertIn('"B" int', schema) - self.assertIn('"MyColumn" int', schema) - self.assertIn('PRIMARY KEY (k, "A")', schema) - self.assertIn('WITH CLUSTERING ORDER BY ("A" DESC)', schema) - self.assertIn('CREATE INDEX myindex ON "AnInterestingKeyspace"."AnInterestingTable" ("MyColumn")', schema) - self.assertIn('CREATE INDEX "AnotherIndex" ON "AnInterestingKeyspace"."AnInterestingTable" ("B")', schema) + assert 'CREATE KEYSPACE "AnInterestingKeyspace"' in schema + assert 'CREATE TABLE "AnInterestingKeyspace"."AnInterestingTable"' in schema + assert '"A" int' in schema + assert '"B" int' in schema + assert '"MyColumn" int' in schema + assert 'PRIMARY KEY (k, "A")' in schema + assert 'WITH CLUSTERING ORDER BY ("A" DESC)' in schema + assert 'CREATE INDEX myindex ON "AnInterestingKeyspace"."AnInterestingTable" ("MyColumn")' in schema + assert 'CREATE INDEX "AnotherIndex" ON "AnInterestingKeyspace"."AnInterestingTable" ("B")' in schema cluster.shutdown() def test_already_exists_exceptions(self): @@ -1268,14 +1259,16 @@ def test_already_exists_exceptions(self): ddl = ''' CREATE KEYSPACE %s - WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '3'}''' - self.assertRaises(AlreadyExists, session.execute, ddl % ksname) + WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '3'}''' + with pytest.raises(AlreadyExists): + session.execute(ddl % ksname) ddl = ''' CREATE TABLE %s.%s ( k int PRIMARY KEY, v int )''' - self.assertRaises(AlreadyExists, session.execute, ddl % (ksname, cfname)) + with pytest.raises(AlreadyExists): + session.execute(ddl % (ksname, cfname)) cluster.shutdown() @local @@ -1288,14 +1281,14 @@ def test_replicas(self): raise unittest.SkipTest('the murmur3 extension is not available') cluster = TestCluster() - self.assertEqual(cluster.metadata.get_replicas('test3rf', 'key'), []) + assert cluster.metadata.get_replicas('test3rf', 'key') == [] cluster.connect('test3rf') - self.assertNotEqual(list(cluster.metadata.get_replicas('test3rf', b'key')), []) + assert list(cluster.metadata.get_replicas('test3rf', b'key')) != [] host = list(cluster.metadata.get_replicas('test3rf', b'key'))[0] - self.assertEqual(host.datacenter, 'dc1') - self.assertEqual(host.rack, 'r1') + assert host.datacenter == 'dc1' + assert host.rack == 'r1' cluster.shutdown() def test_token_map(self): @@ -1310,12 +1303,12 @@ def test_token_map(self): get_replicas = cluster.metadata.token_map.get_replicas for ksname in ('test1rf', 'test2rf', 'test3rf'): - self.assertNotEqual(list(get_replicas(ksname, ring[0])), []) + assert list(get_replicas(ksname, ring[0])) != [] for i, token in enumerate(ring): - self.assertEqual(set(get_replicas('test3rf', token)), set(owners)) - self.assertEqual(set(get_replicas('test2rf', token)), set([owners[i], owners[(i + 1) % 3]])) - self.assertEqual(set(get_replicas('test1rf', token)), set([owners[i]])) + assert set(get_replicas('test3rf', token)) == set(owners) + assert set(get_replicas('test2rf', token)) == set([owners[i], owners[(i + 1) % 3]]) + assert set(get_replicas('test1rf', token)) == set([owners[i]]) cluster.shutdown() @@ -1330,25 +1323,44 @@ def test_token(self): cluster = TestCluster() cluster.connect() tmap = cluster.metadata.token_map - self.assertTrue(issubclass(tmap.token_class, Token)) - self.assertEqual(expected_node_count, len(tmap.ring)) + assert issubclass(tmap.token_class, Token) + assert expected_node_count == len(tmap.ring) cluster.shutdown() -class MetadataTimeoutTest(unittest.TestCase): +class TestMetadataTimeout: """ Test of TokenMap creation and other behavior. """ - def test_timeout(self): - cluster = TestCluster() - cluster.metadata_request_timeout = None + @pytest.mark.parametrize( + "opts, expected_query_chunk", + [ + ( + {"metadata_request_timeout": None}, + # Should be borrowed from control_connection_timeout + "USING TIMEOUT 2000ms" + ), + ( + {"metadata_request_timeout": 0.0}, + False + ), + ( + {"metadata_request_timeout": 4.0}, + "USING TIMEOUT 4000ms" + ), + ( + {"metadata_request_timeout": None, "control_connection_timeout": None}, + False, + ) + ], + ids=["default", "zero", "4s", "both none"] + ) + def test_timeout(self, opts, expected_query_chunk): + cluster = TestCluster(**opts) stmts = [] class ConnectionWrapper(cluster.connection_class): - def __init__(self, *args, **kwargs): - super(ConnectionWrapper, self).__init__(*args, **kwargs) - def send_msg(self, msg, request_id, cb, encoder=ProtocolHandler.encode_message, decoder=ProtocolHandler.decode_message, result_metadata=None): if isinstance(msg, QueryMessage): @@ -1363,8 +1375,10 @@ def send_msg(self, msg, request_id, cb, encoder=ProtocolHandler.encode_message, for stmt in stmts: if "SELECT now() FROM system.local WHERE key='local'" in stmt: continue - if "USING TIMEOUT 2000ms" not in stmt: - self.fail(f"query `{stmt}` does not contain `USING TIMEOUT 2000ms`") + if expected_query_chunk: + assert expected_query_chunk in stmt, f"query `{stmt}` does not contain `{expected_query_chunk}`" + else: + assert 'USING TIMEOUT' not in stmt, f"query `{stmt}` should not contain `USING TIMEOUT`" class KeyspaceAlterMetadata(unittest.TestCase): @@ -1376,7 +1390,7 @@ def setUp(self): self.session = self.cluster.connect() name = self._testMethodName.lower() crt_ks = ''' - CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1} AND durable_writes = true''' % name + CREATE KEYSPACE %s WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': 1} AND durable_writes = true''' % name self.session.execute(crt_ks) def tearDown(self): @@ -1398,13 +1412,13 @@ def test_keyspace_alter(self): self.session.execute('CREATE TABLE %s.d (d INT PRIMARY KEY)' % name) original_keyspace_meta = self.cluster.metadata.keyspaces[name] - self.assertEqual(original_keyspace_meta.durable_writes, True) - self.assertEqual(len(original_keyspace_meta.tables), 1) + assert original_keyspace_meta.durable_writes == True + assert len(original_keyspace_meta.tables) == 1 self.session.execute('ALTER KEYSPACE %s WITH durable_writes = false' % name) new_keyspace_meta = self.cluster.metadata.keyspaces[name] - self.assertNotEqual(original_keyspace_meta, new_keyspace_meta) - self.assertEqual(new_keyspace_meta.durable_writes, False) + assert original_keyspace_meta != new_keyspace_meta + assert new_keyspace_meta.durable_writes == False class IndexMapTests(unittest.TestCase): @@ -1423,11 +1437,9 @@ def setup_class(cls): if cls.keyspace_name in cls.cluster.metadata.keyspaces: cls.session.execute("DROP KEYSPACE %s" % cls.keyspace_name) - cls.session.execute( - """ - CREATE KEYSPACE %s - WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}; - """ % cls.keyspace_name) + ddl = ("CREATE KEYSPACE %s WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '1'}" + + get_tablets_disabled_ddl_suffix()) + cls.session.execute(ddl % cls.keyspace_name) cls.session.set_keyspace(cls.keyspace_name) except Exception: cls.cluster.shutdown() @@ -1451,10 +1463,10 @@ def test_index_updates(self): ks_meta = self.cluster.metadata.keyspaces[self.keyspace_name] table_meta = ks_meta.tables[self.table_name] - self.assertNotIn('a_idx', ks_meta.indexes) - self.assertNotIn('b_idx', ks_meta.indexes) - self.assertNotIn('a_idx', table_meta.indexes) - self.assertNotIn('b_idx', table_meta.indexes) + assert 'a_idx' not in ks_meta.indexes + assert 'b_idx' not in ks_meta.indexes + assert 'a_idx' not in table_meta.indexes + assert 'b_idx' not in table_meta.indexes self.session.execute("CREATE INDEX a_idx ON %s (a)" % self.table_name) self.session.execute("ALTER TABLE %s ADD b int" % self.table_name) @@ -1462,10 +1474,10 @@ def test_index_updates(self): ks_meta = self.cluster.metadata.keyspaces[self.keyspace_name] table_meta = ks_meta.tables[self.table_name] - self.assertIsInstance(ks_meta.indexes['a_idx'], IndexMetadata) - self.assertIsInstance(ks_meta.indexes['b_idx'], IndexMetadata) - self.assertIsInstance(table_meta.indexes['a_idx'], IndexMetadata) - self.assertIsInstance(table_meta.indexes['b_idx'], IndexMetadata) + assert isinstance(ks_meta.indexes['a_idx'], IndexMetadata) + assert isinstance(ks_meta.indexes['b_idx'], IndexMetadata) + assert isinstance(table_meta.indexes['a_idx'], IndexMetadata) + assert isinstance(table_meta.indexes['b_idx'], IndexMetadata) # both indexes updated when index dropped self.session.execute("DROP INDEX a_idx") @@ -1475,17 +1487,17 @@ def test_index_updates(self): ks_meta = self.cluster.metadata.keyspaces[self.keyspace_name] table_meta = ks_meta.tables[self.table_name] - self.assertNotIn('a_idx', ks_meta.indexes) - self.assertIsInstance(ks_meta.indexes['b_idx'], IndexMetadata) - self.assertNotIn('a_idx', table_meta.indexes) - self.assertIsInstance(table_meta.indexes['b_idx'], IndexMetadata) + assert 'a_idx' not in ks_meta.indexes + assert isinstance(ks_meta.indexes['b_idx'], IndexMetadata) + assert 'a_idx' not in table_meta.indexes + assert isinstance(table_meta.indexes['b_idx'], IndexMetadata) # keyspace index updated when table dropped self.drop_basic_table() ks_meta = self.cluster.metadata.keyspaces[self.keyspace_name] - self.assertNotIn(self.table_name, ks_meta.tables) - self.assertNotIn('a_idx', ks_meta.indexes) - self.assertNotIn('b_idx', ks_meta.indexes) + assert self.table_name not in ks_meta.tables + assert 'a_idx' not in ks_meta.indexes + assert 'b_idx' not in ks_meta.indexes def test_index_follows_alter(self): self.create_basic_table() @@ -1494,15 +1506,15 @@ def test_index_follows_alter(self): self.session.execute("CREATE INDEX %s ON %s (a)" % (idx, self.table_name)) ks_meta = self.cluster.metadata.keyspaces[self.keyspace_name] table_meta = ks_meta.tables[self.table_name] - self.assertIsInstance(ks_meta.indexes[idx], IndexMetadata) - self.assertIsInstance(table_meta.indexes[idx], IndexMetadata) + assert isinstance(ks_meta.indexes[idx], IndexMetadata) + assert isinstance(table_meta.indexes[idx], IndexMetadata) self.session.execute('ALTER KEYSPACE %s WITH durable_writes = false' % self.keyspace_name) old_meta = ks_meta ks_meta = self.cluster.metadata.keyspaces[self.keyspace_name] - self.assertIsNot(ks_meta, old_meta) + assert ks_meta is not old_meta table_meta = ks_meta.tables[self.table_name] - self.assertIsInstance(ks_meta.indexes[idx], IndexMetadata) - self.assertIsInstance(table_meta.indexes[idx], IndexMetadata) + assert isinstance(ks_meta.indexes[idx], IndexMetadata) + assert isinstance(table_meta.indexes[idx], IndexMetadata) self.drop_basic_table() @requires_java_udf @@ -1529,7 +1541,7 @@ def setup_class(cls): cls.cluster = TestCluster() cls.keyspace_name = cls.__name__.lower() cls.session = cls.cluster.connect() - cls.session.execute("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}" % cls.keyspace_name) + cls.session.execute("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': 1}" % cls.keyspace_name) cls.session.set_keyspace(cls.keyspace_name) cls.keyspace_function_meta = cls.cluster.metadata.keyspaces[cls.keyspace_name].functions cls.keyspace_aggregate_meta = cls.cluster.metadata.keyspaces[cls.keyspace_name].aggregates @@ -1551,18 +1563,18 @@ def __init__(self, test_case, meta_class, element_meta, **function_kwargs): def __enter__(self): tc = self.test_case expected_meta = self.meta_class(**self.function_kwargs) - tc.assertNotIn(expected_meta.signature, self.element_meta) + assert expected_meta.signature not in self.element_meta tc.session.execute(expected_meta.as_cql_query()) - tc.assertIn(expected_meta.signature, self.element_meta) + assert expected_meta.signature in self.element_meta generated_meta = self.element_meta[expected_meta.signature] - self.test_case.assertEqual(generated_meta.as_cql_query(), expected_meta.as_cql_query()) + assert generated_meta.as_cql_query() == expected_meta.as_cql_query() return self def __exit__(self, exc_type, exc_val, exc_tb): tc = self.test_case tc.session.execute("DROP %s %s.%s" % (self.meta_class.__name__, tc.keyspace_name, self.signature)) - tc.assertNotIn(self.signature, self.element_meta) + assert self.signature not in self.element_meta @property def signature(self): @@ -1616,7 +1628,7 @@ def test_functions_after_udt(self): @test_category function """ - self.assertNotIn(self.function_name, self.keyspace_function_meta) + assert self.function_name not in self.keyspace_function_meta udt_name = 'udtx' self.session.execute("CREATE TYPE %s (x int)" % udt_name) @@ -1626,8 +1638,8 @@ def test_functions_after_udt(self): keyspace_cql = self.cluster.metadata.keyspaces[self.keyspace_name].export_as_string() type_idx = keyspace_cql.rfind("CREATE TYPE") func_idx = keyspace_cql.find("CREATE FUNCTION") - self.assertNotIn(-1, (type_idx, func_idx), "TYPE or FUNCTION not found in keyspace_cql: " + keyspace_cql) - self.assertGreater(func_idx, type_idx) + assert -1 not in (type_idx, func_idx), "TYPE or FUNCTION not found in keyspace_cql: " + keyspace_cql + assert func_idx > type_idx def test_function_same_name_diff_types(self): """ @@ -1647,16 +1659,16 @@ def test_function_same_name_diff_types(self): with self.VerifiedFunction(self, **kwargs): # another function: same name, different type sig. - self.assertGreater(len(kwargs['argument_types']), 1) - self.assertGreater(len(kwargs['argument_names']), 1) + assert len(kwargs['argument_types']) > 1 + assert len(kwargs['argument_names']) > 1 kwargs['argument_types'] = kwargs['argument_types'][:1] kwargs['argument_names'] = kwargs['argument_names'][:1] # Ensure they are surfaced separately with self.VerifiedFunction(self, **kwargs): functions = [f for f in self.keyspace_function_meta.values() if f.name == self.function_name] - self.assertEqual(len(functions), 2) - self.assertNotEqual(functions[0].argument_types, functions[1].argument_types) + assert len(functions) == 2 + assert functions[0].argument_types != functions[1].argument_types def test_function_no_parameters(self): """ @@ -1677,7 +1689,7 @@ def test_function_no_parameters(self): with self.VerifiedFunction(self, **kwargs) as vf: fn_meta = self.keyspace_function_meta[vf.signature] - self.assertRegex(fn_meta.as_cql_query(), r'CREATE FUNCTION.*%s\(\) .*' % kwargs['name']) + assertRegex(fn_meta.as_cql_query(), r'CREATE FUNCTION.*%s\(\) .*' % kwargs['name']) def test_functions_follow_keyspace_alter(self): """ @@ -1701,8 +1713,8 @@ def test_functions_follow_keyspace_alter(self): # After keyspace alter ensure that we maintain function equality. try: new_keyspace_meta = self.cluster.metadata.keyspaces[self.keyspace_name] - self.assertNotEqual(original_keyspace_meta, new_keyspace_meta) - self.assertIs(original_keyspace_meta.functions, new_keyspace_meta.functions) + assert original_keyspace_meta != new_keyspace_meta + assert original_keyspace_meta.functions is new_keyspace_meta.functions finally: self.session.execute('ALTER KEYSPACE %s WITH durable_writes = true' % self.keyspace_name) @@ -1725,12 +1737,12 @@ def test_function_cql_called_on_null(self): kwargs['called_on_null_input'] = True with self.VerifiedFunction(self, **kwargs) as vf: fn_meta = self.keyspace_function_meta[vf.signature] - self.assertRegex(fn_meta.as_cql_query(), r'CREATE FUNCTION.*\) CALLED ON NULL INPUT RETURNS .*') + assertRegex(fn_meta.as_cql_query(), r'CREATE FUNCTION.*\) CALLED ON NULL INPUT RETURNS .*') kwargs['called_on_null_input'] = False with self.VerifiedFunction(self, **kwargs) as vf: fn_meta = self.keyspace_function_meta[vf.signature] - self.assertRegex(fn_meta.as_cql_query(), r'CREATE FUNCTION.*\) RETURNS NULL ON NULL INPUT RETURNS .*') + assertRegex(fn_meta.as_cql_query(), r'CREATE FUNCTION.*\) RETURNS NULL ON NULL INPUT RETURNS .*') @requires_java_udf @@ -1793,7 +1805,7 @@ def test_return_type_meta(self): """ with self.VerifiedAggregate(self, **self.make_aggregate_kwargs('sum_int', 'int', init_cond='1')) as va: - self.assertEqual(self.keyspace_aggregate_meta[va.signature].return_type, 'int') + assert self.keyspace_aggregate_meta[va.signature].return_type == 'int' def test_init_cond(self): """ @@ -1822,16 +1834,15 @@ def test_init_cond(self): cql_init = encoder.cql_encode_all_types(init_cond) with self.VerifiedAggregate(self, **self.make_aggregate_kwargs('sum_int', 'int', init_cond=cql_init)) as va: sum_res = s.execute("SELECT %s(v) AS sum FROM t" % va.function_kwargs['name']).one().sum - self.assertEqual(sum_res, int(init_cond) + sum(expected_values)) + assert sum_res == int(init_cond) + sum(expected_values) # list for init_cond in ([], ['1', '2']): cql_init = encoder.cql_encode_all_types(init_cond) with self.VerifiedAggregate(self, **self.make_aggregate_kwargs('extend_list', 'list', init_cond=cql_init)) as va: list_res = s.execute("SELECT %s(v) AS list_res FROM t" % va.function_kwargs['name']).one().list_res - self.assertListEqual(list_res[:len(init_cond)], init_cond) - self.assertEqual(set(i for i in list_res[len(init_cond):]), - set(str(i) for i in expected_values)) + assertListEqual(list_res[:len(init_cond)], init_cond) + assert set(i for i in list_res[len(init_cond):]) == set(str(i) for i in expected_values) # map expected_map_values = dict((i, i) for i in expected_values) @@ -1840,9 +1851,9 @@ def test_init_cond(self): cql_init = encoder.cql_encode_all_types(init_cond) with self.VerifiedAggregate(self, **self.make_aggregate_kwargs('update_map', 'map', init_cond=cql_init)) as va: map_res = s.execute("SELECT %s(v) AS map_res FROM t" % va.function_kwargs['name']).one().map_res - self.assertLessEqual(expected_map_values.items(), map_res.items()) + assert expected_map_values.items() <= map_res.items() init_not_updated = dict((k, init_cond[k]) for k in set(init_cond) - expected_key_set) - self.assertLessEqual(init_not_updated.items(), map_res.items()) + assert init_not_updated.items() <= map_res.items() c.shutdown() def test_aggregates_after_functions(self): @@ -1864,8 +1875,8 @@ def test_aggregates_after_functions(self): keyspace_cql = self.cluster.metadata.keyspaces[self.keyspace_name].export_as_string() func_idx = keyspace_cql.find("CREATE FUNCTION") aggregate_idx = keyspace_cql.rfind("CREATE AGGREGATE") - self.assertNotIn(-1, (aggregate_idx, func_idx), "AGGREGATE or FUNCTION not found in keyspace_cql: " + keyspace_cql) - self.assertGreater(aggregate_idx, func_idx) + assert -1 not in (aggregate_idx, func_idx), "AGGREGATE or FUNCTION not found in keyspace_cql: " + keyspace_cql + assert aggregate_idx > func_idx def test_same_name_diff_types(self): """ @@ -1886,8 +1897,8 @@ def test_same_name_diff_types(self): kwargs['argument_types'] = ['int', 'int'] with self.VerifiedAggregate(self, **kwargs): aggregates = [a for a in self.keyspace_aggregate_meta.values() if a.name == kwargs['name']] - self.assertEqual(len(aggregates), 2) - self.assertNotEqual(aggregates[0].argument_types, aggregates[1].argument_types) + assert len(aggregates) == 2 + assert aggregates[0].argument_types != aggregates[1].argument_types def test_aggregates_follow_keyspace_alter(self): """ @@ -1908,8 +1919,8 @@ def test_aggregates_follow_keyspace_alter(self): self.session.execute('ALTER KEYSPACE %s WITH durable_writes = false' % self.keyspace_name) try: new_keyspace_meta = self.cluster.metadata.keyspaces[self.keyspace_name] - self.assertNotEqual(original_keyspace_meta, new_keyspace_meta) - self.assertIs(original_keyspace_meta.aggregates, new_keyspace_meta.aggregates) + assert original_keyspace_meta != new_keyspace_meta + assert original_keyspace_meta.aggregates is new_keyspace_meta.aggregates finally: self.session.execute('ALTER KEYSPACE %s WITH durable_writes = true' % self.keyspace_name) @@ -1931,51 +1942,51 @@ def test_cql_optional_params(self): encoder = Encoder() # no initial condition, final func - self.assertIsNone(kwargs['initial_condition']) - self.assertIsNone(kwargs['final_func']) + assert kwargs['initial_condition'] is None + assert kwargs['final_func'] is None with self.VerifiedAggregate(self, **kwargs) as va: meta = self.keyspace_aggregate_meta[va.signature] - self.assertIsNone(meta.initial_condition) - self.assertIsNone(meta.final_func) + assert meta.initial_condition is None + assert meta.final_func is None cql = meta.as_cql_query() - self.assertEqual(cql.find('INITCOND'), -1) - self.assertEqual(cql.find('FINALFUNC'), -1) + assert cql.find('INITCOND') == -1 + assert cql.find('FINALFUNC') == -1 # initial condition, no final func kwargs['initial_condition'] = encoder.cql_encode_all_types(['init', 'cond']) with self.VerifiedAggregate(self, **kwargs) as va: meta = self.keyspace_aggregate_meta[va.signature] - self.assertEqual(meta.initial_condition, kwargs['initial_condition']) - self.assertIsNone(meta.final_func) + assert meta.initial_condition == kwargs['initial_condition'] + assert meta.final_func is None cql = meta.as_cql_query() search_string = "INITCOND %s" % kwargs['initial_condition'] - self.assertGreater(cql.find(search_string), 0, '"%s" search string not found in cql:\n%s' % (search_string, cql)) - self.assertEqual(cql.find('FINALFUNC'), -1) + assert cql.find(search_string) > 0, '"%s" search string not found in cql:\n%s' % (search_string, cql) + assert cql.find('FINALFUNC') == -1 # no initial condition, final func kwargs['initial_condition'] = None kwargs['final_func'] = 'List_As_String' with self.VerifiedAggregate(self, **kwargs) as va: meta = self.keyspace_aggregate_meta[va.signature] - self.assertIsNone(meta.initial_condition) - self.assertEqual(meta.final_func, kwargs['final_func']) + assert meta.initial_condition is None + assert meta.final_func == kwargs['final_func'] cql = meta.as_cql_query() - self.assertEqual(cql.find('INITCOND'), -1) + assert cql.find('INITCOND') == -1 search_string = 'FINALFUNC "%s"' % kwargs['final_func'] - self.assertGreater(cql.find(search_string), 0, '"%s" search string not found in cql:\n%s' % (search_string, cql)) + assert cql.find(search_string) > 0, '"%s" search string not found in cql:\n%s' % (search_string, cql) # both kwargs['initial_condition'] = encoder.cql_encode_all_types(['init', 'cond']) kwargs['final_func'] = 'List_As_String' with self.VerifiedAggregate(self, **kwargs) as va: meta = self.keyspace_aggregate_meta[va.signature] - self.assertEqual(meta.initial_condition, kwargs['initial_condition']) - self.assertEqual(meta.final_func, kwargs['final_func']) + assert meta.initial_condition == kwargs['initial_condition'] + assert meta.final_func == kwargs['final_func'] cql = meta.as_cql_query() init_cond_idx = cql.find("INITCOND %s" % kwargs['initial_condition']) final_func_idx = cql.find('FINALFUNC "%s"' % kwargs['final_func']) - self.assertNotIn(-1, (init_cond_idx, final_func_idx)) - self.assertGreater(init_cond_idx, final_func_idx) + assert -1 not in (init_cond_idx, final_func_idx) + assert init_cond_idx > final_func_idx class BadMetaTest(unittest.TestCase): @@ -1997,7 +2008,8 @@ def setup_class(cls): cls.cluster = TestCluster() cls.keyspace_name = cls.__name__.lower() cls.session = cls.cluster.connect() - cls.session.execute("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}" % cls.keyspace_name) + ddl = "CREATE KEYSPACE %s WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '1'}" + get_tablets_disabled_ddl_suffix() + cls.session.execute(ddl % cls.keyspace_name) cls.session.set_keyspace(cls.keyspace_name) connection = cls.cluster.control_connection._connection @@ -2018,16 +2030,16 @@ def test_bad_keyspace(self): with patch.object(self.parser_class, '_build_keyspace_metadata_internal', side_effect=self.BadMetaException): self.cluster.refresh_keyspace_metadata(self.keyspace_name) m = self.cluster.metadata.keyspaces[self.keyspace_name] - self.assertIs(m._exc_info[0], self.BadMetaException) - self.assertIn("/*\nWarning:", m.export_as_string()) + assert m._exc_info[0] is self.BadMetaException + assert "/*\nWarning:" in m.export_as_string() def test_bad_table(self): self.session.execute('CREATE TABLE %s (k int PRIMARY KEY, v int)' % self.function_name) with patch.object(self.parser_class, '_build_column_metadata', side_effect=self.BadMetaException): self.cluster.refresh_table_metadata(self.keyspace_name, self.function_name) m = self.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_name] - self.assertIs(m._exc_info[0], self.BadMetaException) - self.assertIn("/*\nWarning:", m.export_as_string()) + assert m._exc_info[0] is self.BadMetaException + assert "/*\nWarning:" in m.export_as_string() def test_bad_index(self): self.session.execute('CREATE TABLE %s (k int PRIMARY KEY, v int)' % self.function_name) @@ -2035,8 +2047,8 @@ def test_bad_index(self): with patch.object(self.parser_class, '_build_index_metadata', side_effect=self.BadMetaException): self.cluster.refresh_table_metadata(self.keyspace_name, self.function_name) m = self.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_name] - self.assertIs(m._exc_info[0], self.BadMetaException) - self.assertIn("/*\nWarning:", m.export_as_string()) + assert m._exc_info[0] is self.BadMetaException + assert "/*\nWarning:" in m.export_as_string() @greaterthancass20 def test_bad_user_type(self): @@ -2044,8 +2056,8 @@ def test_bad_user_type(self): with patch.object(self.parser_class, '_build_user_type', side_effect=self.BadMetaException): self.cluster.refresh_schema_metadata() # presently do not capture these errors on udt direct refresh -- make sure it's contained during full refresh m = self.cluster.metadata.keyspaces[self.keyspace_name] - self.assertIs(m._exc_info[0], self.BadMetaException) - self.assertIn("/*\nWarning:", m.export_as_string()) + assert m._exc_info[0] is self.BadMetaException + assert "/*\nWarning:" in m.export_as_string() @greaterthancass21 @requires_java_udf @@ -2063,8 +2075,8 @@ def test_bad_user_function(self): with patch.object(self.parser_class, '_build_function', side_effect=self.BadMetaException): self.cluster.refresh_schema_metadata() # presently do not capture these errors on udt direct refresh -- make sure it's contained during full refresh m = self.cluster.metadata.keyspaces[self.keyspace_name] - self.assertIs(m._exc_info[0], self.BadMetaException) - self.assertIn("/*\nWarning:", m.export_as_string()) + assert m._exc_info[0] is self.BadMetaException + assert "/*\nWarning:" in m.export_as_string() @greaterthancass21 @requires_java_udf @@ -2082,8 +2094,8 @@ def test_bad_user_aggregate(self): with patch.object(self.parser_class, '_build_aggregate', side_effect=self.BadMetaException): self.cluster.refresh_schema_metadata() # presently do not capture these errors on udt direct refresh -- make sure it's contained during full refresh m = self.cluster.metadata.keyspaces[self.keyspace_name] - self.assertIs(m._exc_info[0], self.BadMetaException) - self.assertIn("/*\nWarning:", m.export_as_string()) + assert m._exc_info[0] is self.BadMetaException + assert "/*\nWarning:" in m.export_as_string() class DynamicCompositeTypeTest(BasicSharedKeyspaceUnitTestCase): @@ -2110,23 +2122,31 @@ def test_dct_alias(self): # Format can very slightly between versions, strip out whitespace for consistency sake table_text = dct_table.as_cql_query().replace(" ", "") dynamic_type_text = "c1'org.apache.cassandra.db.marshal.DynamicCompositeType(" - self.assertIn("c1'org.apache.cassandra.db.marshal.DynamicCompositeType(", table_text) + assert "c1'org.apache.cassandra.db.marshal.DynamicCompositeType(" in table_text # Types within in the composite can come out in random order, so grab the type definition and find each one type_definition_start = table_text.index("(", table_text.find(dynamic_type_text)) type_definition_end = table_text.index(")") type_definition_text = table_text[type_definition_start:type_definition_end] - self.assertIn("s=>org.apache.cassandra.db.marshal.UTF8Type", type_definition_text) - self.assertIn("i=>org.apache.cassandra.db.marshal.Int32Type", type_definition_text) + assert "s=>org.apache.cassandra.db.marshal.UTF8Type" in type_definition_text + assert "i=>org.apache.cassandra.db.marshal.Int32Type" in type_definition_text @greaterthanorequalcass30 class MaterializedViewMetadataTestSimple(BasicSharedKeyspaceUnitTestCase): + @classmethod + def create_keyspace(cls, rf): + ddl = "CREATE KEYSPACE {0} WITH replication = {{'class': 'NetworkTopologyStrategy', 'replication_factor': '{1}'}}{2}".format( + cls.ks_name, rf, get_tablets_disabled_ddl_suffix()) + execute_with_long_wait_retry(cls.session, ddl) + + def setUp(self): self.session.execute("CREATE TABLE {0}.{1} (pk int PRIMARY KEY, c int)".format(self.keyspace_name, self.function_table_name)) self.session.execute( "CREATE MATERIALIZED VIEW {0}.mv1 AS SELECT pk, c FROM {0}.{1} " - "WHERE pk IS NOT NULL AND c IS NOT NULL PRIMARY KEY (pk, c)".format( + "WHERE pk IS NOT NULL AND c IS NOT NULL PRIMARY KEY (pk, c) " + "WITH compaction = {{ 'class' : 'SizeTieredCompactionStrategy' }}".format( self.keyspace_name, self.function_table_name) ) @@ -2150,11 +2170,11 @@ def test_materialized_view_metadata_creation(self): @test_category metadata """ - self.assertIn("mv1", self.cluster.metadata.keyspaces[self.keyspace_name].views) - self.assertIn("mv1", self.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views) + assert "mv1" in self.cluster.metadata.keyspaces[self.keyspace_name].views + assert "mv1" in self.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views - self.assertEqual(self.keyspace_name, self.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views["mv1"].keyspace_name) - self.assertEqual(self.function_table_name, self.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views["mv1"].base_table_name) + assert self.keyspace_name == self.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views["mv1"].keyspace_name + assert self.function_table_name == self.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views["mv1"].base_table_name def test_materialized_view_metadata_alter(self): """ @@ -2171,10 +2191,10 @@ def test_materialized_view_metadata_alter(self): @test_category metadata """ - self.assertIn("SizeTieredCompactionStrategy", self.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views["mv1"].options["compaction"]["class"]) + assert "SizeTieredCompactionStrategy" in self.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views["mv1"].options["compaction"]["class"] self.session.execute("ALTER MATERIALIZED VIEW {0}.mv1 WITH compaction = {{ 'class' : 'LeveledCompactionStrategy' }}".format(self.keyspace_name)) - self.assertIn("LeveledCompactionStrategy", self.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views["mv1"].options["compaction"]["class"]) + assert "LeveledCompactionStrategy" in self.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views["mv1"].options["compaction"]["class"] def test_materialized_view_metadata_drop(self): """ @@ -2194,10 +2214,10 @@ def test_materialized_view_metadata_drop(self): self.session.execute("DROP MATERIALIZED VIEW {0}.mv1".format(self.keyspace_name)) - self.assertNotIn("mv1", self.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views) - self.assertNotIn("mv1", self.cluster.metadata.keyspaces[self.keyspace_name].views) - self.assertDictEqual({}, self.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views) - self.assertDictEqual({}, self.cluster.metadata.keyspaces[self.keyspace_name].views) + assert "mv1" not in self.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views + assert "mv1" not in self.cluster.metadata.keyspaces[self.keyspace_name].views + assertDictEqual({}, self.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].views) + assertDictEqual({}, self.cluster.metadata.keyspaces[self.keyspace_name].views) self.session.execute( "CREATE MATERIALIZED VIEW {0}.mv1 AS SELECT pk, c FROM {0}.{1} " @@ -2208,6 +2228,13 @@ def test_materialized_view_metadata_drop(self): @greaterthanorequalcass30 class MaterializedViewMetadataTestComplex(BasicSegregatedKeyspaceUnitTestCase): + + @classmethod + def create_keyspace(cls, rf): + ddl = "CREATE KEYSPACE {0} WITH replication = {{'class': 'NetworkTopologyStrategy', 'replication_factor': '{1}'}}{2}".format( + cls.ks_name, rf, get_tablets_disabled_ddl_suffix()) + execute_with_long_wait_retry(cls.session, ddl) + def test_create_view_metadata(self): """ test to ensure that materialized view metadata is properly constructed @@ -2244,63 +2271,63 @@ def test_create_view_metadata(self): score_table = self.cluster.metadata.keyspaces[self.keyspace_name].tables['scores'] mv = self.cluster.metadata.keyspaces[self.keyspace_name].views['monthlyhigh'] - self.assertIsNotNone(score_table.views["monthlyhigh"]) - self.assertIsNotNone(len(score_table.views), 1) + assert score_table.views["monthlyhigh"] is not None + assert len(score_table.views) is not None, 1 # Make sure user is a partition key, and not null - self.assertEqual(len(score_table.partition_key), 1) - self.assertIsNotNone(score_table.columns['user']) - self.assertTrue(score_table.columns['user'], score_table.partition_key[0]) + assert len(score_table.partition_key) == 1 + assert score_table.columns['user'] is not None + assert score_table.columns['user'], score_table.partition_key[0] # Validate clustering keys - self.assertEqual(len(score_table.clustering_key), 4) + assert len(score_table.clustering_key) == 4 - self.assertIsNotNone(score_table.columns['game']) - self.assertTrue(score_table.columns['game'], score_table.clustering_key[0]) + assert score_table.columns['game'] is not None + assert score_table.columns['game'], score_table.clustering_key[0] - self.assertIsNotNone(score_table.columns['year']) - self.assertTrue(score_table.columns['year'], score_table.clustering_key[1]) + assert score_table.columns['year'] is not None + assert score_table.columns['year'], score_table.clustering_key[1] - self.assertIsNotNone(score_table.columns['month']) - self.assertTrue(score_table.columns['month'], score_table.clustering_key[2]) + assert score_table.columns['month'] is not None + assert score_table.columns['month'], score_table.clustering_key[2] - self.assertIsNotNone(score_table.columns['day']) - self.assertTrue(score_table.columns['day'], score_table.clustering_key[3]) + assert score_table.columns['day'] is not None + assert score_table.columns['day'], score_table.clustering_key[3] - self.assertIsNotNone(score_table.columns['score']) + assert score_table.columns['score'] is not None # Validate basic mv information - self.assertEqual(mv.keyspace_name, self.keyspace_name) - self.assertEqual(mv.name, "monthlyhigh") - self.assertEqual(mv.base_table_name, "scores") - self.assertFalse(mv.include_all_columns) + assert mv.keyspace_name == self.keyspace_name + assert mv.name == "monthlyhigh" + assert mv.base_table_name == "scores" + assert not mv.include_all_columns # Validate that all columns are preset and correct mv_columns = list(mv.columns.values()) - self.assertEqual(len(mv_columns), 6) + assert len(mv_columns) == 6 game_column = mv_columns[0] - self.assertIsNotNone(game_column) - self.assertEqual(game_column.name, 'game') - self.assertEqual(game_column, mv.partition_key[0]) + assert game_column is not None + assert game_column.name == 'game' + assert game_column == mv.partition_key[0] year_column = mv_columns[1] - self.assertIsNotNone(year_column) - self.assertEqual(year_column.name, 'year') - self.assertEqual(year_column, mv.partition_key[1]) + assert year_column is not None + assert year_column.name == 'year' + assert year_column == mv.partition_key[1] month_column = mv_columns[2] - self.assertIsNotNone(month_column) - self.assertEqual(month_column.name, 'month') - self.assertEqual(month_column, mv.partition_key[2]) + assert month_column is not None + assert month_column.name == 'month' + assert month_column == mv.partition_key[2] def compare_columns(a, b, name): - self.assertEqual(a.name, name) - self.assertEqual(a.name, b.name) - self.assertEqual(a.table, b.table) - self.assertEqual(a.cql_type, b.cql_type) - self.assertEqual(a.is_static, b.is_static) - self.assertEqual(a.is_reversed, b.is_reversed) + assert a.name == name + assert a.name == b.name + assert a.table == b.table + assert a.cql_type == b.cql_type + assert a.is_static == b.is_static + assert a.is_reversed == b.is_reversed score_column = mv_columns[3] compare_columns(score_column, mv.clustering_key[0], 'score') @@ -2354,17 +2381,17 @@ def test_base_table_column_addition_mv(self): score_table = self.cluster.metadata.keyspaces[self.keyspace_name].tables['scores'] - self.assertIsNotNone(score_table.views["monthlyhigh"]) - self.assertIsNotNone(score_table.views["alltimehigh"]) - self.assertEqual(len(self.cluster.metadata.keyspaces[self.keyspace_name].views), 2) + assert score_table.views["monthlyhigh"] is not None + assert score_table.views["alltimehigh"] is not None + assert len(self.cluster.metadata.keyspaces[self.keyspace_name].views) == 2 insert_fouls = """ALTER TABLE {0}.scores ADD fouls INT""".format((self.keyspace_name)) self.session.execute(insert_fouls) - self.assertEqual(len(self.cluster.metadata.keyspaces[self.keyspace_name].views), 2) + assert len(self.cluster.metadata.keyspaces[self.keyspace_name].views) == 2 score_table = self.cluster.metadata.keyspaces[self.keyspace_name].tables['scores'] - self.assertIn("fouls", score_table.columns) + assert "fouls" in score_table.columns # This is a workaround for mv notifications being separate from base table schema responses. # This maybe fixed with future protocol changes @@ -2374,10 +2401,10 @@ def test_base_table_column_addition_mv(self): break time.sleep(.2) - self.assertIn("fouls", mv_alltime.columns) + assert "fouls" in mv_alltime.columns mv_alltime_fouls_comumn = self.cluster.metadata.keyspaces[self.keyspace_name].views["alltimehigh"].columns['fouls'] - self.assertEqual(mv_alltime_fouls_comumn.cql_type, 'int') + assert mv_alltime_fouls_comumn.cql_type == 'int' @lessthancass30 def test_base_table_type_alter_mv(self): @@ -2415,13 +2442,13 @@ def test_base_table_type_alter_mv(self): WITH CLUSTERING ORDER BY (score DESC, user ASC, day ASC)""".format(self.keyspace_name) self.session.execute(create_mv) - self.assertEqual(len(self.cluster.metadata.keyspaces[self.keyspace_name].views), 1) + assert len(self.cluster.metadata.keyspaces[self.keyspace_name].views) == 1 alter_scores = """ALTER TABLE {0}.scores ALTER score TYPE blob""".format((self.keyspace_name)) self.session.execute(alter_scores) - self.assertEqual(len(self.cluster.metadata.keyspaces[self.keyspace_name].views), 1) + assert len(self.cluster.metadata.keyspaces[self.keyspace_name].views) == 1 score_column = self.cluster.metadata.keyspaces[self.keyspace_name].tables['scores'].columns['score'] - self.assertEqual(score_column.cql_type, 'blob') + assert score_column.cql_type == 'blob' # until CASSANDRA-9920+CASSANDRA-10500 MV updates are only available later with an async event for i in range(10): @@ -2430,7 +2457,7 @@ def test_base_table_type_alter_mv(self): break time.sleep(.2) - self.assertEqual(score_mv_column.cql_type, 'blob') + assert score_mv_column.cql_type == 'blob' def test_metadata_with_quoted_identifiers(self): """ @@ -2466,63 +2493,48 @@ def test_metadata_with_quoted_identifiers(self): t1_table = self.cluster.metadata.keyspaces[self.keyspace_name].tables['t1'] mv = self.cluster.metadata.keyspaces[self.keyspace_name].views['mv1'] - self.assertIsNotNone(t1_table.views["mv1"]) - self.assertIsNotNone(len(t1_table.views), 1) + assert t1_table.views["mv1"] is not None + assert len(t1_table.views) is not None, 1 # Validate partition key, and not null - self.assertEqual(len(t1_table.partition_key), 1) - self.assertIsNotNone(t1_table.columns['theKey']) - self.assertTrue(t1_table.columns['theKey'], t1_table.partition_key[0]) + assert len(t1_table.partition_key) == 1 + assert t1_table.columns['theKey'] is not None + assert t1_table.columns['theKey'], t1_table.partition_key[0] # Validate clustering key column - self.assertEqual(len(t1_table.clustering_key), 1) - self.assertIsNotNone(t1_table.columns['the;Clustering']) - self.assertTrue(t1_table.columns['the;Clustering'], t1_table.clustering_key[0]) + assert len(t1_table.clustering_key) == 1 + assert t1_table.columns['the;Clustering'] is not None + assert t1_table.columns['the;Clustering'], t1_table.clustering_key[0] # Validate regular column - self.assertIsNotNone(t1_table.columns['the Value']) + assert t1_table.columns['the Value'] is not None # Validate basic mv information - self.assertEqual(mv.keyspace_name, self.keyspace_name) - self.assertEqual(mv.name, "mv1") - self.assertEqual(mv.base_table_name, "t1") - self.assertFalse(mv.include_all_columns) + assert mv.keyspace_name == self.keyspace_name + assert mv.name == "mv1" + assert mv.base_table_name == "t1" + assert not mv.include_all_columns # Validate that all columns are preset and correct mv_columns = list(mv.columns.values()) - self.assertEqual(len(mv_columns), 3) + assert len(mv_columns) == 3 theKey_column = mv_columns[0] - self.assertIsNotNone(theKey_column) - self.assertEqual(theKey_column.name, 'theKey') - self.assertEqual(theKey_column, mv.partition_key[0]) + assert theKey_column is not None + assert theKey_column.name == 'theKey' + assert theKey_column == mv.partition_key[0] cluster_column = mv_columns[1] - self.assertIsNotNone(cluster_column) - self.assertEqual(cluster_column.name, 'the;Clustering') - self.assertEqual(cluster_column.name, mv.clustering_key[0].name) - self.assertEqual(cluster_column.table, mv.clustering_key[0].table) - self.assertEqual(cluster_column.is_static, mv.clustering_key[0].is_static) - self.assertEqual(cluster_column.is_reversed, mv.clustering_key[0].is_reversed) + assert cluster_column is not None + assert cluster_column.name == 'the;Clustering' + assert cluster_column.name == mv.clustering_key[0].name + assert cluster_column.table == mv.clustering_key[0].table + assert cluster_column.is_static == mv.clustering_key[0].is_static + assert cluster_column.is_reversed == mv.clustering_key[0].is_reversed value_column = mv_columns[2] - self.assertIsNotNone(value_column) - self.assertEqual(value_column.name, 'the Value') - - @greaterthanorequaldse51 - def test_dse_workloads(self): - """ - Test to ensure dse_workloads is populated appropriately. - Field added in DSE 5.1 - - @jira_ticket PYTHON-667 - @expected_result dse_workloads set is set on host model - - @test_category metadata - """ - for host in self.cluster.metadata.all_hosts(): - self.assertIsInstance(host.dse_workloads, SortedSet) - self.assertIn("Cassandra", host.dse_workloads) + assert value_column is not None + assert value_column.name == 'the Value' class GroupPerHost(BasicSharedKeyspaceUnitTestCase): @@ -2563,7 +2575,7 @@ def test_group_keys_by_host(self): def _assert_group_keys_by_host(self, keys, table_name, stmt): keys_per_host = group_keys_by_replica(self.session, self.ks_name, table_name, keys) - self.assertNotIn(NO_VALID_REPLICA, keys_per_host) + assert NO_VALID_REPLICA not in keys_per_host prepared_stmt = self.session.prepare(stmt) for key in keys: @@ -2571,48 +2583,3 @@ def _assert_group_keys_by_host(self, keys, table_name, stmt): hosts = self.cluster.metadata.get_replicas(self.ks_name, routing_key) self.assertEqual(1, len(hosts)) # RF is 1 for this keyspace self.assertIn(key, keys_per_host[hosts[0]]) - - -class VirtualKeypaceTest(BasicSharedKeyspaceUnitTestCase): - virtual_ks_names = ('system_virtual_schema', 'system_views') - - def test_existing_keyspaces_have_correct_virtual_tags(self): - for name, ks in self.cluster.metadata.keyspaces.items(): - if name in self.virtual_ks_names: - self.assertTrue( - ks.virtual, - 'incorrect .virtual value for {}'.format(name) - ) - else: - self.assertFalse( - ks.virtual, - 'incorrect .virtual value for {}'.format(name) - ) - - @greaterthanorequalcass40 - @greaterthanorequaldse67 - def test_expected_keyspaces_exist_and_are_virtual(self): - for name in self.virtual_ks_names: - self.assertTrue( - self.cluster.metadata.keyspaces[name].virtual, - 'incorrect .virtual value for {}'.format(name) - ) - - @greaterthanorequalcass40 - @greaterthanorequaldse67 - def test_virtual_keyspaces_have_expected_schema_structure(self): - self.maxDiff = None - - ingested_virtual_ks_structure = defaultdict(dict) - for ks_name, ks in self.cluster.metadata.keyspaces.items(): - if not ks.virtual: - continue - for tab_name, tab in ks.tables.items(): - ingested_virtual_ks_structure[ks_name][tab_name] = set( - tab.columns.keys() - ) - - # Identify a couple known values to verify we parsed the structure correctly - self.assertIn('table_name', ingested_virtual_ks_structure['system_virtual_schema']['tables']) - self.assertIn('type', ingested_virtual_ks_structure['system_virtual_schema']['columns']) - self.assertIn('total', ingested_virtual_ks_structure['system_views']['sstable_tasks']) diff --git a/tests/integration/standard/test_metrics.py b/tests/integration/standard/test_metrics.py index 4b9ddb1351..7ebdded141 100644 --- a/tests/integration/standard/test_metrics.py +++ b/tests/integration/standard/test_metrics.py @@ -25,10 +25,12 @@ from cassandra.cluster import NoHostAvailable, ExecutionProfile, EXEC_PROFILE_DEFAULT from tests.integration import get_cluster, get_node, use_singledc, execute_until_pass, TestCluster -from greplin import scales +from tests.util import wait_until, wait_until_not_raised +from cassandra import metrics from tests.integration import BasicSharedKeyspaceUnitTestCaseRF3WM, BasicExistingKeyspaceUnitTestCase, local import pprint as pp +import pytest def setup_module(): @@ -70,14 +72,16 @@ def test_connection_error(self): # Ensure the nodes are actually down query = SimpleStatement("SELECT * FROM test", consistency_level=ConsistencyLevel.ALL) # both exceptions can happen depending on when the connection has been detected as defunct - with self.assertRaises((NoHostAvailable, ConnectionShutdown)): + with pytest.raises((NoHostAvailable, ConnectionShutdown)): self.session.execute(query) finally: get_cluster().start(wait_for_binary_proto=True, wait_other_notice=True) - # Give some time for the cluster to come back up, for the next test - time.sleep(5) + # Wait for the cluster to come back up for the next test + wait_until_not_raised( + lambda: self.session.execute("SELECT key FROM system.local WHERE key='local'"), + delay=0.5, max_attempts=30) - self.assertGreater(self.cluster.metrics.stats.connection_errors, 0) + assert self.cluster.metrics.stats.connection_errors > 0 def test_write_timeout(self): """ @@ -92,7 +96,7 @@ def test_write_timeout(self): # Assert read query = SimpleStatement("SELECT * FROM test WHERE k=1", consistency_level=ConsistencyLevel.ALL) results = execute_until_pass(self.session, query) - self.assertTrue(results) + assert results # Pause node so it shows as unreachable to coordinator get_node(1).pause() @@ -100,9 +104,9 @@ def test_write_timeout(self): try: # Test write query = SimpleStatement("INSERT INTO test (k, v) VALUES (2, 2)", consistency_level=ConsistencyLevel.ALL) - with self.assertRaises(WriteTimeout): + with pytest.raises(WriteTimeout): self.session.execute(query, timeout=None) - self.assertEqual(1, self.cluster.metrics.stats.write_timeouts) + assert 1 == self.cluster.metrics.stats.write_timeouts finally: get_node(1).resume() @@ -121,7 +125,7 @@ def test_read_timeout(self): # Assert read query = SimpleStatement("SELECT * FROM test WHERE k=1", consistency_level=ConsistencyLevel.ALL) results = execute_until_pass(self.session, query) - self.assertTrue(results) + assert results # Pause node so it shows as unreachable to coordinator get_node(1).pause() @@ -129,9 +133,9 @@ def test_read_timeout(self): try: # Test read query = SimpleStatement("SELECT * FROM test", consistency_level=ConsistencyLevel.ALL) - with self.assertRaises(ReadTimeout): + with pytest.raises(ReadTimeout): self.session.execute(query, timeout=None) - self.assertEqual(1, self.cluster.metrics.stats.read_timeouts) + assert 1 == self.cluster.metrics.stats.read_timeouts finally: get_node(1).resume() @@ -149,29 +153,34 @@ def test_unavailable(self): # Assert read query = SimpleStatement("SELECT * FROM test WHERE k=1", consistency_level=ConsistencyLevel.ALL) results = execute_until_pass(self.session, query) - self.assertTrue(results) + assert results # Stop node gracefully # Sometimes this commands continues with the other nodes having not noticed # 1 is down, and a Timeout error is returned instead of an Unavailable get_node(1).stop(wait=True, wait_other_notice=True) - time.sleep(5) + wait_until( + lambda: not self.cluster.metadata.get_host('127.0.0.1') or + not self.cluster.metadata.get_host('127.0.0.1').is_up, + delay=0.5, max_attempts=30) try: # Test write query = SimpleStatement("INSERT INTO test (k, v) VALUES (2, 2)", consistency_level=ConsistencyLevel.ALL) - with self.assertRaises(Unavailable): + with pytest.raises(Unavailable): self.session.execute(query) - self.assertEqual(self.cluster.metrics.stats.unavailables, 1) + assert self.cluster.metrics.stats.unavailables == 1 # Test write query = SimpleStatement("SELECT * FROM test", consistency_level=ConsistencyLevel.ALL) - with self.assertRaises(Unavailable): + with pytest.raises(Unavailable): self.session.execute(query, timeout=None) - self.assertEqual(self.cluster.metrics.stats.unavailables, 2) + assert self.cluster.metrics.stats.unavailables == 2 finally: get_node(1).start(wait_other_notice=True, wait_for_binary_proto=True) - # Give some time for the cluster to come back up, for the next test - time.sleep(5) + # Wait for the cluster to come back up for the next test + wait_until_not_raised( + lambda: self.session.execute("SELECT key FROM system.local WHERE key='local'"), + delay=0.5, max_attempts=30) self.cluster.shutdown() @@ -206,7 +215,7 @@ def test_metrics_per_cluster(self): ) cluster2.connect(self.ks_name, wait_for_all_pools=True) - self.assertEqual(len(cluster2.metadata.all_hosts()), 3) + assert len(cluster2.metadata.all_hosts()) == 3 query = SimpleStatement("SELECT * FROM {0}.{0}".format(self.ks_name), consistency_level=ConsistencyLevel.ALL) self.session.execute(query) @@ -217,31 +226,31 @@ def test_metrics_per_cluster(self): try: # Test write query = SimpleStatement("INSERT INTO {0}.{0} (k, v) VALUES (2, 2)".format(self.ks_name), consistency_level=ConsistencyLevel.ALL) - with self.assertRaises(WriteTimeout): + with pytest.raises((WriteTimeout, Unavailable)): self.session.execute(query, timeout=None) finally: get_node(1).resume() - # Change the scales stats_name of the cluster2 + # Change the stats_name of the cluster2 cluster2.metrics.set_stats_name('cluster2-metrics') stats_cluster1 = self.cluster.metrics.get_stats() stats_cluster2 = cluster2.metrics.get_stats() # Test direct access to stats - self.assertEqual(1, self.cluster.metrics.stats.write_timeouts) - self.assertEqual(0, cluster2.metrics.stats.write_timeouts) + assert (1 == self.cluster.metrics.stats.write_timeouts or 1 == self.cluster.metrics.stats.unavailables) + assert 0 == cluster2.metrics.stats.write_timeouts # Test direct access to a child stats - self.assertNotEqual(0.0, self.cluster.metrics.request_timer['mean']) - self.assertEqual(0.0, cluster2.metrics.request_timer['mean']) + assert 0.0 != self.cluster.metrics.request_timer['mean'] + assert 0.0 == cluster2.metrics.request_timer['mean'] # Test access via metrics.get_stats() - self.assertNotEqual(0.0, stats_cluster1['request_timer']['mean']) - self.assertEqual(0.0, stats_cluster2['request_timer']['mean']) + assert 0.0 != stats_cluster1['request_timer']['mean'] + assert 0.0 == stats_cluster2['request_timer']['mean'] # Test access by stats_name - self.assertEqual(0.0, scales.getStats()['cluster2-metrics']['request_timer']['mean']) + assert 0.0 == metrics.getStats()['cluster2-metrics']['request_timer']['mean'] cluster2.shutdown() @@ -269,7 +278,7 @@ def test_duplicate_metrics_per_cluster(self): # Ensure duplicate metric names are not allowed cluster2.metrics.set_stats_name("appcluster") cluster2.metrics.set_stats_name("appcluster") - with self.assertRaises(ValueError): + with pytest.raises(ValueError): cluster3.metrics.set_stats_name("appcluster") cluster3.metrics.set_stats_name("devops") @@ -285,12 +294,12 @@ def test_duplicate_metrics_per_cluster(self): query = SimpleStatement("SELECT * FROM {0}.{0}".format(self.ks_name), consistency_level=ConsistencyLevel.ALL) session3.execute(query) - self.assertEqual(cluster2.metrics.get_stats()['request_timer']['count'], 10) - self.assertEqual(cluster3.metrics.get_stats()['request_timer']['count'], 5) + assert cluster2.metrics.get_stats()['request_timer']['count'] == 10 + assert cluster3.metrics.get_stats()['request_timer']['count'] == 5 - # Check scales to ensure they are appropriately named - self.assertTrue("appcluster" in scales._Stats.stats.keys()) - self.assertTrue("devops" in scales._Stats.stats.keys()) + # Check registry to ensure they are appropriately named + assert "appcluster" in metrics._stats_registry.keys() + assert "devops" in metrics._stats_registry.keys() cluster2.shutdown() cluster3.shutdown() @@ -302,15 +311,15 @@ class RequestAnalyzer(object): Also computes statistics on encoded request size. """ - requests = scales.PmfStat('request size') - errors = scales.IntStat('errors') - successful = scales.IntStat("success") + requests = metrics.PmfStat('request size') + errors = metrics.IntStat('errors') + successful = metrics.IntStat("success") # Throw exceptions when invoked. throw_on_success = False throw_on_fail = False def __init__(self, session, throw_on_success=False, throw_on_fail=False): - scales.init(self, '/request') + metrics.init(self, '/request') # each instance will be registered with a session, and receive a callback for each request generated session.add_request_init_listener(self.on_request) self.throw_on_fail = throw_on_fail @@ -354,10 +363,10 @@ def setUpClass(cls): def wait_for_count(self, ra, expected_count, error=False): for _ in range(10): if not error: - if ra.successful is expected_count: + if ra.successful == expected_count: return True else: - if ra.errors is expected_count: + if ra.errors == expected_count: return True time.sleep(.01) return False @@ -385,8 +394,8 @@ def test_metrics_per_cluster(self): except SyntaxException: continue - self.assertTrue(self.wait_for_count(ra, 10)) - self.assertTrue(self.wait_for_count(ra, 3, error=True)) + assert self.wait_for_count(ra, 10) + assert self.wait_for_count(ra, 3, error=True) ra.remove_ra(self.session) diff --git a/tests/integration/standard/test_policies.py b/tests/integration/standard/test_policies.py index faa21efb02..50b431e3c9 100644 --- a/tests/integration/standard/test_policies.py +++ b/tests/integration/standard/test_policies.py @@ -45,9 +45,6 @@ def test_predicate_changes(self): external_event = True contact_point = DefaultEndPoint("127.0.0.1") - single_host = {Host(contact_point, SimpleConvictionPolicy)} - all_hosts = {Host(DefaultEndPoint("127.0.0.{}".format(i)), SimpleConvictionPolicy) for i in (1, 2, 3)} - predicate = lambda host: host.endpoint == contact_point if external_event else True hfp = ExecutionProfile( load_balancing_policy=HostFilterPolicy(RoundRobinPolicy(), predicate=predicate) @@ -62,7 +59,8 @@ def test_predicate_changes(self): response = session.execute("SELECT * from system.local WHERE key='local'") queried_hosts.update(response.response_future.attempted_hosts) - self.assertEqual(queried_hosts, single_host) + assert len(queried_hosts) == 1 + assert queried_hosts.pop().endpoint == contact_point external_event = False futures = session.update_created_pools() @@ -72,7 +70,8 @@ def test_predicate_changes(self): for _ in range(10): response = session.execute("SELECT * from system.local WHERE key='local'") queried_hosts.update(response.response_future.attempted_hosts) - self.assertEqual(queried_hosts, all_hosts) + assert len(queried_hosts) == 3 + assert {host.endpoint for host in queried_hosts} == {DefaultEndPoint(f"127.0.0.{i}") for i in range(1, 4)} class WhiteListRoundRobinPolicyTests(unittest.TestCase): @@ -89,7 +88,7 @@ def test_only_connects_to_subset(self): response = session.execute("SELECT * from system.local WHERE key='local'", execution_profile="white_list") queried_hosts.update(response.response_future.attempted_hosts) queried_hosts = set(host.address for host in queried_hosts) - self.assertEqual(queried_hosts, only_connect_hosts) + assert queried_hosts == only_connect_hosts class ExponentialRetryPolicyTests(unittest.TestCase): @@ -105,5 +104,5 @@ def test_exponential_retries(self): self.session.execute( """ CREATE KEYSPACE preparedtests - WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} + WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '1'} """) diff --git a/tests/integration/standard/test_prepared_statements.py b/tests/integration/standard/test_prepared_statements.py index 5ccc0732fa..37f93c94c6 100644 --- a/tests/integration/standard/test_prepared_statements.py +++ b/tests/integration/standard/test_prepared_statements.py @@ -23,10 +23,11 @@ from cassandra import ConsistencyLevel, ProtocolVersion from cassandra.query import PreparedStatement, UNSET_VALUE -from tests.integration import (get_server_versions, greaterthanorequalcass40, greaterthanorequaldse50, - requirecassandra, BasicSharedKeyspaceUnitTestCase) +from tests.integration import (get_server_versions, greaterthanorequalcass40, + BasicSharedKeyspaceUnitTestCase) import logging +import pytest LOG = logging.getLogger(__name__) @@ -43,7 +44,7 @@ def setUpClass(cls): cls.cass_version = get_server_versions() def setUp(self): - self.cluster = TestCluster(metrics_enabled=True, allow_beta_protocol_version=True) + self.cluster = TestCluster(metrics_enabled=True, allow_beta_protocol_version=True, protocol_version=PROTOCOL_VERSION) self.session = self.cluster.connect() def tearDown(self): @@ -61,7 +62,7 @@ def test_basic(self): self.session.execute( """ CREATE KEYSPACE preparedtests - WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} + WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '1'} """) self.session.set_keyspace("preparedtests") @@ -80,7 +81,7 @@ def test_basic(self): INSERT INTO cf0 (a, b, c) VALUES (?, ?, ?) """) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) bound = prepared.bind(('a', 'b', 'c')) self.session.execute(bound) @@ -89,11 +90,11 @@ def test_basic(self): """ SELECT * FROM cf0 WHERE a=? """) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) bound = prepared.bind(('a')) results = self.session.execute(bound) - self.assertEqual(results, [('a', 'b', 'c')]) + assert results == [('a', 'b', 'c')] # test with new dict binding prepared = self.session.prepare( @@ -101,7 +102,7 @@ def test_basic(self): INSERT INTO cf0 (a, b, c) VALUES (?, ?, ?) """) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) bound = prepared.bind({ 'a': 'x', 'b': 'y', @@ -115,11 +116,11 @@ def test_basic(self): SELECT * FROM cf0 WHERE a=? """) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) bound = prepared.bind({'a': 'x'}) results = self.session.execute(bound) - self.assertEqual(results, [('x', 'y', 'z')]) + assert results == [('x', 'y', 'z')] def test_missing_primary_key(self): """ @@ -133,12 +134,14 @@ def _run_missing_primary_key(self, session): statement_to_prepare = """INSERT INTO test3rf.test (v) VALUES (?)""" # logic needed work with changes in CASSANDRA-6237 if self.cass_version[0] >= (3, 0, 0): - self.assertRaises(InvalidRequest, session.prepare, statement_to_prepare) + with pytest.raises(InvalidRequest): + session.prepare(statement_to_prepare) else: prepared = session.prepare(statement_to_prepare) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) bound = prepared.bind((1,)) - self.assertRaises(InvalidRequest, session.execute, bound) + with pytest.raises(InvalidRequest): + session.execute(bound) def test_missing_primary_key_dicts(self): """ @@ -152,12 +155,14 @@ def _run_missing_primary_key_dicts(self, session): statement_to_prepare = """ INSERT INTO test3rf.test (v) VALUES (?)""" # logic needed work with changes in CASSANDRA-6237 if self.cass_version[0] >= (3, 0, 0): - self.assertRaises(InvalidRequest, session.prepare, statement_to_prepare) + with pytest.raises(InvalidRequest): + session.prepare(statement_to_prepare) else: prepared = session.prepare(statement_to_prepare) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) bound = prepared.bind({'v': 1}) - self.assertRaises(InvalidRequest, session.execute, bound) + with pytest.raises(InvalidRequest): + session.execute(bound) def test_too_many_bind_values(self): """ @@ -169,11 +174,13 @@ def _run_too_many_bind_values(self, session): statement_to_prepare = """ INSERT INTO test3rf.test (v) VALUES (?)""" # logic needed work with changes in CASSANDRA-6237 if self.cass_version[0] >= (2, 2, 8): - self.assertRaises(InvalidRequest, session.prepare, statement_to_prepare) + with pytest.raises(InvalidRequest): + session.prepare(statement_to_prepare) else: prepared = session.prepare(statement_to_prepare) - self.assertIsInstance(prepared, PreparedStatement) - self.assertRaises(ValueError, prepared.bind, (1, 2)) + assert isinstance(prepared, PreparedStatement) + with pytest.raises(ValueError): + prepared.bind((1, 2)) def test_imprecise_bind_values_dicts(self): """ @@ -186,7 +193,7 @@ def test_imprecise_bind_values_dicts(self): INSERT INTO test3rf.test (k, v) VALUES (?, ?) """) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) # too many values is ok - others are ignored prepared.bind({'k': 1, 'v': 2, 'v2': 3}) @@ -194,18 +201,21 @@ def test_imprecise_bind_values_dicts(self): # right number, but one does not belong if PROTOCOL_VERSION < 4: # pre v4, the driver bails with key error when 'v' is found missing - self.assertRaises(KeyError, prepared.bind, {'k': 1, 'v2': 3}) + with pytest.raises(KeyError): + prepared.bind({'k': 1, 'v2': 3}) else: # post v4, the driver uses UNSET_VALUE for 'v' and 'v2' is ignored prepared.bind({'k': 1, 'v2': 3}) # also catch too few variables with dicts - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) if PROTOCOL_VERSION < 4: - self.assertRaises(KeyError, prepared.bind, {}) + with pytest.raises(KeyError): + prepared.bind({}) else: # post v4, the driver attempts to use UNSET_VALUE for unspecified keys - self.assertRaises(ValueError, prepared.bind, {}) + with pytest.raises(ValueError): + prepared.bind({}) def test_none_values(self): """ @@ -217,7 +227,7 @@ def test_none_values(self): INSERT INTO test3rf.test (k, v) VALUES (?, ?) """) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) bound = prepared.bind((1, None)) self.session.execute(bound) @@ -225,11 +235,11 @@ def test_none_values(self): """ SELECT * FROM test3rf.test WHERE k=? """) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) bound = prepared.bind((1,)) results = self.session.execute(bound) - self.assertEqual(results.one().v, None) + assert results.one().v == None def test_unset_values(self): """ @@ -272,9 +282,10 @@ def test_unset_values(self): for params, expected in bind_expected: self.session.execute(insert, params) results = self.session.execute(select, (0,)) - self.assertEqual(results.one(), expected) + assert results.one() == expected - self.assertRaises(ValueError, self.session.execute, select, (UNSET_VALUE, 0, 0)) + with pytest.raises(ValueError): + self.session.execute(select, (UNSET_VALUE, 0, 0)) def test_no_meta(self): @@ -283,7 +294,7 @@ def test_no_meta(self): INSERT INTO test3rf.test (k, v) VALUES (0, 0) """) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) bound = prepared.bind(None) bound.consistency_level = ConsistencyLevel.ALL self.session.execute(bound) @@ -292,12 +303,12 @@ def test_no_meta(self): """ SELECT * FROM test3rf.test WHERE k=0 """) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) bound = prepared.bind(None) bound.consistency_level = ConsistencyLevel.ALL results = self.session.execute(bound) - self.assertEqual(results.one().v, 0) + assert results.one().v == 0 def test_none_values_dicts(self): """ @@ -310,7 +321,7 @@ def test_none_values_dicts(self): INSERT INTO test3rf.test (k, v) VALUES (?, ?) """) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) bound = prepared.bind({'k': 1, 'v': None}) self.session.execute(bound) @@ -318,11 +329,11 @@ def test_none_values_dicts(self): """ SELECT * FROM test3rf.test WHERE k=? """) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) bound = prepared.bind({'k': 1}) results = self.session.execute(bound) - self.assertEqual(results.one().v, None) + assert results.one().v == None def test_async_binding(self): """ @@ -334,7 +345,7 @@ def test_async_binding(self): INSERT INTO test3rf.test (k, v) VALUES (?, ?) """) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) future = self.session.execute_async(prepared, (873, None)) future.result() @@ -342,11 +353,11 @@ def test_async_binding(self): """ SELECT * FROM test3rf.test WHERE k=? """) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) future = self.session.execute_async(prepared, (873,)) results = future.result() - self.assertEqual(results.one().v, None) + assert results.one().v == None def test_async_binding_dicts(self): """ @@ -357,7 +368,7 @@ def test_async_binding_dicts(self): INSERT INTO test3rf.test (k, v) VALUES (?, ?) """) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) future = self.session.execute_async(prepared, {'k': 873, 'v': None}) future.result() @@ -365,11 +376,11 @@ def test_async_binding_dicts(self): """ SELECT * FROM test3rf.test WHERE k=? """) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) future = self.session.execute_async(prepared, {'k': 873}) results = future.result() - self.assertEqual(results.one().v, None) + assert results.one().v == None def test_raise_error_on_prepared_statement_execution_dropped_table(self): """ @@ -392,9 +403,32 @@ def test_raise_error_on_prepared_statement_execution_dropped_table(self): prepared = self.session.prepare("SELECT * FROM test3rf.error_test WHERE k=?") self.session.execute("DROP TABLE test3rf.error_test") - with self.assertRaises(InvalidRequest): + with pytest.raises(InvalidRequest): self.session.execute(prepared, [0]) + def test_recognize_lwt_query(self): + self.session.execute("CREATE TABLE IF NOT EXISTS preparedtests.bound_statement_test (a int PRIMARY KEY, b int)") + # Prepare a non-LWT statement + statementNonLWT = self.session.prepare("UPDATE preparedtests.bound_statement_test SET b = ? WHERE a = ?") + # Prepare an LWT statement + statementLWT = self.session.prepare("UPDATE preparedtests.bound_statement_test SET b = ? WHERE a = ? IF b = ?") + + boundNonLWT = statementNonLWT.bind((3, 1)) + boundLWT = statementLWT.bind((3, 1, 5)) + + # Check LWT detection + assert not boundNonLWT.is_lwt() + assert boundLWT.is_lwt() + + self.session.execute("CREATE TABLE IF NOT EXISTS preparedtests.prepared_statement_test (a int PRIMARY KEY, b int)") + # Prepare a non-LWT statement + statementNonLWT = self.session.prepare("UPDATE preparedtests.prepared_statement_test SET b = ? WHERE a = ?") + # Prepare an LWT statement + statementLWT = self.session.prepare("UPDATE preparedtests.prepared_statement_test SET b = ? WHERE a = ? IF b = ?") + # Check LWT detection + assert not statementNonLWT.is_lwt() + assert statementLWT.is_lwt() + @unittest.skipIf((CASSANDRA_VERSION >= Version('3.11.12') and CASSANDRA_VERSION < Version('4.0')) or \ CASSANDRA_VERSION >= Version('4.0.2'), "Fixed server-side in Cassandra 3.11.12, 4.0.2") @@ -403,16 +437,16 @@ def test_fail_if_different_query_id_on_reprepare(self): keyspace = "test_fail_if_different_query_id_on_reprepare" self.session.execute( "CREATE KEYSPACE IF NOT EXISTS {} WITH replication = " - "{{'class': 'SimpleStrategy', 'replication_factor': 1}}".format(keyspace) + "{{'class': 'NetworkTopologyStrategy', 'replication_factor': 1}}".format(keyspace) ) self.session.execute("CREATE TABLE IF NOT EXISTS {}.foo(k int PRIMARY KEY)".format(keyspace)) prepared = self.session.prepare("SELECT * FROM {}.foo WHERE k=?".format(keyspace)) self.session.execute("DROP TABLE {}.foo".format(keyspace)) self.session.execute("CREATE TABLE {}.foo(k int PRIMARY KEY)".format(keyspace)) self.session.execute("USE {}".format(keyspace)) - with self.assertRaises(DriverException) as e: + with pytest.raises(DriverException) as e: self.session.execute(prepared, [0]) - self.assertIn("ID mismatch", str(e.exception)) + assert "ID mismatch" in str(e.value) @greaterthanorequalcass40 @@ -441,19 +475,19 @@ def test_invalidated_result_metadata(self): """ wildcard_prepared = self.session.prepare("SELECT * FROM {}".format(self.table_name)) original_result_metadata = wildcard_prepared.result_metadata - self.assertEqual(len(original_result_metadata), 3) + assert len(original_result_metadata) == 3 r = self.session.execute(wildcard_prepared) - self.assertEqual(r[0], (1, 1, 1)) + assert r[0] == (1, 1, 1) self.session.execute("ALTER TABLE {} DROP d".format(self.table_name)) # Get a bunch of requests in the pipeline with varying states of result_meta, reprepare, resolved futures = set(self.session.execute_async(wildcard_prepared.bind(None)) for _ in range(200)) for f in futures: - self.assertEqual(f.result()[0], (1, 1)) + assert f.result()[0] == (1, 1) - self.assertIsNot(wildcard_prepared.result_metadata, original_result_metadata) + assert wildcard_prepared.result_metadata is not original_result_metadata def test_prepared_id_is_update(self): """ @@ -468,7 +502,7 @@ def test_prepared_id_is_update(self): """ prepared_statement = self.session.prepare("SELECT * from {} WHERE a = ?".format(self.table_name)) id_before = prepared_statement.result_metadata_id - self.assertEqual(len(prepared_statement.result_metadata), 3) + assert len(prepared_statement.result_metadata) == 3 self.session.execute("ALTER TABLE {} ADD c int".format(self.table_name)) bound_statement = prepared_statement.bind((1, )) @@ -476,8 +510,8 @@ def test_prepared_id_is_update(self): id_after = prepared_statement.result_metadata_id - self.assertNotEqual(id_before, id_after) - self.assertEqual(len(prepared_statement.result_metadata), 4) + assert id_before != id_after + assert len(prepared_statement.result_metadata) == 4 def test_prepared_id_is_updated_across_pages(self): """ @@ -491,12 +525,12 @@ def test_prepared_id_is_updated_across_pages(self): """ prepared_statement = self.session.prepare("SELECT * from {}".format(self.table_name)) id_before = prepared_statement.result_metadata_id - self.assertEqual(len(prepared_statement.result_metadata), 3) + assert len(prepared_statement.result_metadata) == 3 prepared_statement.fetch_size = 2 result = self.session.execute(prepared_statement.bind((None))) - self.assertTrue(result.has_more_pages) + assert result.has_more_pages self.session.execute("ALTER TABLE {} ADD c int".format(self.table_name)) @@ -505,9 +539,9 @@ def test_prepared_id_is_updated_across_pages(self): id_after = prepared_statement.result_metadata_id - self.assertEqual(result_set, expected_result_set) - self.assertNotEqual(id_before, id_after) - self.assertEqual(len(prepared_statement.result_metadata), 4) + assert result_set == expected_result_set + assert id_before != id_after + assert len(prepared_statement.result_metadata) == 4 def test_prepare_id_is_updated_across_session(self): """ @@ -524,7 +558,7 @@ def test_prepare_id_is_updated_across_session(self): stm = "SELECT * from {} WHERE a = ?".format(self.table_name) one_prepared_stm = one_session.prepare(stm) - self.assertEqual(len(one_prepared_stm.result_metadata), 3) + assert len(one_prepared_stm.result_metadata) == 3 one_id_before = one_prepared_stm.result_metadata_id @@ -532,8 +566,8 @@ def test_prepare_id_is_updated_across_session(self): one_session.execute(one_prepared_stm, (1, )) one_id_after = one_prepared_stm.result_metadata_id - self.assertNotEqual(one_id_before, one_id_after) - self.assertEqual(len(one_prepared_stm.result_metadata), 4) + assert one_id_before != one_id_after + assert len(one_prepared_stm.result_metadata) == 4 def test_not_reprepare_invalid_statements(self): """ @@ -546,7 +580,7 @@ def test_not_reprepare_invalid_statements(self): prepared_statement = self.session.prepare( "SELECT a, b, d FROM {} WHERE a = ?".format(self.table_name)) self.session.execute("ALTER TABLE {} DROP d".format(self.table_name)) - with self.assertRaises(InvalidRequest): + with pytest.raises(InvalidRequest): self.session.execute(prepared_statement.bind((1, ))) def test_id_is_not_updated_conditional_v4(self): @@ -563,7 +597,6 @@ def test_id_is_not_updated_conditional_v4(self): self.addCleanup(cluster.shutdown) self._test_updated_conditional(session, 9) - @requirecassandra def test_id_is_not_updated_conditional_v5(self): """ Test that verifies that the result_metadata and the @@ -577,36 +610,6 @@ def test_id_is_not_updated_conditional_v5(self): self.addCleanup(cluster.shutdown) self._test_updated_conditional(session, 10) - @greaterthanorequaldse50 - def test_id_is_not_updated_conditional_dsev1(self): - """ - Test that verifies that the result_metadata and the - result_metadata_id are udpated correctly in conditional statements - in protocol DSE V1 - - @since 3.13 - @jira_ticket PYTHON-847 - """ - cluster = TestCluster(protocol_version=ProtocolVersion.DSE_V1) - session = cluster.connect() - self.addCleanup(cluster.shutdown) - self._test_updated_conditional(session, 10) - - @greaterthanorequaldse50 - def test_id_is_not_updated_conditional_dsev2(self): - """ - Test that verifies that the result_metadata and the - result_metadata_id are udpated correctly in conditional statements - in protocol DSE V2 - - @since 3.13 - @jira_ticket PYTHON-847 - """ - cluster = TestCluster(protocol_version=ProtocolVersion.DSE_V2) - session = cluster.connect() - self.addCleanup(cluster.shutdown) - self._test_updated_conditional(session, 10) - def _test_updated_conditional(self, session, value): prepared_statement = session.prepare( "INSERT INTO {}(a, b, d) VALUES " @@ -615,12 +618,9 @@ def _test_updated_conditional(self, session, value): LOG.debug('initial result_metadata_id: {}'.format(first_id)) def check_result_and_metadata(expected): - self.assertEqual( - session.execute(prepared_statement, (value, value, value)).one(), - expected - ) - self.assertEqual(prepared_statement.result_metadata_id, first_id) - self.assertIsNone(prepared_statement.result_metadata) + assert session.execute(prepared_statement, (value, value, value)).one() == expected + assert prepared_statement.result_metadata_id == first_id + assert prepared_statement.result_metadata is None # Successful conditional update check_result_and_metadata((True,)) diff --git a/tests/integration/standard/test_query.py b/tests/integration/standard/test_query.py index 5c20c50a1a..210f6dacb1 100644 --- a/tests/integration/standard/test_query.py +++ b/tests/integration/standard/test_query.py @@ -26,9 +26,11 @@ from cassandra.policies import HostDistance, RoundRobinPolicy, WhiteListRoundRobinPolicy from tests.integration import use_singledc, PROTOCOL_VERSION, BasicSharedKeyspaceUnitTestCase, \ greaterthanprotocolv3, MockLoggingHandler, get_supported_protocol_versions, local, get_cluster, setup_keyspace, \ - USE_CASS_EXTERNAL, greaterthanorequalcass40, DSE_VERSION, TestCluster, requirecassandra, xfail_scylla + USE_CASS_EXTERNAL, greaterthanorequalcass40, TestCluster, xfail_scylla, xfail_scylla_version_lt, \ + get_tablets_disabled_ddl_suffix, execute_with_long_wait_retry from tests import notwindows from tests.integration import greaterthanorequalcass30, get_node +from tests.util import assertListEqual, wait_until import time import random @@ -63,12 +65,12 @@ def test_query(self): INSERT INTO test3rf.test (k, v) VALUES (?, ?) """.format(self.keyspace_name)) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) bound = prepared.bind((1, None)) - self.assertIsInstance(bound, BoundStatement) - self.assertEqual(2, len(bound.values)) + assert isinstance(bound, BoundStatement) + assert 2 == len(bound.values) self.session.execute(bound) - self.assertEqual(bound.routing_key, b'\x00\x00\x00\x01') + assert bound.routing_key == b'\x00\x00\x00\x01' def test_trace_prints_okay(self): """ @@ -81,7 +83,7 @@ def test_trace_prints_okay(self): # Ensure this does not throw an exception trace = rs.get_query_trace() - self.assertTrue(trace.events) + assert trace.events str(trace) for event in trace.events: str(event) @@ -98,9 +100,9 @@ def test_row_error_message(self): self.session.execute("CREATE TABLE {0}.{1} (k int PRIMARY KEY, v timestamp)".format(self.keyspace_name,self.function_table_name)) ss = SimpleStatement("INSERT INTO {0}.{1} (k, v) VALUES (1, 1000000000000000)".format(self.keyspace_name, self.function_table_name)) self.session.execute(ss) - with self.assertRaises(DriverException) as context: + with pytest.raises(DriverException) as context: self.session.execute("SELECT * FROM {0}.{1}".format(self.keyspace_name, self.function_table_name)) - self.assertIn("Failed decoding result column", str(context.exception)) + assert "Failed decoding result column" in str(context.value) def test_trace_id_to_resultset(self): @@ -109,14 +111,14 @@ def test_trace_id_to_resultset(self): # future should have the current trace rs = future.result() future_trace = future.get_query_trace() - self.assertIsNotNone(future_trace) + assert future_trace is not None rs_trace = rs.get_query_trace() - self.assertEqual(rs_trace, future_trace) - self.assertTrue(rs_trace.events) - self.assertEqual(len(rs_trace.events), len(future_trace.events)) + assert rs_trace == future_trace + assert rs_trace.events + assert len(rs_trace.events) == len(future_trace.events) - self.assertListEqual([rs_trace], rs.get_all_query_traces()) + assertListEqual([rs_trace], rs.get_all_query_traces()) def test_trace_ignores_row_factory(self): with TestCluster( @@ -129,7 +131,7 @@ def test_trace_ignores_row_factory(self): # Ensure this does not throw an exception trace = rs.get_query_trace() - self.assertTrue(trace.events) + assert trace.events str(trace) for event in trace.events: str(event) @@ -170,8 +172,8 @@ def test_client_ip_in_trace(self): pat = re.compile(r'127.0.0.\d{1,3}') # Ensure that ip is set - self.assertIsNotNone(client_ip, "Client IP was not set in trace with C* >= 2.2") - self.assertTrue(pat.match(client_ip), "Client IP from trace did not match the expected value") + assert client_ip is not None, "Client IP was not set in trace with C* >= 2.2" + assert pat.match(client_ip), "Client IP from trace did not match the expected value" def test_trace_cl(self): """ @@ -186,18 +188,18 @@ def test_trace_cl(self): statement = SimpleStatement(query) response_future = self.session.execute_async(statement, trace=True) response_future.result() - with self.assertRaises(Unavailable): + with pytest.raises(Unavailable): response_future.get_query_trace(query_cl=ConsistencyLevel.THREE) # Try again with a smattering of other CL's - self.assertIsNotNone(response_future.get_query_trace(max_wait=2.0, query_cl=ConsistencyLevel.TWO).trace_id) + assert response_future.get_query_trace(max_wait=2.0, query_cl=ConsistencyLevel.TWO).trace_id is not None response_future = self.session.execute_async(statement, trace=True) response_future.result() - self.assertIsNotNone(response_future.get_query_trace(max_wait=2.0, query_cl=ConsistencyLevel.ONE).trace_id) + assert response_future.get_query_trace(max_wait=2.0, query_cl=ConsistencyLevel.ONE).trace_id is not None response_future = self.session.execute_async(statement, trace=True) response_future.result() - with self.assertRaises(InvalidRequest): - self.assertIsNotNone(response_future.get_query_trace(max_wait=2.0, query_cl=ConsistencyLevel.ANY).trace_id) - self.assertIsNotNone(response_future.get_query_trace(max_wait=2.0, query_cl=ConsistencyLevel.QUORUM).trace_id) + with pytest.raises(InvalidRequest): + assert response_future.get_query_trace(max_wait=2.0, query_cl=ConsistencyLevel.ANY).trace_id is not None + assert response_future.get_query_trace(max_wait=2.0, query_cl=ConsistencyLevel.QUORUM).trace_id is not None @notwindows def test_incomplete_query_trace(self): @@ -222,27 +224,28 @@ def test_incomplete_query_trace(self): response_future = self.session.execute_async("SELECT i FROM {0} WHERE k=0".format(self.keyspace_table_name), trace=True) response_future.result() - self.assertEqual(len(response_future._query_traces), 1) + assert len(response_future._query_traces) == 1 trace = response_future._query_traces[0] - self.assertTrue(self._wait_for_trace_to_populate(trace.trace_id)) + assert self._wait_for_trace_to_populate(trace.trace_id) # Delete trace duration from the session (this is what the driver polls for "complete") delete_statement = SimpleStatement("DELETE duration FROM system_traces.sessions WHERE session_id = {0}".format(trace.trace_id), consistency_level=ConsistencyLevel.ALL) self.session.execute(delete_statement) - self.assertTrue(self._wait_for_trace_to_delete(trace.trace_id)) + assert self._wait_for_trace_to_delete(trace.trace_id) # should raise because duration is not set - self.assertRaises(TraceUnavailable, trace.populate, max_wait=0.2, wait_for_complete=True) - self.assertFalse(trace.events) + with pytest.raises(TraceUnavailable): + trace.populate(max_wait=0.2, wait_for_complete=True) + assert not trace.events # should get the events with wait False trace.populate(wait_for_complete=False) - self.assertIsNone(trace.duration) - self.assertIsNotNone(trace.trace_id) - self.assertIsNotNone(trace.request_type) - self.assertIsNotNone(trace.parameters) - self.assertTrue(trace.events) # non-zero list len - self.assertIsNotNone(trace.started_at) + assert trace.duration is None + assert trace.trace_id is not None + assert trace.request_type is not None + assert trace.parameters is not None + assert trace.events # non-zero list len + assert trace.started_at is not None def _wait_for_trace_to_populate(self, trace_id): count = 0 @@ -283,17 +286,17 @@ def test_query_by_id(self): self.session.execute("insert into "+self.keyspace_name+"."+self.function_table_name+" (id, m) VALUES ( 1, {1: 'one', 2: 'two', 3:'three'})") results1 = self.session.execute("select id, m from {0}.{1}".format(self.keyspace_name, self.function_table_name)) - self.assertIsNotNone(results1.column_types) - self.assertEqual(results1.column_types[0].typename, 'int') - self.assertEqual(results1.column_types[1].typename, 'map') - self.assertEqual(results1.column_types[0].cassname, 'Int32Type') - self.assertEqual(results1.column_types[1].cassname, 'MapType') - self.assertEqual(len(results1.column_types[0].subtypes), 0) - self.assertEqual(len(results1.column_types[1].subtypes), 2) - self.assertEqual(results1.column_types[1].subtypes[0].typename, "int") - self.assertEqual(results1.column_types[1].subtypes[1].typename, "varchar") - self.assertEqual(results1.column_types[1].subtypes[0].cassname, "Int32Type") - self.assertEqual(results1.column_types[1].subtypes[1].cassname, "VarcharType") + assert results1.column_types is not None + assert results1.column_types[0].typename == 'int' + assert results1.column_types[1].typename == 'map' + assert results1.column_types[0].cassname == 'Int32Type' + assert results1.column_types[1].cassname == 'MapType' + assert len(results1.column_types[0].subtypes) == 0 + assert len(results1.column_types[1].subtypes) == 2 + assert results1.column_types[1].subtypes[0].typename == "int" + assert results1.column_types[1].subtypes[1].typename == "varchar" + assert results1.column_types[1].subtypes[0].cassname == "Int32Type" + assert results1.column_types[1].subtypes[1].cassname == "VarcharType" def test_column_names(self): """ @@ -319,9 +322,9 @@ def test_column_names(self): self.session.execute(create_table) result_set = self.session.execute("SELECT * FROM {0}.{1}".format(self.keyspace_name, self.function_table_name)) - self.assertIsNotNone(result_set.column_types) + assert result_set.column_types is not None - self.assertEqual(result_set.column_names, [u'user', u'game', u'year', u'month', u'day', u'score']) + assert result_set.column_names == [u'user', u'game', u'year', u'month', u'day', u'score'] @greaterthanorequalcass30 def test_basic_json_query(self): @@ -330,8 +333,8 @@ def test_basic_json_query(self): self.session.execute(insert_query) results = self.session.execute(json_query) - self.assertEqual(results.column_names, ["[json]"]) - self.assertEqual(results.one()[0], '{"k": 1, "v": 1}') + assert results.column_names == ["[json]"] + assert results.one()[0] == '{"k": 1, "v": 1}' def test_host_targeting_query(self): """ @@ -356,9 +359,9 @@ def test_host_targeting_query(self): future = self.session.execute_async(query, host=host, execution_profile=checkable_ep) future.result() # check we're using the selected host - self.assertEqual(host, future.coordinator_host) + assert host == future.coordinator_host # check that this bypasses the LBP - self.assertFalse(checkable_ep.load_balancing_policy.make_query_plan.called) + assert not checkable_ep.load_balancing_policy.make_query_plan.called class PreparedStatementTests(unittest.TestCase): @@ -379,9 +382,9 @@ def test_routing_key(self): INSERT INTO test3rf.test (k, v) VALUES (?, ?) """) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) bound = prepared.bind((1, None)) - self.assertEqual(bound.routing_key, b'\x00\x00\x00\x01') + assert bound.routing_key == b'\x00\x00\x00\x01' def test_empty_routing_key_indexes(self): """ @@ -394,9 +397,9 @@ def test_empty_routing_key_indexes(self): """) prepared.routing_key_indexes = None - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) bound = prepared.bind((1, None)) - self.assertEqual(bound.routing_key, None) + assert bound.routing_key == None def test_predefined_routing_key(self): """ @@ -408,10 +411,10 @@ def test_predefined_routing_key(self): INSERT INTO test3rf.test (k, v) VALUES (?, ?) """) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) bound = prepared.bind((1, None)) bound._set_routing_key('fake_key') - self.assertEqual(bound.routing_key, 'fake_key') + assert bound.routing_key == 'fake_key' def test_multiple_routing_key_indexes(self): """ @@ -421,15 +424,15 @@ def test_multiple_routing_key_indexes(self): """ INSERT INTO test3rf.test (k, v) VALUES (?, ?) """) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) prepared.routing_key_indexes = [0, 1] bound = prepared.bind((1, 2)) - self.assertEqual(bound.routing_key, b'\x00\x04\x00\x00\x00\x01\x00\x00\x04\x00\x00\x00\x02\x00') + assert bound.routing_key == b'\x00\x04\x00\x00\x00\x01\x00\x00\x04\x00\x00\x00\x02\x00' prepared.routing_key_indexes = [1, 0] bound = prepared.bind((1, 2)) - self.assertEqual(bound.routing_key, b'\x00\x04\x00\x00\x00\x02\x00\x00\x04\x00\x00\x00\x01\x00') + assert bound.routing_key == b'\x00\x04\x00\x00\x00\x02\x00\x00\x04\x00\x00\x00\x01\x00' def test_bound_keyspace(self): """ @@ -440,9 +443,9 @@ def test_bound_keyspace(self): INSERT INTO test3rf.test (k, v) VALUES (?, ?) """) - self.assertIsInstance(prepared, PreparedStatement) + assert isinstance(prepared, PreparedStatement) bound = prepared.bind((1, 2)) - self.assertEqual(bound.keyspace, 'test3rf') + assert bound.keyspace == 'test3rf' class ForcedHostIndexPolicy(RoundRobinPolicy): @@ -458,7 +461,8 @@ def make_query_plan(self, working_keyspace=None, query=None): live_hosts = sorted(list(self._live_hosts)) host = [] try: - host = [live_hosts[self.host_index_to_use]] + if len(live_hosts) > 0: + host = [live_hosts[self.host_index_to_use]] except IndexError as e: raise IndexError( 'You specified an index larger than the number of hosts. Total hosts: {}. Index specified: {}'.format( @@ -490,15 +494,15 @@ def test_prepared_metadata_generation(self): session = cluster.connect() select_statement = session.prepare("SELECT * FROM system.local WHERE key='local'") if proto_version == 1: - self.assertEqual(select_statement.result_metadata, None) + assert select_statement.result_metadata == None else: - self.assertNotEqual(select_statement.result_metadata, None) + assert select_statement.result_metadata != None future = session.execute_async(select_statement) results = future.result() if base_line is None: base_line = results.one()._asdict().keys() else: - self.assertEqual(base_line, results.one()._asdict().keys()) + assert base_line == results.one()._asdict().keys() cluster.shutdown() @@ -522,7 +526,7 @@ def test_prepare_on_all_hosts(self): select_statement = session.prepare("SELECT k FROM test3rf.test WHERE k = ?") for host in clus.metadata.all_hosts(): session.execute(select_statement, (1, ), host=host) - self.assertEqual(2, mock_handler.get_message_count('debug', "Re-preparing")) + assert 2 == mock_handler.get_message_count('debug', "Re-preparing") def test_prepare_batch_statement(self): """ @@ -562,12 +566,12 @@ def test_prepare_batch_statement(self): session.execute(batch_statement) # To verify our test assumption that queries are getting re-prepared properly - self.assertEqual(1, mock_handler.get_message_count('debug', "Re-preparing")) + assert 1 == mock_handler.get_message_count('debug', "Re-preparing") select_results = session.execute(SimpleStatement("SELECT * FROM %s WHERE k = 1" % table, consistency_level=ConsistencyLevel.ALL)) first_row = select_results.one()[:2] - self.assertEqual((1, 2), first_row) + assert (1, 2) == first_row def test_prepare_batch_statement_after_alter(self): """ @@ -615,10 +619,10 @@ def test_prepare_batch_statement_after_alter(self): (4, None, 5, None, 6) ] - self.assertEqual(set(expected_results), set(select_results._current_rows)) + assert set(expected_results) == set(select_results._current_rows) # To verify our test assumption that queries are getting re-prepared properly - self.assertEqual(3, mock_handler.get_message_count('debug', "Re-preparing")) + assert 3 == mock_handler.get_message_count('debug', "Re-preparing") class PrintStatementTests(unittest.TestCase): @@ -632,8 +636,7 @@ def test_simple_statement(self): """ ss = SimpleStatement('SELECT * FROM test3rf.test', consistency_level=ConsistencyLevel.ONE) - self.assertEqual(str(ss), - '') + assert str(ss) == '' def test_prepared_statement(self): """ @@ -646,12 +649,10 @@ def test_prepared_statement(self): prepared = session.prepare('INSERT INTO test3rf.test (k, v) VALUES (?, ?)') prepared.consistency_level = ConsistencyLevel.ONE - self.assertEqual(str(prepared), - '') + assert str(prepared) == '' bound = prepared.bind((1, 2)) - self.assertEqual(str(bound), - '') + assert str(bound) == '' cluster.shutdown() @@ -665,8 +666,6 @@ def setUp(self): % (PROTOCOL_VERSION,)) self.cluster = TestCluster() - if PROTOCOL_VERSION < 3: - self.cluster.set_core_connections_per_host(HostDistance.LOCAL, 1) self.session = self.cluster.connect(wait_for_all_pools=True) def tearDown(self): @@ -683,8 +682,8 @@ def confirm_results(self): keys.add(result.k) values.add(result.v) - self.assertEqual(set(range(10)), keys, msg=results) - self.assertEqual(set(range(10)), values, msg=results) + assert set(range(10)) == keys, results + assert set(range(10)) == values, results def test_string_statements(self): batch = BatchStatement(BatchType.LOGGED) @@ -745,9 +744,12 @@ def test_no_parameters(self): batch.add("INSERT INTO test3rf.test (k, v) VALUES (8, 8)", ()) batch.add("INSERT INTO test3rf.test (k, v) VALUES (9, 9)", ()) - self.assertRaises(ValueError, batch.add, prepared.bind([]), (1)) - self.assertRaises(ValueError, batch.add, prepared.bind([]), (1, 2)) - self.assertRaises(ValueError, batch.add, prepared.bind([]), (1, 2, 3)) + with pytest.raises(ValueError): + batch.add(prepared.bind([]), (1)) + with pytest.raises(ValueError): + batch.add(prepared.bind([]), (1, 2)) + with pytest.raises(ValueError): + batch.add(prepared.bind([]), (1, 2, 3)) self.session.execute(batch) self.confirm_results() @@ -781,11 +783,13 @@ def test_too_many_statements(self): b = BatchStatement(batch_type=BatchType.UNLOGGED, consistency_level=ConsistencyLevel.ONE) # max + 1 raises b.add_all([ss] * max_statements, [None] * max_statements) - self.assertRaises(ValueError, b.add, ss) + with pytest.raises(ValueError): + b.add(ss) # also would have bombed trying to encode b._statements_and_parameters.append((False, ss.query_string, ())) - self.assertRaises(NoHostAvailable, self.session.execute, b) + with pytest.raises(NoHostAvailable): + self.session.execute(b) class SerialConsistencyTests(unittest.TestCase): @@ -796,13 +800,14 @@ def setUp(self): % (PROTOCOL_VERSION,)) self.cluster = TestCluster() - if PROTOCOL_VERSION < 3: - self.cluster.set_core_connections_per_host(HostDistance.LOCAL, 1) self.session = self.cluster.connect() def tearDown(self): self.cluster.shutdown() + @xfail_scylla_version_lt(reason='scylladb/scylladb#18068 - LWT is not yet supported with tablets', + scylla_version='2025.4', + raises=InvalidRequest) def test_conditional_update(self): self.session.execute("INSERT INTO test3rf.test (k, v) VALUES (0, 0)") statement = SimpleStatement( @@ -810,23 +815,26 @@ def test_conditional_update(self): serial_consistency_level=ConsistencyLevel.SERIAL) # crazy test, but PYTHON-299 # TODO: expand to check more parameters get passed to statement, and on to messages - self.assertEqual(statement.serial_consistency_level, ConsistencyLevel.SERIAL) + assert statement.serial_consistency_level == ConsistencyLevel.SERIAL future = self.session.execute_async(statement) result = future.result() - self.assertEqual(future.message.serial_consistency_level, ConsistencyLevel.SERIAL) - self.assertTrue(result) - self.assertFalse(result.one().applied) + assert future.message.serial_consistency_level == ConsistencyLevel.SERIAL + assert result + assert not result.one().applied statement = SimpleStatement( "UPDATE test3rf.test SET v=1 WHERE k=0 IF v=0", serial_consistency_level=ConsistencyLevel.LOCAL_SERIAL) - self.assertEqual(statement.serial_consistency_level, ConsistencyLevel.LOCAL_SERIAL) + assert statement.serial_consistency_level == ConsistencyLevel.LOCAL_SERIAL future = self.session.execute_async(statement) result = future.result() - self.assertEqual(future.message.serial_consistency_level, ConsistencyLevel.LOCAL_SERIAL) - self.assertTrue(result) - self.assertTrue(result.one().applied) + assert future.message.serial_consistency_level == ConsistencyLevel.LOCAL_SERIAL + assert result + assert result.one().applied + @xfail_scylla_version_lt(reason='scylladb/scylladb#18068 - LWT is not yet supported with tablets', + scylla_version='2025.4', + raises=InvalidRequest) def test_conditional_update_with_prepared_statements(self): self.session.execute("INSERT INTO test3rf.test (k, v) VALUES (0, 0)") statement = self.session.prepare( @@ -835,9 +843,9 @@ def test_conditional_update_with_prepared_statements(self): statement.serial_consistency_level = ConsistencyLevel.SERIAL future = self.session.execute_async(statement) result = future.result() - self.assertEqual(future.message.serial_consistency_level, ConsistencyLevel.SERIAL) - self.assertTrue(result) - self.assertFalse(result.one().applied) + assert future.message.serial_consistency_level == ConsistencyLevel.SERIAL + assert result + assert not result.one().applied statement = self.session.prepare( "UPDATE test3rf.test SET v=1 WHERE k=0 IF v=0") @@ -845,34 +853,39 @@ def test_conditional_update_with_prepared_statements(self): bound.serial_consistency_level = ConsistencyLevel.LOCAL_SERIAL future = self.session.execute_async(bound) result = future.result() - self.assertEqual(future.message.serial_consistency_level, ConsistencyLevel.LOCAL_SERIAL) - self.assertTrue(result) - self.assertTrue(result.one().applied) + assert future.message.serial_consistency_level == ConsistencyLevel.LOCAL_SERIAL + assert result + assert result.one().applied + @xfail_scylla_version_lt(reason='scylladb/scylladb#18068 - LWT is not yet supported with tablets', + scylla_version='2025.4', + raises=InvalidRequest) def test_conditional_update_with_batch_statements(self): self.session.execute("INSERT INTO test3rf.test (k, v) VALUES (0, 0)") statement = BatchStatement(serial_consistency_level=ConsistencyLevel.SERIAL) statement.add("UPDATE test3rf.test SET v=1 WHERE k=0 IF v=1") - self.assertEqual(statement.serial_consistency_level, ConsistencyLevel.SERIAL) + assert statement.serial_consistency_level == ConsistencyLevel.SERIAL future = self.session.execute_async(statement) result = future.result() - self.assertEqual(future.message.serial_consistency_level, ConsistencyLevel.SERIAL) - self.assertTrue(result) - self.assertFalse(result.one().applied) + assert future.message.serial_consistency_level == ConsistencyLevel.SERIAL + assert result + assert not result.one().applied statement = BatchStatement(serial_consistency_level=ConsistencyLevel.LOCAL_SERIAL) statement.add("UPDATE test3rf.test SET v=1 WHERE k=0 IF v=0") - self.assertEqual(statement.serial_consistency_level, ConsistencyLevel.LOCAL_SERIAL) + assert statement.serial_consistency_level == ConsistencyLevel.LOCAL_SERIAL future = self.session.execute_async(statement) result = future.result() - self.assertEqual(future.message.serial_consistency_level, ConsistencyLevel.LOCAL_SERIAL) - self.assertTrue(result) - self.assertTrue(result.one().applied) + assert future.message.serial_consistency_level == ConsistencyLevel.LOCAL_SERIAL + assert result + assert result.one().applied def test_bad_consistency_level(self): statement = SimpleStatement("foo") - self.assertRaises(ValueError, setattr, statement, 'serial_consistency_level', ConsistencyLevel.ONE) - self.assertRaises(ValueError, SimpleStatement, 'foo', serial_consistency_level=ConsistencyLevel.ONE) + with pytest.raises(ValueError): + setattr(statement, 'serial_consistency_level', ConsistencyLevel.ONE) + with pytest.raises(ValueError): + SimpleStatement('foo', serial_consistency_level=ConsistencyLevel.ONE) class LightweightTransactionTests(unittest.TestCase): @@ -912,6 +925,9 @@ def tearDown(self): self.session.execute("DROP TABLE test3rf.lwt_clustering") self.cluster.shutdown() + @xfail_scylla_version_lt(reason='scylladb/scylladb#18068 - LWT is not yet supported with tablets', + scylla_version='2025.4', + raises=AttributeError) def test_no_connection_refused_on_timeout(self): """ Test for PYTHON-91 "Connection closed after LWT timeout" @@ -939,16 +955,16 @@ def test_no_connection_refused_on_timeout(self): # In this case result is an exception exception_type = type(result).__name__ if exception_type == "NoHostAvailable": - self.fail("PYTHON-91: Disconnected from Cassandra: %s" % result.message) + pytest.fail("PYTHON-91: Disconnected from Cassandra: %s" % result.message) if exception_type in ["WriteTimeout", "WriteFailure", "ReadTimeout", "ReadFailure", "ErrorMessageSub"]: if type(result).__name__ in ["WriteTimeout", "WriteFailure"]: received_timeout = True continue - self.fail("Unexpected exception %s: %s" % (exception_type, result.message)) + pytest.fail("Unexpected exception %s: %s" % (exception_type, result.message)) # Make sure test passed - self.assertTrue(received_timeout) + assert received_timeout @xfail_scylla('Fails on Scylla with error `SERIAL/LOCAL_SERIAL consistency may only be requested for one partition at a time`') def test_was_applied_batch_stmt(self): @@ -975,7 +991,7 @@ def test_was_applied_batch_stmt(self): "INSERT INTO test3rf.lwt_clustering (k, c, v) VALUES (0, 1, 10);", "INSERT INTO test3rf.lwt_clustering (k, c, v) VALUES (0, 2, 10);"], [None] * 3) result = self.session.execute(batch_statement) - #self.assertTrue(result.was_applied) + #assert result.was_applied # Should fail since (0, 0, 10) have already been written # The non conditional insert shouldn't be written as well @@ -985,11 +1001,11 @@ def test_was_applied_batch_stmt(self): "INSERT INTO test3rf.lwt_clustering (k, c, v) VALUES (0, 4, 10);", "INSERT INTO test3rf.lwt_clustering (k, c, v) VALUES (0, 5, 10) IF NOT EXISTS;"], [None] * 4) result = self.session.execute(batch_statement) - self.assertFalse(result.was_applied) + assert not result.was_applied all_rows = self.session.execute("SELECT * from test3rf.lwt_clustering", execution_profile='serial') # Verify the non conditional insert hasn't been inserted - self.assertEqual(len(all_rows.current_rows), 3) + assert len(all_rows.current_rows) == 3 # Should fail since (0, 0, 10) have already been written batch_statement = BatchStatement(batch_type) @@ -997,12 +1013,12 @@ def test_was_applied_batch_stmt(self): "INSERT INTO test3rf.lwt_clustering (k, c, v) VALUES (0, 3, 10) IF NOT EXISTS;", "INSERT INTO test3rf.lwt_clustering (k, c, v) VALUES (0, 5, 10) IF NOT EXISTS;"], [None] * 3) result = self.session.execute(batch_statement) - self.assertFalse(result.was_applied) + assert not result.was_applied # Should fail since (0, 0, 10) have already been written batch_statement.add("INSERT INTO test3rf.lwt_clustering (k, c, v) VALUES (0, 0, 10) IF NOT EXISTS;") result = self.session.execute(batch_statement) - self.assertFalse(result.was_applied) + assert not result.was_applied # Should succeed batch_statement = BatchStatement(batch_type) @@ -1011,11 +1027,11 @@ def test_was_applied_batch_stmt(self): "INSERT INTO test3rf.lwt_clustering (k, c, v) VALUES (0, 5, 10) IF NOT EXISTS;"], [None] * 3) result = self.session.execute(batch_statement) - self.assertTrue(result.was_applied) + assert result.was_applied all_rows = self.session.execute("SELECT * from test3rf.lwt_clustering", execution_profile='serial') for i, row in enumerate(all_rows): - self.assertEqual((0, i, 10), (row[0], row[1], row[2])) + assert (0, i, 10) == (row[0], row[1], row[2]) self.session.execute("TRUNCATE TABLE test3rf.lwt_clustering") @@ -1033,7 +1049,7 @@ def test_empty_batch_statement(self): """ batch_statement = BatchStatement() results = self.session.execute(batch_statement) - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): results.was_applied @pytest.mark.xfail(reason='Skipping until PYTHON-943 is resolved') @@ -1052,7 +1068,7 @@ def test_was_applied_batch_string(self): APPLY batch; """ result = self.session.execute(batch_str) - self.assertFalse(result.was_applied) + assert not result.was_applied batch_str = """ BEGIN unlogged batch @@ -1062,7 +1078,7 @@ def test_was_applied_batch_string(self): APPLY batch; """ result = self.session.execute(batch_str) - self.assertTrue(result.was_applied) + assert result.was_applied class BatchStatementDefaultRoutingKeyTests(unittest.TestCase): @@ -1091,8 +1107,8 @@ def test_rk_from_bound(self): bound = self.prepared.bind((1, None)) batch = BatchStatement() batch.add(bound) - self.assertIsNotNone(batch.routing_key) - self.assertEqual(batch.routing_key, bound.routing_key) + assert batch.routing_key is not None + assert batch.routing_key == bound.routing_key def test_rk_from_simple(self): """ @@ -1100,8 +1116,8 @@ def test_rk_from_simple(self): """ batch = BatchStatement() batch.add(self.simple_statement) - self.assertIsNotNone(batch.routing_key) - self.assertEqual(batch.routing_key, self.simple_statement.routing_key) + assert batch.routing_key is not None + assert batch.routing_key == self.simple_statement.routing_key def test_inherit_first_rk_bound(self): """ @@ -1116,8 +1132,8 @@ def test_inherit_first_rk_bound(self): for i in range(3): batch.add(self.prepared, (i, i)) - self.assertIsNotNone(batch.routing_key) - self.assertEqual(batch.routing_key, bound.routing_key) + assert batch.routing_key is not None + assert batch.routing_key == bound.routing_key def test_inherit_first_rk_simple_statement(self): """ @@ -1132,8 +1148,8 @@ def test_inherit_first_rk_simple_statement(self): for i in range(10): batch.add(self.prepared, (i, i)) - self.assertIsNotNone(batch.routing_key) - self.assertEqual(batch.routing_key, self.simple_statement.routing_key) + assert batch.routing_key is not None + assert batch.routing_key == self.simple_statement.routing_key def test_inherit_first_rk_prepared_param(self): """ @@ -1146,13 +1162,19 @@ def test_inherit_first_rk_prepared_param(self): batch.add(bound) batch.add(self.simple_statement) - self.assertIsNotNone(batch.routing_key) - self.assertEqual(batch.routing_key, self.prepared.bind((1, 0)).routing_key) + assert batch.routing_key is not None + assert batch.routing_key == self.prepared.bind((1, 0)).routing_key @greaterthanorequalcass30 class MaterializedViewQueryTest(BasicSharedKeyspaceUnitTestCase): + @classmethod + def create_keyspace(cls, rf): + ddl = "CREATE KEYSPACE {0} WITH replication = {{'class': 'NetworkTopologyStrategy', 'replication_factor': '{1}'}}{2}".format( + cls.ks_name, rf, get_tablets_disabled_ddl_suffix()) + execute_with_long_wait_retry(cls.session, ddl) + def test_mv_filtering(self): """ Test to ensure that cql filtering where clauses are properly supported in the python driver. @@ -1243,73 +1265,73 @@ def test_mv_filtering(self): query_statement = SimpleStatement("SELECT * FROM {0}.alltimehigh WHERE game='Coup'".format(self.keyspace_name), consistency_level=ConsistencyLevel.QUORUM) results = self.session.execute(query_statement) - self.assertEqual(results.one().game, 'Coup') - self.assertEqual(results.one().year, 2015) - self.assertEqual(results.one().month, 5) - self.assertEqual(results.one().day, 1) - self.assertEqual(results.one().score, 4000) - self.assertEqual(results.one().user, "pcmanus") + assert results.one().game == 'Coup' + assert results.one().year == 2015 + assert results.one().month == 5 + assert results.one().day == 1 + assert results.one().score == 4000 + assert results.one().user == "pcmanus" # Test prepared statement and daily high filtering prepared_query = self.session.prepare("SELECT * FROM {0}.dailyhigh WHERE game=? AND year=? AND month=? and day=?".format(self.keyspace_name)) bound_query = prepared_query.bind(("Coup", 2015, 6, 2)) results = self.session.execute(bound_query) - self.assertEqual(results.one().game, 'Coup') - self.assertEqual(results.one().year, 2015) - self.assertEqual(results.one().month, 6) - self.assertEqual(results.one().day, 2) - self.assertEqual(results.one().score, 2000) - self.assertEqual(results.one().user, "pcmanus") - - self.assertEqual(results[1].game, 'Coup') - self.assertEqual(results[1].year, 2015) - self.assertEqual(results[1].month, 6) - self.assertEqual(results[1].day, 2) - self.assertEqual(results[1].score, 1000) - self.assertEqual(results[1].user, "tjake") + assert results.one().game == 'Coup' + assert results.one().year == 2015 + assert results.one().month == 6 + assert results.one().day == 2 + assert results.one().score == 2000 + assert results.one().user == "pcmanus" + + assert results[1].game == 'Coup' + assert results[1].year == 2015 + assert results[1].month == 6 + assert results[1].day == 2 + assert results[1].score == 1000 + assert results[1].user == "tjake" # Test montly high range queries prepared_query = self.session.prepare("SELECT * FROM {0}.monthlyhigh WHERE game=? AND year=? AND month=? and score >= ? and score <= ?".format(self.keyspace_name)) bound_query = prepared_query.bind(("Coup", 2015, 6, 2500, 3500)) results = self.session.execute(bound_query) - self.assertEqual(results.one().game, 'Coup') - self.assertEqual(results.one().year, 2015) - self.assertEqual(results.one().month, 6) - self.assertEqual(results.one().day, 20) - self.assertEqual(results.one().score, 3500) - self.assertEqual(results.one().user, "jbellis") - - self.assertEqual(results[1].game, 'Coup') - self.assertEqual(results[1].year, 2015) - self.assertEqual(results[1].month, 6) - self.assertEqual(results[1].day, 9) - self.assertEqual(results[1].score, 2700) - self.assertEqual(results[1].user, "jmckenzie") - - self.assertEqual(results[2].game, 'Coup') - self.assertEqual(results[2].year, 2015) - self.assertEqual(results[2].month, 6) - self.assertEqual(results[2].day, 1) - self.assertEqual(results[2].score, 2500) - self.assertEqual(results[2].user, "iamaleksey") + assert results.one().game == 'Coup' + assert results.one().year == 2015 + assert results.one().month == 6 + assert results.one().day == 20 + assert results.one().score == 3500 + assert results.one().user == "jbellis" + + assert results[1].game == 'Coup' + assert results[1].year == 2015 + assert results[1].month == 6 + assert results[1].day == 9 + assert results[1].score == 2700 + assert results[1].user == "jmckenzie" + + assert results[2].game == 'Coup' + assert results[2].year == 2015 + assert results[2].month == 6 + assert results[2].day == 1 + assert results[2].score == 2500 + assert results[2].user == "iamaleksey" # Test filtered user high scores query_statement = SimpleStatement("SELECT * FROM {0}.filtereduserhigh WHERE game='Chess'".format(self.keyspace_name), consistency_level=ConsistencyLevel.QUORUM) results = self.session.execute(query_statement) - self.assertEqual(results.one().game, 'Chess') - self.assertEqual(results.one().year, 2015) - self.assertEqual(results.one().month, 6) - self.assertEqual(results.one().day, 21) - self.assertEqual(results.one().score, 3500) - self.assertEqual(results.one().user, "jbellis") + assert results.one().game == 'Chess' + assert results.one().year == 2015 + assert results.one().month == 6 + assert results.one().day == 21 + assert results.one().score == 3500 + assert results.one().user == "jbellis" - self.assertEqual(results[1].game, 'Chess') - self.assertEqual(results[1].year, 2015) - self.assertEqual(results[1].month, 1) - self.assertEqual(results[1].day, 25) - self.assertEqual(results[1].score, 3200) - self.assertEqual(results[1].user, "pcmanus") + assert results[1].game == 'Chess' + assert results[1].year == 2015 + assert results[1].month == 1 + assert results[1].day == 25 + assert results[1].score == 3200 + assert results[1].user == "pcmanus" class UnicodeQueryTest(BasicSharedKeyspaceUnitTestCase): @@ -1356,12 +1378,12 @@ def setUpClass(cls): cls.table_name = "table_query_keyspace_tests" ddl = """CREATE KEYSPACE {0} WITH replication = - {{'class': 'SimpleStrategy', + {{'class': 'NetworkTopologyStrategy', 'replication_factor': '{1}'}}""".format(cls.ks_name, 1) cls.session.execute(ddl) ddl = """CREATE KEYSPACE {0} WITH replication = - {{'class': 'SimpleStrategy', + {{'class': 'NetworkTopologyStrategy', 'replication_factor': '{1}'}}""".format(cls.alternative_ks, 1) cls.session.execute(ddl) @@ -1403,7 +1425,6 @@ def test_setting_keyspace(self): """ self._check_set_keyspace_in_statement(self.session) - @requirecassandra @greaterthanorequalcass40 def test_setting_keyspace_and_session(self): """ @@ -1472,18 +1493,18 @@ def test_lower_protocol(self): # set on queries with protocol version 5 or higher. Consider setting Cluster.protocol_version to 5.',), # : ConnectionException('Host has been marked down or removed',), # : ConnectionException('Host has been marked down or removed',)}) - with self.assertRaises(NoHostAvailable): + with pytest.raises(NoHostAvailable): session.execute(simple_stmt) def _check_set_keyspace_in_statement(self, session): simple_stmt = SimpleStatement("SELECT * from {}".format(self.table_name), keyspace=self.ks_name) results = session.execute(simple_stmt) - self.assertEqual(results.one(), (1, 1)) + assert results.one() == (1, 1) simple_stmt = SimpleStatement("SELECT * from {}".format(self.table_name)) simple_stmt.keyspace = self.ks_name results = session.execute(simple_stmt) - self.assertEqual(results.one(), (1, 1)) + assert results.one() == (1, 1) @greaterthanorequalcass40 @@ -1508,8 +1529,8 @@ def confirm_results(self): keys.add(result.k) values.add(result.v) - self.assertEqual(set(range(10)), keys, msg=results) - self.assertEqual(set(range(10)), values, msg=results) + assert set(range(10)) == keys, results + assert set(range(10)) == values, results @greaterthanorequalcass40 @@ -1537,14 +1558,14 @@ def test_prepared_with_keyspace_explicit(self): prepared_statement = self.session.prepare(query, keyspace=self.ks_name) results = self.session.execute(prepared_statement, (1, )) - self.assertEqual(results.one(), (1, 1)) + assert results.one() == (1, 1) prepared_statement_alternative = self.session.prepare(query, keyspace=self.alternative_ks) - self.assertNotEqual(prepared_statement.query_id, prepared_statement_alternative.query_id) + assert prepared_statement.query_id != prepared_statement_alternative.query_id results = self.session.execute(prepared_statement_alternative, (2,)) - self.assertEqual(results.one(), (2, 2)) + assert results.one() == (2, 2) def test_reprepare_after_host_is_down(self): """ @@ -1569,15 +1590,16 @@ def test_reprepare_after_host_is_down(self): get_node(1).start(wait_for_binary_proto=True, wait_other_notice=True) - # We wait for cluster._prepare_all_queries to be called - time.sleep(5) - self.assertEqual(1, mock_handler.get_message_count('debug', 'Preparing all known prepared statements')) - + # Wait for cluster._prepare_all_queries to be called + wait_until( + lambda: mock_handler.get_message_count('debug', 'Preparing all known prepared statements') >= 1, + delay=0.5, max_attempts=20) + results = self.session.execute(prepared_statement, (1,), execution_profile="only_first") - self.assertEqual(results.one(), (1, )) + assert results.one() == (1, ) results = self.session.execute(prepared_statement_alternative, (2,), execution_profile="only_first") - self.assertEqual(results.one(), (2, )) + assert results.one() == (2, ) def test_prepared_not_found(self): """ @@ -1601,7 +1623,7 @@ def test_prepared_not_found(self): for _ in range(10): results = session.execute(prepared_statement, (1, )) - self.assertEqual(results.one(), (1,)) + assert results.one() == (1,) def test_prepared_in_query_keyspace(self): """ @@ -1620,12 +1642,12 @@ def test_prepared_in_query_keyspace(self): query = "SELECT k from {}.{} WHERE k = ?".format(self.ks_name, self.table_name) prepared_statement = session.prepare(query) results = session.execute(prepared_statement, (1,)) - self.assertEqual(results.one(), (1,)) + assert results.one() == (1,) query = "SELECT k from {}.{} WHERE k = ?".format(self.alternative_ks, self.table_name) prepared_statement = session.prepare(query) results = session.execute(prepared_statement, (2,)) - self.assertEqual(results.one(), (2,)) + assert results.one() == (2,) def test_prepared_in_query_keyspace_and_explicit(self): """ @@ -1642,9 +1664,9 @@ def test_prepared_in_query_keyspace_and_explicit(self): query = "SELECT k from {}.{} WHERE k = ?".format(self.ks_name, self.table_name) prepared_statement = self.session.prepare(query, keyspace="system") results = self.session.execute(prepared_statement, (1,)) - self.assertEqual(results.one(), (1,)) + assert results.one() == (1,) query = "SELECT k from {}.{} WHERE k = ?".format(self.ks_name, self.table_name) prepared_statement = self.session.prepare(query, keyspace=self.alternative_ks) results = self.session.execute(prepared_statement, (1,)) - self.assertEqual(results.one(), (1,)) + assert results.one() == (1,) diff --git a/tests/integration/standard/test_query_paging.py b/tests/integration/standard/test_query_paging.py index 26c1ca0da6..e0c67cd309 100644 --- a/tests/integration/standard/test_query_paging.py +++ b/tests/integration/standard/test_query_paging.py @@ -20,6 +20,7 @@ from itertools import cycle, count from threading import Event +import pytest from cassandra import ConsistencyLevel from cassandra.cluster import EXEC_PROFILE_DEFAULT, ExecutionProfile @@ -27,6 +28,8 @@ from cassandra.policies import HostDistance from cassandra.query import SimpleStatement +from tests.util import assertSequenceEqual + def setup_module(): use_singledc() @@ -43,8 +46,6 @@ def setUp(self): self.cluster = TestCluster( execution_profiles={EXEC_PROFILE_DEFAULT: ExecutionProfile(consistency_level=ConsistencyLevel.LOCAL_QUORUM)} ) - if PROTOCOL_VERSION < 3: - self.cluster.set_core_connections_per_host(HostDistance.LOCAL, 1) self.session = self.cluster.connect(wait_for_all_pools=True) self.session.execute("TRUNCATE test3rf.test") @@ -60,12 +61,12 @@ def test_paging(self): for fetch_size in (2, 3, 7, 10, 99, 100, 101, 10000): self.session.default_fetch_size = fetch_size - self.assertEqual(100, len(list(self.session.execute("SELECT * FROM test3rf.test")))) + assert 100 == len(list(self.session.execute("SELECT * FROM test3rf.test"))) statement = SimpleStatement("SELECT * FROM test3rf.test") - self.assertEqual(100, len(list(self.session.execute(statement)))) + assert 100 == len(list(self.session.execute(statement))) - self.assertEqual(100, len(list(self.session.execute(prepared)))) + assert 100 == len(list(self.session.execute(prepared))) def test_paging_state(self): """ @@ -86,14 +87,14 @@ def test_paging_state(self): result_set = self.session.execute("SELECT * FROM test3rf.test") while(result_set.has_more_pages): for row in result_set.current_rows: - self.assertNotIn(row, list_all_results) + assert row not in list_all_results list_all_results.extend(result_set.current_rows) page_state = result_set.paging_state result_set = self.session.execute("SELECT * FROM test3rf.test", paging_state=page_state) if(len(result_set.current_rows) > 0): list_all_results.append(result_set.current_rows) - self.assertEqual(len(list_all_results), 100) + assert len(list_all_results) == 100 def test_paging_verify_writes(self): statements_and_params = zip(cycle(["INSERT INTO test3rf.test (k, v) VALUES (%s, 0)"]), @@ -111,8 +112,8 @@ def test_paging_verify_writes(self): result_array.add(result.k) result_set.add(result.v) - self.assertEqual(set(range(100)), result_array) - self.assertEqual(set([0]), result_set) + assert set(range(100)) == result_array + assert set([0]) == result_set statement = SimpleStatement("SELECT * FROM test3rf.test") results = self.session.execute(statement) @@ -122,8 +123,8 @@ def test_paging_verify_writes(self): result_array.add(result.k) result_set.add(result.v) - self.assertEqual(set(range(100)), result_array) - self.assertEqual(set([0]), result_set) + assert set(range(100)) == result_array + assert set([0]) == result_set results = self.session.execute(prepared) result_array = set() @@ -132,8 +133,8 @@ def test_paging_verify_writes(self): result_array.add(result.k) result_set.add(result.v) - self.assertEqual(set(range(100)), result_array) - self.assertEqual(set([0]), result_set) + assert set(range(100)) == result_array + assert set([0]) == result_set def test_paging_verify_with_composite_keys(self): ddl = ''' @@ -161,8 +162,8 @@ def test_paging_verify_with_composite_keys(self): result_array.append(result.k2) value_array.append(result.v) - self.assertSequenceEqual(range(100), result_array) - self.assertSequenceEqual(range(1, 101), value_array) + assertSequenceEqual(range(100), result_array) + assertSequenceEqual(range(1, 101), value_array) statement = SimpleStatement("SELECT * FROM test3rf.test_paging_verify_2") results = self.session.execute(statement) @@ -172,8 +173,8 @@ def test_paging_verify_with_composite_keys(self): result_array.append(result.k2) value_array.append(result.v) - self.assertSequenceEqual(range(100), result_array) - self.assertSequenceEqual(range(1, 101), value_array) + assertSequenceEqual(range(100), result_array) + assertSequenceEqual(range(1, 101), value_array) results = self.session.execute(prepared) result_array = [] @@ -182,8 +183,8 @@ def test_paging_verify_with_composite_keys(self): result_array.append(result.k2) value_array.append(result.v) - self.assertSequenceEqual(range(100), result_array) - self.assertSequenceEqual(range(1, 101), value_array) + assertSequenceEqual(range(100), result_array) + assertSequenceEqual(range(1, 101), value_array) def test_async_paging(self): statements_and_params = zip(cycle(["INSERT INTO test3rf.test (k, v) VALUES (%s, 0)"]), @@ -194,12 +195,12 @@ def test_async_paging(self): for fetch_size in (2, 3, 7, 10, 99, 100, 101, 10000): self.session.default_fetch_size = fetch_size - self.assertEqual(100, len(list(self.session.execute_async("SELECT * FROM test3rf.test").result()))) + assert 100 == len(list(self.session.execute_async("SELECT * FROM test3rf.test").result())) statement = SimpleStatement("SELECT * FROM test3rf.test") - self.assertEqual(100, len(list(self.session.execute_async(statement).result()))) + assert 100 == len(list(self.session.execute_async(statement).result())) - self.assertEqual(100, len(list(self.session.execute_async(prepared).result()))) + assert 100 == len(list(self.session.execute_async(prepared).result())) def test_async_paging_verify_writes(self): ddl = ''' @@ -227,8 +228,8 @@ def test_async_paging_verify_writes(self): result_array.append(result.k2) value_array.append(result.v) - self.assertSequenceEqual(range(100), result_array) - self.assertSequenceEqual(range(1, 101), value_array) + assertSequenceEqual(range(100), result_array) + assertSequenceEqual(range(1, 101), value_array) statement = SimpleStatement("SELECT * FROM test3rf.test_async_paging_verify") results = self.session.execute_async(statement).result() @@ -238,8 +239,8 @@ def test_async_paging_verify_writes(self): result_array.append(result.k2) value_array.append(result.v) - self.assertSequenceEqual(range(100), result_array) - self.assertSequenceEqual(range(1, 101), value_array) + assertSequenceEqual(range(100), result_array) + assertSequenceEqual(range(1, 101), value_array) results = self.session.execute_async(prepared).result() result_array = [] @@ -248,8 +249,8 @@ def test_async_paging_verify_writes(self): result_array.append(result.k2) value_array.append(result.v) - self.assertSequenceEqual(range(100), result_array) - self.assertSequenceEqual(range(1, 101), value_array) + assertSequenceEqual(range(100), result_array) + assertSequenceEqual(range(1, 101), value_array) def test_paging_callbacks(self): """ @@ -287,13 +288,13 @@ def handle_page(rows, future, counter, number_of_calls): def handle_error(err): event.set() - self.fail(err) + pytest.fail(err) future.add_callbacks(callback=handle_page, callback_args=(future, counter, number_of_calls), errback=handle_error) event.wait() - self.assertEqual(next(number_of_calls), 100 // fetch_size + 1) - self.assertEqual(next(counter), 100) + assert next(number_of_calls) == 100 // fetch_size + 1 + assert next(counter) == 100 # simple statement future = self.session.execute_async(SimpleStatement("SELECT * FROM test3rf.test"), timeout=20) @@ -304,8 +305,8 @@ def handle_error(err): future.add_callbacks(callback=handle_page, callback_args=(future, counter, number_of_calls), errback=handle_error) event.wait() - self.assertEqual(next(number_of_calls), 100 // fetch_size + 1) - self.assertEqual(next(counter), 100) + assert next(number_of_calls) == 100 // fetch_size + 1 + assert next(counter) == 100 # prepared statement future = self.session.execute_async(prepared, timeout=20) @@ -316,8 +317,8 @@ def handle_error(err): future.add_callbacks(callback=handle_page, callback_args=(future, counter, number_of_calls), errback=handle_error) event.wait() - self.assertEqual(next(number_of_calls), 100 // fetch_size + 1) - self.assertEqual(next(counter), 100) + assert next(number_of_calls) == 100 // fetch_size + 1 + assert next(counter) == 100 def test_concurrent_with_paging(self): statements_and_params = zip(cycle(["INSERT INTO test3rf.test (k, v) VALUES (%s, 0)"]), @@ -329,10 +330,10 @@ def test_concurrent_with_paging(self): for fetch_size in (2, 3, 7, 10, 99, 100, 101, 10000): self.session.default_fetch_size = fetch_size results = execute_concurrent_with_args(self.session, prepared, [None] * 10) - self.assertEqual(10, len(results)) + assert 10 == len(results) for (success, result) in results: - self.assertTrue(success) - self.assertEqual(100, len(list(result))) + assert success + assert 100 == len(list(result)) def test_fetch_size(self): """ @@ -346,66 +347,66 @@ def test_fetch_size(self): self.session.default_fetch_size = 10 result = self.session.execute(prepared, []) - self.assertTrue(result.has_more_pages) + assert result.has_more_pages self.session.default_fetch_size = 2000 result = self.session.execute(prepared, []) - self.assertFalse(result.has_more_pages) + assert not result.has_more_pages self.session.default_fetch_size = None result = self.session.execute(prepared, []) - self.assertFalse(result.has_more_pages) + assert not result.has_more_pages self.session.default_fetch_size = 10 prepared.fetch_size = 2000 result = self.session.execute(prepared, []) - self.assertFalse(result.has_more_pages) + assert not result.has_more_pages prepared.fetch_size = None result = self.session.execute(prepared, []) - self.assertFalse(result.has_more_pages) + assert not result.has_more_pages prepared.fetch_size = 10 result = self.session.execute(prepared, []) - self.assertTrue(result.has_more_pages) + assert result.has_more_pages prepared.fetch_size = 2000 bound = prepared.bind([]) result = self.session.execute(bound, []) - self.assertFalse(result.has_more_pages) + assert not result.has_more_pages prepared.fetch_size = None bound = prepared.bind([]) result = self.session.execute(bound, []) - self.assertFalse(result.has_more_pages) + assert not result.has_more_pages prepared.fetch_size = 10 bound = prepared.bind([]) result = self.session.execute(bound, []) - self.assertTrue(result.has_more_pages) + assert result.has_more_pages bound.fetch_size = 2000 result = self.session.execute(bound, []) - self.assertFalse(result.has_more_pages) + assert not result.has_more_pages bound.fetch_size = None result = self.session.execute(bound, []) - self.assertFalse(result.has_more_pages) + assert not result.has_more_pages bound.fetch_size = 10 result = self.session.execute(bound, []) - self.assertTrue(result.has_more_pages) + assert result.has_more_pages s = SimpleStatement("SELECT * FROM test3rf.test", fetch_size=None) result = self.session.execute(s, []) - self.assertFalse(result.has_more_pages) + assert not result.has_more_pages s = SimpleStatement("SELECT * FROM test3rf.test") result = self.session.execute(s, []) - self.assertTrue(result.has_more_pages) + assert result.has_more_pages s = SimpleStatement("SELECT * FROM test3rf.test") s.fetch_size = None result = self.session.execute(s, []) - self.assertFalse(result.has_more_pages) + assert not result.has_more_pages diff --git a/tests/integration/standard/test_rack_aware_policy.py b/tests/integration/standard/test_rack_aware_policy.py index 5d7a69642f..d2a358373d 100644 --- a/tests/integration/standard/test_rack_aware_policy.py +++ b/tests/integration/standard/test_rack_aware_policy.py @@ -66,24 +66,24 @@ def test_rack_aware(self): for i in range (10): bound = prepared.bind([i]) results = self.session.execute(bound) - self.assertEqual(results, [(i, i%5, i%2)]) + assert results == [(i, i%5, i%2)] coordinator = str(results.response_future.coordinator_host.endpoint) - self.assertTrue(coordinator in set(["127.0.0.1:9042", "127.0.0.2:9042"])) + assert coordinator in set(["127.0.0.1:9042", "127.0.0.2:9042"]) self.node2.stop(wait_other_notice=True, gently=True) for i in range (10): bound = prepared.bind([i]) results = self.session.execute(bound) - self.assertEqual(results, [(i, i%5, i%2)]) + assert results == [(i, i%5, i%2)] coordinator =str(results.response_future.coordinator_host.endpoint) - self.assertEqual(coordinator, "127.0.0.1:9042") + assert coordinator == "127.0.0.1:9042" self.node1.stop(wait_other_notice=True, gently=True) for i in range (10): bound = prepared.bind([i]) results = self.session.execute(bound) - self.assertEqual(results, [(i, i%5, i%2)]) + assert results == [(i, i%5, i%2)] coordinator = str(results.response_future.coordinator_host.endpoint) - self.assertTrue(coordinator in set(["127.0.0.3:9042", "127.0.0.4:9042"])) + assert coordinator in set(["127.0.0.3:9042", "127.0.0.4:9042"]) diff --git a/tests/integration/standard/test_rate_limit_exceeded.py b/tests/integration/standard/test_rate_limit_exceeded.py index 280d6426e1..5a7fc5dc74 100644 --- a/tests/integration/standard/test_rate_limit_exceeded.py +++ b/tests/integration/standard/test_rate_limit_exceeded.py @@ -4,12 +4,13 @@ from cassandra.cluster import Cluster from cassandra.policies import ConstantReconnectionPolicy, RoundRobinPolicy, TokenAwarePolicy -from tests.integration import PROTOCOL_VERSION, use_cluster +from tests.integration import PROTOCOL_VERSION, use_singledc +import pytest LOGGER = logging.getLogger(__name__) def setup_module(): - use_cluster('rate_limit', [3], start=True) + use_singledc() class TestRateLimitExceededException(unittest.TestCase): @classmethod @@ -31,17 +32,17 @@ def test_rate_limit_exceeded(self): ) self.session.execute( """ - CREATE KEYSPACE IF NOT EXISTS ratetests - WITH REPLICATION = {'class' : 'SimpleStrategy', 'replication_factor' : 1} + CREATE KEYSPACE IF NOT EXISTS ratetests + WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1} """) self.session.execute("USE ratetests") self.session.execute( """ - CREATE TABLE tbl (pk int PRIMARY KEY, v int) + CREATE TABLE tbl (pk int PRIMARY KEY, v int) WITH per_partition_rate_limit = {'max_writes_per_second': 1} """) - + prepared = self.session.prepare( """ INSERT INTO tbl (pk, v) VALUES (?, ?) @@ -53,7 +54,7 @@ def execute_write(): for _ in range(1000): self.session.execute(prepared.bind((123, 456))) - with self.assertRaises(RateLimitReached) as context: + with pytest.raises(RateLimitReached) as context: execute_write() - self.assertEqual(context.exception.op_type, OperationType.Write) + assert context.value.op_type == OperationType.Write diff --git a/tests/integration/standard/test_routing.py b/tests/integration/standard/test_routing.py index 7d6651cf8b..ae45e7ade4 100644 --- a/tests/integration/standard/test_routing.py +++ b/tests/integration/standard/test_routing.py @@ -50,7 +50,7 @@ def insert_select_token(self, insert, select, key_values): cass_token = s.execute(select, key_values).one()[0] token = s.cluster.metadata.token_map.token_class(cass_token) - self.assertEqual(my_token, token) + assert my_token == token def create_prepare(self, key_types): s = self.session diff --git a/tests/integration/standard/test_row_factories.py b/tests/integration/standard/test_row_factories.py index 413a6bf50b..818f11c061 100644 --- a/tests/integration/standard/test_row_factories.py +++ b/tests/integration/standard/test_row_factories.py @@ -12,10 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from tests.integration import get_server_versions, use_singledc, \ +from tests.integration import get_server_versions, use_single_node, \ BasicSharedKeyspaceUnitTestCaseWFunctionTable, BasicSharedKeyspaceUnitTestCase, execute_until_pass, TestCluster import unittest +import pytest from cassandra.cluster import ResultSet, ExecutionProfile, EXEC_PROFILE_DEFAULT from cassandra.query import tuple_factory, named_tuple_factory, dict_factory, ordered_dict_factory @@ -23,7 +24,7 @@ def setup_module(): - use_singledc() + use_single_node() class NameTupleFactory(BasicSharedKeyspaceUnitTestCase): @@ -66,9 +67,9 @@ def test_sanitizing(self): query = "SELECT v1 AS duplicate, v2 AS duplicate, v3 AS duplicate from {0}.{1}".format(self.ks_name, self.function_table_name) rs = self.session.execute(query) row = rs.one() - self.assertTrue(hasattr(row, 'duplicate')) - self.assertTrue(hasattr(row, 'duplicate_')) - self.assertTrue(hasattr(row, 'duplicate__')) + assert hasattr(row, 'duplicate') + assert hasattr(row, 'duplicate_') + assert hasattr(row, 'duplicate__') class RowFactoryTests(BasicSharedKeyspaceUnitTestCaseWFunctionTable): @@ -92,44 +93,44 @@ def _results_from_row_factory(self, row_factory): def test_tuple_factory(self): result = self._results_from_row_factory(tuple_factory) - self.assertIsInstance(result, ResultSet) - self.assertIsInstance(result.one(), tuple) + assert isinstance(result, ResultSet) + assert isinstance(result.one(), tuple) result = result.all() for row in result: - self.assertEqual(row[0], row[1]) + assert row[0] == row[1] - self.assertEqual(result[0][0], result[0][1]) - self.assertEqual(result[0][0], 1) - self.assertEqual(result[1][0], result[1][1]) - self.assertEqual(result[1][0], 2) + assert result[0][0] == result[0][1] + assert result[0][0] == 1 + assert result[1][0] == result[1][1] + assert result[1][0] == 2 def test_named_tuple_factory(self): result = self._results_from_row_factory(named_tuple_factory) - self.assertIsInstance(result, ResultSet) + assert isinstance(result, ResultSet) result = result.all() for row in result: - self.assertEqual(row.k, row.v) + assert row.k == row.v - self.assertEqual(result[0].k, result[0].v) - self.assertEqual(result[0].k, 1) - self.assertEqual(result[1].k, result[1].v) - self.assertEqual(result[1].k, 2) + assert result[0].k == result[0].v + assert result[0].k == 1 + assert result[1].k == result[1].v + assert result[1].k == 2 def _test_dict_factory(self, row_factory, row_type): result = self._results_from_row_factory(row_factory) - self.assertIsInstance(result, ResultSet) - self.assertIsInstance(result.one(), row_type) + assert isinstance(result, ResultSet) + assert isinstance(result.one(), row_type) result = result.all() for row in result: - self.assertEqual(row['k'], row['v']) + assert row['k'] == row['v'] - self.assertEqual(result[0]['k'], result[0]['v']) - self.assertEqual(result[0]['k'], 1) - self.assertEqual(result[1]['k'], result[1]['v']) - self.assertEqual(result[1]['k'], 2) + assert result[0]['k'] == result[0]['v'] + assert result[0]['k'] == 1 + assert result[1]['k'] == result[1]['v'] + assert result[1]['k'] == 2 def test_dict_factory(self): self._test_dict_factory(dict_factory, dict) @@ -164,9 +165,9 @@ def _gen_row_factory(rows): ( 1 , 1 ) '''.format(self.keyspace_name, self.function_table_name)) result = session.execute(self.select) - self.assertIsInstance(result, ResultSet) + assert isinstance(result, ResultSet) first_row = result.one() - self.assertEqual(first_row[0], first_row[1]) + assert first_row[0] == first_row[1] class NamedTupleFactoryAndNumericColNamesTests(unittest.TestCase): @@ -194,7 +195,7 @@ def test_no_exception_on_select(self): try: self.session.execute('SELECT * FROM test1rf.table_num_col') except ValueError as e: - self.fail("Unexpected ValueError exception: %s" % e.message) + pytest.fail("Unexpected ValueError exception: %s" % e.message) def test_can_select_using_alias(self): """ @@ -206,7 +207,7 @@ def test_can_select_using_alias(self): try: self.session.execute('SELECT key, "626972746864617465" AS my_col from test1rf.table_num_col') except ValueError as e: - self.fail("Unexpected ValueError exception: %s" % e.message) + pytest.fail("Unexpected ValueError exception: %s" % e.message) def test_can_select_with_dict_factory(self): """ @@ -218,4 +219,4 @@ def test_can_select_with_dict_factory(self): try: cluster.connect().execute('SELECT * FROM test1rf.table_num_col') except ValueError as e: - self.fail("Unexpected ValueError exception: %s" % e.message) + pytest.fail("Unexpected ValueError exception: %s" % e.message) diff --git a/tests/integration/standard/test_scylla_cloud.py b/tests/integration/standard/test_scylla_cloud.py deleted file mode 100644 index 5679d959bb..0000000000 --- a/tests/integration/standard/test_scylla_cloud.py +++ /dev/null @@ -1,129 +0,0 @@ -import json -import logging -import os.path -from unittest import TestCase -from ccmlib.utils.ssl_utils import generate_ssl_stores -from ccmlib.utils.sni_proxy import refresh_certs, start_sni_proxy, create_cloud_config, NodeInfo - -from cassandra import DependencyException -from cassandra.policies import TokenAwarePolicy, RoundRobinPolicy, ConstantReconnectionPolicy -from tests.integration import use_cluster, PROTOCOL_VERSION -from cassandra.cluster import Cluster, TwistedConnection - - -supported_connection_classes = [TwistedConnection] - -try: - from cassandra.io.libevreactor import LibevConnection - supported_connection_classes += [LibevConnection] -except DependencyException: - pass - - -try: - from cassandra.io.asyncorereactor import AsyncoreConnection - supported_connection_classes += [AsyncoreConnection] -except DependencyException: - pass - -#from cassandra.io.geventreactor import GeventConnection -#from cassandra.io.eventletreactor import EventletConnection -#from cassandra.io.asyncioreactor import AsyncioConnection - -# need to run them with specific configuration like `gevent.monkey.patch_all()` or under async functions -# unsupported_connection_classes = [GeventConnection, AsyncioConnection, EventletConnection] -LOGGER = logging.getLogger(__name__) - - -def get_cluster_info(cluster, port=9142): - session = Cluster( - contact_points=list(map(lambda node: node.address(), cluster.nodelist())), protocol_version=PROTOCOL_VERSION, - load_balancing_policy=TokenAwarePolicy(RoundRobinPolicy()), - reconnection_policy=ConstantReconnectionPolicy(5) - ).connect() - - nodes_info = [] - - for row in session.execute('select host_id, broadcast_address, data_center from system.local'): - if row[0] and row[1]: - nodes_info.append(NodeInfo(address=row[1], - port=port, - host_id=row[0], - data_center=row[2])) - - for row in session.execute('select host_id, broadcast_address, data_center from system.local'): - nodes_info.append(NodeInfo(address=row[1], - port=port, - host_id=row[0], - data_center=row[2])) - - return nodes_info - - -class ScyllaCloudConfigTests(TestCase): - def start_cluster_with_proxy(self): - ccm_cluster = self.ccm_cluster - generate_ssl_stores(ccm_cluster.get_path()) - ssl_port = 9142 - sni_port = 443 - ccm_cluster.set_configuration_options(dict( - client_encryption_options= - dict(require_client_auth=True, - truststore=os.path.join(ccm_cluster.get_path(), 'ccm_node.cer'), - certificate=os.path.join(ccm_cluster.get_path(), 'ccm_node.pem'), - keyfile=os.path.join(ccm_cluster.get_path(), 'ccm_node.key'), - enabled=True), - native_transport_port_ssl=ssl_port)) - - ccm_cluster._update_config() - - ccm_cluster.start(wait_for_binary_proto=True, wait_other_notice=True) - - nodes_info = get_cluster_info(ccm_cluster, port=ssl_port) - refresh_certs(ccm_cluster, nodes_info) - - docker_id, listen_address, listen_port = \ - start_sni_proxy(ccm_cluster.get_path(), nodes_info=nodes_info, listen_port=sni_port) - ccm_cluster.sni_proxy_docker_ids = [docker_id] - ccm_cluster.sni_proxy_listen_port = listen_port - ccm_cluster._update_config() - - config_data_yaml, config_path_yaml = create_cloud_config(ccm_cluster.get_path(), - port=listen_port, address=listen_address, - nodes_info=nodes_info) - return config_data_yaml, config_path_yaml - - def test_1_node_cluster(self): - self.ccm_cluster = use_cluster("sni_proxy", [1], start=False) - config_data_yaml, config_path_yaml = self.start_cluster_with_proxy() - - for config in [config_path_yaml, config_data_yaml]: - for connection_class in supported_connection_classes: - logging.warning('testing with class: %s', connection_class.__name__) - cluster = Cluster(scylla_cloud=config, connection_class=connection_class) - try: - with cluster.connect() as session: - res = session.execute("SELECT * FROM system.local WHERE key='local'") - assert res.all() - - assert len(cluster.metadata._hosts) == 1 - assert len(cluster.metadata._host_id_by_endpoint) == 1 - finally: - cluster.shutdown() - - def test_3_node_cluster(self): - self.ccm_cluster = use_cluster("sni_proxy", [3], start=False) - config_data_yaml, config_path_yaml = self.start_cluster_with_proxy() - - for config in [config_path_yaml, config_data_yaml]: - for connection_class in supported_connection_classes: - logging.warning('testing with class: %s', connection_class.__name__) - cluster = Cluster(scylla_cloud=config, connection_class=connection_class) - try: - with cluster.connect() as session: - res = session.execute("SELECT * FROM system.local WHERE key='local'") - assert res.all() - assert len(cluster.metadata._hosts) == 3 - assert len(cluster.metadata._host_id_by_endpoint) == 3 - finally: - cluster.shutdown() diff --git a/tests/integration/standard/test_shard_aware.py b/tests/integration/standard/test_shard_aware.py index cf8f17e209..4a6c7887d8 100644 --- a/tests/integration/standard/test_shard_aware.py +++ b/tests/integration/standard/test_shard_aware.py @@ -12,20 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. import os -import time import random from subprocess import run import logging -try: - from concurrent.futures import ThreadPoolExecutor, as_completed -except ImportError: - from futures import ThreadPoolExecutor, as_completed # noqa +from concurrent.futures import ThreadPoolExecutor, as_completed -try: - import unittest2 as unittest -except ImportError: - import unittest # noqa +import unittest import pytest from cassandra.cluster import Cluster @@ -33,13 +26,26 @@ from cassandra import OperationTimedOut, ConsistencyLevel from tests.integration import use_cluster, get_node, PROTOCOL_VERSION +from tests.util import wait_until_not_raised LOGGER = logging.getLogger(__name__) +_saved_scylla_ext_opts = None + + def setup_module(): + global _saved_scylla_ext_opts + _saved_scylla_ext_opts = os.environ.get('SCYLLA_EXT_OPTS') os.environ['SCYLLA_EXT_OPTS'] = "--smp 2" - use_cluster('shard_aware', [3], start=True) + use_cluster('cluster_tests', [3], start=True) + + +def teardown_module(): + if _saved_scylla_ext_opts is None: + os.environ.pop('SCYLLA_EXT_OPTS', None) + else: + os.environ['SCYLLA_EXT_OPTS'] = _saved_scylla_ext_opts class TestShardAwareIntegration(unittest.TestCase): @@ -62,8 +68,8 @@ def verify_same_shard_in_tracing(self, results, shard_name): for event in events: LOGGER.info("%s %s %s", event.source, event.thread_name, event.description) for event in events: - self.assertIn(shard_name, event.thread_name) - self.assertIn('querying locally', "\n".join([event.description for event in events])) + assert shard_name in event.thread_name + assert 'querying locally' in "\n".join([event.description for event in events]) trace_id = results.response_future.get_query_trace_ids()[0] traces = self.session.execute("SELECT * FROM system_traces.events WHERE session_id = %s", (trace_id,)) @@ -71,8 +77,8 @@ def verify_same_shard_in_tracing(self, results, shard_name): for event in events: LOGGER.info("%s %s", event.thread, event.activity) for event in events: - self.assertIn(shard_name, event.thread) - self.assertIn('querying locally', "\n".join([event.activity for event in events])) + assert shard_name in event.thread + assert 'querying locally' in "\n".join([event.activity for event in events]) def create_ks_and_cf(self): self.session.execute( @@ -83,7 +89,7 @@ def create_ks_and_cf(self): self.session.execute( """ CREATE KEYSPACE preparedtests - WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '3'} + WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '3'} AND tablets = {'enabled': false} """) self.session.execute("USE preparedtests") @@ -120,13 +126,13 @@ def query_data(self, session, verify_in_tracing=True): bound = prepared.bind(('a', 'b')) results = session.execute(bound, trace=True) - self.assertEqual(results, [('a', 'b', 'c')]) + assert results == [('a', 'b', 'c')] if verify_in_tracing: self.verify_same_shard_in_tracing(results, "shard 0") bound = prepared.bind(('100002', 'f')) results = session.execute(bound, trace=True) - self.assertEqual(results, [('100002', 'f', 'g')]) + assert results == [('100002', 'f', 'g')] if verify_in_tracing: self.verify_same_shard_in_tracing(results, "shard 1") @@ -137,12 +143,39 @@ def query_data(self, session, verify_in_tracing=True): if verify_in_tracing: self.verify_same_shard_in_tracing(results, "shard 0") + def _assert_blocked_node_disconnected(self, node_ip_address, node_port): + control_connection = self.cluster.control_connection + active_control_connection = control_connection._connection if control_connection else None + if active_control_connection and \ + active_control_connection.endpoint.address == node_ip_address and \ + active_control_connection.endpoint.port == node_port: + assert active_control_connection.is_closed or active_control_connection.is_defunct + + pools = getattr(self.session, '_pools', None) or {} + for host, pool in pools.items(): + if host.endpoint.address != node_ip_address or host.endpoint.port != node_port: + continue + + open_connections = [ + connection for connection in pool._connections.values() + if not (connection.is_closed or connection.is_defunct) + ] + assert not open_connections + + pending_connections = [ + connection for connection in pool._pending_connections + if not (connection.is_closed or connection.is_defunct) + ] + assert not pending_connections + def test_all_tracing_coming_one_shard(self): """ Testing that shard aware driver is sending the requests to the correct shards using the traces to validate that all the action been executed on the the same shard. this test is using prepared SELECT statements for this validation + + Requires tablets to be disabled to ensure shard consistency. """ self.create_ks_and_cf() @@ -184,11 +217,13 @@ def test_closing_connections(self): continue shard_id = random.choice(list(pool._connections.keys())) pool._connections.get(shard_id).close() - time.sleep(5) - self.query_data(self.session, verify_in_tracing=False) + wait_until_not_raised( + lambda: self.query_data(self.session, verify_in_tracing=False), + delay=0.5, max_attempts=30) - time.sleep(10) - self.query_data(self.session) + wait_until_not_raised( + lambda: self.query_data(self.session), + delay=0.5, max_attempts=60) @pytest.mark.skip def test_blocking_connections(self): @@ -218,13 +253,18 @@ def remove_iptables(): '--destination {node1_ip_address}/32 -j REJECT --reject-with icmp-port-unreachable' ).format(node1_ip_address=node1_ip_address, node1_port=node1_port).split(' ') ) - time.sleep(5) + + wait_until_not_raised( + lambda: self._assert_blocked_node_disconnected(node1_ip_address, node1_port), + delay=0.1, + max_attempts=50) try: self.query_data(self.session, verify_in_tracing=False) except OperationTimedOut: pass remove_iptables() - time.sleep(5) - self.query_data(self.session, verify_in_tracing=False) + wait_until_not_raised( + lambda: self.query_data(self.session, verify_in_tracing=False), + delay=0.5, max_attempts=30) self.query_data(self.session) diff --git a/tests/integration/standard/test_single_interface.py b/tests/integration/standard/test_single_interface.py index e836b5f428..5fd9ef45d3 100644 --- a/tests/integration/standard/test_single_interface.py +++ b/tests/integration/standard/test_single_interface.py @@ -13,18 +13,19 @@ # limitations under the License. import unittest +import pytest from cassandra import ConsistencyLevel from cassandra.query import SimpleStatement from packaging.version import Version from tests.integration import use_singledc, PROTOCOL_VERSION, \ - remove_cluster, greaterthanorequalcass40, notdse, \ - CASSANDRA_VERSION, DSE_VERSION, TestCluster, DEFAULT_SINGLE_INTERFACE_PORT + remove_cluster, greaterthanorequalcass40, \ + CASSANDRA_VERSION, TestCluster, DEFAULT_SINGLE_INTERFACE_PORT def setup_module(): - if not DSE_VERSION and CASSANDRA_VERSION >= Version('4-a'): + if CASSANDRA_VERSION >= Version('4-a'): remove_cluster() use_singledc(use_single_interface=True) @@ -32,7 +33,6 @@ def teardown_module(): remove_cluster() -@notdse @greaterthanorequalcass40 class SingleInterfaceTest(unittest.TestCase): @@ -44,8 +44,6 @@ def tearDown(self): if self.cluster is not None: self.cluster.shutdown() - # TODO: enable after https://github.com/scylladb/python-driver/issues/121 is fixed - @unittest.skip('Fails on scylla due to the broadcast_rpc_port is None') def test_single_interface(self): """ Test that we can connect to a multiple hosts bound to a single interface. @@ -53,17 +51,15 @@ def test_single_interface(self): hosts = self.cluster.metadata._hosts broadcast_rpc_ports = [] broadcast_ports = [] - self.assertEqual(len(hosts), 3) + assert len(hosts) == 3 for endpoint, host in hosts.items(): - self.assertEqual(endpoint.address, host.broadcast_rpc_address) - self.assertEqual(endpoint.port, host.broadcast_rpc_port) + assert endpoint.address == host.broadcast_rpc_address + assert endpoint.port == host.broadcast_rpc_port - if host.broadcast_rpc_port in broadcast_rpc_ports: - self.fail("Duplicate broadcast_rpc_port") + assert host.broadcast_rpc_port not in broadcast_rpc_ports, "Duplicate broadcast_rpc_port" broadcast_rpc_ports.append(host.broadcast_rpc_port) - if host.broadcast_port in broadcast_ports: - self.fail("Duplicate broadcast_port") + assert host.broadcast_port not in broadcast_ports, "Duplicate broadcast_port" broadcast_ports.append(host.broadcast_port) for _ in range(1, 100): @@ -71,4 +67,4 @@ def test_single_interface(self): consistency_level=ConsistencyLevel.ALL)) for pool in self.session.get_pools(): - self.assertEqual(1, pool.get_state()['open_count']) + assert 1 == pool.get_state()['open_count'] diff --git a/tests/integration/standard/test_tablets.py b/tests/integration/standard/test_tablets.py index 79dd166603..45e8a807ea 100644 --- a/tests/integration/standard/test_tablets.py +++ b/tests/integration/standard/test_tablets.py @@ -1,19 +1,15 @@ -import time - import pytest -from cassandra.cluster import Cluster +from cassandra.cluster import Cluster, EXEC_PROFILE_DEFAULT, ExecutionProfile from cassandra.policies import ConstantReconnectionPolicy, RoundRobinPolicy, TokenAwarePolicy -from tests.integration import PROTOCOL_VERSION, use_cluster +from tests.integration import PROTOCOL_VERSION, use_cluster, get_cluster +from tests.util import wait_until from tests.unit.test_host_connection_pool import LOGGER -CCM_CLUSTER = None def setup_module(): - global CCM_CLUSTER - - CCM_CLUSTER = use_cluster('tablets', [3], start=True) + use_cluster('tablets', [3], start=True, set_keyspace=False) class TestTabletsIntegration: @@ -31,7 +27,7 @@ def teardown_class(cls): cls.cluster.shutdown() def verify_hosts_in_tracing(self, results, expected): - traces = results.get_query_trace() + traces = results.get_query_trace(max_wait_sec=10) events = traces.events host_set = set() for event in events: @@ -57,7 +53,7 @@ def get_tablet_record(self, query): return metadata._tablets.get_tablet_for_key(query.keyspace, query.table, metadata.token_map.token_class.from_key(query.routing_key)) def verify_same_shard_in_tracing(self, results): - traces = results.get_query_trace() + traces = results.get_query_trace(max_wait_sec=10) events = traces.events shard_set = set() for event in events: @@ -166,6 +162,36 @@ def test_tablets_shard_awareness(self): self.query_data_shard_select(self.session) self.query_data_shard_insert(self.session) + def test_tablets_lbp_in_profile(self): + cluster = Cluster(contact_points=["127.0.0.1", "127.0.0.2", "127.0.0.3"], protocol_version=PROTOCOL_VERSION, + execution_profiles={ + EXEC_PROFILE_DEFAULT: ExecutionProfile( + load_balancing_policy=TokenAwarePolicy(RoundRobinPolicy()), + )}, + reconnection_policy=ConstantReconnectionPolicy(1)) + session = cluster.connect() + try: + self.query_data_host_select(self.session) + self.query_data_host_insert(self.session) + finally: + session.shutdown() + cluster.shutdown() + + def test_tablets_shard_awareness_lbp_in_profile(self): + cluster = Cluster(contact_points=["127.0.0.1", "127.0.0.2", "127.0.0.3"], protocol_version=PROTOCOL_VERSION, + execution_profiles={ + EXEC_PROFILE_DEFAULT: ExecutionProfile( + load_balancing_policy=TokenAwarePolicy(RoundRobinPolicy()), + )}, + reconnection_policy=ConstantReconnectionPolicy(1)) + session = cluster.connect() + try: + self.query_data_shard_select(self.session) + self.query_data_shard_insert(self.session) + finally: + session.shutdown() + cluster.shutdown() + def test_tablets_invalidation_drop_ks_while_reconnecting(self): def recreate_while_reconnecting(_): # Kill control connection @@ -185,7 +211,10 @@ def test_tablets_invalidation_drop_ks(self): def drop_ks(_): # Drop and recreate ks and table to trigger tablets invalidation self.create_ks_and_cf(self.cluster.connect()) - time.sleep(3) + # Wait for tablet metadata to be refreshed + wait_until( + lambda: 'test1' in self.cluster.metadata.keyspaces, + delay=0.5, max_attempts=20) self.run_tablets_invalidation_test(drop_ks) @@ -193,7 +222,7 @@ def drop_ks(_): def test_tablets_invalidation_decommission_non_cc_node(self): def decommission_non_cc_node(rec): # Drop and recreate ks and table to trigger tablets invalidation - for node in CCM_CLUSTER.nodes.values(): + for node in get_cluster().nodes.values(): if self.cluster.control_connection._connection.endpoint.address == node.network_interfaces["storage"][0]: # Ignore node that control connection is connected to continue @@ -206,7 +235,12 @@ def decommission_non_cc_node(rec): break else: assert False, "failed to find node to decommission" - time.sleep(10) + # Wait for decommission to complete and metadata to update + wait_until( + lambda: len([h for h in self.cluster.metadata.all_hosts() if h.is_up]) < 3, + delay=1, max_attempts=60) + # Tablet metadata invalidation may take additional time to propagate; + # run_tablets_invalidation_test will poll for the expected result. self.run_tablets_invalidation_test(decommission_non_cc_node) @@ -230,5 +264,7 @@ def run_tablets_invalidation_test(self, invalidate): invalidate(rec) - # Check if tablets information was purged - assert self.get_tablet_record(bound) is None, "tablet was not deleted, invalidation did not work" + # Wait for tablets information to be purged (invalidation is async) + wait_until( + lambda: self.get_tablet_record(bound) is None, + delay=0.5, max_attempts=20) diff --git a/tests/integration/standard/test_types.py b/tests/integration/standard/test_types.py index 3d0dc0ed7c..559a6b3da0 100644 --- a/tests/integration/standard/test_types.py +++ b/tests/integration/standard/test_types.py @@ -36,16 +36,19 @@ from cassandra.query import dict_factory, ordered_dict_factory from cassandra.util import sortedset, Duration, OrderedMap from tests.unit.cython.utils import cythontest +from tests.util import assertEqual -from tests.integration import use_singledc, execute_until_pass, notprotocolv1, \ - BasicSharedKeyspaceUnitTestCase, greaterthancass21, lessthancass30, greaterthanorequaldse51, \ - DSE_VERSION, greaterthanorequalcass3_10, requiredse, TestCluster, requires_composite_type, greaterthanorequalcass50 +from tests.integration import use_single_node, execute_until_pass, notprotocolv1, \ + BasicSharedKeyspaceUnitTestCase, greaterthancass21, lessthancass30, \ + greaterthanorequalcass3_10, TestCluster, requires_composite_type, \ + requires_vector_type from tests.integration.datatype_utils import update_datatypes, PRIMITIVE_DATATYPES, COLLECTION_TYPES, PRIMITIVE_DATATYPES_KEYS, \ get_sample, get_all_samples, get_collection_sample +import pytest def setup_module(): - use_singledc() + use_single_node() update_datatypes() @@ -72,7 +75,7 @@ def test_can_insert_blob_type_as_string(self): results = s.execute("SELECT * FROM blobstring").one() for expected, actual in zip(params, results): - self.assertEqual(expected, actual) + assert expected == actual def test_can_insert_blob_type_as_bytearray(self): """ @@ -87,7 +90,7 @@ def test_can_insert_blob_type_as_bytearray(self): results = s.execute("SELECT * FROM blobbytes").one() for expected, actual in zip(params, results): - self.assertEqual(expected, actual) + assert expected == actual @unittest.skipIf(not hasattr(cassandra, 'deserializers'), "Cython required for to test DesBytesTypeArray deserializer") def test_des_bytes_type_array(self): @@ -114,7 +117,7 @@ def test_des_bytes_type_array(self): results = s.execute("SELECT * FROM blobbytes2").one() for expected, actual in zip(params, results): - self.assertEqual(expected, actual) + assert expected == actual finally: if original is not None: cassandra.deserializers.DesBytesType=original @@ -149,7 +152,7 @@ def test_can_insert_primitive_datatypes(self): # verify data results = s.execute("SELECT {0} FROM alltypes WHERE zz=0".format(columns_string)).one() for expected, actual in zip(params, results): - self.assertEqual(actual, expected) + assert actual == expected # try the same thing sending one insert at the time s.execute("TRUNCATE alltypes;") @@ -169,7 +172,7 @@ def test_can_insert_primitive_datatypes(self): if isinstance(data_sample, ipaddress.IPv4Address) or isinstance(data_sample, ipaddress.IPv6Address): compare_value = str(data_sample) - self.assertEqual(result, compare_value) + assert result == compare_value # try the same thing with a prepared statement placeholders = ','.join(["?"] * len(col_names)) @@ -180,13 +183,13 @@ def test_can_insert_primitive_datatypes(self): # verify data results = s.execute("SELECT {0} FROM alltypes WHERE zz=0".format(columns_string)).one() for expected, actual in zip(params, results): - self.assertEqual(actual, expected) + assert actual == expected # verify data with prepared statement query select = s.prepare("SELECT {0} FROM alltypes WHERE zz=?".format(columns_string)) results = s.execute(select.bind([0])).one() for expected, actual in zip(params, results): - self.assertEqual(actual, expected) + assert actual == expected # verify data with with prepared statement, use dictionary with no explicit columns select = s.prepare("SELECT * FROM alltypes") @@ -194,7 +197,7 @@ def test_can_insert_primitive_datatypes(self): execution_profile=s.execution_profile_clone_update(EXEC_PROFILE_DEFAULT, row_factory=ordered_dict_factory)).one() for expected, actual in zip(params, results.values()): - self.assertEqual(actual, expected) + assert actual == expected c.shutdown() @@ -242,7 +245,7 @@ def test_can_insert_collection_datatypes(self): # verify data results = s.execute("SELECT {0} FROM allcoltypes WHERE zz=0".format(columns_string)).one() for expected, actual in zip(params, results): - self.assertEqual(actual, expected) + assert actual == expected # create the input for prepared statement params = [0] @@ -258,13 +261,13 @@ def test_can_insert_collection_datatypes(self): # verify data results = s.execute("SELECT {0} FROM allcoltypes WHERE zz=0".format(columns_string)).one() for expected, actual in zip(params, results): - self.assertEqual(actual, expected) + assert actual == expected # verify data with prepared statement query select = s.prepare("SELECT {0} FROM allcoltypes WHERE zz=?".format(columns_string)) results = s.execute(select.bind([0])).one() for expected, actual in zip(params, results): - self.assertEqual(actual, expected) + assert actual == expected # verify data with with prepared statement, use dictionary with no explicit columns select = s.prepare("SELECT * FROM allcoltypes") @@ -273,7 +276,7 @@ def test_can_insert_collection_datatypes(self): row_factory=ordered_dict_factory)).one() for expected, actual in zip(params, results.values()): - self.assertEqual(actual, expected) + assert actual == expected c.shutdown() @@ -307,12 +310,12 @@ def test_can_insert_empty_strings_and_nulls(self): columns_string = ','.join(col_names) s.execute("INSERT INTO all_empty (zz) VALUES (2)") results = s.execute("SELECT {0} FROM all_empty WHERE zz=2".format(columns_string)).one() - self.assertTrue(all(x is None for x in results)) + assert all(x is None for x in results) # verify all types initially null with prepared statement select = s.prepare("SELECT {0} FROM all_empty WHERE zz=?".format(columns_string)) results = s.execute(select.bind([2])).one() - self.assertTrue(all(x is None for x in results)) + assert all(x is None for x in results) # insert empty strings for string-like fields expected_values = dict((col, '') for col in string_columns) @@ -323,21 +326,21 @@ def test_can_insert_empty_strings_and_nulls(self): # verify string types empty with simple statement results = s.execute("SELECT {0} FROM all_empty WHERE zz=3".format(columns_string)).one() for expected, actual in zip(expected_values.values(), results): - self.assertEqual(actual, expected) + assert actual == expected # verify string types empty with prepared statement results = s.execute(s.prepare("SELECT {0} FROM all_empty WHERE zz=?".format(columns_string)), [3]).one() for expected, actual in zip(expected_values.values(), results): - self.assertEqual(actual, expected) + assert actual == expected # non-string types shouldn't accept empty strings for col in non_string_columns: query = "INSERT INTO all_empty (zz, {0}) VALUES (4, %s)".format(col) - with self.assertRaises(InvalidRequest): + with pytest.raises(InvalidRequest): s.execute(query, ['']) insert = s.prepare("INSERT INTO all_empty (zz, {0}) VALUES (4, ?)".format(col)) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): s.execute(insert, ['']) # verify that Nones can be inserted and overwrites existing data @@ -360,13 +363,13 @@ def test_can_insert_empty_strings_and_nulls(self): query = "SELECT {0} FROM all_empty WHERE zz=5".format(columns_string) results = s.execute(query).one() for col in results: - self.assertEqual(None, col) + assert None == col # check via prepared statement select = s.prepare("SELECT {0} FROM all_empty WHERE zz=?".format(columns_string)) results = s.execute(select.bind([5])).one() for col in results: - self.assertEqual(None, col) + assert None == col # do the same thing again, but use a prepared statement to insert the nulls s.execute(simple_insert, params) @@ -377,11 +380,11 @@ def test_can_insert_empty_strings_and_nulls(self): results = s.execute(query).one() for col in results: - self.assertEqual(None, col) + assert None == col results = s.execute(select.bind([5])).one() for col in results: - self.assertEqual(None, col) + assert None == col def test_can_insert_empty_values_for_int32(self): """ @@ -394,7 +397,7 @@ def test_can_insert_empty_values_for_int32(self): try: Int32Type.support_empty_values = True results = execute_until_pass(s, "SELECT b FROM empty_values WHERE a='a'").one() - self.assertIs(EMPTY, results.b) + assert EMPTY is results.b finally: Int32Type.support_empty_values = False @@ -403,14 +406,10 @@ def test_timezone_aware_datetimes_are_timestamps(self): Ensure timezone-aware datetimes are converted to timestamps correctly """ - try: - import pytz - except ImportError as exc: - raise unittest.SkipTest('pytz is not available: %r' % (exc,)) + from zoneinfo import ZoneInfo - dt = datetime(1997, 8, 29, 11, 14) - eastern_tz = pytz.timezone('US/Eastern') - eastern_tz.localize(dt) + eastern_tz = ZoneInfo('US/Eastern') + dt = datetime(1997, 8, 29, 11, 14, tzinfo=eastern_tz) s = self.session @@ -419,13 +418,13 @@ def test_timezone_aware_datetimes_are_timestamps(self): # test non-prepared statement s.execute("INSERT INTO tz_aware (a, b) VALUES ('key1', %s)", [dt]) result = s.execute("SELECT b FROM tz_aware WHERE a='key1'").one().b - self.assertEqual(dt.utctimetuple(), result.utctimetuple()) + assert dt.utctimetuple() == result.utctimetuple() # test prepared statement insert = s.prepare("INSERT INTO tz_aware (a, b) VALUES ('key2', ?)") s.execute(insert.bind([dt])) result = s.execute("SELECT b FROM tz_aware WHERE a='key2'").one().b - self.assertEqual(dt.utctimetuple(), result.utctimetuple()) + assert dt.utctimetuple() == result.utctimetuple() def test_can_insert_tuples(self): """ @@ -447,20 +446,20 @@ def test_can_insert_tuples(self): complete = ('foo', 123, True) s.execute("INSERT INTO tuple_type (a, b) VALUES (0, %s)", parameters=(complete,)) result = s.execute("SELECT b FROM tuple_type WHERE a=0").one() - self.assertEqual(complete, result.b) + assert complete == result.b partial = ('bar', 456) partial_result = partial + (None,) s.execute("INSERT INTO tuple_type (a, b) VALUES (1, %s)", parameters=(partial,)) result = s.execute("SELECT b FROM tuple_type WHERE a=1").one() - self.assertEqual(partial_result, result.b) + assert partial_result == result.b # test single value tuples subpartial = ('zoo',) subpartial_result = subpartial + (None, None) s.execute("INSERT INTO tuple_type (a, b) VALUES (2, %s)", parameters=(subpartial,)) result = s.execute("SELECT b FROM tuple_type WHERE a=2").one() - self.assertEqual(subpartial_result, result.b) + assert subpartial_result == result.b # test prepared statement prepared = s.prepare("INSERT INTO tuple_type (a, b) VALUES (?, ?)") @@ -469,12 +468,13 @@ def test_can_insert_tuples(self): s.execute(prepared, parameters=(5, subpartial)) # extra items in the tuple should result in an error - self.assertRaises(ValueError, s.execute, prepared, parameters=(0, (1, 2, 3, 4, 5, 6))) + with pytest.raises(ValueError): + s.execute(prepared, parameters=(0, (1, 2, 3, 4, 5, 6))) prepared = s.prepare("SELECT b FROM tuple_type WHERE a=?") - self.assertEqual(complete, s.execute(prepared, (3,)).one().b) - self.assertEqual(partial_result, s.execute(prepared, (4,)).one().b) - self.assertEqual(subpartial_result, s.execute(prepared, (5,)).one().b) + assert complete == s.execute(prepared, (3,)).one().b + assert partial_result == s.execute(prepared, (4,)).one().b + assert subpartial_result == s.execute(prepared, (5,)).one().b c.shutdown() @@ -507,7 +507,8 @@ def test_can_insert_tuples_with_varying_lengths(self): for i in lengths: # ensure tuples of larger sizes throw an error created_tuple = tuple(range(0, i + 1)) - self.assertRaises(InvalidRequest, s.execute, "INSERT INTO tuple_lengths (k, v_%s) VALUES (0, %s)", (i, created_tuple)) + with pytest.raises(InvalidRequest): + s.execute("INSERT INTO tuple_lengths (k, v_%s) VALUES (0, %s)", (i, created_tuple)) # ensure tuples of proper sizes are written and read correctly created_tuple = tuple(range(0, i)) @@ -515,7 +516,7 @@ def test_can_insert_tuples_with_varying_lengths(self): s.execute("INSERT INTO tuple_lengths (k, v_%s) VALUES (0, %s)", (i, created_tuple)) result = s.execute("SELECT v_%s FROM tuple_lengths WHERE k=0", (i,)).one() - self.assertEqual(tuple(created_tuple), result['v_%s' % i]) + assert tuple(created_tuple) == result['v_%s' % i] c.shutdown() def test_can_insert_tuples_all_primitive_datatypes(self): @@ -543,7 +544,7 @@ def test_can_insert_tuples_all_primitive_datatypes(self): expected = tuple(values + [None] * (type_count - len(values))) s.execute("INSERT INTO tuple_primitive (k, v) VALUES (%s, %s)", (i, tuple(values))) result = s.execute("SELECT v FROM tuple_primitive WHERE k=%s", (i,)).one() - self.assertEqual(result.v, expected) + assert result.v == expected c.shutdown() def test_can_insert_tuples_all_collection_datatypes(self): @@ -598,7 +599,7 @@ def test_can_insert_tuples_all_collection_datatypes(self): s.execute("INSERT INTO tuple_non_primative (k, v_%s) VALUES (0, %s)", (i, created_tuple)) result = s.execute("SELECT v_%s FROM tuple_non_primative WHERE k=0", (i,)).one() - self.assertEqual(created_tuple, result['v_%s' % i]) + assert created_tuple == result['v_%s' % i] i += 1 # test tuple> @@ -607,7 +608,7 @@ def test_can_insert_tuples_all_collection_datatypes(self): s.execute("INSERT INTO tuple_non_primative (k, v_%s) VALUES (0, %s)", (i, created_tuple)) result = s.execute("SELECT v_%s FROM tuple_non_primative WHERE k=0", (i,)).one() - self.assertEqual(created_tuple, result['v_%s' % i]) + assert created_tuple == result['v_%s' % i] i += 1 # test tuple> @@ -621,7 +622,7 @@ def test_can_insert_tuples_all_collection_datatypes(self): s.execute("INSERT INTO tuple_non_primative (k, v_%s) VALUES (0, %s)", (i, created_tuple)) result = s.execute("SELECT v_%s FROM tuple_non_primative WHERE k=0", (i,)).one() - self.assertEqual(created_tuple, result['v_%s' % i]) + assert created_tuple == result['v_%s' % i] i += 1 c.shutdown() @@ -682,7 +683,7 @@ def test_can_insert_nested_tuples(self): # verify tuple was written and read correctly result = s.execute("SELECT v_%s FROM nested_tuples WHERE k=%s", (i, i)).one() - self.assertEqual(created_tuple, result['v_%s' % i]) + assert created_tuple == result['v_%s' % i] c.shutdown() def test_can_insert_tuples_with_nulls(self): @@ -701,16 +702,16 @@ def test_can_insert_tuples_with_nulls(self): s.execute(insert, [(None, None, None, None)]) result = s.execute("SELECT * FROM tuples_nulls WHERE k=0") - self.assertEqual((None, None, None, None), result.one().t) + assert (None, None, None, None) == result.one().t read = s.prepare("SELECT * FROM tuples_nulls WHERE k=0") - self.assertEqual((None, None, None, None), s.execute(read).one().t) + assert (None, None, None, None) == s.execute(read).one().t # also test empty strings where compatible s.execute(insert, [('', None, None, b'')]) result = s.execute("SELECT * FROM tuples_nulls WHERE k=0") - self.assertEqual(('', None, None, b''), result.one().t) - self.assertEqual(('', None, None, b''), s.execute(read).one().t) + assert ('', None, None, b'') == result.one().t + assert ('', None, None, b'') == s.execute(read).one().t def test_insert_collection_with_null_fails(self): """ @@ -729,9 +730,11 @@ def test_insert_collection_with_null_fails(self): s.execute(f'CREATE TABLE collection_nulls (k int PRIMARY KEY, {", ".join(columns)})') def raises_simple_and_prepared(exc_type, query_str, args): - self.assertRaises(exc_type, lambda: s.execute(query_str, args)) + with pytest.raises(exc_type): + s.execute(query_str, args) p = s.prepare(query_str.replace('%s', '?')) - self.assertRaises(exc_type, lambda: s.execute(p, args)) + with pytest.raises(exc_type): + s.execute(p, args) i = 0 for simple_type in PRIMITIVE_DATATYPES_KEYS: @@ -781,14 +784,14 @@ def test_can_read_composite_type(self): # CompositeType string literals are split on ':' chars s.execute("INSERT INTO composites (a, b) VALUES (0, 'abc:123')") result = s.execute("SELECT * FROM composites WHERE a = 0").one() - self.assertEqual(0, result.a) - self.assertEqual(('abc', 123), result.b) + assert 0 == result.a + assert ('abc', 123) == result.b # CompositeType values can omit elements at the end s.execute("INSERT INTO composites (a, b) VALUES (0, 'abc')") result = s.execute("SELECT * FROM composites WHERE a = 0").one() - self.assertEqual(0, result.a) - self.assertEqual(('abc',), result.b) + assert 0 == result.a + assert ('abc',) == result.b @notprotocolv1 def test_special_float_cql_encoding(self): @@ -815,11 +818,11 @@ def verify_insert_select(ins_statement, sel_statement): for f in items: row = s.execute(sel_statement, (f,)).one() if math.isnan(f): - self.assertTrue(math.isnan(row.f)) - self.assertTrue(math.isnan(row.d)) + assert math.isnan(row.f) + assert math.isnan(row.d) else: - self.assertEqual(row.f, f) - self.assertEqual(row.d, f) + assert row.f == f + assert row.d == f # cql encoding verify_insert_select('INSERT INTO float_cql_encoding (f, d) VALUES (%s, %s)', @@ -847,7 +850,7 @@ def test_cython_decimal(self): try: self.session.execute("INSERT INTO {0} (dc) VALUES (-1.08430792318105707)".format(self.function_table_name)) results = self.session.execute("SELECT * FROM {0}".format(self.function_table_name)) - self.assertTrue(str(results.one().dc) == '-1.08430792318105707') + assert str(results.one().dc) == '-1.08430792318105707' finally: self.session.execute("DROP TABLE {0}".format(self.function_table_name)) @@ -891,382 +894,18 @@ def test_smoke_duration_values(self): results = self.session.execute("SELECT * FROM duration_smoke") v = results.one()[1] - self.assertEqual(Duration(month_day_value, month_day_value, nanosecond_value), v, - "Error encoding value {0},{0},{1}".format(month_day_value, nanosecond_value)) + assert Duration(month_day_value, month_day_value, nanosecond_value) == v, "Error encoding value {0},{0},{1}".format(month_day_value, nanosecond_value) - self.assertRaises(ValueError, self.session.execute, prepared, + with pytest.raises(ValueError): + self.session.execute(prepared, (1, Duration(0, 0, int("8FFFFFFFFFFFFFF0", 16)))) - self.assertRaises(ValueError, self.session.execute, prepared, + with pytest.raises(ValueError): + self.session.execute(prepared, (1, Duration(0, int("8FFFFFFFFFFFFFF0", 16), 0))) - self.assertRaises(ValueError, self.session.execute, prepared, + with pytest.raises(ValueError): + self.session.execute(prepared, (1, Duration(int("8FFFFFFFFFFFFFF0", 16), 0, 0))) - -@requiredse -class AbstractDateRangeTest(): - - def test_single_value_daterange_round_trip(self): - self._daterange_round_trip( - util.DateRange( - value=util.DateRangeBound( - datetime(2014, 10, 1, 0), - util.DateRangePrecision.YEAR - ) - ), - util.DateRange( - value=util.DateRangeBound( - datetime(2014, 1, 1, 0), - util.DateRangePrecision.YEAR - ) - ) - ) - - def test_open_high_daterange_round_trip(self): - self._daterange_round_trip( - util.DateRange( - lower_bound=util.DateRangeBound( - datetime(2013, 10, 1, 6, 20, 39), - util.DateRangePrecision.SECOND - ) - ) - ) - - def test_open_low_daterange_round_trip(self): - self._daterange_round_trip( - util.DateRange( - upper_bound=util.DateRangeBound( - datetime(2013, 10, 28), - util.DateRangePrecision.DAY - ) - ) - ) - - def test_open_both_daterange_round_trip(self): - self._daterange_round_trip( - util.DateRange( - lower_bound=util.OPEN_BOUND, - upper_bound=util.OPEN_BOUND, - ) - ) - - def test_closed_daterange_round_trip(self): - insert = util.DateRange( - lower_bound=util.DateRangeBound( - datetime(2015, 3, 1, 10, 15, 30, 1000), - util.DateRangePrecision.MILLISECOND - ), - upper_bound=util.DateRangeBound( - datetime(2016, 1, 1, 10, 15, 30, 999000), - util.DateRangePrecision.MILLISECOND - ) - ) - self._daterange_round_trip(insert) - - def test_epoch_value_round_trip(self): - insert = util.DateRange( - value=util.DateRangeBound( - datetime(1970, 1, 1), - util.DateRangePrecision.YEAR - ) - ) - self._daterange_round_trip(insert) - - def test_double_bounded_daterange_round_trip_from_string(self): - self._daterange_round_trip( - '[2015-03-01T10:15:30.010Z TO 2016-01-01T10:15:30.999Z]', - util.DateRange( - lower_bound=util.DateRangeBound( - datetime(2015, 3, 1, 10, 15, 30, 10000), - util.DateRangePrecision.MILLISECOND - ), - upper_bound=util.DateRangeBound( - datetime(2016, 1, 1, 10, 15, 30, 999000), - util.DateRangePrecision.MILLISECOND - ), - ) - ) - - def test_open_high_daterange_round_trip_from_string(self): - self._daterange_round_trip( - '[2015-03 TO *]', - util.DateRange( - lower_bound=util.DateRangeBound( - datetime(2015, 3, 1, 0, 0), - util.DateRangePrecision.MONTH - ), - upper_bound=util.DateRangeBound(None, None) - ) - ) - - def test_open_low_daterange_round_trip_from_string(self): - self._daterange_round_trip( - '[* TO 2015-03]', - util.DateRange( - lower_bound=util.DateRangeBound(None, None), - upper_bound=util.DateRangeBound( - datetime(2015, 3, 1, 0, 0), - 'MONTH' - ) - ) - ) - - def test_no_bounds_daterange_round_trip_from_string(self): - self._daterange_round_trip( - '[* TO *]', - util.DateRange( - lower_bound=(None, None), - upper_bound=(None, None) - ) - ) - - def test_single_no_bounds_daterange_round_trip_from_string(self): - self._daterange_round_trip( - '*', - util.DateRange( - value=(None, None) - ) - ) - - def test_single_value_daterange_round_trip_from_string(self): - self._daterange_round_trip( - '2001-01-01T12:30:30.000Z', - util.DateRange( - value=util.DateRangeBound( - datetime(2001, 1, 1, 12, 30, 30), - 'MILLISECOND' - ) - ) - ) - - def test_daterange_with_negative_bound_round_trip_from_string(self): - self._daterange_round_trip( - '[-1991-01-01T00:00:00.001 TO 1990-02-03]', - util.DateRange( - lower_bound=(-124997039999999, 'MILLISECOND'), - upper_bound=util.DateRangeBound( - datetime(1990, 2, 3, 12, 30, 30), - 'DAY' - ) - ) - ) - - def test_epoch_value_round_trip_from_string(self): - self._daterange_round_trip( - '1970', - util.DateRange( - value=util.DateRangeBound( - datetime(1970, 1, 1), - util.DateRangePrecision.YEAR - ) - ) - ) - - -@greaterthanorequaldse51 -class TestDateRangePrepared(AbstractDateRangeTest, BasicSharedKeyspaceUnitTestCase): - """ - Tests various inserts and queries using Date-ranges and prepared queries - - @since 2.0.0 - @jira_ticket PYTHON-668 - @expected_result Date ranges will be inserted and retrieved succesfully - - @test_category data_types - """ - - @classmethod - def setUpClass(cls): - super(TestDateRangePrepared, cls).setUpClass() - cls.session.set_keyspace(cls.ks_name) - if DSE_VERSION and DSE_VERSION >= Version('5.1'): - cls.session.execute("CREATE TABLE tab (dr 'DateRangeType' PRIMARY KEY)") - - - def _daterange_round_trip(self, to_insert, expected=None): - if isinstance(to_insert, util.DateRange): - prep = self.session.prepare("INSERT INTO tab (dr) VALUES (?);") - self.session.execute(prep, (to_insert,)) - prep_sel = self.session.prepare("SELECT * FROM tab WHERE dr = ? ") - results = self.session.execute(prep_sel, (to_insert,)) - else: - prep = self.session.prepare("INSERT INTO tab (dr) VALUES ('%s');" % (to_insert,)) - self.session.execute(prep) - prep_sel = self.session.prepare("SELECT * FROM tab WHERE dr = '%s' " % (to_insert,)) - results = self.session.execute(prep_sel) - - dr = results.one().dr - # sometimes this is truncated in the assertEqual output on failure; - if isinstance(expected, str): - self.assertEqual(str(dr), expected) - else: - self.assertEqual(dr, expected or to_insert) - - # This can only be run as a prepared statement - def test_daterange_wide(self): - self._daterange_round_trip( - util.DateRange( - lower_bound=(-9223372036854775808, 'MILLISECOND'), - upper_bound=(9223372036854775807, 'MILLISECOND') - ), - '[-9223372036854775808ms TO 9223372036854775807ms]' - ) - # This can only be run as a prepared statement - def test_daterange_with_negative_bound_round_trip_to_string(self): - self._daterange_round_trip( - util.DateRange( - lower_bound=(-124997039999999, 'MILLISECOND'), - upper_bound=util.DateRangeBound( - datetime(1990, 2, 3, 12, 30, 30), - 'DAY' - ) - ), - '[-124997039999999ms TO 1990-02-03]' - ) - -@greaterthanorequaldse51 -class TestDateRangeSimple(AbstractDateRangeTest, BasicSharedKeyspaceUnitTestCase): - """ - Tests various inserts and queries using Date-ranges and simple queries - - @since 2.0.0 - @jira_ticket PYTHON-668 - @expected_result DateRanges will be inserted and retrieved successfully - @test_category data_types - """ - @classmethod - def setUpClass(cls): - super(TestDateRangeSimple, cls).setUpClass() - cls.session.set_keyspace(cls.ks_name) - if DSE_VERSION and DSE_VERSION >= Version('5.1'): - cls.session.execute("CREATE TABLE tab (dr 'DateRangeType' PRIMARY KEY)") - - - def _daterange_round_trip(self, to_insert, expected=None): - - query = "INSERT INTO tab (dr) VALUES ('{0}');".format(to_insert) - self.session.execute("INSERT INTO tab (dr) VALUES ('{0}');".format(to_insert)) - query = "SELECT * FROM tab WHERE dr = '{0}' ".format(to_insert) - results= self.session.execute("SELECT * FROM tab WHERE dr = '{0}' ".format(to_insert)) - - dr = results.one().dr - # sometimes this is truncated in the assertEqual output on failure; - if isinstance(expected, str): - self.assertEqual(str(dr), expected) - else: - self.assertEqual(dr, expected or to_insert) - - -@greaterthanorequaldse51 -class TestDateRangeCollection(BasicSharedKeyspaceUnitTestCase): - - - @classmethod - def setUpClass(cls): - super(TestDateRangeCollection, cls).setUpClass() - cls.session.set_keyspace(cls.ks_name) - - def test_date_range_collection(self): - """ - Tests DateRange type in collections - - @since 2.0.0 - @jira_ticket PYTHON-668 - @expected_result DateRanges will be inserted and retrieved successfully when part of a list or map - @test_category data_types - """ - self.session.execute("CREATE TABLE dateRangeIntegrationTest5 (k int PRIMARY KEY, l list<'DateRangeType'>, s set<'DateRangeType'>, dr2i map<'DateRangeType', int>, i2dr map)") - self.session.execute("INSERT INTO dateRangeIntegrationTest5 (k, l, s, i2dr, dr2i) VALUES (" + - "1, " + - "['[2000-01-01T10:15:30.001Z TO 2020]', '[2010-01-01T10:15:30.001Z TO 2020]', '2001-01-02'], " + - "{'[2000-01-01T10:15:30.001Z TO 2020]', '[2000-01-01T10:15:30.001Z TO 2020]', '[2010-01-01T10:15:30.001Z TO 2020]'}, " + - "{1: '[2000-01-01T10:15:30.001Z TO 2020]', 2: '[2010-01-01T10:15:30.001Z TO 2020]'}, " + - "{'[2000-01-01T10:15:30.001Z TO 2020]': 1, '[2010-01-01T10:15:30.001Z TO 2020]': 2})") - results = self.session.execute("SELECT * FROM dateRangeIntegrationTest5").all() - self.assertEqual(len(results),1) - - lower_bound_1 = util.DateRangeBound(datetime(2000, 1, 1, 10, 15, 30, 1000), 'MILLISECOND') - - lower_bound_2 = util.DateRangeBound(datetime(2010, 1, 1, 10, 15, 30, 1000), 'MILLISECOND') - - upper_bound_1 = util.DateRangeBound(datetime(2020, 1, 1), 'YEAR') - - value_1 = util.DateRangeBound(datetime(2001, 1, 2), 'DAY') - - dt = util.DateRange(lower_bound=lower_bound_1, upper_bound=upper_bound_1) - dt2 = util.DateRange(lower_bound=lower_bound_2, upper_bound=upper_bound_1) - dt3 = util.DateRange(value=value_1) - - - - list_result = results[0].l - self.assertEqual(3, len(list_result)) - self.assertEqual(list_result[0],dt) - self.assertEqual(list_result[1],dt2) - self.assertEqual(list_result[2],dt3) - - set_result = results[0].s - self.assertEqual(len(set_result), 2) - self.assertIn(dt, set_result) - self.assertIn(dt2, set_result) - - d2i = results[0].dr2i - self.assertEqual(len(d2i), 2) - self.assertEqual(d2i[dt],1) - self.assertEqual(d2i[dt2],2) - - i2r = results[0].i2dr - self.assertEqual(len(i2r), 2) - self.assertEqual(i2r[1],dt) - self.assertEqual(i2r[2],dt2) - - def test_allow_date_range_in_udt_tuple(self): - """ - Tests DateRanges in tuples and udts - - @since 2.0.0 - @jira_ticket PYTHON-668 - @expected_result DateRanges will be inserted and retrieved successfully in udt's and tuples - @test_category data_types - """ - self.session.execute("CREATE TYPE IF NOT EXISTS test_udt (i int, range 'DateRangeType')") - self.session.execute("CREATE TABLE dateRangeIntegrationTest4 (k int PRIMARY KEY, u test_udt, uf frozen, t tuple<'DateRangeType', int>, tf frozen>)") - self.session.execute("INSERT INTO dateRangeIntegrationTest4 (k, u, uf, t, tf) VALUES (" + - "1, " + - "{i: 10, range: '[2000-01-01T10:15:30.003Z TO 2020-01-01T10:15:30.001Z]'}, " + - "{i: 20, range: '[2000-01-01T10:15:30.003Z TO 2020-01-01T10:15:30.001Z]'}, " + - "('[2000-01-01T10:15:30.003Z TO 2020-01-01T10:15:30.001Z]', 30), " + - "('[2000-01-01T10:15:30.003Z TO 2020-01-01T10:15:30.001Z]', 40))") - - lower_bound = util.DateRangeBound( - datetime(2000, 1, 1, 10, 15, 30, 3000), - 'MILLISECOND') - - upper_bound = util.DateRangeBound( - datetime(2020, 1, 1, 10, 15, 30, 1000), - 'MILLISECOND') - - expected_dt = util.DateRange(lower_bound=lower_bound ,upper_bound=upper_bound) - - results_list = list(self.session.execute("SELECT * FROM dateRangeIntegrationTest4")) - self.assertEqual(len(results_list), 1) - udt = results_list[0].u - self.assertEqual(udt.range, expected_dt) - self.assertEqual(udt.i, 10) - - - uf = results_list[0].uf - self.assertEqual(uf.range, expected_dt) - self.assertEqual(uf.i, 20) - - t = results_list[0].t - self.assertEqual(t[0], expected_dt) - self.assertEqual(t[1], 30) - - tf = results_list[0].tf - self.assertEqual(tf[0], expected_dt) - self.assertEqual(tf[1], 40) - - class TypeTestsProtocol(BasicSharedKeyspaceUnitTestCase): @greaterthancass21 @@ -1314,16 +953,16 @@ def read_inserts_at_level(self, proto_ver): session = TestCluster(protocol_version=proto_ver).connect(self.keyspace_name) try: results = session.execute('select * from t').one() - self.assertEqual("[SortedSet([1, 2]), SortedSet([3, 5])]", str(results.v)) + assert "[SortedSet([1, 2]), SortedSet([3, 5])]" == str(results.v) results = session.execute('select * from u').one() - self.assertEqual("SortedSet([[1, 2], [3, 5]])", str(results.v)) + assert "SortedSet([[1, 2], [3, 5]])" == str(results.v) results = session.execute('select * from v').one() - self.assertEqual("{SortedSet([1, 2]): [1, 2, 3], SortedSet([3, 5]): [4, 5, 6]}", str(results.v)) + assert "{SortedSet([1, 2]): [1, 2, 3], SortedSet([3, 5]): [4, 5, 6]}" == str(results.v) results = session.execute('select * from w').one() - self.assertEqual("typ(v0=OrderedMapSerializedKey([(1, [1, 2, 3]), (2, [4, 5, 6])]), v1=[7, 8, 9])", str(results.v)) + assert "typ(v0=OrderedMapSerializedKey([(1, [1, 2, 3]), (2, [4, 5, 6])]), v1=[7, 8, 9])" == str(results.v) finally: session.cluster.shutdown() @@ -1346,12 +985,12 @@ def run_inserts_at_version(self, proto_ver): finally: session.cluster.shutdown() -@greaterthanorequalcass50 +@requires_vector_type class TypeTestsVector(BasicSharedKeyspaceUnitTestCase): def _get_first_j(self, rs): rows = rs.all() - self.assertEqual(len(rows), 1) + assert len(rows) == 1 return rows[0].j def _get_row_simple(self, idx, table_name): @@ -1403,14 +1042,14 @@ def random_subtype_vector(): test_fn(observed2[idx], expected2[idx]) def test_round_trip_integers(self): - self._round_trip_test("int", partial(random.randint, 0, 2 ** 31), self.assertEqual) - self._round_trip_test("bigint", partial(random.randint, 0, 2 ** 63), self.assertEqual) - self._round_trip_test("smallint", partial(random.randint, 0, 2 ** 15), self.assertEqual) - self._round_trip_test("tinyint", partial(random.randint, 0, (2 ** 7) - 1), self.assertEqual) - self._round_trip_test("varint", partial(random.randint, 0, 2 ** 63), self.assertEqual) + self._round_trip_test("int", partial(random.randint, 0, 2 ** 31), assertEqual) + self._round_trip_test("bigint", partial(random.randint, 0, 2 ** 63), assertEqual) + self._round_trip_test("smallint", partial(random.randint, 0, 2 ** 15), assertEqual) + self._round_trip_test("tinyint", partial(random.randint, 0, (2 ** 7) - 1), assertEqual) + self._round_trip_test("varint", partial(random.randint, 0, 2 ** 63), assertEqual) def test_round_trip_floating_point(self): - _almost_equal_test_fn = partial(self.assertAlmostEqual, places=5) + _almost_equal_test_fn = partial(pytest.approx, abs=1e-5) def _random_decimal(): return Decimal(random.uniform(0.0, 100.0)) @@ -1424,11 +1063,11 @@ def test_round_trip_text(self): def _random_string(): return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(24)) - self._round_trip_test("ascii", _random_string, self.assertEqual) - self._round_trip_test("text", _random_string, self.assertEqual) + self._round_trip_test("ascii", _random_string, assertEqual) + self._round_trip_test("text", _random_string, assertEqual) def test_round_trip_date_and_time(self): - _almost_equal_test_fn = partial(self.assertAlmostEqual, delta=timedelta(seconds=1)) + _almost_equal_test_fn = partial(pytest.approx, abs=timedelta(seconds=1)) def _random_datetime(): return datetime.today() - timedelta(hours=random.randint(0,18), days=random.randint(1,1000)) def _random_date(): @@ -1436,13 +1075,13 @@ def _random_date(): def _random_time(): return _random_datetime().time() - self._round_trip_test("date", _random_date, self.assertEqual) - self._round_trip_test("time", _random_time, self.assertEqual) + self._round_trip_test("date", _random_date, assertEqual) + self._round_trip_test("time", _random_time, assertEqual) self._round_trip_test("timestamp", _random_datetime, _almost_equal_test_fn) def test_round_trip_uuid(self): - self._round_trip_test("uuid", uuid.uuid1, self.assertEqual) - self._round_trip_test("timeuuid", uuid.uuid1, self.assertEqual) + self._round_trip_test("uuid", uuid.uuid1, assertEqual) + self._round_trip_test("timeuuid", uuid.uuid1, assertEqual) def test_round_trip_miscellany(self): def _random_bytes(): @@ -1454,10 +1093,10 @@ def _random_duration(): def _random_inet(): return socket.inet_ntoa(_random_bytes()) - self._round_trip_test("boolean", _random_boolean, self.assertEqual) - self._round_trip_test("duration", _random_duration, self.assertEqual) - self._round_trip_test("inet", _random_inet, self.assertEqual) - self._round_trip_test("blob", _random_bytes, self.assertEqual) + self._round_trip_test("boolean", _random_boolean, assertEqual) + self._round_trip_test("duration", _random_duration, assertEqual) + self._round_trip_test("inet", _random_inet, assertEqual) + self._round_trip_test("blob", _random_bytes, assertEqual) def test_round_trip_collections(self): def _random_seq(): @@ -1468,21 +1107,21 @@ def _random_map(): return {k:v for (k,v) in zip(_random_seq(), _random_seq())} # Goal here is to test collections of both fixed and variable size subtypes - self._round_trip_test("list", _random_seq, self.assertEqual) - self._round_trip_test("list", _random_seq, self.assertEqual) - self._round_trip_test("set", _random_set, self.assertEqual) - self._round_trip_test("set", _random_set, self.assertEqual) - self._round_trip_test("map", _random_map, self.assertEqual) - self._round_trip_test("map", _random_map, self.assertEqual) - self._round_trip_test("map", _random_map, self.assertEqual) - self._round_trip_test("map", _random_map, self.assertEqual) + self._round_trip_test("list", _random_seq, assertEqual) + self._round_trip_test("list", _random_seq, assertEqual) + self._round_trip_test("set", _random_set, assertEqual) + self._round_trip_test("set", _random_set, assertEqual) + self._round_trip_test("map", _random_map, assertEqual) + self._round_trip_test("map", _random_map, assertEqual) + self._round_trip_test("map", _random_map, assertEqual) + self._round_trip_test("map", _random_map, assertEqual) def test_round_trip_vector_of_vectors(self): def _random_vector(): return [random.randint(0,100000) for _ in range(2)] - self._round_trip_test("vector", _random_vector, self.assertEqual) - self._round_trip_test("vector", _random_vector, self.assertEqual) + self._round_trip_test("vector", _random_vector, assertEqual) + self._round_trip_test("vector", _random_vector, assertEqual) def test_round_trip_tuples(self): def _random_tuple(): @@ -1490,15 +1129,15 @@ def _random_tuple(): # Unfortunately we can't use positional parameters when inserting tuples because the driver will try to encode # them as lists before sending them to the server... and that confuses the parsing logic. - self._round_trip_test("tuple", _random_tuple, self.assertEqual, use_positional_parameters=False) - self._round_trip_test("tuple", _random_tuple, self.assertEqual, use_positional_parameters=False) - self._round_trip_test("tuple", _random_tuple, self.assertEqual, use_positional_parameters=False) - self._round_trip_test("tuple", _random_tuple, self.assertEqual, use_positional_parameters=False) + self._round_trip_test("tuple", _random_tuple, assertEqual, use_positional_parameters=False) + self._round_trip_test("tuple", _random_tuple, assertEqual, use_positional_parameters=False) + self._round_trip_test("tuple", _random_tuple, assertEqual, use_positional_parameters=False) + self._round_trip_test("tuple", _random_tuple, assertEqual, use_positional_parameters=False) def test_round_trip_udts(self): def _udt_equal_test_fn(udt1, udt2): - self.assertEqual(udt1.a, udt2.a) - self.assertEqual(udt1.b, udt2.b) + assert udt1.a == udt2.a + assert udt1.b == udt2.b self.session.execute("create type {}.fixed_type (a int, b int)".format(self.keyspace_name)) self.session.execute("create type {}.mixed_type_one (a int, b varint)".format(self.keyspace_name)) diff --git a/tests/integration/standard/test_udts.py b/tests/integration/standard/test_udts.py index 7188bf3eb8..11888adda4 100644 --- a/tests/integration/standard/test_udts.py +++ b/tests/integration/standard/test_udts.py @@ -21,17 +21,18 @@ from cassandra.query import dict_factory from cassandra.util import OrderedMap -from tests.integration import use_singledc, execute_until_pass, \ +from tests.integration import use_single_node, execute_until_pass, \ BasicSegregatedKeyspaceUnitTestCase, greaterthancass20, lessthancass30, greaterthanorequalcass36, TestCluster from tests.integration.datatype_utils import update_datatypes, PRIMITIVE_DATATYPES, PRIMITIVE_DATATYPES_KEYS, \ COLLECTION_TYPES, get_sample, get_collection_sample +import pytest nested_collection_udt = namedtuple('nested_collection_udt', ['m', 't', 'l', 's']) nested_collection_udt_nested = namedtuple('nested_collection_udt_nested', ['m', 't', 'l', 's', 'u']) def setup_module(): - use_singledc() + use_single_node() update_datatypes() @@ -65,9 +66,9 @@ def test_non_frozen_udts(self): self.session.execute("INSERT INTO {0} (a, b) VALUES (%s, %s)".format(self.function_table_name), (0, User("Nebraska", True))) self.session.execute("UPDATE {0} SET b.has_corn = False where a = 0".format(self.function_table_name)) result = self.session.execute("SELECT * FROM {0}".format(self.function_table_name)) - self.assertFalse(result.one().b.has_corn) + assert not result.one().b.has_corn table_sql = self.cluster.metadata.keyspaces[self.keyspace_name].tables[self.function_table_name].as_cql_query() - self.assertNotIn("", table_sql) + assert "" not in table_sql def test_can_insert_unprepared_registered_udts(self): """ @@ -86,14 +87,14 @@ def test_can_insert_unprepared_registered_udts(self): s.execute("INSERT INTO mytable (a, b) VALUES (%s, %s)", (0, User(42, 'bob'))) result = s.execute("SELECT b FROM mytable WHERE a=0") row = result.one() - self.assertEqual(42, row.b.age) - self.assertEqual('bob', row.b.name) - self.assertTrue(type(row.b) is User) + assert 42 == row.b.age + assert 'bob' == row.b.name + assert type(row.b) is User # use the same UDT name in a different keyspace s.execute(""" CREATE KEYSPACE udt_test_unprepared_registered2 - WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1' } + WITH replication = { 'class' : 'NetworkTopologyStrategy', 'replication_factor': '1' } """) s.set_keyspace("udt_test_unprepared_registered2") s.execute("CREATE TYPE user (state text, is_cool boolean)") @@ -105,9 +106,9 @@ def test_can_insert_unprepared_registered_udts(self): s.execute("INSERT INTO mytable (a, b) VALUES (%s, %s)", (0, User('Texas', True))) result = s.execute("SELECT b FROM mytable WHERE a=0") row = result.one() - self.assertEqual('Texas', row.b.state) - self.assertEqual(True, row.b.is_cool) - self.assertTrue(type(row.b) is User) + assert 'Texas' == row.b.state + assert True == row.b.is_cool + assert type(row.b) is User s.execute("DROP KEYSPACE udt_test_unprepared_registered2") @@ -123,14 +124,14 @@ def test_can_register_udt_before_connecting(self): s.execute(""" CREATE KEYSPACE udt_test_register_before_connecting - WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1' } + WITH replication = { 'class' : 'NetworkTopologyStrategy', 'replication_factor': '1' } """) s.execute("CREATE TYPE udt_test_register_before_connecting.user (age int, name text)") s.execute("CREATE TABLE udt_test_register_before_connecting.mytable (a int PRIMARY KEY, b frozen)") s.execute(""" CREATE KEYSPACE udt_test_register_before_connecting2 - WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1' } + WITH replication = { 'class' : 'NetworkTopologyStrategy', 'replication_factor': '1' } """) s.execute("CREATE TYPE udt_test_register_before_connecting2.user (state text, is_cool boolean)") s.execute("CREATE TABLE udt_test_register_before_connecting2.mytable (a int PRIMARY KEY, b frozen)") @@ -146,22 +147,22 @@ def test_can_register_udt_before_connecting(self): c.register_user_type("udt_test_register_before_connecting2", "user", User2) s = c.connect(wait_for_all_pools=True) - c.control_connection.wait_for_schema_agreement() + s.wait_for_schema_agreement() s.execute("INSERT INTO udt_test_register_before_connecting.mytable (a, b) VALUES (%s, %s)", (0, User1(42, 'bob'))) result = s.execute("SELECT b FROM udt_test_register_before_connecting.mytable WHERE a=0") row = result.one() - self.assertEqual(42, row.b.age) - self.assertEqual('bob', row.b.name) - self.assertTrue(type(row.b) is User1) + assert 42 == row.b.age + assert 'bob' == row.b.name + assert type(row.b) is User1 # use the same UDT name in a different keyspace s.execute("INSERT INTO udt_test_register_before_connecting2.mytable (a, b) VALUES (%s, %s)", (0, User2('Texas', True))) result = s.execute("SELECT b FROM udt_test_register_before_connecting2.mytable WHERE a=0") row = result.one() - self.assertEqual('Texas', row.b.state) - self.assertEqual(True, row.b.is_cool) - self.assertTrue(type(row.b) is User2) + assert 'Texas' == row.b.state + assert True == row.b.is_cool + assert type(row.b) is User2 s.execute("DROP KEYSPACE udt_test_register_before_connecting") s.execute("DROP KEYSPACE udt_test_register_before_connecting2") @@ -186,13 +187,13 @@ def test_can_insert_prepared_unregistered_udts(self): select = s.prepare("SELECT b FROM mytable WHERE a=?") result = s.execute(select, (0,)) row = result.one() - self.assertEqual(42, row.b.age) - self.assertEqual('bob', row.b.name) + assert 42 == row.b.age + assert 'bob' == row.b.name # use the same UDT name in a different keyspace s.execute(""" CREATE KEYSPACE udt_test_prepared_unregistered2 - WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1' } + WITH replication = { 'class' : 'NetworkTopologyStrategy', 'replication_factor': '1' } """) s.set_keyspace("udt_test_prepared_unregistered2") s.execute("CREATE TYPE user (state text, is_cool boolean)") @@ -205,8 +206,8 @@ def test_can_insert_prepared_unregistered_udts(self): select = s.prepare("SELECT b FROM mytable WHERE a=?") result = s.execute(select, (0,)) row = result.one() - self.assertEqual('Texas', row.b.state) - self.assertEqual(True, row.b.is_cool) + assert 'Texas' == row.b.state + assert True == row.b.is_cool s.execute("DROP KEYSPACE udt_test_prepared_unregistered2") @@ -232,14 +233,14 @@ def test_can_insert_prepared_registered_udts(self): select = s.prepare("SELECT b FROM mytable WHERE a=?") result = s.execute(select, (0,)) row = result.one() - self.assertEqual(42, row.b.age) - self.assertEqual('bob', row.b.name) - self.assertTrue(type(row.b) is User) + assert 42 == row.b.age + assert 'bob' == row.b.name + assert type(row.b) is User # use the same UDT name in a different keyspace s.execute(""" CREATE KEYSPACE udt_test_prepared_registered2 - WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor': '1' } + WITH replication = { 'class' : 'NetworkTopologyStrategy', 'replication_factor': '1' } """) s.set_keyspace("udt_test_prepared_registered2") s.execute("CREATE TYPE user (state text, is_cool boolean)") @@ -254,9 +255,9 @@ def test_can_insert_prepared_registered_udts(self): select = s.prepare("SELECT b FROM mytable WHERE a=?") result = s.execute(select, (0,)) row = result.one() - self.assertEqual('Texas', row.b.state) - self.assertEqual(True, row.b.is_cool) - self.assertTrue(type(row.b) is User) + assert 'Texas' == row.b.state + assert True == row.b.is_cool + assert type(row.b) is User s.execute("DROP KEYSPACE udt_test_prepared_registered2") @@ -280,15 +281,15 @@ def test_can_insert_udts_with_nulls(self): s.execute(insert, [User(None, None, None, None)]) results = s.execute("SELECT b FROM mytable WHERE a=0") - self.assertEqual((None, None, None, None), results.one().b) + assert (None, None, None, None) == results.one().b select = s.prepare("SELECT b FROM mytable WHERE a=0") - self.assertEqual((None, None, None, None), s.execute(select).one().b) + assert (None, None, None, None) == s.execute(select).one().b # also test empty strings s.execute(insert, [User('', None, None, bytes())]) results = s.execute("SELECT b FROM mytable WHERE a=0") - self.assertEqual(('', None, None, bytes()), results.one().b) + assert ('', None, None, bytes()) == results.one().b c.shutdown() @@ -328,7 +329,7 @@ def test_can_insert_udts_with_varying_lengths(self): # verify udt was written and read correctly, increase timeout to avoid the query failure on slow systems result = s.execute("SELECT v FROM mytable WHERE k=0").one() - self.assertEqual(created_udt, result.v) + assert created_udt == result.v c.shutdown() @@ -366,7 +367,7 @@ def nested_udt_verification_helper(self, session, max_nesting_depth, udts): # verify udt was written and read correctly result = session.execute("SELECT v_{0} FROM mytable WHERE k=0".format(i)).one() - self.assertEqual(udt, result["v_{0}".format(i)]) + assert udt == result["v_{0}".format(i)] # write udt via prepared statement insert = session.prepare("INSERT INTO mytable (k, v_{0}) VALUES (1, ?)".format(i)) @@ -374,7 +375,7 @@ def nested_udt_verification_helper(self, session, max_nesting_depth, udts): # verify udt was written and read correctly result = session.execute("SELECT v_{0} FROM mytable WHERE k=1".format(i)).one() - self.assertEqual(udt, result["v_{0}".format(i)]) + assert udt == result["v_{0}".format(i)] def _cluster_default_dict_factory(self): return TestCluster( @@ -442,7 +443,7 @@ def test_can_insert_nested_unregistered_udts(self): # verify udt was written and read correctly result = s.execute("SELECT v_{0} FROM mytable WHERE k=0".format(i)).one() - self.assertEqual(udt, result["v_{0}".format(i)]) + assert udt == result["v_{0}".format(i)] def test_can_insert_nested_registered_udts_with_different_namedtuples(self): """ @@ -482,13 +483,13 @@ def test_raise_error_on_nonexisting_udts(self): s = c.connect(self.keyspace_name, wait_for_all_pools=True) User = namedtuple('user', ('age', 'name')) - with self.assertRaises(UserTypeDoesNotExist): + with pytest.raises(UserTypeDoesNotExist): c.register_user_type("some_bad_keyspace", "user", User) - with self.assertRaises(UserTypeDoesNotExist): + with pytest.raises(UserTypeDoesNotExist): c.register_user_type("system", "user", User) - with self.assertRaises(InvalidRequest): + with pytest.raises(InvalidRequest): s.execute("CREATE TABLE mytable (a int PRIMARY KEY, b frozen)") c.shutdown() @@ -534,7 +535,7 @@ def test_can_insert_udt_all_datatypes(self): row = results.one().b for expected, actual in zip(params, row): - self.assertEqual(expected, actual) + assert expected == actual c.shutdown() @@ -592,7 +593,7 @@ def test_can_insert_udt_all_collection_datatypes(self): row = results.one().b for expected, actual in zip(params, row): - self.assertEqual(expected, actual) + assert expected == actual c.shutdown() @@ -600,7 +601,7 @@ def insert_select_column(self, session, table_name, column_name, value): insert = session.prepare("INSERT INTO %s (k, %s) VALUES (?, ?)" % (table_name, column_name)) session.execute(insert, (0, value)) result = session.execute("SELECT %s FROM %s WHERE k=%%s" % (column_name, table_name), (0,)).one()[0] - self.assertEqual(result, value) + assert result == value def test_can_insert_nested_collections(self): """ @@ -669,15 +670,15 @@ def test_non_alphanum_identifiers(self): row = s.execute('SELECT * FROM %s' % (self.table_name,)).one() k, v = row.non_alphanum_type_map.popitem() - self.assertEqual(v, 0) - self.assertEqual(k.__class__, tuple) - self.assertEqual(k[0], 'nonalphanum') + assert v == 0 + assert k.__class__ == tuple + assert k[0] == 'nonalphanum' k, v = row.alphanum_type_map.popitem() - self.assertEqual(v, 1) - self.assertNotEqual(k.__class__, tuple) # should be the namedtuple type - self.assertEqual(k[0], 'alphanum') - self.assertEqual(k.field_0_, 'alphanum') # named tuple with positional field name + assert v == 1 + assert k.__class__ != tuple # should be the namedtuple type + assert k[0] == 'alphanum' + assert k.field_0_ == 'alphanum' # named tuple with positional field name @lessthancass30 def test_type_alteration(self): @@ -686,9 +687,9 @@ def test_type_alteration(self): """ s = self.session type_name = "type_name" - self.assertNotIn(type_name, s.cluster.metadata.keyspaces['udttests'].user_types) + assert type_name not in s.cluster.metadata.keyspaces['udttests'].user_types s.execute('CREATE TYPE %s (v0 int)' % (type_name,)) - self.assertIn(type_name, s.cluster.metadata.keyspaces['udttests'].user_types) + assert type_name in s.cluster.metadata.keyspaces['udttests'].user_types s.execute('CREATE TABLE %s (k int PRIMARY KEY, v frozen<%s>)' % (self.table_name, type_name)) s.execute('INSERT INTO %s (k, v) VALUES (0, {v0 : 1})' % (self.table_name,)) @@ -696,24 +697,24 @@ def test_type_alteration(self): s.cluster.register_user_type('udttests', type_name, dict) val = s.execute('SELECT v FROM %s' % self.table_name).one()[0] - self.assertEqual(val['v0'], 1) + assert val['v0'] == 1 # add field s.execute('ALTER TYPE %s ADD v1 text' % (type_name,)) val = s.execute('SELECT v FROM %s' % self.table_name).one()[0] - self.assertEqual(val['v0'], 1) - self.assertIsNone(val['v1']) + assert val['v0'] == 1 + assert val['v1'] is None s.execute("INSERT INTO %s (k, v) VALUES (0, {v0 : 2, v1 : 'sometext'})" % (self.table_name,)) val = s.execute('SELECT v FROM %s' % self.table_name).one()[0] - self.assertEqual(val['v0'], 2) - self.assertEqual(val['v1'], 'sometext') + assert val['v0'] == 2 + assert val['v1'] == 'sometext' # alter field type s.execute('ALTER TYPE %s ALTER v1 TYPE blob' % (type_name,)) s.execute("INSERT INTO %s (k, v) VALUES (0, {v0 : 3, v1 : 0xdeadbeef})" % (self.table_name,)) val = s.execute('SELECT v FROM %s' % self.table_name).one()[0] - self.assertEqual(val['v0'], 3) - self.assertEqual(val['v1'], b'\xde\xad\xbe\xef') + assert val['v0'] == 3 + assert val['v1'] == b'\xde\xad\xbe\xef' @lessthancass30 def test_alter_udt(self): @@ -736,8 +737,8 @@ def test_alter_udt(self): self.session.execute(insert_statement, [1, typetoalter(1)]) results = self.session.execute("SELECT * from {0}".format(self.function_table_name)) for result in results: - self.assertTrue(hasattr(result.typetoalter, 'a')) - self.assertFalse(hasattr(result.typetoalter, 'b')) + assert hasattr(result.typetoalter, 'a') + assert not hasattr(result.typetoalter, 'b') # Alter UDT and ensure the alter is honored in results self.session.execute("ALTER TYPE typetoalter add b int") @@ -745,5 +746,5 @@ def test_alter_udt(self): self.session.execute(insert_statement, [2, typetoalter(2, 2)]) results = self.session.execute("SELECT * from {0}".format(self.function_table_name)) for result in results: - self.assertTrue(hasattr(result.typetoalter, 'a')) - self.assertTrue(hasattr(result.typetoalter, 'b')) + assert hasattr(result.typetoalter, 'a') + assert hasattr(result.typetoalter, 'b') diff --git a/tests/integration/standard/test_use_keyspace.py b/tests/integration/standard/test_use_keyspace.py index 42cf03a553..9eb3f5be36 100644 --- a/tests/integration/standard/test_use_keyspace.py +++ b/tests/integration/standard/test_use_keyspace.py @@ -2,12 +2,9 @@ import time import logging -try: - import unittest2 as unittest -except ImportError: - import unittest # noqa +import unittest -from mock import patch +from unittest.mock import patch from cassandra.connection import Connection from cassandra.cluster import Cluster @@ -17,12 +14,23 @@ LOGGER = logging.getLogger(__name__) +_saved_scylla_ext_opts = None + def setup_module(): + global _saved_scylla_ext_opts + _saved_scylla_ext_opts = os.environ.get('SCYLLA_EXT_OPTS') os.environ['SCYLLA_EXT_OPTS'] = "--smp 2 --memory 2048M" use_cluster('shared_aware', [3], start=True) +def teardown_module(): + if _saved_scylla_ext_opts is None: + os.environ.pop('SCYLLA_EXT_OPTS', None) + else: + os.environ['SCYLLA_EXT_OPTS'] = _saved_scylla_ext_opts + + @local class TestUseKeyspace(unittest.TestCase): @classmethod @@ -57,7 +65,7 @@ def patched_set_keyspace_blocking(*args, **kwargs): return original_set_keyspace_blocking(*args, **kwargs) with patch.object(Connection, "set_keyspace_blocking", patched_set_keyspace_blocking): - self.session.execute("CREATE KEYSPACE test_set_keyspace WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}") + self.session.execute("CREATE KEYSPACE test_set_keyspace WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': 1}") self.session.execute("CREATE TABLE test_set_keyspace.set_keyspace_slow_connection(pk int, PRIMARY KEY(pk))") session2 = self.cluster.connect() diff --git a/tests/integration/upgrade/__init__.py b/tests/integration/upgrade/__init__.py index c5c06c4b01..fab6fed34a 100644 --- a/tests/integration/upgrade/__init__.py +++ b/tests/integration/upgrade/__init__.py @@ -26,7 +26,7 @@ from ccmlib.node import TimeoutError import time import logging - +import pytest import unittest @@ -78,7 +78,7 @@ def setUpClass(cls): cls.logger_handler = MockLoggingHandler() cls.logger = logging.getLogger(cluster.__name__) cls.logger.addHandler(cls.logger_handler) - + @classmethod def tearDownClass(cls): cls.logger.removeHandler(cls.logger_handler) @@ -166,7 +166,7 @@ def upgrade_node(self, node): try: node.start(wait_for_binary_proto=True, wait_other_notice=True) except TimeoutError: - self.fail("Error starting C* node while upgrading") + pytest.fail("Error starting C* node while upgrading") return True @@ -182,9 +182,21 @@ class UpgradeBaseAuth(UpgradeBase): def _upgrade_step_setup(self): """ - We sleep here for the same reason as we do in test_authentication.py: - there seems to be some race, with some versions of C* taking longer to - get the auth (and default user) setup. Sleep here to give it a chance + Wait for PasswordAuthenticator to finish initializing (creating the + default superuser). Poll by attempting to authenticate rather than + using a fixed sleep. """ super(UpgradeBaseAuth, self)._upgrade_step_setup() - time.sleep(10) + + from cassandra.auth import PlainTextAuthProvider + from tests.util import wait_until_not_raised + + def _check_auth_ready(): + c = Cluster(auth_provider=PlainTextAuthProvider('cassandra', 'cassandra')) + try: + s = c.connect() + s.execute("SELECT * FROM system.local WHERE key='local'") + finally: + c.shutdown() + + wait_until_not_raised(_check_auth_ready, delay=1, max_attempts=30) diff --git a/tests/integration/upgrade/test_upgrade.py b/tests/integration/upgrade/test_upgrade.py index 25d14427f2..45827723b3 100644 --- a/tests/integration/upgrade/test_upgrade.py +++ b/tests/integration/upgrade/test_upgrade.py @@ -19,8 +19,20 @@ from cassandra.cluster import ConsistencyLevel, Cluster, DriverException, ExecutionProfile from cassandra.policies import ConstantSpeculativeExecutionPolicy from tests.integration.upgrade import UpgradeBase, UpgradeBaseAuth, UpgradePath, upgrade_paths +from tests.util import wait_until import unittest +import pytest + + +def _wait_for_control_connection(cluster_driver, timeout=60): + """Wait for the driver's control connection to be established.""" + wait_until( + lambda: cluster_driver.control_connection._connection is not None + and not cluster_driver.control_connection._connection.is_closed, + delay=1, + max_attempts=timeout, + ) # Previous Cassandra upgrade @@ -57,9 +69,9 @@ def test_can_write(self): time.sleep(0.0001) total_number_of_inserted = self.session.execute("SELECT COUNT(*) from test3rf.test", execution_profile="all").one()[0] - self.assertEqual(total_number_of_inserted, next(c)) + assert total_number_of_inserted == next(c) - self.assertEqual(self.logger_handler.get_message_count("error", ""), 0) + assert self.logger_handler.get_message_count("error", "") == 0 @two_to_three_path def test_can_connect(self): @@ -79,10 +91,10 @@ def connect_and_shutdown(): queried_hosts = set() for _ in range(10): results = session.execute("SELECT * from system.local WHERE key='local'") - self.assertGreater(len(results.current_rows), 0) - self.assertEqual(len(results.response_future.attempted_hosts), 1) + assert len(results.current_rows) > 0 + assert len(results.response_future.attempted_hosts) == 1 queried_hosts.add(results.response_future.attempted_hosts[0]) - self.assertEqual(len(queried_hosts), 3) + assert len(queried_hosts) == 3 cluster.shutdown() connect_and_shutdown() @@ -116,9 +128,9 @@ def test_can_write(self): time.sleep(0.0001) total_number_of_inserted = self.session.execute("SELECT COUNT(*) from test3rf.test", execution_profile="all").one()[0] - self.assertEqual(total_number_of_inserted, next(c)) + assert total_number_of_inserted == next(c) - self.assertEqual(self.logger_handler.get_message_count("error", ""), 0) + assert self.logger_handler.get_message_count("error", "") == 0 @two_to_three_path def test_schema_metadata_gets_refreshed(self): @@ -141,16 +153,16 @@ def test_schema_metadata_gets_refreshed(self): for node in nodes[1:]: self.upgrade_node(node) # Wait for the control connection to reconnect - time.sleep(20) + _wait_for_control_connection(self.cluster_driver) - with self.assertRaises(DriverException): + with pytest.raises(DriverException): self.cluster_driver.refresh_schema_metadata(max_schema_agreement_wait=10) self.upgrade_node(nodes[0]) # Wait for the control connection to reconnect - time.sleep(20) + _wait_for_control_connection(self.cluster_driver) self.cluster_driver.refresh_schema_metadata(max_schema_agreement_wait=40) - self.assertNotEqual(original_meta, self.cluster_driver.metadata.keyspaces) + assert original_meta != self.cluster_driver.metadata.keyspaces @two_to_three_path def test_schema_nodes_gets_refreshed(self): @@ -170,16 +182,16 @@ def test_schema_nodes_gets_refreshed(self): token_map = self.cluster_driver.metadata.token_map self.upgrade_node(node) # Wait for the control connection to reconnect - time.sleep(20) + _wait_for_control_connection(self.cluster_driver) self.cluster_driver.refresh_nodes(force_token_rebuild=True) self._assert_same_token_map(token_map, self.cluster_driver.metadata.token_map) def _assert_same_token_map(self, original, new): - self.assertIsNot(original, new) - self.assertEqual(original.tokens_to_hosts_by_ks, new.tokens_to_hosts_by_ks) - self.assertEqual(original.token_to_host_owner, new.token_to_host_owner) - self.assertEqual(original.ring, new.ring) + assert original is not new + assert original.tokens_to_hosts_by_ks == new.tokens_to_hosts_by_ks + assert original.token_to_host_owner == new.token_to_host_owner + assert original.ring == new.ring two_to_three_with_auth_path = upgrade_paths([ @@ -243,10 +255,10 @@ def connect_and_shutdown(self, auth_provider): queried_hosts = set() for _ in range(10): results = session.execute("SELECT * from system.local WHERE key='local'") - self.assertGreater(len(results.current_rows), 0) - self.assertEqual(len(results.response_future.attempted_hosts), 1) + assert len(results.current_rows) > 0 + assert len(results.response_future.attempted_hosts) == 1 queried_hosts.add(results.response_future.attempted_hosts[0]) - self.assertEqual(len(queried_hosts), 3) + assert len(queried_hosts) == 3 cluster.shutdown() @@ -280,6 +292,6 @@ def test_can_write_speculative(self): time.sleep(0.0001) total_number_of_inserted = session.execute("SELECT COUNT(*) from test3rf.test", execution_profile="all").one()[0] - self.assertEqual(total_number_of_inserted, next(c)) + assert total_number_of_inserted == next(c) - self.assertEqual(self.logger_handler.get_message_count("error", ""), 0) + assert self.logger_handler.get_message_count("error", "") == 0 diff --git a/tests/integration/util.py b/tests/integration/util.py index bcc4cb829b..7cbdfdb22d 100644 --- a/tests/integration/util.py +++ b/tests/integration/util.py @@ -18,14 +18,14 @@ import time -def assert_quiescent_pool_state(test_case, cluster, wait=None): +def assert_quiescent_pool_state(cluster, wait=None): """ Checking the quiescent pool state checks that none of the requests ids have been lost. However, the callback corresponding to a request_id is called before the request_id is returned back to the pool, therefore session.execute("SELECT * from system.local") - assert_quiescent_pool_state(self, session.cluster) + assert_quiescent_pool_state(session.cluster) (with no wait) might fail because when execute comes back the request_id hasn't yet been returned to the pool, therefore the wait. @@ -35,23 +35,23 @@ def assert_quiescent_pool_state(test_case, cluster, wait=None): for session in cluster.sessions: pool_states = session.get_pool_state().values() - test_case.assertTrue(pool_states) + assert pool_states for state in pool_states: - test_case.assertFalse(state['shutdown']) - test_case.assertGreater(state['open_count'], 0) + assert not state['shutdown'] + assert state['open_count'] > 0 no_in_flight = all((i == 0 for i in state['in_flights'])) orphans_and_inflights = zip(state['orphan_requests'],state['in_flights']) all_orphaned = all((len(orphans) == inflight for (orphans,inflight) in orphans_and_inflights)) - test_case.assertTrue(no_in_flight or all_orphaned) + assert no_in_flight or all_orphaned for holder in cluster.get_connection_holders(): for connection in holder.get_connections(): # all ids are unique req_ids = connection.request_ids orphan_ids = connection.orphaned_request_ids - test_case.assertEqual(len(req_ids), len(set(req_ids))) - test_case.assertEqual(connection.highest_request_id, len(req_ids) + len(orphan_ids) - 1) - test_case.assertEqual(connection.highest_request_id, max(chain(req_ids, orphan_ids))) + assert len(req_ids) == len(set(req_ids)) + assert connection.highest_request_id == len(req_ids) + len(orphan_ids) - 1 + assert connection.highest_request_id == max(chain(req_ids, orphan_ids)) if PROTOCOL_VERSION < 3: - test_case.assertEqual(connection.highest_request_id, connection.max_request_id) + assert connection.highest_request_id == connection.max_request_id diff --git a/tests/stress_tests/test_multi_inserts.py b/tests/stress_tests/test_multi_inserts.py index 84dfc5e6f7..1a5a596aec 100644 --- a/tests/stress_tests/test_multi_inserts.py +++ b/tests/stress_tests/test_multi_inserts.py @@ -77,4 +77,4 @@ def test_in_flight_is_one(self): break i = i + 1 - self.assertFalse(leaking_connections, 'Detected leaking connection after %s iterations' % i) + assert not leaking_connections, 'Detected leaking connection after %s iterations' % i diff --git a/tests/unit/advanced/test_auth.py b/tests/unit/advanced/test_auth.py deleted file mode 100644 index 840073e9e1..0000000000 --- a/tests/unit/advanced/test_auth.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright DataStax, Inc. -# -# 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 -# -# http://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 -from puresasl import QOP - -import unittest - -from cassandra.auth import DSEGSSAPIAuthProvider - -# Cannot import requiredse from tests.integration -# This auth provider requires kerberos and puresals -DSE_VERSION = os.getenv('DSE_VERSION', None) -@unittest.skipUnless(DSE_VERSION, "DSE required") -class TestGSSAPI(unittest.TestCase): - - def test_host_resolution(self): - # resolves by default - provider = DSEGSSAPIAuthProvider(service='test', qops=QOP.all) - authenticator = provider.new_authenticator('127.0.0.1') - self.assertEqual(authenticator.sasl.host, 'localhost') - - # numeric fallback okay - authenticator = provider.new_authenticator('192.0.2.1') - self.assertEqual(authenticator.sasl.host, '192.0.2.1') - - # disable explicitly - provider = DSEGSSAPIAuthProvider(service='test', qops=QOP.all, resolve_host_name=False) - authenticator = provider.new_authenticator('127.0.0.1') - self.assertEqual(authenticator.sasl.host, '127.0.0.1') - diff --git a/tests/unit/advanced/test_execution_profile.py b/tests/unit/advanced/test_execution_profile.py index 478322f95b..bc51388c00 100644 --- a/tests/unit/advanced/test_execution_profile.py +++ b/tests/unit/advanced/test_execution_profile.py @@ -23,9 +23,9 @@ class GraphExecutionProfileTest(unittest.TestCase): def test_graph_source_can_be_set_with_graph_execution_profile(self): options = GraphOptions(graph_source='a') ep = GraphExecutionProfile(graph_options=options) - self.assertEqual(ep.graph_options.graph_source, b'a') + assert ep.graph_options.graph_source == b'a' def test_graph_source_is_preserve_with_graph_analytics_execution_profile(self): options = GraphOptions(graph_source='doesnt_matter') ep = GraphAnalyticsExecutionProfile(graph_options=options) - self.assertEqual(ep.graph_options.graph_source, b'a') # graph source is set automatically + assert ep.graph_options.graph_source == b'a' # graph source is set automatically diff --git a/tests/unit/advanced/test_geometry.py b/tests/unit/advanced/test_geometry.py index d85f1bc293..1927b51da7 100644 --- a/tests/unit/advanced/test_geometry.py +++ b/tests/unit/advanced/test_geometry.py @@ -20,6 +20,7 @@ from cassandra.protocol import ProtocolVersion from cassandra.cqltypes import PointType, LineStringType, PolygonType, WKBGeometryType from cassandra.util import Point, LineString, Polygon, _LinearRing, Distance, _HAS_GEOMET +import pytest wkb_be = 0 wkb_le = 1 @@ -35,12 +36,12 @@ def test_marshal_platform(self): for proto_ver in protocol_versions: for geo in self.samples: cql_type = lookup_casstype(geo.__class__.__name__ + 'Type') - self.assertEqual(cql_type.from_binary(cql_type.to_binary(geo, proto_ver), proto_ver), geo) + assert cql_type.from_binary(cql_type.to_binary(geo, proto_ver), proto_ver) == geo def _verify_both_endian(self, typ, body_fmt, params, expected): for proto_ver in protocol_versions: - self.assertEqual(typ.from_binary(struct.pack(">BI" + body_fmt, wkb_be, *params), proto_ver), expected) - self.assertEqual(typ.from_binary(struct.pack("BI" + body_fmt, wkb_be, *params), proto_ver) == expected + assert typ.from_binary(struct.pack(" base map base = GraphOptions(**self.api_params) - self.assertEqual(GraphOptions().get_options_map(base), base._graph_options) + assert GraphOptions().get_options_map(base) == base._graph_options # something set overrides kwargs = self.api_params.copy() # this test concept got strange after we added default values for a couple GraphOption attrs @@ -276,9 +282,9 @@ def test_get_options(self): other = GraphOptions(**kwargs) options = base.get_options_map(other) updated = self.opt_mapping['graph_name'] - self.assertEqual(options[updated], b'unit_test') + assert options[updated] == b'unit_test' for name in (n for n in self.opt_mapping.values() if n != updated): - self.assertEqual(options[name], base._graph_options[name]) + assert options[name] == base._graph_options[name] # base unchanged self._verify_api_params(base, self.api_params) @@ -286,24 +292,24 @@ def test_get_options(self): def test_set_attr(self): expected = 'test@@@@' opts = GraphOptions(graph_name=expected) - self.assertEqual(opts.graph_name, expected.encode()) + assert opts.graph_name == expected.encode() expected = 'somethingelse####' opts.graph_name = expected - self.assertEqual(opts.graph_name, expected.encode()) + assert opts.graph_name == expected.encode() # will update options with set value another = GraphOptions() - self.assertIsNone(another.graph_name) + assert another.graph_name is None another.update(opts) - self.assertEqual(another.graph_name, expected.encode()) + assert another.graph_name == expected.encode() opts.graph_name = None - self.assertIsNone(opts.graph_name) + assert opts.graph_name is None # will not update another with its set-->unset value another.update(opts) - self.assertEqual(another.graph_name, expected.encode()) # remains unset + assert another.graph_name == expected.encode() # remains unset opt_map = another.get_options_map(opts) - self.assertEqual(opt_map, another._graph_options) + assert opt_map == another._graph_options def test_del_attr(self): opts = GraphOptions(**self.api_params) @@ -313,14 +319,14 @@ def test_del_attr(self): self._verify_api_params(opts, test_params) def _verify_api_params(self, opts, api_params): - self.assertEqual(len(opts._graph_options), len(api_params)) + assert len(opts._graph_options) == len(api_params) for name, value in api_params.items(): try: value = value.encode() except: pass # already bytes - self.assertEqual(getattr(opts, name), value) - self.assertEqual(opts._graph_options[self.opt_mapping[name]], value) + assert getattr(opts, name) == value + assert opts._graph_options[self.opt_mapping[name]] == value def test_consistency_levels(self): read_cl = ConsistencyLevel.ONE @@ -328,49 +334,49 @@ def test_consistency_levels(self): # set directly opts = GraphOptions(graph_read_consistency_level=read_cl, graph_write_consistency_level=write_cl) - self.assertEqual(opts.graph_read_consistency_level, read_cl) - self.assertEqual(opts.graph_write_consistency_level, write_cl) + assert opts.graph_read_consistency_level == read_cl + assert opts.graph_write_consistency_level == write_cl # mapping from base opt_map = opts.get_options_map() - self.assertEqual(opt_map['graph-read-consistency'], ConsistencyLevel.value_to_name[read_cl].encode()) - self.assertEqual(opt_map['graph-write-consistency'], ConsistencyLevel.value_to_name[write_cl].encode()) + assert opt_map['graph-read-consistency'] == ConsistencyLevel.value_to_name[read_cl].encode() + assert opt_map['graph-write-consistency'] == ConsistencyLevel.value_to_name[write_cl].encode() # empty by default new_opts = GraphOptions() opt_map = new_opts.get_options_map() - self.assertNotIn('graph-read-consistency', opt_map) - self.assertNotIn('graph-write-consistency', opt_map) + assert 'graph-read-consistency' not in opt_map + assert 'graph-write-consistency' not in opt_map # set from other opt_map = new_opts.get_options_map(opts) - self.assertEqual(opt_map['graph-read-consistency'], ConsistencyLevel.value_to_name[read_cl].encode()) - self.assertEqual(opt_map['graph-write-consistency'], ConsistencyLevel.value_to_name[write_cl].encode()) + assert opt_map['graph-read-consistency'] == ConsistencyLevel.value_to_name[read_cl].encode() + assert opt_map['graph-write-consistency'] == ConsistencyLevel.value_to_name[write_cl].encode() def test_graph_source_convenience_attributes(self): opts = GraphOptions() - self.assertEqual(opts.graph_source, b'g') - self.assertFalse(opts.is_analytics_source) - self.assertTrue(opts.is_graph_source) - self.assertFalse(opts.is_default_source) + assert opts.graph_source == b'g' + assert not opts.is_analytics_source + assert opts.is_graph_source + assert not opts.is_default_source opts.set_source_default() - self.assertIsNotNone(opts.graph_source) - self.assertFalse(opts.is_analytics_source) - self.assertFalse(opts.is_graph_source) - self.assertTrue(opts.is_default_source) + assert opts.graph_source is not None + assert not opts.is_analytics_source + assert not opts.is_graph_source + assert opts.is_default_source opts.set_source_analytics() - self.assertIsNotNone(opts.graph_source) - self.assertTrue(opts.is_analytics_source) - self.assertFalse(opts.is_graph_source) - self.assertFalse(opts.is_default_source) + assert opts.graph_source is not None + assert opts.is_analytics_source + assert not opts.is_graph_source + assert not opts.is_default_source opts.set_source_graph() - self.assertIsNotNone(opts.graph_source) - self.assertFalse(opts.is_analytics_source) - self.assertTrue(opts.is_graph_source) - self.assertFalse(opts.is_default_source) + assert opts.graph_source is not None + assert not opts.is_analytics_source + assert opts.is_graph_source + assert not opts.is_default_source class GraphStatementTests(unittest.TestCase): @@ -384,11 +390,12 @@ def test_init(self): 'custom_payload': object()} statement = SimpleGraphStatement(**kwargs) for k, v in kwargs.items(): - self.assertIs(getattr(statement, k), v) + assert getattr(statement, k) is v # but not a bogus parameter kwargs['bogus'] = object() - self.assertRaises(TypeError, SimpleGraphStatement, **kwargs) + with pytest.raises(TypeError): + SimpleGraphStatement(**kwargs) class GraphRowFactoryTests(unittest.TestCase): @@ -396,12 +403,12 @@ class GraphRowFactoryTests(unittest.TestCase): def test_object_row_factory(self): col_names = [] # unused rows = [object() for _ in range(10)] - self.assertEqual(single_object_row_factory(col_names, ((o,) for o in rows)), rows) + assert single_object_row_factory(col_names, ((o,) for o in rows)) == rows def test_graph_result_row_factory(self): col_names = [] # unused rows = [json.dumps({'result': i}) for i in range(10)] results = graph_result_row_factory(col_names, ((o,) for o in rows)) for i, res in enumerate(results): - self.assertIsInstance(res, Result) - self.assertEqual(res.value, i) + assert isinstance(res, Result) + assert res.value == i diff --git a/tests/unit/advanced/test_insights.py b/tests/unit/advanced/test_insights.py index 4047fe12b8..ec9b918866 100644 --- a/tests/unit/advanced/test_insights.py +++ b/tests/unit/advanced/test_insights.py @@ -66,18 +66,15 @@ class NoConfAsDict(object): # no default # ... as a policy - self.assertEqual(insights_registry.serialize(obj, policy=True), - {'type': 'NoConfAsDict', - 'namespace': ns, - 'options': {}}) + assert insights_registry.serialize(obj, policy=True) == {'type': 'NoConfAsDict', + 'namespace': ns, + 'options': {}} # ... not as a policy (default) - self.assertEqual(insights_registry.serialize(obj), - {'type': 'NoConfAsDict', - 'namespace': ns, - }) + assert insights_registry.serialize(obj) == {'type': 'NoConfAsDict', + 'namespace': ns, + } # with default - self.assertIs(insights_registry.serialize(obj, default=sentinel.attr_err_default), - sentinel.attr_err_default) + assert insights_registry.serialize(obj, default=sentinel.attr_err_default) is sentinel.attr_err_default def test_successful_return(self): @@ -91,14 +88,11 @@ class SubclassSentinel(SuperclassSentinel): def superclass_sentinel_serializer(obj): return sentinel.serialized_superclass - self.assertIs(insights_registry.serialize(SuperclassSentinel()), - sentinel.serialized_superclass) - self.assertIs(insights_registry.serialize(SubclassSentinel()), - sentinel.serialized_superclass) + assert insights_registry.serialize(SuperclassSentinel()) is sentinel.serialized_superclass + assert insights_registry.serialize(SubclassSentinel()) is sentinel.serialized_superclass # with default -- same behavior - self.assertIs(insights_registry.serialize(SubclassSentinel(), default=object()), - sentinel.serialized_superclass) + assert insights_registry.serialize(SubclassSentinel(), default=object()) is sentinel.serialized_superclass class TestConfigAsDict(unittest.TestCase): @@ -116,190 +110,145 @@ def test_graph_options(self): log.debug(go._graph_options) - self.assertEqual( - insights_registry.serialize(go), - {'source': 'source_for_test', - 'language': 'lang_for_test', - 'graphProtocol': 'protocol_for_test', - # no graph_invalid_option - } - ) + assert insights_registry.serialize(go) == {'source': 'source_for_test', + 'language': 'lang_for_test', + 'graphProtocol': 'protocol_for_test', + # no graph_invalid_option + } # cluster.py def test_execution_profile(self): self.maxDiff = None - self.assertEqual( - insights_registry.serialize(ExecutionProfile()), - {'consistency': 'LOCAL_ONE', - 'continuousPagingOptions': None, - 'loadBalancing': {'namespace': 'cassandra.policies', - 'options': {'child_policy': {'namespace': 'cassandra.policies', - 'options': {'local_dc': '', - 'used_hosts_per_remote_dc': 0}, - 'type': 'DCAwareRoundRobinPolicy'}, - 'shuffle_replicas': False}, - 'type': 'TokenAwarePolicy'}, - 'readTimeout': 10.0, - 'retry': {'namespace': 'cassandra.policies', 'options': {}, 'type': 'RetryPolicy'}, - 'serialConsistency': None, - 'speculativeExecution': {'namespace': 'cassandra.policies', - 'options': {}, 'type': 'NoSpeculativeExecutionPolicy'}, - 'graphOptions': None - } - ) + assert insights_registry.serialize(ExecutionProfile()) == {'consistency': 'LOCAL_ONE', + 'continuousPagingOptions': None, + 'loadBalancing': {'namespace': 'cassandra.policies', + 'options': {'child_policy': {'namespace': 'cassandra.policies', + 'options': {'local_dc': '', + 'used_hosts_per_remote_dc': 0}, + 'type': 'DCAwareRoundRobinPolicy'}, + 'shuffle_replicas': True}, + 'type': 'TokenAwarePolicy'}, + 'readTimeout': 10.0, + 'retry': {'namespace': 'cassandra.policies', 'options': {}, 'type': 'RetryPolicy'}, + 'serialConsistency': None, + 'speculativeExecution': {'namespace': 'cassandra.policies', + 'options': {}, 'type': 'NoSpeculativeExecutionPolicy'}, + 'graphOptions': None + } def test_graph_execution_profile(self): self.maxDiff = None - self.assertEqual( - insights_registry.serialize(GraphExecutionProfile()), - {'consistency': 'LOCAL_ONE', - 'continuousPagingOptions': None, - 'loadBalancing': {'namespace': 'cassandra.policies', - 'options': {'child_policy': {'namespace': 'cassandra.policies', - 'options': {'local_dc': '', - 'used_hosts_per_remote_dc': 0}, - 'type': 'DCAwareRoundRobinPolicy'}, - 'shuffle_replicas': False}, - 'type': 'TokenAwarePolicy'}, - 'readTimeout': 30.0, - 'retry': {'namespace': 'cassandra.policies', 'options': {}, 'type': 'NeverRetryPolicy'}, - 'serialConsistency': None, - 'speculativeExecution': {'namespace': 'cassandra.policies', - 'options': {}, 'type': 'NoSpeculativeExecutionPolicy'}, - 'graphOptions': {'graphProtocol': None, - 'language': 'gremlin-groovy', - 'source': 'g'}, - } - ) + assert insights_registry.serialize(GraphExecutionProfile()) == {'consistency': 'LOCAL_ONE', + 'continuousPagingOptions': None, + 'loadBalancing': {'namespace': 'cassandra.policies', + 'options': {'child_policy': {'namespace': 'cassandra.policies', + 'options': {'local_dc': '', + 'used_hosts_per_remote_dc': 0}, + 'type': 'DCAwareRoundRobinPolicy'}, + 'shuffle_replicas': True}, + 'type': 'TokenAwarePolicy'}, + 'readTimeout': 30.0, + 'retry': {'namespace': 'cassandra.policies', 'options': {}, 'type': 'NeverRetryPolicy'}, + 'serialConsistency': None, + 'speculativeExecution': {'namespace': 'cassandra.policies', + 'options': {}, 'type': 'NoSpeculativeExecutionPolicy'}, + 'graphOptions': {'graphProtocol': None, + 'language': 'gremlin-groovy', + 'source': 'g'}, + } def test_graph_analytics_execution_profile(self): self.maxDiff = None - self.assertEqual( - insights_registry.serialize(GraphAnalyticsExecutionProfile()), - {'consistency': 'LOCAL_ONE', - 'continuousPagingOptions': None, - 'loadBalancing': {'namespace': 'cassandra.policies', - 'options': {'child_policy': {'namespace': 'cassandra.policies', - 'options': {'child_policy': {'namespace': 'cassandra.policies', - 'options': {'local_dc': '', - 'used_hosts_per_remote_dc': 0}, - 'type': 'DCAwareRoundRobinPolicy'}, - 'shuffle_replicas': False}, - 'type': 'TokenAwarePolicy'}}, - 'type': 'DefaultLoadBalancingPolicy'}, - 'readTimeout': 604800.0, - 'retry': {'namespace': 'cassandra.policies', 'options': {}, 'type': 'NeverRetryPolicy'}, - 'serialConsistency': None, - 'speculativeExecution': {'namespace': 'cassandra.policies', - 'options': {}, 'type': 'NoSpeculativeExecutionPolicy'}, - 'graphOptions': {'graphProtocol': None, - 'language': 'gremlin-groovy', - 'source': 'a'}, - } - ) + assert insights_registry.serialize(GraphAnalyticsExecutionProfile()) == {'consistency': 'LOCAL_ONE', + 'continuousPagingOptions': None, + 'loadBalancing': {'namespace': 'cassandra.policies', + 'options': {'child_policy': {'namespace': 'cassandra.policies', + 'options': {'child_policy': {'namespace': 'cassandra.policies', + 'options': {'local_dc': '', + 'used_hosts_per_remote_dc': 0}, + 'type': 'DCAwareRoundRobinPolicy'}, + 'shuffle_replicas': True}, + 'type': 'TokenAwarePolicy'}}, + 'type': 'DefaultLoadBalancingPolicy'}, + 'readTimeout': 604800.0, + 'retry': {'namespace': 'cassandra.policies', 'options': {}, 'type': 'NeverRetryPolicy'}, + 'serialConsistency': None, + 'speculativeExecution': {'namespace': 'cassandra.policies', + 'options': {}, 'type': 'NoSpeculativeExecutionPolicy'}, + 'graphOptions': {'graphProtocol': None, + 'language': 'gremlin-groovy', + 'source': 'a'}, + } # policies.py def test_DC_aware_round_robin_policy(self): - self.assertEqual( - insights_registry.serialize(DCAwareRoundRobinPolicy()), - {'namespace': 'cassandra.policies', - 'options': {'local_dc': '', 'used_hosts_per_remote_dc': 0}, - 'type': 'DCAwareRoundRobinPolicy'} - ) - self.assertEqual( - insights_registry.serialize(DCAwareRoundRobinPolicy(local_dc='fake_local_dc', - used_hosts_per_remote_dc=15)), - {'namespace': 'cassandra.policies', - 'options': {'local_dc': 'fake_local_dc', 'used_hosts_per_remote_dc': 15}, - 'type': 'DCAwareRoundRobinPolicy'} - ) + assert insights_registry.serialize(DCAwareRoundRobinPolicy()) == {'namespace': 'cassandra.policies', + 'options': {'local_dc': '', 'used_hosts_per_remote_dc': 0}, + 'type': 'DCAwareRoundRobinPolicy'} + assert insights_registry.serialize(DCAwareRoundRobinPolicy(local_dc='fake_local_dc', + used_hosts_per_remote_dc=15)) == {'namespace': 'cassandra.policies', + 'options': {'local_dc': 'fake_local_dc', 'used_hosts_per_remote_dc': 15}, + 'type': 'DCAwareRoundRobinPolicy'} def test_token_aware_policy(self): - self.assertEqual( - insights_registry.serialize(TokenAwarePolicy(child_policy=LoadBalancingPolicy())), - {'namespace': 'cassandra.policies', - 'options': {'child_policy': {'namespace': 'cassandra.policies', - 'options': {}, - 'type': 'LoadBalancingPolicy'}, - 'shuffle_replicas': False}, - 'type': 'TokenAwarePolicy'} - ) + assert insights_registry.serialize(TokenAwarePolicy(child_policy=LoadBalancingPolicy())) == {'namespace': 'cassandra.policies', + 'options': {'child_policy': {'namespace': 'cassandra.policies', + 'options': {}, + 'type': 'LoadBalancingPolicy'}, + 'shuffle_replicas': True}, + 'type': 'TokenAwarePolicy'} def test_whitelist_round_robin_policy(self): - self.assertEqual( - insights_registry.serialize(WhiteListRoundRobinPolicy(['127.0.0.3'])), - {'namespace': 'cassandra.policies', - 'options': {'allowed_hosts': ('127.0.0.3',)}, - 'type': 'WhiteListRoundRobinPolicy'} - ) + assert insights_registry.serialize(WhiteListRoundRobinPolicy(['127.0.0.3'])) == {'namespace': 'cassandra.policies', + 'options': {'allowed_hosts': ('127.0.0.3',)}, + 'type': 'WhiteListRoundRobinPolicy'} def test_host_filter_policy(self): def my_predicate(s): return False - self.assertEqual( - insights_registry.serialize(HostFilterPolicy(LoadBalancingPolicy(), my_predicate)), - {'namespace': 'cassandra.policies', - 'options': {'child_policy': {'namespace': 'cassandra.policies', - 'options': {}, - 'type': 'LoadBalancingPolicy'}, - 'predicate': 'my_predicate'}, - 'type': 'HostFilterPolicy'} - ) + assert insights_registry.serialize(HostFilterPolicy(LoadBalancingPolicy(), my_predicate)) == {'namespace': 'cassandra.policies', + 'options': {'child_policy': {'namespace': 'cassandra.policies', + 'options': {}, + 'type': 'LoadBalancingPolicy'}, + 'predicate': 'my_predicate'}, + 'type': 'HostFilterPolicy'} def test_constant_reconnection_policy(self): - self.assertEqual( - insights_registry.serialize(ConstantReconnectionPolicy(3, 200)), - {'type': 'ConstantReconnectionPolicy', - 'namespace': 'cassandra.policies', - 'options': {'delay': 3, 'max_attempts': 200} - } - ) + assert insights_registry.serialize(ConstantReconnectionPolicy(3, 200)) == {'type': 'ConstantReconnectionPolicy', + 'namespace': 'cassandra.policies', + 'options': {'delay': 3, 'max_attempts': 200} + } def test_exponential_reconnection_policy(self): - self.assertEqual( - insights_registry.serialize(ExponentialReconnectionPolicy(4, 100, 10)), - {'type': 'ExponentialReconnectionPolicy', - 'namespace': 'cassandra.policies', - 'options': {'base_delay': 4, 'max_delay': 100, 'max_attempts': 10} - } - ) + assert insights_registry.serialize(ExponentialReconnectionPolicy(4, 100, 10)) == {'type': 'ExponentialReconnectionPolicy', + 'namespace': 'cassandra.policies', + 'options': {'base_delay': 4, 'max_delay': 100, 'max_attempts': 10} + } def test_retry_policy(self): - self.assertEqual( - insights_registry.serialize(RetryPolicy()), - {'type': 'RetryPolicy', - 'namespace': 'cassandra.policies', - 'options': {} - } - ) + assert insights_registry.serialize(RetryPolicy()) == {'type': 'RetryPolicy', + 'namespace': 'cassandra.policies', + 'options': {} + } def test_spec_exec_policy(self): - self.assertEqual( - insights_registry.serialize(SpeculativeExecutionPolicy()), - {'type': 'SpeculativeExecutionPolicy', - 'namespace': 'cassandra.policies', - 'options': {} - } - ) + assert insights_registry.serialize(SpeculativeExecutionPolicy()) == {'type': 'SpeculativeExecutionPolicy', + 'namespace': 'cassandra.policies', + 'options': {} + } def test_constant_spec_exec_policy(self): - self.assertEqual( - insights_registry.serialize(ConstantSpeculativeExecutionPolicy(100, 101)), - {'type': 'ConstantSpeculativeExecutionPolicy', - 'namespace': 'cassandra.policies', - 'options': {'delay': 100, - 'max_attempts': 101} - } - ) + assert insights_registry.serialize(ConstantSpeculativeExecutionPolicy(100, 101)) == {'type': 'ConstantSpeculativeExecutionPolicy', + 'namespace': 'cassandra.policies', + 'options': {'delay': 100, + 'max_attempts': 101} + } def test_wrapper_policy(self): - self.assertEqual( - insights_registry.serialize(WrapperPolicy(LoadBalancingPolicy())), - {'namespace': 'cassandra.policies', - 'options': {'child_policy': {'namespace': 'cassandra.policies', - 'options': {}, - 'type': 'LoadBalancingPolicy'} - }, - 'type': 'WrapperPolicy'} - ) + assert insights_registry.serialize(WrapperPolicy(LoadBalancingPolicy())) == {'namespace': 'cassandra.policies', + 'options': {'child_policy': {'namespace': 'cassandra.policies', + 'options': {}, + 'type': 'LoadBalancingPolicy'} + }, + 'type': 'WrapperPolicy'} diff --git a/tests/unit/advanced/test_metadata.py b/tests/unit/advanced/test_metadata.py index 20f80b4da4..d68a87961d 100644 --- a/tests/unit/advanced/test_metadata.py +++ b/tests/unit/advanced/test_metadata.py @@ -34,8 +34,8 @@ def _create_vertex_metadata(self, label_name='label'): def _create_keyspace_metadata(self, graph_engine): return KeyspaceMetadata( - 'keyspace', True, 'org.apache.cassandra.locator.SimpleStrategy', - {'replication_factor': 1}, graph_engine=graph_engine) + 'keyspace', True, 'org.apache.cassandra.locator.NetworkTopologyStrategy', + {'dc1': 1}, graph_engine=graph_engine) def _create_table_metadata(self, with_vertex=False, with_edge=False): tm = TableMetadataDSE68('keyspace', 'table') @@ -48,95 +48,71 @@ def _create_table_metadata(self, with_vertex=False, with_edge=False): def test_keyspace_no_graph_engine(self): km = self._create_keyspace_metadata(None) - self.assertEqual(km.graph_engine, None) - self.assertNotIn( - "graph_engine", - km.as_cql_query() - ) + assert km.graph_engine == None + assert "graph_engine" not in km.as_cql_query() def test_keyspace_with_graph_engine(self): graph_engine = 'Core' km = self._create_keyspace_metadata(graph_engine) - self.assertEqual(km.graph_engine, graph_engine) + assert km.graph_engine == graph_engine cql = km.as_cql_query() - self.assertIn( - "graph_engine", - cql - ) - self.assertIn( - "Core", - cql - ) + assert "graph_engine" in cql + assert "Core" in cql def test_table_no_vertex_or_edge(self): tm = self._create_table_metadata() - self.assertIsNone(tm.vertex) - self.assertIsNone(tm.edge) + assert tm.vertex is None + assert tm.edge is None cql = tm.as_cql_query() - self.assertNotIn("VERTEX LABEL", cql) - self.assertNotIn("EDGE LABEL", cql) + assert "VERTEX LABEL" not in cql + assert "EDGE LABEL" not in cql def test_table_with_vertex(self): tm = self._create_table_metadata(with_vertex=True) - self.assertIsInstance(tm.vertex, VertexMetadata) - self.assertIsNone(tm.edge) + assert isinstance(tm.vertex, VertexMetadata) + assert tm.edge is None cql = tm.as_cql_query() - self.assertIn("VERTEX LABEL", cql) - self.assertNotIn("EDGE LABEL", cql) + assert "VERTEX LABEL" in cql + assert "EDGE LABEL" not in cql def test_table_with_edge(self): tm = self._create_table_metadata(with_edge=True) - self.assertIsNone(tm.vertex) - self.assertIsInstance(tm.edge, EdgeMetadata) + assert tm.vertex is None + assert isinstance(tm.edge, EdgeMetadata) cql = tm.as_cql_query() - self.assertNotIn("VERTEX LABEL", cql) - self.assertIn("EDGE LABEL", cql) - self.assertIn("FROM from_label", cql) - self.assertIn("TO to_label", cql) + assert "VERTEX LABEL" not in cql + assert "EDGE LABEL" in cql + assert "FROM from_label" in cql + assert "TO to_label" in cql def test_vertex_with_label(self): tm = self. _create_table_metadata(with_vertex=True) - self.assertTrue(tm.as_cql_query().endswith('VERTEX LABEL label')) + assert tm.as_cql_query().endswith('VERTEX LABEL label') def test_edge_single_partition_key_and_clustering_key(self): tm = self._create_table_metadata(with_edge=True) - self.assertIn( - 'FROM from_label(pk1, c1)', - tm.as_cql_query() - ) + assert 'FROM from_label(pk1, c1)' in tm.as_cql_query() def test_edge_multiple_partition_keys(self): edge = self._create_edge_metadata(partition_keys=['pk1', 'pk2']) tm = self. _create_table_metadata(with_edge=edge) - self.assertIn( - 'FROM from_label((pk1, pk2), ', - tm.as_cql_query() - ) + assert 'FROM from_label((pk1, pk2), ' in tm.as_cql_query() def test_edge_no_clustering_keys(self): edge = self._create_edge_metadata(clustering_keys=[]) tm = self. _create_table_metadata(with_edge=edge) - self.assertIn( - 'FROM from_label(pk1) ', - tm.as_cql_query() - ) + assert 'FROM from_label(pk1) ' in tm.as_cql_query() def test_edge_multiple_clustering_keys(self): edge = self._create_edge_metadata(clustering_keys=['c1', 'c2']) tm = self. _create_table_metadata(with_edge=edge) - self.assertIn( - 'FROM from_label(pk1, c1, c2) ', - tm.as_cql_query() - ) + assert 'FROM from_label(pk1, c1, c2) ' in tm.as_cql_query() def test_edge_multiple_partition_and_clustering_keys(self): edge = self._create_edge_metadata(partition_keys=['pk1', 'pk2'], clustering_keys=['c1', 'c2']) tm = self. _create_table_metadata(with_edge=edge) - self.assertIn( - 'FROM from_label((pk1, pk2), c1, c2) ', - tm.as_cql_query() - ) + assert 'FROM from_label((pk1, pk2), c1, c2) ' in tm.as_cql_query() class SchemaParsersTests(unittest.TestCase): @@ -159,16 +135,14 @@ def wait_for_responses(self, *msgs, **kwargs): p._query_all() for q in conn.queries: - if "USING TIMEOUT" in q.query: - self.fail(f"<{schemaClass.__name__}> query `{q.query}` contains `USING TIMEOUT`, while should not") + assert "USING TIMEOUT" not in q.query, f"<{schemaClass.__name__}> query `{q.query}` contains `USING TIMEOUT`, while should not" conn = FakeConnection() p = schemaClass(conn, 2.0, 1000, datetime.timedelta(seconds=2)) p._query_all() for q in conn.queries: - if "USING TIMEOUT 2000ms" not in q.query: - self.fail(f"{schemaClass.__name__} query `{q.query}` does not contain `USING TIMEOUT 2000ms`") + assert "USING TIMEOUT 2000ms" in q.query, f"{schemaClass.__name__} query `{q.query}` does not contain `USING TIMEOUT 2000ms`" def get_all_schema_parser_classes(cl): diff --git a/tests/unit/advanced/test_policies.py b/tests/unit/advanced/test_policies.py index 553e7dba87..75cfd3fbf9 100644 --- a/tests/unit/advanced/test_policies.py +++ b/tests/unit/advanced/test_policies.py @@ -13,6 +13,7 @@ # limitations under the License. import unittest from unittest.mock import Mock +import uuid from cassandra.pool import Host from cassandra.policies import RoundRobinPolicy @@ -37,7 +38,7 @@ def test_no_target(self): policy.populate(Mock(metadata=ClusterMetaMock()), hosts) for _ in range(node_count): query_plan = list(policy.make_query_plan(None, Mock(target_host=None))) - self.assertEqual(sorted(query_plan), hosts) + assert sorted(query_plan) == hosts def test_status_updates(self): node_count = 4 @@ -49,7 +50,7 @@ def test_status_updates(self): policy.on_up(4) policy.on_add(5) query_plan = list(policy.make_query_plan()) - self.assertEqual(sorted(query_plan), [2, 3, 4, 5]) + assert sorted(query_plan) == [2, 3, 4, 5] def test_no_live_nodes(self): hosts = [0, 1, 2, 3] @@ -60,7 +61,7 @@ def test_no_live_nodes(self): policy.on_down(i) query_plan = list(policy.make_query_plan()) - self.assertEqual(query_plan, []) + assert query_plan == [] def test_target_no_host(self): node_count = 4 @@ -68,26 +69,26 @@ def test_target_no_host(self): policy = DSELoadBalancingPolicy(RoundRobinPolicy()) policy.populate(Mock(metadata=ClusterMetaMock()), hosts) query_plan = list(policy.make_query_plan(None, Mock(target_host='127.0.0.1'))) - self.assertEqual(sorted(query_plan), hosts) + assert sorted(query_plan) == hosts def test_target_host_down(self): node_count = 4 - hosts = [Host(i, Mock()) for i in range(node_count)] + hosts = [Host(i, Mock(), host_id=uuid.uuid4()) for i in range(node_count)] target_host = hosts[1] policy = DSELoadBalancingPolicy(RoundRobinPolicy()) policy.populate(Mock(metadata=ClusterMetaMock({'127.0.0.1': target_host})), hosts) query_plan = list(policy.make_query_plan(None, Mock(target_host='127.0.0.1'))) - self.assertEqual(sorted(query_plan), hosts) + assert sorted(query_plan) == hosts target_host.is_up = False policy.on_down(target_host) query_plan = list(policy.make_query_plan(None, Mock(target_host='127.0.0.1'))) - self.assertNotIn(target_host, query_plan) + assert target_host not in query_plan def test_target_host_nominal(self): node_count = 4 - hosts = [Host(i, Mock()) for i in range(node_count)] + hosts = [Host(i, Mock(), host_id=uuid.uuid4()) for i in range(node_count)] target_host = hosts[1] target_host.is_up = True @@ -95,5 +96,5 @@ def test_target_host_nominal(self): policy.populate(Mock(metadata=ClusterMetaMock({'127.0.0.1': target_host})), hosts) for _ in range(10): query_plan = list(policy.make_query_plan(None, Mock(target_host='127.0.0.1'))) - self.assertEqual(sorted(query_plan), hosts) - self.assertEqual(query_plan[0], target_host) + assert sorted(query_plan) == hosts + assert query_plan[0] == target_host diff --git a/tests/unit/column_encryption/test_policies.py b/tests/unit/column_encryption/test_policies.py index 27e7c62ce7..1bd83ecf89 100644 --- a/tests/unit/column_encryption/test_policies.py +++ b/tests/unit/column_encryption/test_policies.py @@ -18,6 +18,7 @@ from cassandra.policies import ColDesc from cassandra.column_encryption.policies import AES256ColumnEncryptionPolicy, \ AES256_BLOCK_SIZE_BYTES, AES256_KEY_SIZE_BYTES +import pytest @unittest.skip("Skip until https://github.com/scylladb/python-driver/issues/365 is sorted out") class AES256ColumnEncryptionPolicyTest(unittest.TestCase): @@ -33,7 +34,7 @@ def _test_round_trip(self, bytes): policy = AES256ColumnEncryptionPolicy() policy.add_column(coldesc, self._random_key(), "blob") encrypted_bytes = policy.encrypt(coldesc, bytes) - self.assertEqual(bytes, policy.decrypt(coldesc, encrypted_bytes)) + assert bytes == policy.decrypt(coldesc, encrypted_bytes) def test_no_padding_necessary(self): self._test_round_trip(self._random_block()) @@ -50,10 +51,10 @@ def test_add_column_invalid_key_size_raises(self): coldesc = ColDesc('ks1','table1','col1') policy = AES256ColumnEncryptionPolicy() for key_size in range(1,AES256_KEY_SIZE_BYTES - 1): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): policy.add_column(coldesc, os.urandom(key_size), "blob") for key_size in range(AES256_KEY_SIZE_BYTES + 1,(2 * AES256_KEY_SIZE_BYTES) - 1): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): policy.add_column(coldesc, os.urandom(key_size), "blob") def test_add_column_invalid_iv_size_raises(self): @@ -64,54 +65,54 @@ def test_iv_size(iv_size): coldesc = ColDesc('ks1','table1','col1') for iv_size in range(1,AES256_BLOCK_SIZE_BYTES - 1): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): test_iv_size(iv_size) for iv_size in range(AES256_BLOCK_SIZE_BYTES + 1,(2 * AES256_BLOCK_SIZE_BYTES) - 1): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): test_iv_size(iv_size) # Finally, confirm that the expected IV size has no issue test_iv_size(AES256_BLOCK_SIZE_BYTES) def test_add_column_null_coldesc_raises(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): policy = AES256ColumnEncryptionPolicy() policy.add_column(None, self._random_block(), "blob") def test_add_column_null_key_raises(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): policy = AES256ColumnEncryptionPolicy() coldesc = ColDesc('ks1','table1','col1') policy.add_column(coldesc, None, "blob") def test_add_column_null_type_raises(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): policy = AES256ColumnEncryptionPolicy() coldesc = ColDesc('ks1','table1','col1') policy.add_column(coldesc, self._random_block(), None) def test_add_column_unknown_type_raises(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): policy = AES256ColumnEncryptionPolicy() coldesc = ColDesc('ks1','table1','col1') policy.add_column(coldesc, self._random_block(), "foobar") def test_encode_and_encrypt_null_coldesc_raises(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): policy = AES256ColumnEncryptionPolicy() coldesc = ColDesc('ks1','table1','col1') policy.add_column(coldesc, self._random_key(), "blob") policy.encode_and_encrypt(None, self._random_block()) def test_encode_and_encrypt_null_obj_raises(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): policy = AES256ColumnEncryptionPolicy() coldesc = ColDesc('ks1','table1','col1') policy.add_column(coldesc, self._random_key(), "blob") policy.encode_and_encrypt(coldesc, None) def test_encode_and_encrypt_unknown_coldesc_raises(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): policy = AES256ColumnEncryptionPolicy() coldesc = ColDesc('ks1','table1','col1') policy.add_column(coldesc, self._random_key(), "blob") @@ -121,14 +122,14 @@ def test_contains_column(self): coldesc = ColDesc('ks1','table1','col1') policy = AES256ColumnEncryptionPolicy() policy.add_column(coldesc, self._random_key(), "blob") - self.assertTrue(policy.contains_column(coldesc)) - self.assertFalse(policy.contains_column(ColDesc('ks2','table1','col1'))) - self.assertFalse(policy.contains_column(ColDesc('ks1','table2','col1'))) - self.assertFalse(policy.contains_column(ColDesc('ks1','table1','col2'))) - self.assertFalse(policy.contains_column(ColDesc('ks2','table2','col2'))) + assert policy.contains_column(coldesc) + assert not policy.contains_column(ColDesc('ks2','table1','col1')) + assert not policy.contains_column(ColDesc('ks1','table2','col1')) + assert not policy.contains_column(ColDesc('ks1','table1','col2')) + assert not policy.contains_column(ColDesc('ks2','table2','col2')) def test_encrypt_unknown_column(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): policy = AES256ColumnEncryptionPolicy() coldesc = ColDesc('ks1','table1','col1') policy.add_column(coldesc, self._random_key(), "blob") @@ -139,7 +140,7 @@ def test_decrypt_unknown_column(self): coldesc = ColDesc('ks1','table1','col1') policy.add_column(coldesc, self._random_key(), "blob") encrypted_bytes = policy.encrypt(coldesc, self._random_block()) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): policy.decrypt(ColDesc('ks2','table2','col2'), encrypted_bytes) def test_cache_info(self): @@ -157,14 +158,14 @@ def test_cache_info(self): for _ in range(10): policy.encrypt(coldesc1, self._random_block()) cache_info = policy.cache_info() - self.assertEqual(cache_info.hits, 9) - self.assertEqual(cache_info.misses, 1) - self.assertEqual(cache_info.maxsize, 128) + assert cache_info.hits == 9 + assert cache_info.misses == 1 + assert cache_info.maxsize == 128 # Important note: we're measuring the size of the cache of ciphers, NOT stored # keys. We won't have a cipher here until we actually encrypt something - self.assertEqual(cache_info.currsize, 1) + assert cache_info.currsize == 1 policy.encrypt(coldesc2, self._random_block()) - self.assertEqual(policy.cache_info().currsize, 2) + assert policy.cache_info().currsize == 2 policy.encrypt(coldesc3, self._random_block()) - self.assertEqual(policy.cache_info().currsize, 3) + assert policy.cache_info().currsize == 3 diff --git a/tests/unit/cqlengine/test_columns.py b/tests/unit/cqlengine/test_columns.py index a7bf74ec23..136e8ba339 100644 --- a/tests/unit/cqlengine/test_columns.py +++ b/tests/unit/cqlengine/test_columns.py @@ -14,7 +14,7 @@ import unittest -from cassandra.cqlengine.columns import Column +from cassandra.cqlengine.columns import Column, List, Set, Map, Text, Integer class ColumnTest(unittest.TestCase): @@ -22,41 +22,41 @@ class ColumnTest(unittest.TestCase): def test_comparisons(self): c0 = Column() c1 = Column() - self.assertEqual(c1.position - c0.position, 1) + assert c1.position - c0.position == 1 # __ne__ - self.assertNotEqual(c0, c1) - self.assertNotEqual(c0, object()) + assert c0 != c1 + assert c0 != object() # __eq__ - self.assertEqual(c0, c0) - self.assertFalse(c0 == object()) + assert c0 == c0 + assert not c0 == object() # __lt__ - self.assertLess(c0, c1) + assert c0 < c1 try: c0 < object() # this raises for Python 3 except TypeError: pass # __le__ - self.assertLessEqual(c0, c1) - self.assertLessEqual(c0, c0) + assert c0 <= c1 + assert c0 <= c0 try: c0 <= object() # this raises for Python 3 except TypeError: pass # __gt__ - self.assertGreater(c1, c0) + assert c1 > c0 try: c1 > object() # this raises for Python 3 except TypeError: pass # __ge__ - self.assertGreaterEqual(c1, c0) - self.assertGreaterEqual(c1, c1) + assert c1 >= c0 + assert c1 >= c1 try: c1 >= object() # this raises for Python 3 except TypeError: @@ -64,5 +64,46 @@ def test_comparisons(self): def test_hash(self): c0 = Column() - self.assertEqual(id(c0), c0.__hash__()) + assert id(c0) == c0.__hash__() + + +class FrozenCollectionTest(unittest.TestCase): + """Test frozen parameter for collection columns (List, Set, Map).""" + + def test_list_default_not_frozen(self): + col = List(Text) + assert col.frozen is False + assert col.db_type == 'list' + + def test_list_frozen_true(self): + col = List(Text, frozen=True) + assert col.frozen is True + assert col.db_type == 'frozen>' + + def test_set_default_not_frozen(self): + col = Set(Text) + assert col.frozen is False + assert col.db_type == 'set' + + def test_set_frozen_true(self): + col = Set(Text, frozen=True) + assert col.frozen is True + assert col.db_type == 'frozen>' + + def test_map_default_not_frozen(self): + col = Map(Text, Integer) + assert col.frozen is False + assert col.db_type == 'map' + + def test_map_frozen_true(self): + col = Map(Text, Integer, frozen=True) + assert col.frozen is True + assert col.db_type == 'frozen>' + + def test_frozen_with_index(self): + """Test that frozen collections can be created with index=True.""" + col = List(Text, frozen=True, index=True) + assert col.frozen is True + assert col.index is True + assert col.db_type == 'frozen>' diff --git a/tests/unit/cqlengine/test_connection.py b/tests/unit/cqlengine/test_connection.py index 76266cff23..9bce715f4e 100644 --- a/tests/unit/cqlengine/test_connection.py +++ b/tests/unit/cqlengine/test_connection.py @@ -18,6 +18,7 @@ from cassandra.cluster import _ConfigMode from cassandra.cqlengine import connection from cassandra.query import dict_factory +import pytest class ConnectionTest(unittest.TestCase): @@ -26,10 +27,7 @@ class ConnectionTest(unittest.TestCase): def setUp(self): super(ConnectionTest, self).setUp() - self.assertFalse( - connection._connections, - 'Test precondition not met: connections are registered: {cs}'.format(cs=connection._connections) - ) + assert not connection._connections, 'Test precondition not met: connections are registered: {cs}'.format(cs=connection._connections) def test_set_session_without_existing_connection(self): """ @@ -49,12 +47,12 @@ def test_get_session_fails_without_existing_connection(self): """ Users can't get the default session without having a default connection set. """ - with self.assertRaisesRegex(connection.CQLEngineException, self.no_registered_connection_msg): + with pytest.raises(connection.CQLEngineException, match=self.no_registered_connection_msg): connection.get_session(connection=None) def test_get_cluster_fails_without_existing_connection(self): """ Users can't get the default cluster without having a default connection set. """ - with self.assertRaisesRegex(connection.CQLEngineException, self.no_registered_connection_msg): + with pytest.raises(connection.CQLEngineException, match=self.no_registered_connection_msg): connection.get_cluster(connection=None) diff --git a/tests/unit/cython/bytesio_testhelper.pyx b/tests/unit/cython/bytesio_testhelper.pyx index 7ba91bc4c0..595cd29cc8 100644 --- a/tests/unit/cython/bytesio_testhelper.pyx +++ b/tests/unit/cython/bytesio_testhelper.pyx @@ -13,32 +13,27 @@ # limitations under the License. from cassandra.bytesio cimport BytesIOReader +import pytest -def test_read1(assert_equal, assert_raises): +def test_read1(): cdef BytesIOReader reader = BytesIOReader(b'abcdef') - assert_equal(reader.read(2)[:2], b'ab') - assert_equal(reader.read(2)[:2], b'cd') - assert_equal(reader.read(0)[:0], b'') - assert_equal(reader.read(2)[:2], b'ef') + assert reader.read(2)[:2] == b'ab' + assert reader.read(2)[:2] == b'cd' + assert reader.read(0)[:0] == b'' + assert reader.read(2)[:2] == b'ef' -def test_read2(assert_equal, assert_raises): +def test_read2(): cdef BytesIOReader reader = BytesIOReader(b'abcdef') reader.read(5) reader.read(1) -def test_read3(assert_equal, assert_raises): +def test_read3(): cdef BytesIOReader reader = BytesIOReader(b'abcdef') reader.read(6) -def test_read_eof(assert_equal, assert_raises): +def test_read_eof(): cdef BytesIOReader reader = BytesIOReader(b'abcdef') reader.read(5) - # cannot convert reader.read to an object, do it manually - # assert_raises(EOFError, reader.read, 2) - try: + with pytest.raises(EOFError): reader.read(2) - except EOFError: - pass - else: - raise Exception("Expected an EOFError") reader.read(1) # see that we can still read this diff --git a/tests/unit/cython/test_bytesio.py b/tests/unit/cython/test_bytesio.py index cd4ea86f52..0f27663391 100644 --- a/tests/unit/cython/test_bytesio.py +++ b/tests/unit/cython/test_bytesio.py @@ -23,10 +23,10 @@ class BytesIOTest(unittest.TestCase): @cythontest def test_reading(self): - bytesio_testhelper.test_read1(self.assertEqual, self.assertRaises) - bytesio_testhelper.test_read2(self.assertEqual, self.assertRaises) - bytesio_testhelper.test_read3(self.assertEqual, self.assertRaises) + bytesio_testhelper.test_read1() + bytesio_testhelper.test_read2() + bytesio_testhelper.test_read3() @cythontest def test_reading_error(self): - bytesio_testhelper.test_read_eof(self.assertEqual, self.assertRaises) + bytesio_testhelper.test_read_eof() diff --git a/tests/unit/cython/test_types.py b/tests/unit/cython/test_types.py index 545b82fc11..996be266c0 100644 --- a/tests/unit/cython/test_types.py +++ b/tests/unit/cython/test_types.py @@ -22,8 +22,8 @@ class TypesTest(unittest.TestCase): @cythontest def test_datetype(self): - types_testhelper.test_datetype(self.assertEqual) + types_testhelper.test_datetype() @cythontest def test_date_side_by_side(self): - types_testhelper.test_date_side_by_side(self.assertEqual) + types_testhelper.test_date_side_by_side() diff --git a/tests/unit/cython/test_utils.py b/tests/unit/cython/test_utils.py index 0e79c235d8..b9b5cea554 100644 --- a/tests/unit/cython/test_utils.py +++ b/tests/unit/cython/test_utils.py @@ -23,4 +23,8 @@ class UtilsTest(unittest.TestCase): @cythontest def test_datetime_from_timestamp(self): - utils_testhelper.test_datetime_from_timestamp(self.assertEqual) + utils_testhelper.test_datetime_from_timestamp() + + @cythontest + def test_datetime_from_ms_timestamp(self): + utils_testhelper.test_datetime_from_ms_timestamp() diff --git a/tests/unit/cython/types_testhelper.pyx b/tests/unit/cython/types_testhelper.pyx index 66d2516319..81f9dca114 100644 --- a/tests/unit/cython/types_testhelper.pyx +++ b/tests/unit/cython/types_testhelper.pyx @@ -28,7 +28,7 @@ from cassandra.buffer cimport Buffer from cassandra.deserializers cimport from_binary, Deserializer -def test_datetype(assert_equal): +def test_datetype(): cdef Deserializer des = find_deserializer(DateType) @@ -52,27 +52,27 @@ def test_datetype(assert_equal): # deserialize # epoc expected = 0 - assert_equal(deserialize(expected), datetime.datetime.fromtimestamp(expected, tz=datetime.timezone.utc).replace(tzinfo=None)) + assert deserialize(expected) == datetime.datetime.fromtimestamp(expected, tz=datetime.timezone.utc).replace(tzinfo=None) # beyond 32b expected = 2 ** 33 - assert_equal(deserialize(expected), datetime.datetime(2242, 3, 16, 12, 56, 32)) + assert deserialize(expected) == datetime.datetime(2242, 3, 16, 12, 56, 32) # less than epoc (PYTHON-119) expected = -770172256 - assert_equal(deserialize(expected), datetime.datetime(1945, 8, 5, 23, 15, 44)) + assert deserialize(expected) == datetime.datetime(1945, 8, 5, 23, 15, 44) # work around rounding difference among Python versions (PYTHON-230) # This wont pass with the cython extension until we fix the microseconds alignment with CPython #expected = 1424817268.274 - #assert_equal(deserialize(expected), datetime.datetime(2015, 2, 24, 22, 34, 28, 274000)) + #assert deserialize(expected) == datetime.datetime(2015, 2, 24, 22, 34, 28, 274000) # Large date overflow (PYTHON-452) expected = 2177403010.123 - assert_equal(deserialize(expected), datetime.datetime(2038, 12, 31, 10, 10, 10, 123000)) + assert deserialize(expected) == datetime.datetime(2038, 12, 31, 10, 10, 10, 123000) -def test_date_side_by_side(assert_equal): +def test_date_side_by_side(): # Test pure python and cython date deserialization side-by-side # This is meant to detect inconsistent rounding or conversion (PYTHON-480 for example) # The test covers the full range of time deserializable in Python. It bounds through @@ -91,7 +91,7 @@ def test_date_side_by_side(assert_equal): buf.size = bior.size cython_deserialized = from_binary(cython_deserializer, &buf, 0) python_deserialized = DateType.deserialize(blob, 0) - assert_equal(cython_deserialized, python_deserialized) + assert cython_deserialized == python_deserialized # min -> 0 x = int(calendar.timegm(datetime.datetime(1, 1, 1).utctimetuple()) * 1000) diff --git a/tests/unit/cython/utils_testhelper.pyx b/tests/unit/cython/utils_testhelper.pyx index fe67691aa8..cf05c1806e 100644 --- a/tests/unit/cython/utils_testhelper.pyx +++ b/tests/unit/cython/utils_testhelper.pyx @@ -14,10 +14,23 @@ import datetime -from cassandra.cython_utils cimport datetime_from_timestamp +from cassandra.cython_utils cimport datetime_from_timestamp, datetime_from_ms_timestamp -def test_datetime_from_timestamp(assert_equal): - assert_equal(datetime_from_timestamp(1454781157.123456), datetime.datetime(2016, 2, 6, 17, 52, 37, 123456)) +def test_datetime_from_timestamp(): + assert datetime_from_timestamp(1454781157.123456) == datetime.datetime(2016, 2, 6, 17, 52, 37, 123456) # PYTHON-452 - assert_equal(datetime_from_timestamp(2177403010.123456), datetime.datetime(2038, 12, 31, 10, 10, 10, 123456)) + assert datetime_from_timestamp(2177403010.123456) == datetime.datetime(2038, 12, 31, 10, 10, 10, 123456) + + +def test_datetime_from_ms_timestamp(): + # epoch + assert datetime_from_ms_timestamp(0) == datetime.datetime(1970, 1, 1) + # positive with millisecond precision + assert datetime_from_ms_timestamp(1454781157123) == datetime.datetime(2016, 2, 6, 17, 52, 37, 123000) + # large positive far from epoch (GH-532) + assert datetime_from_ms_timestamp(10413792000001) == datetime.datetime(2300, 1, 1, 0, 0, 0, 1000) + # negative timestamp + assert datetime_from_ms_timestamp(-770172256000) == datetime.datetime(1945, 8, 5, 23, 15, 44) + # large negative with millisecond precision + assert datetime_from_ms_timestamp(-11676095999999) == datetime.datetime(1600, 1, 1, 0, 0, 0, 1000) diff --git a/tests/unit/io/eventlet_utils.py b/tests/unit/io/eventlet_utils.py index 785856be20..2a5a8c78d0 100644 --- a/tests/unit/io/eventlet_utils.py +++ b/tests/unit/io/eventlet_utils.py @@ -16,21 +16,15 @@ import os import select import socket -try: - import thread - import Queue - import __builtin__ - #For python3 compatibility -except ImportError: - import _thread as thread - import queue as Queue - import builtins as __builtin__ +import _thread as thread +import queue as Queue +import builtins as __builtin__ import threading import ssl import time import eventlet -from imp import reload +from importlib import reload def eventlet_un_patch_all(): """ diff --git a/tests/unit/io/test_asyncioreactor.py b/tests/unit/io/test_asyncioreactor.py index a6179e122d..f3ed942090 100644 --- a/tests/unit/io/test_asyncioreactor.py +++ b/tests/unit/io/test_asyncioreactor.py @@ -1,7 +1,6 @@ AsyncioConnection, ASYNCIO_AVAILABLE = None, False try: from cassandra.io.asyncioreactor import AsyncioConnection - import asynctest ASYNCIO_AVAILABLE = True except (ImportError, SyntaxError, AttributeError): AsyncioConnection = None @@ -10,8 +9,8 @@ from tests import is_monkey_patched, connection_class from tests.unit.io.utils import TimerCallback, TimerTestMixin -from unittest.mock import patch - +from unittest.mock import patch, MagicMock +import selectors import unittest import time @@ -56,7 +55,7 @@ def setUp(self): socket_patcher.start() old_selector = AsyncioConnection._loop._selector - AsyncioConnection._loop._selector = asynctest.TestSelector() + AsyncioConnection._loop._selector = MagicMock(spec=selectors.BaseSelector) def reset_selector(): AsyncioConnection._loop._selector = old_selector @@ -74,4 +73,4 @@ def test_timer_cancellation(self): # Release context allow for timer thread to run. time.sleep(.2) # Assert that the cancellation was honored - self.assertFalse(callback.was_invoked()) + assert not callback.was_invoked() diff --git a/tests/unit/io/test_eventletreactor.py b/tests/unit/io/test_eventletreactor.py index e49f2459c1..d3962196a4 100644 --- a/tests/unit/io/test_eventletreactor.py +++ b/tests/unit/io/test_eventletreactor.py @@ -14,12 +14,12 @@ import unittest -from mock import patch + +from unittest.mock import patch from tests.unit.io.utils import TimerTestMixin from tests import notpypy, EVENT_LOOP_MANAGER -from unittest.mock import patch try: from eventlet import monkey_patch diff --git a/tests/unit/io/test_libevreactor.py b/tests/unit/io/test_libevreactor.py index 17e03d0fd5..cf7e7caf77 100644 --- a/tests/unit/io/test_libevreactor.py +++ b/tests/unit/io/test_libevreactor.py @@ -83,8 +83,8 @@ def test_watchers_are_finished(self): # be called libev__cleanup(_global_loop) for conn in live_connections: - self.assertTrue(conn._write_watcher.stop.mock_calls) - self.assertTrue(conn._read_watcher.stop.mock_calls) + assert conn._write_watcher.stop.mock_calls + assert conn._read_watcher.stop.mock_calls _global_loop._shutdown = False diff --git a/tests/unit/io/test_libevreactor_shutdown.py b/tests/unit/io/test_libevreactor_shutdown.py new file mode 100644 index 0000000000..9578d22df1 --- /dev/null +++ b/tests/unit/io/test_libevreactor_shutdown.py @@ -0,0 +1,236 @@ +# Copyright ScyllaDB, Inc. +# +# 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 +# +# http://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. + +""" +Test to demonstrate the libevwrapper atexit cleanup issue. + +This test demonstrates the problem where the atexit callback is registered +with _global_loop=None at import time, causing it to receive None during +shutdown instead of the actual loop instance. +""" + +import unittest +import sys +import subprocess +import tempfile +import os +from pathlib import Path + +from cassandra import DependencyException + +try: + from cassandra.io.libevreactor import LibevConnection +except (ImportError, DependencyException): + LibevConnection = None + +from tests import is_monkey_patched + + +class LibevAtexitCleanupTest(unittest.TestCase): + """ + Test case to demonstrate the atexit cleanup bug in libevreactor. + + The bug: atexit.register(partial(_cleanup, _global_loop)) is called when + _global_loop is None, so the cleanup function receives None at shutdown + instead of the actual LibevLoop instance that was created later. + """ + + def setUp(self): + if is_monkey_patched(): + raise unittest.SkipTest("Can't test libev with monkey patching") + if LibevConnection is None: + raise unittest.SkipTest('libev does not appear to be installed correctly') + + def test_atexit_callback_uses_current_global_loop(self): + """ + Test that verifies the atexit callback fix. + + The fix uses a wrapper function _atexit_cleanup() that looks up the + current value of _global_loop at shutdown time, instead of capturing + it at import time with partial(). + + @since 3.29 + @jira_ticket PYTHON-XXX + @expected_result The atexit handler calls cleanup with the actual loop + + @test_category connection + """ + from cassandra.io import libevreactor + + # Verify the fix: _atexit_cleanup should exist as a module-level function + self.assertTrue(hasattr(libevreactor, '_atexit_cleanup'), + "Module should have _atexit_cleanup function") + + # Verify it's not a partial (the old buggy implementation) + from functools import partial + self.assertNotIsInstance(libevreactor._atexit_cleanup, partial, + "The _atexit_cleanup should NOT be a partial function") + + # Verify it's actually a function + self.assertTrue(callable(libevreactor._atexit_cleanup), + "_atexit_cleanup should be callable") + + # Initialize the reactor + LibevConnection.initialize_reactor() + + # At this point, libevreactor._global_loop is not None + self.assertIsNotNone(libevreactor._global_loop, + "Global loop should be initialized") + + # The fix: _atexit_cleanup is a function that will look up + # _global_loop when it's called, not a partial with captured args + self.assertEqual(libevreactor._atexit_cleanup.__name__, '_atexit_cleanup', + "The function should have the correct name") + + def test_shutdown_cleanup_works_with_fix(self): + """ + Test that verifies the atexit cleanup fix works in a subprocess. + + This test creates a minimal script that: + 1. Imports the driver + 2. Initializes the reactor (creates the global loop) + 3. Verifies the _atexit_cleanup function is available + 4. Exits without explicit cleanup + + With the fix, atexit should properly clean up the loop using the + wrapper function that looks up _global_loop at shutdown time. + + @since 3.29 + @jira_ticket PYTHON-XXX + @expected_result The subprocess shows the fix is working + + @test_category connection + """ + # Create a test script that verifies the fix + test_script = ''' +import sys +import os + +# Add the driver path +sys.path.insert(0, {driver_path!r}) + +# Import and setup +from cassandra.io import libevreactor +from cassandra.io.libevreactor import LibevConnection +import atexit + +# Initialize the reactor (creates the global loop) +LibevConnection.initialize_reactor() + +print("Global loop initialized:", libevreactor._global_loop is not None) + +# Verify the fix is in place: _atexit_cleanup should be a module-level function +if hasattr(libevreactor, '_atexit_cleanup'): + print("FIXED: Module has _atexit_cleanup function") + print("This function will look up _global_loop at shutdown time") + # Verify it's not using partial with None + import inspect + source = inspect.getsource(libevreactor._atexit_cleanup) + if "global _global_loop" in source and "_global_loop is not None" in source: + print("Verified: _atexit_cleanup uses current _global_loop value") +else: + print("BUG: No _atexit_cleanup function found") + +# Exit without explicit cleanup - atexit should handle it properly with the fix! +print("Exiting with proper cleanup...") +''' + + driver_path = str(Path(__file__).parent.parent.parent.parent) + script_content = test_script.format(driver_path=driver_path) + + with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: + f.write(script_content) + script_path = f.name + + try: + result = subprocess.run( + [sys.executable, script_path], + capture_output=True, + text=True, + timeout=5 + ) + + output = result.stdout + print("\n=== Subprocess Output ===") + print(output) + print("=== End Output ===\n") + + # Verify the output shows the fix is working + self.assertIn("Global loop initialized: True", output) + self.assertIn("FIXED: Module has _atexit_cleanup function", output) + self.assertIn("Verified: _atexit_cleanup uses current _global_loop value", output) + self.assertNotIn("BUG", output.replace("BUG STILL PRESENT", "").replace("DEBUG", "")) # Allow "BUG" only in success message + + finally: + os.unlink(script_path) + + +class LibevShutdownRaceConditionTest(unittest.TestCase): + """ + Tests to analyze potential race conditions and crashes during shutdown. + """ + + def setUp(self): + if is_monkey_patched(): + raise unittest.SkipTest("Can't test libev with monkey patching") + if LibevConnection is None: + raise unittest.SkipTest('libev does not appear to be installed correctly') + + def test_cleanup_with_fix_properly_shuts_down(self): + """ + Test to verify the fix properly shuts down the event loop. + + With the fix in place, the atexit cleanup will: + 1. Look up the current _global_loop value (not None) + 2. Call _cleanup with the actual loop instance + 3. Properly shut down the loop and its watchers + + This prevents the crash scenario where: + - Various modules are being torn down during Python shutdown + - The libev event loop is still running + - Callbacks fire and try to access deallocated Python objects + + @since 3.29 + @jira_ticket PYTHON-XXX + @expected_result Cleanup properly shuts down the loop with the fix + + @test_category connection + """ + from cassandra.io import libevreactor + from cassandra.io.libevreactor import _cleanup, _atexit_cleanup + + LibevConnection.initialize_reactor() + + # Verify the loop exists + self.assertIsNotNone(libevreactor._global_loop) + + # Before cleanup, the loop should not be shut down + self.assertFalse(libevreactor._global_loop._shutdown, + "Loop should not be shut down initially") + + # Simulate what the OLD buggy code would do + _cleanup(None) # This does nothing + self.assertFalse(libevreactor._global_loop._shutdown, + "Loop should NOT be shut down when cleanup receives None") + + # Now test the FIX: call the wrapper that looks up _global_loop + _atexit_cleanup() # This is what atexit will actually call + + # With the fix, the loop should be properly shut down + self.assertTrue(libevreactor._global_loop._shutdown, + "Loop should be shut down when _atexit_cleanup is called") + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/io/test_twistedreactor.py b/tests/unit/io/test_twistedreactor.py index fd17d8454f..8ba9ca5b1d 100644 --- a/tests/unit/io/test_twistedreactor.py +++ b/tests/unit/io/test_twistedreactor.py @@ -76,7 +76,7 @@ def test_makeConnection(self): object that a successful connection was made. """ self.obj_ut.makeConnection(self.tr) - self.assertTrue(self.mock_connection.client_connection_made.called) + assert self.mock_connection.client_connection_made.called def test_receiving_data(self): """ @@ -85,7 +85,7 @@ def test_receiving_data(self): """ self.obj_ut.makeConnection(self.tr) self.obj_ut.dataReceived('foobar') - self.assertTrue(self.mock_connection.handle_read.called) + assert self.mock_connection.handle_read.called self.mock_connection._iobuf.write.assert_called_with("foobar") @@ -99,14 +99,23 @@ def setUp(self): self.reactor_cft_patcher = patch( 'twisted.internet.reactor.callFromThread') self.reactor_run_patcher = patch('twisted.internet.reactor.run') + # Patch reactor.running to False so maybe_start() always enters + # the branch that spawns the reactor thread. Without this, leaked + # reactor state from prior tests can cause reactor.running to be + # True, making maybe_start() a no-op and the reactor.run mock + # never called — leading to a flaky test_connection_initialization. + self.reactor_running_patcher = patch( + 'twisted.internet.reactor.running', new=False) self.mock_reactor_cft = self.reactor_cft_patcher.start() self.mock_reactor_run = self.reactor_run_patcher.start() + self.reactor_running_patcher.start() self.obj_ut = twistedreactor.TwistedConnection(DefaultEndPoint('1.2.3.4'), cql_version='3.0.1') def tearDown(self): self.reactor_cft_patcher.stop() self.reactor_run_patcher.stop() + self.reactor_running_patcher.stop() def test_connection_initialization(self): """ @@ -136,36 +145,35 @@ def test_close(self, mock_connectTCP): self.obj_ut.is_closed = False self.obj_ut.close() - self.assertTrue(self.obj_ut.connected_event.is_set()) - self.assertTrue(self.obj_ut.error_all_requests.called) + assert self.obj_ut.connected_event.is_set() + assert self.obj_ut.error_all_requests.called def test_handle_read__incomplete(self): """ Verify that handle_read() processes incomplete messages properly. """ self.obj_ut.process_msg = Mock() - self.assertEqual(self.obj_ut._iobuf.getvalue(), b'') # buf starts empty + assert self.obj_ut._iobuf.getvalue() == b'' # buf starts empty # incomplete header self.obj_ut._iobuf.write(b'\x84\x00\x00\x00\x00') self.obj_ut.handle_read() - self.assertEqual(self.obj_ut._io_buffer.cql_frame_buffer.getvalue(), b'\x84\x00\x00\x00\x00') + assert self.obj_ut._io_buffer.cql_frame_buffer.getvalue() == b'\x84\x00\x00\x00\x00' # full header, but incomplete body self.obj_ut._iobuf.write(b'\x00\x00\x00\x15') self.obj_ut.handle_read() - self.assertEqual(self.obj_ut._io_buffer.cql_frame_buffer.getvalue(), - b'\x84\x00\x00\x00\x00\x00\x00\x00\x15') - self.assertEqual(self.obj_ut._current_frame.end_pos, 30) + assert self.obj_ut._io_buffer.cql_frame_buffer.getvalue() == b'\x84\x00\x00\x00\x00\x00\x00\x00\x15' + assert self.obj_ut._current_frame.end_pos == 30 # verify we never attempted to process the incomplete message - self.assertFalse(self.obj_ut.process_msg.called) + assert not self.obj_ut.process_msg.called def test_handle_read__fullmessage(self): """ Verify that handle_read() processes complete messages properly. """ self.obj_ut.process_msg = Mock() - self.assertEqual(self.obj_ut._iobuf.getvalue(), b'') # buf starts empty + assert self.obj_ut._iobuf.getvalue() == b'' # buf starts empty # write a complete message, plus 'NEXT' (to simulate next message) # assumes protocol v3+ as default Connection.protocol_version @@ -174,7 +182,7 @@ def test_handle_read__fullmessage(self): self.obj_ut._iobuf.write( b'\x84\x01\x00\x02\x03\x00\x00\x00\x15' + body + extra) self.obj_ut.handle_read() - self.assertEqual(self.obj_ut._io_buffer.cql_frame_buffer.getvalue(), extra) + assert self.obj_ut._io_buffer.cql_frame_buffer.getvalue() == extra self.obj_ut.process_msg.assert_called_with( _Frame(version=4, flags=1, stream=2, opcode=3, body_offset=9, end_pos=9 + len(body)), body) diff --git a/tests/unit/io/utils.py b/tests/unit/io/utils.py index a26d898c5f..f43224058c 100644 --- a/tests/unit/io/utils.py +++ b/tests/unit/io/utils.py @@ -36,6 +36,7 @@ from socket import error as socket_error import ssl import time +import pytest log = logging.getLogger(__name__) @@ -127,8 +128,10 @@ def submit_and_wait_for_completion(unit_test, create_timer, start, end, incremen time.sleep(.1) # ensure they are all called back in a timely fashion + # Use a generous tolerance (500ms) to account for CI environments under heavy load, + # especially Windows during wheel building where timing can be significantly less precise for callback in completed_callbacks: - unit_test.assertAlmostEqual(callback.expected_wait, callback.get_wait_time(), delta=.15) + assert callback.expected_wait == pytest.approx(callback.get_wait_time(), abs=.5) def noop_if_monkey_patched(f): @@ -181,9 +184,9 @@ def test_timer_cancellation(self): time.sleep(timeout * 2) timer_manager = self._timers # Assert that the cancellation was honored - self.assertFalse(timer_manager._queue) - self.assertFalse(timer_manager._new_timers) - self.assertFalse(callback.was_invoked()) + assert not timer_manager._queue + assert not timer_manager._new_timers + assert not callback.was_invoked() class ReactorTestMixin(object): @@ -197,10 +200,11 @@ def get_socket(self, connection): def set_socket(self, connection, obj): return setattr(connection, self.socket_attr_name, obj) - def make_header_prefix(self, message_class, version=2, stream_id=0): + def make_header_prefix(self, message_class, version=3, stream_id=0): return bytes().join(map(uint8_pack, [ 0xff & (HEADER_DIRECTION_TO_CLIENT | version), 0, # flags (compression) + 0, # MSB for v3+ stream stream_id, message_class.opcode # opcode ])) @@ -248,7 +252,7 @@ def test_successful_connection(self): self.get_socket(c).recv.return_value = self.make_msg(header) c.handle_read(*self.null_handle_function_args) - self.assertTrue(c.connected_event.is_set()) + assert c.connected_event.is_set() return c def test_eagain_on_buffer_size(self): @@ -309,7 +313,7 @@ def chunk(size): # Ensure the message size is the good one and that the # message has been processed if it is non-empty - self.assertEqual(c._io_buffer.io_buffer.tell(), expected_size) + assert c._io_buffer.io_buffer.tell() == expected_size if expected_size == 0: c.process_io_buffer.assert_not_called() else: @@ -328,9 +332,9 @@ def test_protocol_error(self): c.handle_read(*self.null_handle_function_args) # make sure it errored correctly - self.assertTrue(c.is_defunct) - self.assertTrue(c.connected_event.is_set()) - self.assertIsInstance(c.last_error, ProtocolError) + assert c.is_defunct + assert c.connected_event.is_set() + assert isinstance(c.last_error, ProtocolError) def test_error_message_on_startup(self): c = self.make_connection() @@ -353,9 +357,9 @@ def test_error_message_on_startup(self): c.handle_read(*self.null_handle_function_args) # make sure it errored correctly - self.assertTrue(c.is_defunct) - self.assertIsInstance(c.last_error, ConnectionException) - self.assertTrue(c.connected_event.is_set()) + assert c.is_defunct + assert isinstance(c.last_error, ConnectionException) + assert c.connected_event.is_set() def test_socket_error_on_write(self): c = self.make_connection() @@ -365,9 +369,9 @@ def test_socket_error_on_write(self): c.handle_write(*self.null_handle_function_args) # make sure it errored correctly - self.assertTrue(c.is_defunct) - self.assertIsInstance(c.last_error, socket_error) - self.assertTrue(c.connected_event.is_set()) + assert c.is_defunct + assert isinstance(c.last_error, socket_error) + assert c.connected_event.is_set() def test_blocking_on_write(self): c = self.make_connection() @@ -377,13 +381,13 @@ def test_blocking_on_write(self): "socket busy") c.handle_write(*self.null_handle_function_args) - self.assertFalse(c.is_defunct) + assert not c.is_defunct # try again with normal behavior self.get_socket(c).send.side_effect = lambda x: len(x) c.handle_write(*self.null_handle_function_args) - self.assertFalse(c.is_defunct) - self.assertTrue(self.get_socket(c).send.call_args is not None) + assert not c.is_defunct + assert self.get_socket(c).send.call_args is not None def test_partial_send(self): c = self.make_connection() @@ -398,10 +402,9 @@ def test_partial_send(self): expected_writes = int(math.ceil(float(msg_size) / write_size)) size_mod = msg_size % write_size last_write_size = size_mod if size_mod else write_size - self.assertFalse(c.is_defunct) - self.assertEqual(expected_writes, self.get_socket(c).send.call_count) - self.assertEqual(last_write_size, - len(self.get_socket(c).send.call_args[0][0])) + assert not c.is_defunct + assert expected_writes == self.get_socket(c).send.call_count + assert last_write_size == len(self.get_socket(c).send.call_args[0][0]) def test_socket_error_on_read(self): c = self.make_connection() @@ -415,9 +418,9 @@ def test_socket_error_on_read(self): c.handle_read(*self.null_handle_function_args) # make sure it errored correctly - self.assertTrue(c.is_defunct) - self.assertIsInstance(c.last_error, socket_error) - self.assertTrue(c.connected_event.is_set()) + assert c.is_defunct + assert isinstance(c.last_error, socket_error) + assert c.connected_event.is_set() def test_partial_header_read(self): c = self.make_connection() @@ -428,11 +431,11 @@ def test_partial_header_read(self): self.get_socket(c).recv.return_value = message[0:1] c.handle_read(*self.null_handle_function_args) - self.assertEqual(c._io_buffer.cql_frame_buffer.getvalue(), message[0:1]) + assert c._io_buffer.cql_frame_buffer.getvalue() == message[0:1] self.get_socket(c).recv.return_value = message[1:] c.handle_read(*self.null_handle_function_args) - self.assertEqual(bytes(), c._io_buffer.io_buffer.getvalue()) + assert bytes() == c._io_buffer.io_buffer.getvalue() # let it write out a StartupMessage c.handle_write(*self.null_handle_function_args) @@ -441,8 +444,8 @@ def test_partial_header_read(self): self.get_socket(c).recv.return_value = self.make_msg(header) c.handle_read(*self.null_handle_function_args) - self.assertTrue(c.connected_event.is_set()) - self.assertFalse(c.is_defunct) + assert c.connected_event.is_set() + assert not c.is_defunct def test_partial_message_read(self): c = self.make_connection() @@ -454,12 +457,12 @@ def test_partial_message_read(self): # read in the first nine bytes self.get_socket(c).recv.return_value = message[:9] c.handle_read(*self.null_handle_function_args) - self.assertEqual(c._io_buffer.cql_frame_buffer.getvalue(), message[:9]) + assert c._io_buffer.cql_frame_buffer.getvalue() == message[:9] # ... then read in the rest self.get_socket(c).recv.return_value = message[9:] c.handle_read(*self.null_handle_function_args) - self.assertEqual(bytes(), c._io_buffer.io_buffer.getvalue()) + assert bytes() == c._io_buffer.io_buffer.getvalue() # let it write out a StartupMessage c.handle_write(*self.null_handle_function_args) @@ -468,8 +471,8 @@ def test_partial_message_read(self): self.get_socket(c).recv.return_value = self.make_msg(header) c.handle_read(*self.null_handle_function_args) - self.assertTrue(c.connected_event.is_set()) - self.assertFalse(c.is_defunct) + assert c.connected_event.is_set() + assert not c.is_defunct def test_mixed_message_and_buffer_sizes(self): """ diff --git a/tests/unit/test_auth.py b/tests/unit/test_auth.py index 0a2427c7ff..776cbd6973 100644 --- a/tests/unit/test_auth.py +++ b/tests/unit/test_auth.py @@ -22,7 +22,4 @@ class TestPlainTextAuthenticator(unittest.TestCase): def test_evaluate_challenge_with_unicode_data(self): authenticator = PlainTextAuthenticator("johnӁ", "doeӁ") - self.assertEqual( - authenticator.evaluate_challenge(b'PLAIN-START'), - "\x00johnӁ\x00doeӁ".encode('utf-8') - ) + assert authenticator.evaluate_challenge(b'PLAIN-START') == "\x00johnӁ\x00doeӁ".encode('utf-8') diff --git a/tests/unit/test_client_routes.py b/tests/unit/test_client_routes.py new file mode 100644 index 0000000000..0aa82fc76a --- /dev/null +++ b/tests/unit/test_client_routes.py @@ -0,0 +1,482 @@ +# Copyright 2026 ScyllaDB, Inc. +# +# 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 +# +# http://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 socket +import ssl +import unittest +import uuid +from unittest.mock import Mock, patch + +from cassandra.client_routes import ( + ClientRouteProxy, + ClientRoutesChangeType, + ClientRoutesConfig, + _RouteStore, + _Route, + _ClientRoutesHandler +) +from cassandra.connection import ClientRoutesEndPoint, ClientRoutesEndPointFactory +from cassandra.cluster import Cluster + + +class TestClientRouteProxy(unittest.TestCase): + + def test_endpoint_none_connection_id(self): + with self.assertRaises(ValueError): + ClientRouteProxy(None) + + +class TestClientRoutesConfig(unittest.TestCase): + + def test_config_with_proxies(self): + ep1 = ClientRouteProxy(str(uuid.uuid4()), "10.0.0.1") + ep2 = ClientRouteProxy(str(uuid.uuid4()), "10.0.0.2") + config = ClientRoutesConfig([ep1, ep2]) + self.assertEqual(len(config.proxies), 2) + + def test_config_empty_proxies(self): + with self.assertRaises(ValueError): + ClientRoutesConfig([]) + + def test_config_invalid_proxy_type(self): + with self.assertRaises(TypeError): + ClientRoutesConfig(["not-a-proxy"]) + + + +class TestRouteStore(unittest.TestCase): + + def test_get_by_host_id(self): + routes = _RouteStore() + host_id = uuid.uuid4() + route = _Route( + connection_id=str(uuid.uuid4()), + host_id=host_id, + address="example.com", + port=9042, + ) + + routes.update([route]) + + retrieved = routes.get_by_host_id(host_id) + self.assertEqual(retrieved.host_id, host_id) + self.assertEqual(retrieved.address, "example.com") + + def test_merge_routes(self): + routes = _RouteStore() + host_id1 = uuid.uuid4() + host_id2 = uuid.uuid4() + + route1 = _Route( + connection_id=str(uuid.uuid4()), host_id=host_id1, + address="host1.com", port=9042, + ) + + route2 = _Route( + connection_id=str(uuid.uuid4()), host_id=host_id2, + address="host2.com", port=9042, + ) + + routes.update([route1]) + routes.merge([route2], affected_host_ids={host_id2}) + + self.assertIsNotNone(routes.get_by_host_id(host_id1)) + self.assertIsNotNone(routes.get_by_host_id(host_id2)) + + def test_merge_deletes_affected_host_with_no_new_route(self): + """When an affected host_id has no corresponding new route, it should be removed.""" + store = _RouteStore() + host_id1 = uuid.uuid4() + host_id2 = uuid.uuid4() + conn_id = str(uuid.uuid4()) + + store.update([ + _Route(connection_id=conn_id, host_id=host_id1, address="a.com", port=9042), + _Route(connection_id=conn_id, host_id=host_id2, address="b.com", port=9042), + ]) + self.assertIsNotNone(store.get_by_host_id(host_id1)) + self.assertIsNotNone(store.get_by_host_id(host_id2)) + + # Merge with host_id2 affected but no new route for it → deletion + store.merge([], affected_host_ids={host_id2}) + + self.assertIsNotNone(store.get_by_host_id(host_id1)) + self.assertIsNone(store.get_by_host_id(host_id2)) + + def test_select_preferred_routes_keeps_existing_connection_id(self): + """When multiple connection_ids provide routes for the same host_id, + the one already in use should be preferred.""" + store = _RouteStore() + host_id = uuid.uuid4() + conn_a = "conn-a" + conn_b = "conn-b" + + # Populate store with conn_a for host_id + store.update([_Route(connection_id=conn_a, host_id=host_id, address="a.com", port=9042)]) + self.assertEqual(store.get_by_host_id(host_id).connection_id, conn_a) + + # Update with both conn_a and conn_b for the same host_id + store.update([ + _Route(connection_id=conn_b, host_id=host_id, address="b.com", port=9042), + _Route(connection_id=conn_a, host_id=host_id, address="a-new.com", port=9042), + ]) + # conn_a should be preferred since it was already in use + result = store.get_by_host_id(host_id) + self.assertEqual(result.connection_id, conn_a) + self.assertEqual(result.address, "a-new.com") + + def test_select_preferred_routes_falls_back_when_existing_gone(self): + """When the existing connection_id is no longer among candidates, + the first candidate should be selected.""" + store = _RouteStore() + host_id = uuid.uuid4() + + store.update([_Route(connection_id="old-conn", host_id=host_id, address="old.com", port=9042)]) + + # Update only has new connection_ids + store.update([ + _Route(connection_id="new-a", host_id=host_id, address="a.com", port=9042), + _Route(connection_id="new-b", host_id=host_id, address="b.com", port=9042), + ]) + result = store.get_by_host_id(host_id) + self.assertEqual(result.connection_id, "new-a") + + +class TestClientRoutesHandler(unittest.TestCase): + + def setUp(self): + self.conn_id = uuid.uuid4() + self.proxy = ClientRouteProxy(str(self.conn_id), "10.0.0.1") + self.config = ClientRoutesConfig([self.proxy]) + + def test_handler_initialization(self): + handler = _ClientRoutesHandler(self.config, ssl_enabled=False) + self.assertIsNotNone(handler) + self.assertEqual(handler.ssl_enabled, False) + + @patch.object(_ClientRoutesHandler, '_query_all_routes_for_connections') + def test_initialize(self, mock_query): + host_id = uuid.uuid4() + mock_query.return_value = [ + _Route( + connection_id=self.conn_id, + host_id=host_id, + address="node1.example.com", + port=9042, + ) + ] + + handler = _ClientRoutesHandler(self.config) + mock_conn = Mock() + + handler.initialize(mock_conn, timeout=5.0) + + mock_query.assert_called_once() + route = handler._routes.get_by_host_id(host_id) + self.assertIsNotNone(route) + self.assertEqual(route.address, "node1.example.com") + + @patch.object(_ClientRoutesHandler, '_query_routes_for_change_event') + def test_handle_change_filters_by_configured_connection_ids(self, mock_query): + """Events with unrelated connection_ids should be ignored.""" + handler = _ClientRoutesHandler(self.config) + mock_conn = Mock() + host_id = str(uuid.uuid4()) + + # Event with a connection_id NOT in our config → should return early + handler.handle_client_routes_change( + mock_conn, 5.0, + ClientRoutesChangeType.UPDATE_NODES, + connection_ids=["unrelated-conn-id"], + host_ids=[host_id], + ) + mock_query.assert_not_called() + + @patch.object(_ClientRoutesHandler, '_query_routes_for_change_event') + def test_handle_change_merges_when_host_ids_present(self, mock_query): + """When host_ids are provided, routes should be merged (not full replace).""" + handler = _ClientRoutesHandler(self.config) + mock_conn = Mock() + + existing_host = uuid.uuid4() + new_host = uuid.uuid4() + conn_id = str(self.conn_id) + + # Pre-populate a route + handler._routes.update([ + _Route(connection_id=conn_id, host_id=existing_host, address="old.com", port=9042), + ]) + + mock_query.return_value = [ + _Route(connection_id=conn_id, host_id=new_host, address="new.com", port=9042), + ] + + handler.handle_client_routes_change( + mock_conn, 5.0, + ClientRoutesChangeType.UPDATE_NODES, + connection_ids=[conn_id], + host_ids=[str(new_host)], + ) + + # Existing route should still be there (merge, not replace) + self.assertIsNotNone(handler._routes.get_by_host_id(existing_host)) + self.assertIsNotNone(handler._routes.get_by_host_id(new_host)) + + @patch.object(_ClientRoutesHandler, '_query_all_routes_for_connections') + def test_handle_change_updates_when_no_host_ids(self, mock_query): + """When no host_ids are provided, routes should be fully replaced.""" + handler = _ClientRoutesHandler(self.config) + mock_conn = Mock() + conn_id = str(self.conn_id) + + old_host = uuid.uuid4() + handler._routes.update([ + _Route(connection_id=conn_id, host_id=old_host, address="old.com", port=9042), + ]) + + new_host = uuid.uuid4() + mock_query.return_value = [ + _Route(connection_id=conn_id, host_id=new_host, address="new.com", port=9042), + ] + + handler.handle_client_routes_change( + mock_conn, 5.0, + ClientRoutesChangeType.UPDATE_NODES, + connection_ids=None, + host_ids=None, + ) + + # Full replace: old_host gone, new_host present + self.assertIsNone(handler._routes.get_by_host_id(old_host)) + self.assertIsNotNone(handler._routes.get_by_host_id(new_host)) + + @patch.object(_ClientRoutesHandler, '_query_routes_for_change_event') + def test_handle_change_propagates_query_failure(self, mock_query): + """If _query_routes raises, handle_client_routes_change should propagate.""" + handler = _ClientRoutesHandler(self.config) + mock_conn = Mock() + mock_query.side_effect = Exception("network error") + + conn_id = self.proxy.connection_id + host_id = str(uuid.uuid4()) + with self.assertRaises(Exception) as cm: + handler.handle_client_routes_change( + mock_conn, 5.0, + ClientRoutesChangeType.UPDATE_NODES, + connection_ids=[conn_id], + host_ids=[host_id], + ) + self.assertIn("network error", str(cm.exception)) + + @patch.object(_ClientRoutesHandler, '_query_all_routes_for_connections') + def test_initialize_propagates_exception_on_failure(self, mock_query): + """initialize should propagate exceptions to caller.""" + handler = _ClientRoutesHandler(self.config) + mock_conn = Mock() + mock_query.side_effect = Exception("query failed") + + with self.assertRaises(Exception) as ctx: + handler.initialize(mock_conn, 5.0) + self.assertIn("query failed", str(ctx.exception)) + self.assertEqual(mock_query.call_count, 1) + + @patch.object(_ClientRoutesHandler, '_query_all_routes_for_connections') + def test_initialize_keeps_old_routes_on_failure(self, mock_query): + """On failure, existing routes must be preserved (critical for PL clusters).""" + handler = _ClientRoutesHandler(self.config) + mock_conn = Mock() + host_id = uuid.uuid4() + + # Pre-populate a route + handler._routes.update([ + _Route(connection_id=str(self.conn_id), host_id=host_id, address="old.com", port=9042), + ]) + + mock_query.side_effect = Exception("query failed") + with self.assertRaises(Exception): + handler.initialize(mock_conn, 5.0) + + # Old route must still be there + self.assertIsNotNone(handler._routes.get_by_host_id(host_id)) + + @patch.object(_ClientRoutesHandler, '_query_all_routes_for_connections') + def test_initialize_updates_routes_on_success(self, mock_query): + """initialize should update routes on success.""" + handler = _ClientRoutesHandler(self.config) + mock_conn = Mock() + host_id = uuid.uuid4() + + mock_query.return_value = [ + _Route(connection_id=str(self.conn_id), host_id=host_id, address="new.com", port=9042), + ] + + handler.initialize(mock_conn, 5.0) + + self.assertEqual(mock_query.call_count, 1) + route = handler._routes.get_by_host_id(host_id) + self.assertIsNotNone(route) + self.assertEqual(route.address, "new.com") + +class TestClientRoutesEndPoint(unittest.TestCase): + + def setUp(self): + self.conn_id = uuid.uuid4() + self.proxy = ClientRouteProxy(str(self.conn_id), "10.0.0.1") + self.config = ClientRoutesConfig([self.proxy]) + self.handler = _ClientRoutesHandler(self.config, ssl_enabled=False) + + def test_resolve_falls_back_when_no_mapping(self): + """resolve() should return original address/port when no route mapping exists.""" + host_id = uuid.uuid4() + ep = ClientRoutesEndPoint( + host_id=host_id, + handler=self.handler, + original_address="10.0.0.1", + original_port=9042, + ) + self.assertEqual(ep.resolve(), ("10.0.0.1", 9042)) + + @patch('cassandra.client_routes.socket.getaddrinfo', + return_value=[(socket.AF_INET, socket.SOCK_STREAM, 0, '', ("192.168.1.100", 9042))]) + def test_resolve_returns_address_when_route_exists(self, _mock_getaddrinfo): + """resolve() should return the DNS-resolved address and port when a route exists.""" + host_id = uuid.uuid4() + self.handler._routes.update([ + _Route(connection_id=str(self.conn_id), host_id=host_id, + address="nlb.example.com", port=9042), + ]) + ep = ClientRoutesEndPoint( + host_id=host_id, + handler=self.handler, + original_address="10.0.0.1", + original_port=9042, + ) + self.assertEqual(ep.resolve(), ("192.168.1.100", 9042)) + _mock_getaddrinfo.assert_called_once_with( + "nlb.example.com", 9042, socket.AF_UNSPEC, socket.SOCK_STREAM) + + @patch('cassandra.client_routes.socket.getaddrinfo', + side_effect=socket.gaierror("DNS resolution failed")) + def test_resolve_host_dns_failure_raises(self, _mock_getaddrinfo): + """resolve_host should propagate socket.gaierror on DNS failure.""" + host_id = uuid.uuid4() + self.handler._routes.update([ + _Route(connection_id=str(self.conn_id), host_id=host_id, + address="nonexistent.example.com", port=9042), + ]) + with self.assertRaises(socket.gaierror): + self.handler.resolve_host(host_id) + + def test_resolve_host_missing_port_raises(self): + """resolve_host should raise ValueError when route has no port.""" + host_id = uuid.uuid4() + self.handler._routes.update([ + _Route(connection_id=str(self.conn_id), host_id=host_id, + address="host.com", port=0), + ]) + with self.assertRaises(ValueError): + self.handler.resolve_host(host_id) + + +class TestClientRoutesEndPointFactory(unittest.TestCase): + + def setUp(self): + self.conn_id = uuid.uuid4() + proxy = ClientRouteProxy(str(self.conn_id), "10.0.0.1") + self.config = ClientRoutesConfig([proxy]) + self.handler = _ClientRoutesHandler(self.config, ssl_enabled=False) + self.factory = ClientRoutesEndPointFactory(self.handler, default_port=9042) + + def test_create_from_row(self): + """Factory should create a ClientRoutesEndPoint from a peers row.""" + host_id = uuid.uuid4() + row = { + "host_id": host_id, + "rpc_address": "10.0.0.5", + "native_transport_port": 9042, + "peer": "10.0.0.5", + } + ep = self.factory.create(row) + self.assertIsInstance(ep, ClientRoutesEndPoint) + self.assertEqual(ep.host_id, host_id) + self.assertEqual(ep.address, "10.0.0.5") + + def test_create_missing_host_id_raises(self): + """Factory should raise ValueError when row has no host_id.""" + row = {"rpc_address": "10.0.0.5", "native_transport_port": 9042} + with self.assertRaises(ValueError): + self.factory.create(row) + +class TestClientRoutesSSLValidation(unittest.TestCase): + + def test_check_hostname_with_ssl_context_raises(self): + """Cluster should reject check_hostname=True with client_routes_config.""" + ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + self.assertTrue(ssl_ctx.check_hostname) + + config = ClientRoutesConfig( + proxies=[ClientRouteProxy(str(uuid.uuid4()), "10.0.0.1")] + ) + with self.assertRaises(ValueError) as cm: + Cluster( + contact_points=["10.0.0.1"], + ssl_context=ssl_ctx, + client_routes_config=config, + ) + self.assertIn("check_hostname", str(cm.exception)) + + def test_check_hostname_with_ssl_options_raises(self): + """Cluster should reject check_hostname=True in ssl_options with client_routes_config.""" + config = ClientRoutesConfig( + proxies=[ClientRouteProxy(str(uuid.uuid4()), "10.0.0.1")] + ) + with self.assertRaises(ValueError) as cm: + Cluster( + contact_points=["10.0.0.1"], + ssl_options={'check_hostname': True}, + client_routes_config=config, + ) + self.assertIn("check_hostname", str(cm.exception)) + + def test_disabled_check_hostname_with_client_routes_ok(self): + """Cluster should allow check_hostname=False with client_routes_config.""" + ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ssl_ctx.check_hostname = False + + config = ClientRoutesConfig( + proxies=[ClientRouteProxy(str(uuid.uuid4()), "10.0.0.1")] + ) + # Should not raise + cluster = Cluster( + contact_points=["10.0.0.1"], + ssl_context=ssl_ctx, + client_routes_config=config, + ) + cluster.shutdown() + + def test_no_ssl_with_client_routes_ok(self): + """Cluster should allow client_routes_config without SSL.""" + config = ClientRoutesConfig( + proxies=[ClientRouteProxy(str(uuid.uuid4()), "10.0.0.1")] + ) + # Should not raise + cluster = Cluster( + contact_points=["10.0.0.1"], + client_routes_config=config, + ) + cluster.shutdown() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_cluster.py b/tests/unit/test_cluster.py index ec7d51bc2d..3d55bc1860 100644 --- a/tests/unit/test_cluster.py +++ b/tests/unit/test_cluster.py @@ -13,20 +13,25 @@ # limitations under the License. import unittest +from concurrent.futures import Future import logging import socket +from types import SimpleNamespace from unittest.mock import patch, Mock +import uuid from cassandra import ConsistencyLevel, DriverException, Timeout, Unavailable, RequestExecutionException, ReadTimeout, WriteTimeout, CoordinationFailure, ReadFailure, WriteFailure, FunctionFailure, AlreadyExists,\ InvalidRequest, Unauthorized, AuthenticationFailed, OperationTimedOut, UnsupportedOperation, RequestValidationException, ConfigurationException, ProtocolVersion -from cassandra.cluster import _Scheduler, Session, Cluster, default_lbp_factory, \ +from cassandra.cluster import _Scheduler, Session, Cluster, ResultSet, SchemaAgreementScope, ControlConnectionQueryFallback, default_lbp_factory, \ ExecutionProfile, _ConfigMode, EXEC_PROFILE_DEFAULT +from cassandra.connection import ConnectionBusy, ConnectionException from cassandra.pool import Host from cassandra.policies import HostDistance, RetryPolicy, RoundRobinPolicy, DowngradingConsistencyRetryPolicy, SimpleConvictionPolicy from cassandra.query import SimpleStatement, named_tuple_factory, tuple_factory from tests.unit.utils import mock_session_pools from tests import connection_class +import pytest log = logging.getLogger(__name__) @@ -38,103 +43,241 @@ def test_exception_types(self): PYTHON-443 Sanity check to ensure we don't unintentionally change class hierarchy of exception types """ - self.assertTrue(issubclass(Unavailable, DriverException)) - self.assertTrue(issubclass(Unavailable, RequestExecutionException)) - - self.assertTrue(issubclass(ReadTimeout, DriverException)) - self.assertTrue(issubclass(ReadTimeout, RequestExecutionException)) - self.assertTrue(issubclass(ReadTimeout, Timeout)) - - self.assertTrue(issubclass(WriteTimeout, DriverException)) - self.assertTrue(issubclass(WriteTimeout, RequestExecutionException)) - self.assertTrue(issubclass(WriteTimeout, Timeout)) - - self.assertTrue(issubclass(CoordinationFailure, DriverException)) - self.assertTrue(issubclass(CoordinationFailure, RequestExecutionException)) - - self.assertTrue(issubclass(ReadFailure, DriverException)) - self.assertTrue(issubclass(ReadFailure, RequestExecutionException)) - self.assertTrue(issubclass(ReadFailure, CoordinationFailure)) - - self.assertTrue(issubclass(WriteFailure, DriverException)) - self.assertTrue(issubclass(WriteFailure, RequestExecutionException)) - self.assertTrue(issubclass(WriteFailure, CoordinationFailure)) - - self.assertTrue(issubclass(FunctionFailure, DriverException)) - self.assertTrue(issubclass(FunctionFailure, RequestExecutionException)) - - self.assertTrue(issubclass(RequestValidationException, DriverException)) - - self.assertTrue(issubclass(ConfigurationException, DriverException)) - self.assertTrue(issubclass(ConfigurationException, RequestValidationException)) - - self.assertTrue(issubclass(AlreadyExists, DriverException)) - self.assertTrue(issubclass(AlreadyExists, RequestValidationException)) - self.assertTrue(issubclass(AlreadyExists, ConfigurationException)) - - self.assertTrue(issubclass(InvalidRequest, DriverException)) - self.assertTrue(issubclass(InvalidRequest, RequestValidationException)) - - self.assertTrue(issubclass(Unauthorized, DriverException)) - self.assertTrue(issubclass(Unauthorized, RequestValidationException)) - - self.assertTrue(issubclass(AuthenticationFailed, DriverException)) - - self.assertTrue(issubclass(OperationTimedOut, DriverException)) - - self.assertTrue(issubclass(UnsupportedOperation, DriverException)) + assert issubclass(Unavailable, DriverException) + assert issubclass(Unavailable, RequestExecutionException) + + assert issubclass(ReadTimeout, DriverException) + assert issubclass(ReadTimeout, RequestExecutionException) + assert issubclass(ReadTimeout, Timeout) + + assert issubclass(WriteTimeout, DriverException) + assert issubclass(WriteTimeout, RequestExecutionException) + assert issubclass(WriteTimeout, Timeout) + + assert issubclass(CoordinationFailure, DriverException) + assert issubclass(CoordinationFailure, RequestExecutionException) + + assert issubclass(ReadFailure, DriverException) + assert issubclass(ReadFailure, RequestExecutionException) + assert issubclass(ReadFailure, CoordinationFailure) + + assert issubclass(WriteFailure, DriverException) + assert issubclass(WriteFailure, RequestExecutionException) + assert issubclass(WriteFailure, CoordinationFailure) + + assert issubclass(FunctionFailure, DriverException) + assert issubclass(FunctionFailure, RequestExecutionException) + + assert issubclass(RequestValidationException, DriverException) + + assert issubclass(ConfigurationException, DriverException) + assert issubclass(ConfigurationException, RequestValidationException) + + assert issubclass(AlreadyExists, DriverException) + assert issubclass(AlreadyExists, RequestValidationException) + assert issubclass(AlreadyExists, ConfigurationException) + + assert issubclass(InvalidRequest, DriverException) + assert issubclass(InvalidRequest, RequestValidationException) + + assert issubclass(Unauthorized, DriverException) + assert issubclass(Unauthorized, RequestValidationException) + + assert issubclass(AuthenticationFailed, DriverException) + + assert issubclass(OperationTimedOut, DriverException) + + assert issubclass(UnsupportedOperation, DriverException) + + +class OperationTimedOutTest(unittest.TestCase): + + def test_message_without_timeout(self): + """Default message format when no timeout info is provided.""" + exc = OperationTimedOut(errors={'host1': 'some error'}, last_host='host1') + msg = str(exc) + assert "errors={'host1': 'some error'}" in msg + assert "last_host=host1" in msg + assert "timeout=" not in msg + assert "in_flight=" not in msg + + def test_message_with_timeout_and_in_flight(self): + """Message includes timeout and in_flight when both are provided.""" + exc = OperationTimedOut(errors={'host1': 'err'}, last_host='host1', + timeout=10.0, in_flight=42) + msg = str(exc) + assert "(timeout=10.0s, in_flight=42)" in msg + + def test_message_with_timeout_no_in_flight(self): + """Message includes timeout but not in_flight when only timeout is set.""" + exc = OperationTimedOut(timeout=5.0) + msg = str(exc) + assert "(timeout=5.0s)" in msg + assert "in_flight=" not in msg + + def test_message_no_args(self): + """No-argument form should not crash and should have clean message.""" + exc = OperationTimedOut() + msg = str(exc) + assert "errors=None, last_host=None" in msg + assert "timeout=" not in msg + + def test_attributes_accessible(self): + """New and existing attributes should be readable.""" + exc = OperationTimedOut(errors={'h': 'e'}, last_host='h', + timeout=10.0, in_flight=42) + assert exc.errors == {'h': 'e'} + assert exc.last_host == 'h' + assert exc.timeout == 10.0 + assert exc.in_flight == 42 + + def test_attributes_default_none(self): + """New attributes should default to None when not provided.""" + exc = OperationTimedOut() + assert exc.timeout is None + assert exc.in_flight is None + assert exc.errors is None + assert exc.last_host is None + + def test_backward_compat_positional(self): + """Existing two-positional-arg form should still work.""" + exc = OperationTimedOut({'h': 'err'}, 'host1') + assert exc.errors == {'h': 'err'} + assert exc.last_host == 'host1' + assert exc.timeout is None + assert exc.in_flight is None class ClusterTest(unittest.TestCase): def test_tuple_for_contact_points(self): cluster = Cluster(contact_points=[('localhost', 9045), ('127.0.0.2', 9046), '127.0.0.3'], port=9999) - localhost_addr = set([addr[0] for addr in [t for (_,_,_,_,t) in socket.getaddrinfo("localhost",80)]]) + # Refactored for clarity + addr_info = socket.getaddrinfo("localhost", 80) + sockaddr_tuples = [info[4] for info in addr_info] # info[4] is sockaddr + localhost_addr = set([sockaddr[0] for sockaddr in sockaddr_tuples]) for cp in cluster.endpoints_resolved: if cp.address in localhost_addr: - self.assertEqual(cp.port, 9045) + assert cp.port == 9045 elif cp.address == '127.0.0.2': - self.assertEqual(cp.port, 9046) + assert cp.port == 9046 else: - self.assertEqual(cp.address, '127.0.0.3') - self.assertEqual(cp.port, 9999) + assert cp.address == '127.0.0.3' + assert cp.port == 9999 def test_invalid_contact_point_types(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): Cluster(contact_points=[None], protocol_version=4, connect_timeout=1) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): Cluster(contact_points="not a sequence", protocol_version=4, connect_timeout=1) - def test_requests_in_flight_threshold(self): - d = HostDistance.LOCAL - mn = 3 - mx = 5 - c = Cluster(protocol_version=2) - c.set_min_requests_per_connection(d, mn) - c.set_max_requests_per_connection(d, mx) - # min underflow, max, overflow - for n in (-1, mx, 127): - self.assertRaises(ValueError, c.set_min_requests_per_connection, d, n) - # max underflow, under min, overflow - for n in (0, mn, 128): - self.assertRaises(ValueError, c.set_max_requests_per_connection, d, n) - def test_port_str(self): - """Check port passed as tring is converted and checked properly""" + """Check port passed as string is converted and checked properly""" cluster = Cluster(contact_points=['127.0.0.1'], port='1111') for cp in cluster.endpoints_resolved: if cp.address in ('::1', '127.0.0.1'): - self.assertEqual(cp.port, 1111) + assert cp.port == 1111 - with self.assertRaises(ValueError): + with pytest.raises(ValueError): cluster = Cluster(contact_points=['127.0.0.1'], port='string') def test_port_range(self): for invalid_port in [0, 65536, -1]: - with self.assertRaises(ValueError): + with pytest.raises(ValueError): cluster = Cluster(contact_points=['127.0.0.1'], port=invalid_port) + def test_control_connection_query_fallback_modes(self): + assert Cluster().allow_control_connection_query_fallback is ControlConnectionQueryFallback.Disabled + with pytest.raises(TypeError): + Cluster(allow_control_connection_query_fallback=False) + with pytest.raises(TypeError): + Cluster(allow_control_connection_query_fallback=True) + assert ( + Cluster(allow_control_connection_query_fallback=ControlConnectionQueryFallback.Fallback) + .allow_control_connection_query_fallback + is ControlConnectionQueryFallback.Fallback + ) + assert Cluster( + allow_control_connection_query_fallback=ControlConnectionQueryFallback.SkipPoolCreation + ).allow_control_connection_query_fallback is ControlConnectionQueryFallback.SkipPoolCreation + + def test_control_connection_query_fallback_no_node_pool_mode_skips_pool_creation(self): + cluster = Cluster( + allow_control_connection_query_fallback=ControlConnectionQueryFallback.SkipPoolCreation, + monitor_reporting_enabled=False, + ) + host = Host("127.0.0.1", SimpleConvictionPolicy, host_id=uuid.uuid4()) + + with patch.object(Session, "add_or_renew_pool") as mocked_add_or_renew_pool: + session = Session(cluster, [host]) + + mocked_add_or_renew_pool.assert_not_called() + assert session._initial_connect_futures == set() + assert session._pools == {} + assert session.update_created_pools() == set() + + def test_control_connection_query_fallback_fallback_tolerates_empty_initial_pools(self): + cluster = Cluster( + allow_control_connection_query_fallback=ControlConnectionQueryFallback.Fallback, + monitor_reporting_enabled=False, + ) + host = Host("127.0.0.1", SimpleConvictionPolicy, host_id=uuid.uuid4()) + future = Future() + future.set_result(False) + + with patch.object(Session, "add_or_renew_pool", return_value=future) as mocked_add_or_renew_pool: + session = Session(cluster, [host]) + + mocked_add_or_renew_pool.assert_called_once_with(host, is_host_addition=False) + assert session._initial_connect_futures == {future} + assert session._pools == {} + + def test_compression_autodisabled_without_libraries(self): + with patch.dict('cassandra.cluster.locally_supported_compressions', {}, clear=True): + with patch('cassandra.cluster.log') as patched_logger: + cluster = Cluster(compression=True) + + patched_logger.error.assert_called_once() + assert cluster.compression is False + + def test_compression_validates_requested_algorithm(self): + with patch.dict('cassandra.cluster.locally_supported_compressions', {}, clear=True): + with pytest.raises(ValueError): + Cluster(compression='lz4') + + with patch.dict('cassandra.cluster.locally_supported_compressions', {'lz4': ('c', 'd')}, clear=True): + with patch('cassandra.cluster.log') as patched_logger: + cluster = Cluster(compression='lz4') + + patched_logger.error.assert_not_called() + assert cluster.compression == 'lz4' + + def test_compression_type_validation(self): + with pytest.raises(TypeError): + Cluster(compression=123) + + def test_connection_factory_passes_compression_kwarg(self): + endpoint = Mock(address='127.0.0.1') + scenarios = [ + ({}, True, False), + ({'snappy': ('c', 'd')}, True, True), + ({'lz4': ('c', 'd')}, 'lz4', 'lz4'), + ({'lz4': ('c', 'd'), 'snappy': ('c', 'd')}, False, False), + ({'lz4': ('c', 'd'), 'snappy': ('c', 'd')}, None, False), + ] + + for supported, configured, expected in scenarios: + with patch.dict('cassandra.cluster.locally_supported_compressions', supported, clear=True): + with patch.object(Cluster.connection_class, 'factory', autospec=True, return_value='connection') as factory: + cluster = Cluster(compression=configured) + conn = cluster.connection_factory(endpoint) + + assert conn == 'connection' + assert factory.call_count == 1 + assert factory.call_args.kwargs['compression'] == expected + assert cluster.compression == expected + class SchedulerTest(unittest.TestCase): # TODO: this suite could be expanded; for now just adding a test covering a ticket @@ -149,15 +292,127 @@ def test_event_delay_timing(self, *_): """ sched = _Scheduler(None) sched.schedule(0, lambda: None) - sched.schedule(0, lambda: None) # pre-473: "TypeError: unorderable types: function() < function()"t + sched.schedule(0, lambda: None) # pre-473: "TypeError: unorderable types: function() < function()" class SessionTest(unittest.TestCase): + class FakeTime(object): + + def __init__(self): + self.clock = 0 + + def time(self): + return self.clock + + def sleep(self, amount): + self.clock += amount + + class MockPool(object): + + def __init__(self, host, connection): + self.host = host + self.host_distance = HostDistance.LOCAL + self.is_shutdown = False + self.connection = connection + + def _get_connection_for_routing_key(self): + return self.connection + + class MockSchemaVersionFuture(object): + + def __init__(self, outcome, auto_complete=True): + self._outcome = outcome + self._auto_complete = auto_complete + self._delivered = False + self._callback_state = None + self._col_names = ("schema_version",) + self._col_types = None + self.has_more_pages = False + self._continuous_paging_session = None + + def _deliver(self): + if self._delivered or self._callback_state is None: + return + + self._delivered = True + callback, errback, callback_args, callback_kwargs, errback_args, errback_kwargs = self._callback_state + if isinstance(self._outcome, Exception): + errback(self._outcome, *errback_args, **errback_kwargs) + else: + row = SimpleNamespace(schema_version=self._outcome) + callback([row], *callback_args, **callback_kwargs) + + def add_callbacks(self, callback, errback, + callback_args=(), callback_kwargs=None, + errback_args=(), errback_kwargs=None): + self._callback_state = ( + callback, + errback, + callback_args, + callback_kwargs or {}, + errback_args, + errback_kwargs or {}, + ) + if self._auto_complete: + self._deliver() + return self + + def complete(self): + self._deliver() + + def result(self): + if isinstance(self._outcome, Exception): + raise self._outcome + return ResultSet(self, [SimpleNamespace(schema_version=self._outcome)]) + def setUp(self): if connection_class is None: raise unittest.SkipTest('libev does not appear to be installed correctly') connection_class.initialize_reactor() + def _mock_schema_future(self, outcome): + return self.MockSchemaVersionFuture(outcome) + + def _host_query_count(self, session, target_host): + return sum(1 for call in session.execute_async.call_args_list if call.kwargs.get('host') is target_host) + + def _new_schema_agreement_session(self, schema_versions, distances=None): + hosts = [] + connections = {} + distance_map = {} + if distances is None: + distances = [HostDistance.LOCAL] * len(schema_versions) + + for index, schema_version in enumerate(schema_versions): + host = Host("127.0.0.%d" % (index + 1), SimpleConvictionPolicy, host_id=uuid.uuid4()) + host.set_up() + hosts.append(host) + distance_map[host] = distances[index] + + cluster = Cluster(protocol_version=4) + for host in hosts: + cluster.metadata.add_or_return_host(host) + + session = Session(cluster, hosts) + session._profile_manager.distance = Mock(side_effect=lambda host: distance_map.get(host, HostDistance.LOCAL)) + session._pools = {} + for host, schema_version in zip(hosts, schema_versions): + connection = Mock(endpoint=host.endpoint) + connection.future_outcomes = [schema_version] + session._pools[host] = self.MockPool(host, connection) + connections[host] = connection + + def execute_async(query, parameters=None, trace=False, + custom_payload=None, execution_profile=None, + paging_state=None, timeout=None, host=None, execute_as=None): + connection = connections[host] + outcome = connection.future_outcomes.pop(0) if len(connection.future_outcomes) > 1 else connection.future_outcomes[0] + return self._mock_schema_future(outcome) + + session.execute_async = Mock(side_effect=execute_async) + + return session, hosts, connections + # TODO: this suite could be expanded; for now just adding a test covering a PR @mock_session_pools def test_default_serial_consistency_level_ep(self, *_): @@ -168,25 +423,25 @@ def test_default_serial_consistency_level_ep(self, *_): PR #510 """ c = Cluster(protocol_version=4) - s = Session(c, [Host("127.0.0.1", SimpleConvictionPolicy)]) + s = Session(c, [Host("127.0.0.1", SimpleConvictionPolicy, host_id=uuid.uuid4())]) c.connection_class.initialize_reactor() # default is None default_profile = c.profile_manager.default - self.assertIsNone(default_profile.serial_consistency_level) + assert default_profile.serial_consistency_level is None for cl in (None, ConsistencyLevel.LOCAL_SERIAL, ConsistencyLevel.SERIAL): s.get_execution_profile(EXEC_PROFILE_DEFAULT).serial_consistency_level = cl # default is passed through f = s.execute_async(query='') - self.assertEqual(f.message.serial_consistency_level, cl) + assert f.message.serial_consistency_level == cl # any non-None statement setting takes precedence for cl_override in (ConsistencyLevel.LOCAL_SERIAL, ConsistencyLevel.SERIAL): f = s.execute_async(SimpleStatement(query_string='', serial_consistency_level=cl_override)) - self.assertEqual(default_profile.serial_consistency_level, cl) - self.assertEqual(f.message.serial_consistency_level, cl_override) + assert default_profile.serial_consistency_level == cl + assert f.message.serial_consistency_level == cl_override @mock_session_pools def test_default_serial_consistency_level_legacy(self, *_): @@ -197,15 +452,15 @@ def test_default_serial_consistency_level_legacy(self, *_): PR #510 """ c = Cluster(protocol_version=4) - s = Session(c, [Host("127.0.0.1", SimpleConvictionPolicy)]) + s = Session(c, [Host("127.0.0.1", SimpleConvictionPolicy, host_id=uuid.uuid4())]) c.connection_class.initialize_reactor() # default is None - self.assertIsNone(s.default_serial_consistency_level) + assert s.default_serial_consistency_level is None # Should fail - with self.assertRaises(ValueError): + with pytest.raises(ValueError): s.default_serial_consistency_level = ConsistencyLevel.ANY - with self.assertRaises(ValueError): + with pytest.raises(ValueError): s.default_serial_consistency_level = 1001 for cl in (None, ConsistencyLevel.LOCAL_SERIAL, ConsistencyLevel.SERIAL): @@ -214,33 +469,173 @@ def test_default_serial_consistency_level_legacy(self, *_): # any non-None statement setting takes precedence for cl_override in (ConsistencyLevel.LOCAL_SERIAL, ConsistencyLevel.SERIAL): f = s.execute_async(SimpleStatement(query_string='', serial_consistency_level=cl_override)) - self.assertEqual(s.default_serial_consistency_level, cl) - self.assertEqual(f.message.serial_consistency_level, cl_override) + assert s.default_serial_consistency_level == cl + assert f.message.serial_consistency_level == cl_override + + + + @mock_session_pools + def test_set_keyspace_escapes_quotes(self, *_): + """ + Test that Session.set_keyspace properly escapes double quotes in + keyspace names to prevent CQL injection. + Requested in review of PR #758. + """ + c = Cluster(protocol_version=4) + s = Session(c, [Host("127.0.0.1", SimpleConvictionPolicy, host_id=uuid.uuid4())]) + c.connection_class.initialize_reactor() + + s.execute = Mock() + + s.set_keyspace('my"ks') + query = s.execute.call_args[0][0] + assert query == 'USE "my""ks"', ( + "Double quotes in keyspace name must be escaped as double-double quotes, " + "got: %r" % query) + + # Also verify a simple keyspace name doesn't get unnecessarily quoted + s.execute.reset_mock() + s.set_keyspace('simple_ks') + query = s.execute.call_args[0][0] + assert query == 'USE simple_ks', ( + "Simple keyspace names should not be quoted, got: %r" % query) + + @mock_session_pools + def test_wait_for_schema_agreement_default_scope_queries_all_connected_hosts(self, *_): + session, hosts, _ = self._new_schema_agreement_session( + ["a", "a"], + distances=[HostDistance.LOCAL_RACK, HostDistance.REMOTE]) + + assert session.wait_for_schema_agreement(wait_time=1) + + for host in hosts: + assert self._host_query_count(session, host) == 1 + + @mock_session_pools + def test_wait_for_schema_agreement_retries_until_local_hosts_match(self, *_): + session, hosts, connections = self._new_schema_agreement_session(["a", "b"]) + clock = self.FakeTime() + connections[hosts[1]].future_outcomes = ["b", "a"] + + with patch('cassandra.cluster.time', new=clock): + assert session.wait_for_schema_agreement(wait_time=1) + for host in hosts: + assert self._host_query_count(session, host) == 2 + assert clock.clock == 0.2 + + @mock_session_pools + def test_wait_for_schema_agreement_retries_when_local_connection_is_busy(self, *_): + session, hosts, connections = self._new_schema_agreement_session(["a", "a"]) + clock = self.FakeTime() + connections[hosts[1]].future_outcomes = [ + ConnectionBusy("connection overloaded"), + "a"] + + with patch('cassandra.cluster.time', new=clock): + assert session.wait_for_schema_agreement(wait_time=1) + for host in hosts: + assert self._host_query_count(session, host) == 2 + assert clock.clock == 0.2 + + @mock_session_pools + def test_wait_for_schema_agreement_ignores_local_hosts_without_session_pool(self, *_): + session, hosts, _ = self._new_schema_agreement_session(["a"]) + + unconnected_host = Host("127.0.0.2", SimpleConvictionPolicy, host_id=uuid.uuid4()) + unconnected_host.set_up() + session.cluster.metadata.add_or_return_host(unconnected_host) + + assert session.wait_for_schema_agreement(wait_time=1) + assert self._host_query_count(session, hosts[0]) == 1 + + @mock_session_pools + def test_wait_for_schema_agreement_queries_hosts_in_order(self, *_): + session, hosts, _ = self._new_schema_agreement_session(["a"] * 11) + + assert session.wait_for_schema_agreement(wait_time=1) + assert [call.kwargs['host'] for call in session.execute_async.call_args_list] == list(hosts) + + @mock_session_pools + def test_wait_for_schema_agreement_rack_scope_only_queries_local_rack_connections(self, *_): + session, hosts, _ = self._new_schema_agreement_session( + ["a", "a", "a"], + distances=[HostDistance.LOCAL_RACK, HostDistance.LOCAL, HostDistance.REMOTE]) + + assert session.wait_for_schema_agreement(wait_time=1, scope=SchemaAgreementScope.RACK) + assert self._host_query_count(session, hosts[0]) == 1 + assert self._host_query_count(session, hosts[1]) == 0 + assert self._host_query_count(session, hosts[2]) == 0 + + @mock_session_pools + def test_wait_for_schema_agreement_cluster_scope_skips_ignored_hosts(self, *_): + session, hosts, _ = self._new_schema_agreement_session( + ["a", "a"], + distances=[HostDistance.IGNORED, HostDistance.LOCAL]) + + assert session.wait_for_schema_agreement(wait_time=1, scope=SchemaAgreementScope.CLUSTER) + + assert self._host_query_count(session, hosts[0]) == 0 + assert self._host_query_count(session, hosts[1]) == 1 + + @mock_session_pools + def test_wait_for_schema_agreement_cluster_scope_excludes_hosts_with_unknown_status(self, *_): + session, hosts, _ = self._new_schema_agreement_session( + ["a", "a"], + distances=[HostDistance.LOCAL_RACK, HostDistance.LOCAL]) + + hosts[0].is_up = None + + assert session.wait_for_schema_agreement(wait_time=1, scope=SchemaAgreementScope.CLUSTER) + + assert self._host_query_count(session, hosts[0]) == 0 + assert self._host_query_count(session, hosts[1]) == 1 + + @mock_session_pools + def test_wait_for_schema_agreement_rejects_unknown_scope(self, *_): + session, _, _ = self._new_schema_agreement_session(["a"]) + + with pytest.raises(ValueError): + session.wait_for_schema_agreement(wait_time=1, scope='planet') + + @mock_session_pools + def test_set_keyspace_for_all_pools_reports_all_errors(self, *_): + cluster = Cluster() + session = Session( + cluster, + [Host("127.0.0.1", SimpleConvictionPolicy, host_id=uuid.uuid4())], + ) + + pool1 = Mock(host='host1') + pool2 = Mock(host='host2') + keyspace_error = ConnectionException("boom") + + pool1._set_keyspace_for_all_conns.side_effect = ( + lambda keyspace, callback: callback(pool1, [keyspace_error]) + ) + pool2._set_keyspace_for_all_conns.side_effect = ( + lambda keyspace, callback: callback(pool2, []) + ) + session._pools = {'host1': pool1, 'host2': pool2} + + callback = Mock() + session._set_keyspace_for_all_pools('ks', callback) + + callback.assert_called_once() + assert callback.call_args.args[0] == {'host1': [keyspace_error]} class ProtocolVersionTests(unittest.TestCase): def test_protocol_downgrade_test(self): - lower = ProtocolVersion.get_lower_supported(ProtocolVersion.DSE_V2) - self.assertEqual(ProtocolVersion.DSE_V1, lower) - lower = ProtocolVersion.get_lower_supported(ProtocolVersion.DSE_V1) - self.assertEqual(ProtocolVersion.V5,lower) lower = ProtocolVersion.get_lower_supported(ProtocolVersion.V5) - self.assertEqual(ProtocolVersion.V4,lower) + assert ProtocolVersion.V4 == lower lower = ProtocolVersion.get_lower_supported(ProtocolVersion.V4) - self.assertEqual(ProtocolVersion.V3,lower) + assert ProtocolVersion.V3 == lower lower = ProtocolVersion.get_lower_supported(ProtocolVersion.V3) - self.assertEqual(ProtocolVersion.V2,lower) - lower = ProtocolVersion.get_lower_supported(ProtocolVersion.V2) - self.assertEqual(ProtocolVersion.V1, lower) - lower = ProtocolVersion.get_lower_supported(ProtocolVersion.V1) - self.assertEqual(0, lower) - - self.assertTrue(ProtocolVersion.uses_error_code_map(ProtocolVersion.DSE_V1)) - self.assertTrue(ProtocolVersion.uses_int_query_flags(ProtocolVersion.DSE_V1)) + assert 0 == lower - self.assertFalse(ProtocolVersion.uses_error_code_map(ProtocolVersion.V4)) - self.assertFalse(ProtocolVersion.uses_int_query_flags(ProtocolVersion.V4)) + assert not ProtocolVersion.uses_error_code_map(ProtocolVersion.V4) + assert not ProtocolVersion.uses_int_query_flags(ProtocolVersion.V4) class ExecutionProfileTest(unittest.TestCase): @@ -250,36 +645,36 @@ def setUp(self): connection_class.initialize_reactor() def _verify_response_future_profile(self, rf, prof): - self.assertEqual(rf._load_balancer, prof.load_balancing_policy) - self.assertEqual(rf._retry_policy, prof.retry_policy) - self.assertEqual(rf.message.consistency_level, prof.consistency_level) - self.assertEqual(rf.message.serial_consistency_level, prof.serial_consistency_level) - self.assertEqual(rf.timeout, prof.request_timeout) - self.assertEqual(rf.row_factory, prof.row_factory) + assert rf._load_balancer == prof.load_balancing_policy + assert rf._retry_policy == prof.retry_policy + assert rf.message.consistency_level == prof.consistency_level + assert rf.message.serial_consistency_level == prof.serial_consistency_level + assert rf.timeout == prof.request_timeout + assert rf.row_factory == prof.row_factory @mock_session_pools def test_default_exec_parameters(self): cluster = Cluster() - self.assertEqual(cluster._config_mode, _ConfigMode.UNCOMMITTED) - self.assertEqual(cluster.load_balancing_policy.__class__, default_lbp_factory().__class__) - self.assertEqual(cluster.profile_manager.default.load_balancing_policy.__class__, default_lbp_factory().__class__) - self.assertEqual(cluster.default_retry_policy.__class__, RetryPolicy) - self.assertEqual(cluster.profile_manager.default.retry_policy.__class__, RetryPolicy) - session = Session(cluster, hosts=[Host("127.0.0.1", SimpleConvictionPolicy)]) - self.assertEqual(session.default_timeout, 10.0) - self.assertEqual(cluster.profile_manager.default.request_timeout, 10.0) - self.assertEqual(session.default_consistency_level, ConsistencyLevel.LOCAL_ONE) - self.assertEqual(cluster.profile_manager.default.consistency_level, ConsistencyLevel.LOCAL_ONE) - self.assertEqual(session.default_serial_consistency_level, None) - self.assertEqual(cluster.profile_manager.default.serial_consistency_level, None) - self.assertEqual(session.row_factory, named_tuple_factory) - self.assertEqual(cluster.profile_manager.default.row_factory, named_tuple_factory) + assert cluster._config_mode == _ConfigMode.UNCOMMITTED + assert cluster.load_balancing_policy.__class__ == default_lbp_factory().__class__ + assert cluster.profile_manager.default.load_balancing_policy.__class__ == default_lbp_factory().__class__ + assert cluster.default_retry_policy.__class__ == RetryPolicy + assert cluster.profile_manager.default.retry_policy.__class__ == RetryPolicy + session = Session(cluster, hosts=[Host("127.0.0.1", SimpleConvictionPolicy, host_id=uuid.uuid4())]) + assert session.default_timeout == 10.0 + assert cluster.profile_manager.default.request_timeout == 10.0 + assert session.default_consistency_level == ConsistencyLevel.LOCAL_ONE + assert cluster.profile_manager.default.consistency_level == ConsistencyLevel.LOCAL_ONE + assert session.default_serial_consistency_level is None + assert cluster.profile_manager.default.serial_consistency_level is None + assert session.row_factory == named_tuple_factory + assert cluster.profile_manager.default.row_factory == named_tuple_factory @mock_session_pools def test_default_legacy(self): cluster = Cluster(load_balancing_policy=RoundRobinPolicy(), default_retry_policy=DowngradingConsistencyRetryPolicy()) - self.assertEqual(cluster._config_mode, _ConfigMode.LEGACY) - session = Session(cluster, hosts=[Host("127.0.0.1", SimpleConvictionPolicy)]) + assert cluster._config_mode == _ConfigMode.LEGACY + session = Session(cluster, hosts=[Host("127.0.0.1", SimpleConvictionPolicy, host_id=uuid.uuid4())]) session.default_timeout = 3.7 session.default_consistency_level = ConsistencyLevel.ALL session.default_serial_consistency_level = ConsistencyLevel.SERIAL @@ -293,9 +688,9 @@ def test_default_legacy(self): def test_default_profile(self): non_default_profile = ExecutionProfile(RoundRobinPolicy(), *[object() for _ in range(2)]) cluster = Cluster(execution_profiles={'non-default': non_default_profile}) - session = Session(cluster, hosts=[Host("127.0.0.1", SimpleConvictionPolicy)]) + session = Session(cluster, hosts=[Host("127.0.0.1", SimpleConvictionPolicy, host_id=uuid.uuid4())]) - self.assertEqual(cluster._config_mode, _ConfigMode.PROFILES) + assert cluster._config_mode == _ConfigMode.PROFILES default_profile = cluster.profile_manager.profiles[EXEC_PROFILE_DEFAULT] rf = session.execute_async("query") @@ -305,10 +700,10 @@ def test_default_profile(self): self._verify_response_future_profile(rf, non_default_profile) for name, ep in cluster.profile_manager.profiles.items(): - self.assertEqual(ep, session.get_execution_profile(name)) + assert ep == session.get_execution_profile(name) # invalid ep - with self.assertRaises(ValueError): + with pytest.raises(ValueError): session.get_execution_profile('non-existent') def test_serial_consistency_level_validation(self): @@ -317,25 +712,25 @@ def test_serial_consistency_level_validation(self): ep = ExecutionProfile(RoundRobinPolicy(), serial_consistency_level=ConsistencyLevel.LOCAL_SERIAL) # should not pass - with self.assertRaises(ValueError): + with pytest.raises(ValueError): ep = ExecutionProfile(RoundRobinPolicy(), serial_consistency_level=ConsistencyLevel.ANY) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): ep = ExecutionProfile(RoundRobinPolicy(), serial_consistency_level=42) @mock_session_pools def test_statement_params_override_legacy(self): cluster = Cluster(load_balancing_policy=RoundRobinPolicy(), default_retry_policy=DowngradingConsistencyRetryPolicy()) - self.assertEqual(cluster._config_mode, _ConfigMode.LEGACY) - session = Session(cluster, hosts=[Host("127.0.0.1", SimpleConvictionPolicy)]) + assert cluster._config_mode == _ConfigMode.LEGACY + session = Session(cluster, hosts=[Host("127.0.0.1", SimpleConvictionPolicy, host_id=uuid.uuid4())]) ss = SimpleStatement("query", retry_policy=DowngradingConsistencyRetryPolicy(), consistency_level=ConsistencyLevel.ALL, serial_consistency_level=ConsistencyLevel.SERIAL) my_timeout = 1.1234 - self.assertNotEqual(ss.retry_policy.__class__, cluster.default_retry_policy) - self.assertNotEqual(ss.consistency_level, session.default_consistency_level) - self.assertNotEqual(ss._serial_consistency_level, session.default_serial_consistency_level) - self.assertNotEqual(my_timeout, session.default_timeout) + assert ss.retry_policy.__class__ != cluster.default_retry_policy + assert ss.consistency_level != session.default_consistency_level + assert ss._serial_consistency_level != session.default_serial_consistency_level + assert my_timeout != session.default_timeout rf = session.execute_async(ss, timeout=my_timeout) expected_profile = ExecutionProfile(load_balancing_policy=cluster.load_balancing_policy, retry_policy=ss.retry_policy, @@ -347,9 +742,9 @@ def test_statement_params_override_legacy(self): def test_statement_params_override_profile(self): non_default_profile = ExecutionProfile(RoundRobinPolicy(), *[object() for _ in range(2)]) cluster = Cluster(execution_profiles={'non-default': non_default_profile}) - session = Session(cluster, hosts=[Host("127.0.0.1", SimpleConvictionPolicy)]) + session = Session(cluster, hosts=[Host("127.0.0.1", SimpleConvictionPolicy, host_id=uuid.uuid4())]) - self.assertEqual(cluster._config_mode, _ConfigMode.PROFILES) + assert cluster._config_mode == _ConfigMode.PROFILES rf = session.execute_async("query", execution_profile='non-default') @@ -357,10 +752,10 @@ def test_statement_params_override_profile(self): consistency_level=ConsistencyLevel.ALL, serial_consistency_level=ConsistencyLevel.SERIAL) my_timeout = 1.1234 - self.assertNotEqual(ss.retry_policy.__class__, rf._load_balancer.__class__) - self.assertNotEqual(ss.consistency_level, rf.message.consistency_level) - self.assertNotEqual(ss._serial_consistency_level, rf.message.serial_consistency_level) - self.assertNotEqual(my_timeout, rf.timeout) + assert ss.retry_policy.__class__ != rf._load_balancer.__class__ + assert ss.consistency_level != rf.message.consistency_level + assert ss._serial_consistency_level != rf.message.serial_consistency_level + assert my_timeout != rf.timeout rf = session.execute_async(ss, timeout=my_timeout, execution_profile='non-default') expected_profile = ExecutionProfile(non_default_profile.load_balancing_policy, ss.retry_policy, @@ -370,28 +765,34 @@ def test_statement_params_override_profile(self): @mock_session_pools def test_no_profile_with_legacy(self): # don't construct with both - self.assertRaises(ValueError, Cluster, load_balancing_policy=RoundRobinPolicy(), execution_profiles={'a': ExecutionProfile()}) - self.assertRaises(ValueError, Cluster, default_retry_policy=DowngradingConsistencyRetryPolicy(), execution_profiles={'a': ExecutionProfile()}) - self.assertRaises(ValueError, Cluster, load_balancing_policy=RoundRobinPolicy(), + with pytest.raises(ValueError): + Cluster(load_balancing_policy=RoundRobinPolicy(), execution_profiles={'a': ExecutionProfile()}) + with pytest.raises(ValueError): + Cluster(default_retry_policy=DowngradingConsistencyRetryPolicy(), execution_profiles={'a': ExecutionProfile()}) + with pytest.raises(ValueError): + Cluster(load_balancing_policy=RoundRobinPolicy(), default_retry_policy=DowngradingConsistencyRetryPolicy(), execution_profiles={'a': ExecutionProfile()}) # can't add after cluster = Cluster(load_balancing_policy=RoundRobinPolicy()) - self.assertRaises(ValueError, cluster.add_execution_profile, 'name', ExecutionProfile()) + with pytest.raises(ValueError): + cluster.add_execution_profile('name', ExecutionProfile()) # session settings lock out profiles cluster = Cluster() - session = Session(cluster, hosts=[Host("127.0.0.1", SimpleConvictionPolicy)]) + session = Session(cluster, hosts=[Host("127.0.0.1", SimpleConvictionPolicy, host_id=uuid.uuid4())]) for attr, value in (('default_timeout', 1), ('default_consistency_level', ConsistencyLevel.ANY), ('default_serial_consistency_level', ConsistencyLevel.SERIAL), ('row_factory', tuple_factory)): cluster._config_mode = _ConfigMode.UNCOMMITTED setattr(session, attr, value) - self.assertRaises(ValueError, cluster.add_execution_profile, 'name' + attr, ExecutionProfile()) + with pytest.raises(ValueError): + cluster.add_execution_profile('name' + attr, ExecutionProfile()) # don't accept profile - self.assertRaises(ValueError, session.execute_async, "query", execution_profile='some name here') + with pytest.raises(ValueError): + session.execute_async("query", execution_profile='some name here') @mock_session_pools def test_no_legacy_with_profile(self): @@ -403,21 +804,23 @@ def test_no_legacy_with_profile(self): # don't allow legacy parameters set for attr, value in (('default_retry_policy', RetryPolicy()), ('load_balancing_policy', default_lbp_factory())): - self.assertRaises(ValueError, setattr, cluster, attr, value) - session = Session(cluster, hosts=[Host("127.0.0.1", SimpleConvictionPolicy)]) + with pytest.raises(ValueError): + setattr(cluster, attr, value) + session = Session(cluster, hosts=[Host("127.0.0.1", SimpleConvictionPolicy, host_id=uuid.uuid4())]) for attr, value in (('default_timeout', 1), ('default_consistency_level', ConsistencyLevel.ANY), ('default_serial_consistency_level', ConsistencyLevel.SERIAL), ('row_factory', tuple_factory)): - self.assertRaises(ValueError, setattr, session, attr, value) + with pytest.raises(ValueError): + setattr(session, attr, value) @mock_session_pools def test_profile_name_value(self): internalized_profile = ExecutionProfile(RoundRobinPolicy(), *[object() for _ in range(2)]) cluster = Cluster(execution_profiles={'by-name': internalized_profile}) - session = Session(cluster, hosts=[Host("127.0.0.1", SimpleConvictionPolicy)]) - self.assertEqual(cluster._config_mode, _ConfigMode.PROFILES) + session = Session(cluster, hosts=[Host("127.0.0.1", SimpleConvictionPolicy, host_id=uuid.uuid4())]) + assert cluster._config_mode == _ConfigMode.PROFILES rf = session.execute_async("query", execution_profile='by-name') self._verify_response_future_profile(rf, internalized_profile) @@ -430,7 +833,7 @@ def test_profile_name_value(self): def test_exec_profile_clone(self): cluster = Cluster(execution_profiles={EXEC_PROFILE_DEFAULT: ExecutionProfile(), 'one': ExecutionProfile()}) - session = Session(cluster, hosts=[Host("127.0.0.1", SimpleConvictionPolicy)]) + session = Session(cluster, hosts=[Host("127.0.0.1", SimpleConvictionPolicy, host_id=uuid.uuid4())]) profile_attrs = {'request_timeout': 1, 'consistency_level': ConsistencyLevel.ANY, @@ -444,34 +847,38 @@ def test_exec_profile_clone(self): for profile in (EXEC_PROFILE_DEFAULT, 'one'): active = session.get_execution_profile(profile) clone = session.execution_profile_clone_update(profile) - self.assertIsNot(clone, active) + assert clone is not active all_updated = session.execution_profile_clone_update(clone, **profile_attrs) - self.assertIsNot(all_updated, clone) + assert all_updated is not clone for attr, value in profile_attrs.items(): - self.assertEqual(getattr(clone, attr), getattr(active, attr)) + assert getattr(clone, attr) == getattr(active, attr) if attr in reference_attributes: - self.assertIs(getattr(clone, attr), getattr(active, attr)) - self.assertNotEqual(getattr(all_updated, attr), getattr(active, attr)) + assert getattr(clone, attr) is getattr(active, attr) + assert getattr(all_updated, attr) != getattr(active, attr) # cannot clone nonexistent profile - self.assertRaises(ValueError, session.execution_profile_clone_update, 'DOES NOT EXIST', **profile_attrs) + with pytest.raises(ValueError): + session.execution_profile_clone_update('DOES NOT EXIST', **profile_attrs) def test_no_profiles_same_name(self): # can override default in init cluster = Cluster(execution_profiles={EXEC_PROFILE_DEFAULT: ExecutionProfile(), 'one': ExecutionProfile()}) # cannot update default - self.assertRaises(ValueError, cluster.add_execution_profile, EXEC_PROFILE_DEFAULT, ExecutionProfile()) + with pytest.raises(ValueError): + cluster.add_execution_profile(EXEC_PROFILE_DEFAULT, ExecutionProfile()) # cannot update named init - self.assertRaises(ValueError, cluster.add_execution_profile, 'one', ExecutionProfile()) + with pytest.raises(ValueError): + cluster.add_execution_profile('one', ExecutionProfile()) # can add new name cluster.add_execution_profile('two', ExecutionProfile()) # cannot add a profile added dynamically - self.assertRaises(ValueError, cluster.add_execution_profile, 'two', ExecutionProfile()) + with pytest.raises(ValueError): + cluster.add_execution_profile('two', ExecutionProfile()) def test_warning_on_no_lbp_with_contact_points_legacy_mode(self): """ @@ -511,8 +918,8 @@ def _check_warning_on_no_lbp_with_contact_points(self, cluster_kwargs): Cluster(**cluster_kwargs) patched_logger.warning.assert_called_once() warning_message = patched_logger.warning.call_args[0][0] - self.assertIn('please specify a load-balancing policy', warning_message) - self.assertIn("contact_points = ['127.0.0.1']", warning_message) + assert 'please specify a load-balancing policy' in warning_message + assert "contact_points = ['127.0.0.1']" in warning_message def test_no_warning_on_contact_points_with_lbp_legacy_mode(self): """ @@ -580,9 +987,9 @@ def test_warning_adding_no_lbp_ep_to_cluster_with_contact_points(self): patched_logger.warning.assert_called_once() warning_message = patched_logger.warning.call_args[0][0] - self.assertIn('no_lbp', warning_message) - self.assertIn('trying to add', warning_message) - self.assertIn('please specify a load-balancing policy', warning_message) + assert 'no_lbp' in warning_message + assert 'trying to add' in warning_message + assert 'please specify a load-balancing policy' in warning_message @mock_session_pools def test_no_warning_adding_lbp_ep_to_cluster_with_contact_points(self): diff --git a/tests/unit/test_concurrent.py b/tests/unit/test_concurrent.py index bdfd08126e..d3888aa9de 100644 --- a/tests/unit/test_concurrent.py +++ b/tests/unit/test_concurrent.py @@ -22,12 +22,14 @@ from queue import PriorityQueue import sys import platform +import uuid from cassandra.cluster import Cluster, Session from cassandra.concurrent import execute_concurrent, execute_concurrent_with_args from cassandra.pool import Host from cassandra.policies import SimpleConvictionPolicy from tests.unit.utils import mock_session_pools +import pytest class MockResponseResponseFuture(): @@ -229,14 +231,14 @@ def validate_result_ordering(self, results): """ last_time_added = 0 for success, result in results: - self.assertTrue(success) + assert success current_time_added = list(result)[0] #Windows clock granularity makes this equal most of the times if "Windows" in platform.system(): - self.assertLessEqual(last_time_added, current_time_added) + assert last_time_added <= current_time_added else: - self.assertLess(last_time_added, current_time_added) + assert last_time_added < current_time_added last_time_added = current_time_added @mock_session_pools @@ -247,11 +249,61 @@ def test_recursion_limited(self): PYTHON-585 """ max_recursion = sys.getrecursionlimit() - s = Session(Cluster(), [Host("127.0.0.1", SimpleConvictionPolicy)]) - self.assertRaises(TypeError, execute_concurrent_with_args, s, "doesn't matter", [('param',)] * max_recursion, raise_on_first_error=True) + s = Session(Cluster(), [Host("127.0.0.1", SimpleConvictionPolicy, host_id=uuid.uuid4())]) + with pytest.raises(TypeError): + execute_concurrent_with_args(s, "doesn't matter", [('param',)] * max_recursion, raise_on_first_error=True) results = execute_concurrent_with_args(s, "doesn't matter", [('param',)] * max_recursion, raise_on_first_error=False) # previously - self.assertEqual(len(results), max_recursion) + assert len(results) == max_recursion for r in results: - self.assertFalse(r[0]) - self.assertIsInstance(r[1], TypeError) + assert not r[0] + assert isinstance(r[1], TypeError) + + def test_no_recursion_on_synchronous_errback(self): + """ + Verify that execute_concurrent does not blow the stack when every + future completes with an error *before* add_callbacks is called + (i.e. the errback fires synchronously inside add_callbacks). + + This exercises a different code path from test_recursion_limited: + that test covers execute_async raising an exception, while this one + covers execute_async returning a future whose errback fires inline. + """ + count = sys.getrecursionlimit() + error = Exception("immediate failure") + + class AlreadyFailedFuture: + """A future that already has _final_exception set.""" + _query_trace = None + _col_names = None + _col_types = None + has_more_pages = False + + def add_callback(self, fn, *args, **kwargs): + pass + + def add_errback(self, fn, *args, **kwargs): + # Fire errback synchronously, mimicking a future that + # completed before add_callbacks was called. + fn(error, *args, **kwargs) + + def add_callbacks(self, callback, errback, + callback_args=(), callback_kwargs=None, + errback_args=(), errback_kwargs=None): + self.add_callback(callback, *callback_args, **(callback_kwargs or {})) + self.add_errback(errback, *errback_args, **(errback_kwargs or {})) + + def clear_callbacks(self): + pass + + mock_session = Mock() + mock_session.execute_async.return_value = AlreadyFailedFuture() + + statements_and_params = [("SELECT 1", ())] * count + results = execute_concurrent(mock_session, statements_and_params, + raise_on_first_error=False) + + assert len(results) == count + for success, result in results: + assert not success + assert result is error diff --git a/tests/unit/test_connection.py b/tests/unit/test_connection.py index 51e6247313..cf4607fbed 100644 --- a/tests/unit/test_connection.py +++ b/tests/unit/test_connection.py @@ -11,6 +11,7 @@ # 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 itertools import unittest from io import BytesIO import time @@ -20,39 +21,33 @@ from cassandra import OperationTimedOut from cassandra.cluster import Cluster from cassandra.connection import (Connection, HEADER_DIRECTION_TO_CLIENT, ProtocolError, - locally_supported_compressions, ConnectionHeartbeat, _Frame, Timer, TimerManager, - ConnectionException, DefaultEndPoint) + locally_supported_compressions, ConnectionHeartbeat, HeartbeatFuture, _Frame, Timer, TimerManager, + ConnectionException, ConnectionShutdown, DefaultEndPoint, ShardAwarePortGenerator) from cassandra.marshal import uint8_pack, uint32_pack, int32_pack from cassandra.protocol import (write_stringmultimap, write_int, write_string, - SupportedMessage, ProtocolHandler) + SupportedMessage, ProtocolHandler, ResultMessage, + RESULT_KIND_SET_KEYSPACE) -from tests.util import wait_until +from tests.util import wait_until, assertRegex +import pytest class ConnectionTest(unittest.TestCase): - def make_connection(self): - c = Connection(DefaultEndPoint('1.2.3.4')) + def make_connection(self, **kwargs): + c = Connection(DefaultEndPoint('1.2.3.4'), **kwargs) c._socket = Mock() c._socket.send.side_effect = lambda x: len(x) return c def make_header_prefix(self, message_class, version=Connection.protocol_version, stream_id=0): - if Connection.protocol_version < 3: - return bytes().join(map(uint8_pack, [ - 0xff & (HEADER_DIRECTION_TO_CLIENT | version), - 0, # flags (compression) - stream_id, - message_class.opcode # opcode - ])) - else: - return bytes().join(map(uint8_pack, [ - 0xff & (HEADER_DIRECTION_TO_CLIENT | version), - 0, # flags (compression) - 0, # MSB for v3+ stream - stream_id, - message_class.opcode # opcode - ])) + return bytes().join(map(uint8_pack, [ + 0xff & (HEADER_DIRECTION_TO_CLIENT | version), + 0, # flags (compression) + 0, # MSB for v3+ stream + stream_id, + message_class.opcode # opcode + ])) def make_options_body(self): options_buf = BytesIO() @@ -74,17 +69,17 @@ def make_msg(self, header, body=""): def test_connection_endpoint(self): endpoint = DefaultEndPoint('1.2.3.4') c = Connection(endpoint) - self.assertEqual(c.endpoint, endpoint) - self.assertEqual(c.endpoint.address, endpoint.address) + assert c.endpoint == endpoint + assert c.endpoint.address == endpoint.address c = Connection(host=endpoint) # kwarg - self.assertEqual(c.endpoint, endpoint) - self.assertEqual(c.endpoint.address, endpoint.address) + assert c.endpoint == endpoint + assert c.endpoint.address == endpoint.address c = Connection('10.0.0.1') endpoint = DefaultEndPoint('10.0.0.1') - self.assertEqual(c.endpoint, endpoint) - self.assertEqual(c.endpoint.address, endpoint.address) + assert c.endpoint == endpoint + assert c.endpoint.address == endpoint.address def test_bad_protocol_version(self, *args): c = self.make_connection() @@ -102,7 +97,7 @@ def test_bad_protocol_version(self, *args): # make sure it errored correctly c.defunct.assert_called_once_with(ANY) args, kwargs = c.defunct.call_args - self.assertIsInstance(args[0], ProtocolError) + assert isinstance(args[0], ProtocolError) def test_negative_body_length(self, *args): c = self.make_connection() @@ -119,7 +114,7 @@ def test_negative_body_length(self, *args): # make sure it errored correctly c.defunct.assert_called_once_with(ANY) args, kwargs = c.defunct.call_args - self.assertIsInstance(args[0], ProtocolError) + assert isinstance(args[0], ProtocolError) def test_unsupported_cql_version(self, *args): c = self.make_connection() @@ -139,7 +134,7 @@ def test_unsupported_cql_version(self, *args): # make sure it errored correctly c.defunct.assert_called_once_with(ANY) args, kwargs = c.defunct.call_args - self.assertIsInstance(args[0], ProtocolError) + assert isinstance(args[0], ProtocolError) def test_prefer_lz4_compression(self, *args): c = self.make_connection() @@ -162,7 +157,7 @@ def test_prefer_lz4_compression(self, *args): c.process_msg(_Frame(version=4, flags=0, stream=0, opcode=SupportedMessage.opcode, body_offset=9, end_pos=9 + len(options)), options) - self.assertEqual(c.decompressor, locally_supported_compressions['lz4'][1]) + assert c.decompressor == locally_supported_compressions['lz4'][1] def test_requested_compression_not_available(self, *args): c = self.make_connection() @@ -189,10 +184,10 @@ def test_requested_compression_not_available(self, *args): # make sure it errored correctly c.defunct.assert_called_once_with(ANY) args, kwargs = c.defunct.call_args - self.assertIsInstance(args[0], ProtocolError) + assert isinstance(args[0], ProtocolError) def test_use_requested_compression(self, *args): - c = self.make_connection() + c = self.make_connection(protocol_version=4) c._requests = {0: (c._handle_options_response, ProtocolHandler.decode_message, [])} c.defunct = Mock() # request snappy compression @@ -213,7 +208,7 @@ def test_use_requested_compression(self, *args): c.process_msg(_Frame(version=4, flags=0, stream=0, opcode=SupportedMessage.opcode, body_offset=9, end_pos=9 + len(options)), options) - self.assertEqual(c.decompressor, locally_supported_compressions['snappy'][1]) + assert c.decompressor == locally_supported_compressions['snappy'][1] def test_disable_compression(self, *args): c = self.make_connection() @@ -241,29 +236,132 @@ def test_disable_compression(self, *args): message = self.make_msg(header, options) c.process_msg(message, len(message) - 8) - self.assertEqual(c.decompressor, None) + assert c.decompressor == None def test_not_implemented(self): """ Ensure the following methods throw NIE's. If not, come back and test them. """ c = self.make_connection() - self.assertRaises(NotImplementedError, c.close) + with pytest.raises(NotImplementedError): + c.close() def test_set_keyspace_blocking(self): c = self.make_connection() - self.assertEqual(c.keyspace, None) + assert c.keyspace == None c.set_keyspace_blocking(None) - self.assertEqual(c.keyspace, None) + assert c.keyspace == None c.keyspace = 'ks' c.set_keyspace_blocking('ks') - self.assertEqual(c.keyspace, 'ks') + assert c.keyspace == 'ks' + + def test_set_keyspace_blocking_escapes_quotes(self): + """ + Test that set_keyspace_blocking properly escapes double quotes in + keyspace names to prevent CQL injection. This is the Python equivalent + of the vulnerability fixed in the Go driver: + https://github.com/scylladb/gocql/pull/783 + """ + c = self.make_connection() + c.wait_for_response = Mock(return_value=ResultMessage(kind=RESULT_KIND_SET_KEYSPACE)) + + c.set_keyspace_blocking('my"ks') + query_msg = c.wait_for_response.call_args[0][0] + assert query_msg.query == 'USE "my""ks"', ( + "Double quotes in keyspace name must be escaped as double-double quotes") + + def test_set_keyspace_async_escapes_quotes(self): + """ + Test that set_keyspace_async properly escapes double quotes in + keyspace names to prevent CQL injection. + """ + c = self.make_connection() + c.lock = Lock() + c.in_flight = 0 + c.max_request_id = 100 + c.get_request_id = Mock(return_value=1) + c.send_msg = Mock() + + callback = Mock() + c.set_keyspace_async('my"ks', callback) + + query_msg = c.send_msg.call_args[0][0] + assert query_msg.query == 'USE "my""ks"', ( + "Double quotes in keyspace name must be escaped as double-double quotes") def test_set_connection_class(self): cluster = Cluster(connection_class='test') - self.assertEqual('test', cluster.connection_class) + assert 'test' == cluster.connection_class + + def test_connection_shutdown_includes_last_error(self): + """ + Test that ConnectionShutdown exceptions include the last_error when available. + This helps debug issues like "Bad file descriptor" by showing the original cause. + See https://github.com/scylladb/python-driver/issues/614 + """ + c = self.make_connection() + c.lock = Lock() + c._requests = {} + + # Simulate the connection becoming defunct with a specific error + original_error = OSError(9, "Bad file descriptor") + c.is_defunct = True + c.last_error = original_error + + # send_msg should raise ConnectionShutdown that includes the last_error + with pytest.raises(ConnectionShutdown) as exc_info: + c.send_msg(Mock(), 1, Mock()) + + # Verify the error message includes the original error + error_message = str(exc_info.value) + assert "is defunct" in error_message + assert "Bad file descriptor" in error_message + + def test_connection_shutdown_closed_includes_last_error(self): + """ + Test that ConnectionShutdown exceptions for closed connections include last_error. + """ + c = self.make_connection() + c.lock = Lock() + c._requests = {} + + # Simulate the connection being closed with a specific error + original_error = OSError(9, "Bad file descriptor") + c.is_closed = True + c.last_error = original_error + + # send_msg should raise ConnectionShutdown that includes the last_error + with pytest.raises(ConnectionShutdown) as exc_info: + c.send_msg(Mock(), 1, Mock()) + + # Verify the error message includes the original error + error_message = str(exc_info.value) + assert "is closed" in error_message + assert "Bad file descriptor" in error_message + + def test_wait_for_responses_shutdown_includes_last_error(self): + """ + Test that wait_for_responses raises ConnectionShutdown with last_error. + """ + c = self.make_connection() + c.lock = Lock() + c._requests = {} + + # Simulate the connection being defunct with a specific error + original_error = OSError(9, "Bad file descriptor") + c.is_defunct = True + c.last_error = original_error + + # wait_for_responses should raise ConnectionShutdown that includes the last_error + with pytest.raises(ConnectionShutdown) as exc_info: + c.wait_for_responses(Mock()) + + # Verify the error message includes the original error + error_message = str(exc_info.value) + assert "already closed" in error_message + assert "Bad file descriptor" in error_message @patch('cassandra.connection.ConnectionHeartbeat._raise_if_stopped') @@ -285,7 +383,7 @@ def run_heartbeat(self, get_holders_fun, count=2, interval=0.05, timeout=0.05): wait_until(lambda: get_holders_fun.call_count > 0, 0.01, 100) time.sleep(interval * (count-1)) ch.stop() - self.assertTrue(get_holders_fun.call_count) + assert get_holders_fun.call_count def test_empty_connections(self, *args): count = 3 @@ -293,8 +391,8 @@ def test_empty_connections(self, *args): self.run_heartbeat(get_holders, count) - self.assertGreaterEqual(get_holders.call_count, count-1) - self.assertLessEqual(get_holders.call_count, count) + assert get_holders.call_count >= count-1 + assert get_holders.call_count <= count holder = get_holders.return_value[0] holder.get_connections.assert_has_calls([call()] * get_holders.call_count) @@ -322,11 +420,11 @@ def send_msg(msg, req_id, msg_callback): self.run_heartbeat(get_holders) holder.get_connections.assert_has_calls([call()] * get_holders.call_count) - self.assertEqual(idle_connection.in_flight, 0) - self.assertEqual(non_idle_connection.in_flight, 0) + assert idle_connection.in_flight == 0 + assert non_idle_connection.in_flight == 0 idle_connection.send_msg.assert_has_calls([call(ANY, request_id, ANY)] * get_holders.call_count) - self.assertEqual(non_idle_connection.send_msg.call_count, 0) + assert non_idle_connection.send_msg.call_count == 0 def test_closed_defunct(self, *args): get_holders = self.make_get_holders(1) @@ -339,10 +437,10 @@ def test_closed_defunct(self, *args): self.run_heartbeat(get_holders) holder.get_connections.assert_has_calls([call()] * get_holders.call_count) - self.assertEqual(closed_connection.in_flight, 0) - self.assertEqual(defunct_connection.in_flight, 0) - self.assertEqual(closed_connection.send_msg.call_count, 0) - self.assertEqual(defunct_connection.send_msg.call_count, 0) + assert closed_connection.in_flight == 0 + assert defunct_connection.in_flight == 0 + assert closed_connection.send_msg.call_count == 0 + assert defunct_connection.send_msg.call_count == 0 def test_no_req_ids(self, *args): in_flight = 3 @@ -358,13 +456,38 @@ def test_no_req_ids(self, *args): self.run_heartbeat(get_holders) holder.get_connections.assert_has_calls([call()] * get_holders.call_count) - self.assertEqual(max_connection.in_flight, in_flight) - self.assertEqual(max_connection.send_msg.call_count, 0) - self.assertEqual(max_connection.send_msg.call_count, 0) + assert max_connection.in_flight == in_flight + assert max_connection.send_msg.call_count == 0 + assert max_connection.send_msg.call_count == 0 max_connection.defunct.assert_has_calls([call(ANY)] * get_holders.call_count) holder.return_connection.assert_has_calls( [call(max_connection)] * get_holders.call_count) + def test_heartbeat_future_releases_request_id_when_send_fails(self, *args): + connection = Connection(DefaultEndPoint('1.2.3.4')) + connection.push = Mock(side_effect=ConnectionException("write failed")) + owner = Mock() + initial_in_flight = connection.in_flight + initial_request_ids = len(connection.request_ids) + + # HostConnection.return_connection releases the heartbeat's in-flight slot. + def return_connection(conn): + with conn.lock: + conn.in_flight -= 1 + + owner.return_connection.side_effect = return_connection + + future = HeartbeatFuture(connection, owner) + + with pytest.raises(ConnectionException): + future.wait(0) + + owner.return_connection(connection) + + assert connection.in_flight == initial_in_flight + assert len(connection.request_ids) == initial_request_ids + assert not connection._requests + def test_unexpected_response(self, *args): request_id = 999 @@ -385,12 +508,12 @@ def send_msg(msg, req_id, msg_callback): self.run_heartbeat(get_holders) - self.assertEqual(connection.in_flight, get_holders.call_count) + assert connection.in_flight == get_holders.call_count connection.send_msg.assert_has_calls([call(ANY, request_id, ANY)] * get_holders.call_count) connection.defunct.assert_has_calls([call(ANY)] * get_holders.call_count) exc = connection.defunct.call_args_list[0][0][0] - self.assertIsInstance(exc, ConnectionException) - self.assertRegex(exc.args[0], r'^Received unexpected response to OptionsMessage.*') + assert isinstance(exc, ConnectionException) + assertRegex(exc.args[0], r'^Received unexpected response to OptionsMessage.*') holder.return_connection.assert_has_calls( [call(connection)] * get_holders.call_count) @@ -415,13 +538,15 @@ def send_msg(msg, req_id, msg_callback): self.run_heartbeat(get_holders) - self.assertEqual(connection.in_flight, get_holders.call_count) + assert connection.in_flight == get_holders.call_count connection.send_msg.assert_has_calls([call(ANY, request_id, ANY)] * get_holders.call_count) connection.defunct.assert_has_calls([call(ANY)] * get_holders.call_count) exc = connection.defunct.call_args_list[0][0][0] - self.assertIsInstance(exc, OperationTimedOut) - self.assertEqual(exc.errors, 'Connection heartbeat timeout after 0.05 seconds') - self.assertEqual(exc.last_host, DefaultEndPoint('localhost')) + assert isinstance(exc, OperationTimedOut) + assert exc.errors == 'Connection heartbeat timeout after 0.05 seconds' + assert exc.last_host == DefaultEndPoint('localhost') + assert exc.timeout == 0.05 + assert isinstance(exc.in_flight, int) holder.return_connection.assert_has_calls( [call(connection)] * get_holders.call_count) @@ -446,43 +571,65 @@ class DefaultEndPointTest(unittest.TestCase): def test_default_endpoint_properties(self): endpoint = DefaultEndPoint('10.0.0.1') - self.assertEqual(endpoint.address, '10.0.0.1') - self.assertEqual(endpoint.port, 9042) - self.assertEqual(str(endpoint), '10.0.0.1:9042') + assert endpoint.address == '10.0.0.1' + assert endpoint.port == 9042 + assert str(endpoint) == '10.0.0.1:9042' endpoint = DefaultEndPoint('10.0.0.1', 8888) - self.assertEqual(endpoint.address, '10.0.0.1') - self.assertEqual(endpoint.port, 8888) - self.assertEqual(str(endpoint), '10.0.0.1:8888') + assert endpoint.address == '10.0.0.1' + assert endpoint.port == 8888 + assert str(endpoint) == '10.0.0.1:8888' def test_endpoint_equality(self): - self.assertEqual( - DefaultEndPoint('10.0.0.1'), - DefaultEndPoint('10.0.0.1') - ) - - self.assertEqual( - DefaultEndPoint('10.0.0.1'), - DefaultEndPoint('10.0.0.1', 9042) - ) - - self.assertNotEqual( - DefaultEndPoint('10.0.0.1'), - DefaultEndPoint('10.0.0.2') - ) - - self.assertNotEqual( - DefaultEndPoint('10.0.0.1'), - DefaultEndPoint('10.0.0.1', 0000) - ) + assert DefaultEndPoint('10.0.0.1') == DefaultEndPoint('10.0.0.1') + + assert DefaultEndPoint('10.0.0.1') == DefaultEndPoint('10.0.0.1', 9042) + + assert DefaultEndPoint('10.0.0.1') != DefaultEndPoint('10.0.0.2') + + assert DefaultEndPoint('10.0.0.1') != DefaultEndPoint('10.0.0.1', 0000) def test_endpoint_resolve(self): - self.assertEqual( - DefaultEndPoint('10.0.0.1').resolve(), - ('10.0.0.1', 9042) - ) - - self.assertEqual( - DefaultEndPoint('10.0.0.1', 3232).resolve(), - ('10.0.0.1', 3232) - ) + assert DefaultEndPoint('10.0.0.1').resolve() == ('10.0.0.1', 9042) + + assert DefaultEndPoint('10.0.0.1', 3232).resolve() == ('10.0.0.1', 3232) + + +class TestShardawarePortGenerator(unittest.TestCase): + @patch('random.randrange') + def test_generate_ports_basic(self, mock_randrange): + mock_randrange.return_value = 10005 + gen = ShardAwarePortGenerator(10000, 10020) + ports = list(itertools.islice(gen.generate(shard_id=1, total_shards=3), 5)) + + # Starting from aligned 10005 + shard_id (1), step by 3 + assert ports == [10006, 10009, 10012, 10015, 10018] + + @patch('random.randrange') + def test_wraps_around_to_start(self, mock_randrange): + mock_randrange.return_value = 10008 + gen = ShardAwarePortGenerator(10000, 10020) + ports = list(itertools.islice(gen.generate(shard_id=2, total_shards=4), 5)) + + # Expected wrap-around from start_port after end_port is exceeded + assert ports == [10010, 10014, 10018, 10002, 10006] + + @patch('random.randrange') + def test_all_ports_have_correct_modulo(self, mock_randrange): + mock_randrange.return_value = 10012 + total_shards = 5 + shard_id = 3 + gen = ShardAwarePortGenerator(10000, 10020) + + for port in gen.generate(shard_id=shard_id, total_shards=total_shards): + assert port % total_shards == shard_id + + @patch('random.randrange') + def test_generate_is_repeatable_with_same_mock(self, mock_randrange): + mock_randrange.return_value = 10010 + gen = ShardAwarePortGenerator(10000, 10020) + + first_run = list(itertools.islice(gen.generate(0, 2), 5)) + second_run = list(itertools.islice(gen.generate(0, 2), 5)) + + assert first_run == second_run diff --git a/tests/unit/test_control_connection.py b/tests/unit/test_control_connection.py index 71a6b024cd..fd62323f33 100644 --- a/tests/unit/test_control_connection.py +++ b/tests/unit/test_control_connection.py @@ -15,7 +15,7 @@ import unittest from concurrent.futures import ThreadPoolExecutor -from unittest.mock import Mock, ANY, call +from unittest.mock import Mock, ANY, call, patch from cassandra import OperationTimedOut, SchemaTargetType, SchemaChangeType from cassandra.protocol import ResultMessage, RESULT_KIND_ROWS @@ -210,30 +210,41 @@ def test_wait_for_schema_agreement(self): """ Basic test with all schema versions agreeing """ - self.assertTrue(self.control_connection.wait_for_schema_agreement()) + assert self.control_connection._wait_for_schema_agreement() # the control connection should not have slept at all - self.assertEqual(self.time.clock, 0) + assert self.time.clock == 0 + + @patch('cassandra.cluster.warn') + def test_wait_for_schema_agreement_warns_about_deprecation(self, mocked_warn): + assert self.control_connection.wait_for_schema_agreement() + + mocked_warn.assert_called_once() + warning_args, warning_kwargs = mocked_warn.call_args + assert 'ControlConnection.wait_for_schema_agreement is deprecated' in str(warning_args[0]) + assert 'Use Session.wait_for_schema_agreement instead.' in str(warning_args[0]) + assert warning_args[1] is DeprecationWarning + assert warning_kwargs['stacklevel'] == 2 def test_wait_for_schema_agreement_uses_preloaded_results_if_given(self): """ wait_for_schema_agreement uses preloaded results if given for shared table queries """ preloaded_results = self._matching_schema_preloaded_results - self.assertTrue(self.control_connection.wait_for_schema_agreement(preloaded_results=preloaded_results)) + assert self.control_connection._wait_for_schema_agreement(preloaded_results=preloaded_results) # the control connection should not have slept at all - self.assertEqual(self.time.clock, 0) + assert self.time.clock == 0 # the connection should not have made any queries if given preloaded results - self.assertEqual(self.connection.wait_for_responses.call_count, 0) + assert self.connection.wait_for_responses.call_count == 0 def test_wait_for_schema_agreement_falls_back_to_querying_if_schemas_dont_match_preloaded_result(self): """ wait_for_schema_agreement requery if schema does not match using preloaded results """ preloaded_results = self._nonmatching_schema_preloaded_results - self.assertTrue(self.control_connection.wait_for_schema_agreement(preloaded_results=preloaded_results)) + assert self.control_connection._wait_for_schema_agreement(preloaded_results=preloaded_results) # the control connection should not have slept at all - self.assertEqual(self.time.clock, 0) - self.assertEqual(self.connection.wait_for_responses.call_count, 1) + assert self.time.clock == 0 + assert self.connection.wait_for_responses.call_count == 1 def test_wait_for_schema_agreement_fails(self): """ @@ -241,9 +252,9 @@ def test_wait_for_schema_agreement_fails(self): """ # change the schema version on one node self.connection.peer_results[1][1][2] = 'b' - self.assertFalse(self.control_connection.wait_for_schema_agreement()) + assert not self.control_connection._wait_for_schema_agreement() # the control connection should have slept until it hit the limit - self.assertGreaterEqual(self.time.clock, self.cluster.max_schema_agreement_wait) + assert self.time.clock >= self.cluster.max_schema_agreement_wait def test_wait_for_schema_agreement_skipping(self): """ @@ -262,8 +273,8 @@ def test_wait_for_schema_agreement_skipping(self): self.connection.peer_results[1][1][3] = 'c' self.cluster.metadata.get_host(DefaultEndPoint('192.168.1.1')).is_up = False - self.assertTrue(self.control_connection.wait_for_schema_agreement()) - self.assertEqual(self.time.clock, 0) + assert self.control_connection._wait_for_schema_agreement() + assert self.time.clock == 0 def test_wait_for_schema_agreement_rpc_lookup(self): """ @@ -279,38 +290,52 @@ def test_wait_for_schema_agreement_rpc_lookup(self): # even though the new host has a different schema version, it's # marked as down, so the control connection shouldn't care - self.assertTrue(self.control_connection.wait_for_schema_agreement()) - self.assertEqual(self.time.clock, 0) + assert self.control_connection._wait_for_schema_agreement() + assert self.time.clock == 0 # but once we mark it up, the control connection will care host.is_up = True - self.assertFalse(self.control_connection.wait_for_schema_agreement()) - self.assertGreaterEqual(self.time.clock, self.cluster.max_schema_agreement_wait) + assert not self.control_connection._wait_for_schema_agreement() + assert self.time.clock >= self.cluster.max_schema_agreement_wait + + + def test_wait_for_schema_agreement_none_timeout(self): + """ + When control_connection_timeout is None, wait_for_schema_agreement + should not raise a TypeError on the min() call. + """ + cc = ControlConnection(self.cluster, timeout=None, + schema_event_refresh_window=0, + topology_event_refresh_window=0, + status_event_refresh_window=0) + cc._connection = self.connection + cc._time = self.time + assert cc._wait_for_schema_agreement() def test_refresh_nodes_and_tokens(self): self.control_connection.refresh_node_list_and_token_map() meta = self.cluster.metadata - self.assertEqual(meta.partitioner, 'Murmur3Partitioner') - self.assertEqual(meta.cluster_name, 'foocluster') + assert meta.partitioner == 'Murmur3Partitioner' + assert meta.cluster_name == 'foocluster' # check token map - self.assertEqual(sorted(meta.all_hosts()), sorted(meta.token_map.keys())) + assert sorted(meta.all_hosts()) == sorted(meta.token_map.keys()) for token_list in meta.token_map.values(): - self.assertEqual(3, len(token_list)) + assert 3 == len(token_list) # check datacenter/rack for host in meta.all_hosts(): - self.assertEqual(host.datacenter, "dc1") - self.assertEqual(host.rack, "rack1") + assert host.datacenter == "dc1" + assert host.rack == "rack1" - self.assertEqual(self.connection.wait_for_responses.call_count, 1) + assert self.connection.wait_for_responses.call_count == 1 def test_refresh_nodes_and_tokens_with_invalid_peers(self): def refresh_and_validate_added_hosts(): self.connection.wait_for_responses = Mock(return_value=_node_meta_results( self.connection.local_results, self.connection.peer_results)) self.control_connection.refresh_node_list_and_token_map() - self.assertEqual(1, len(self.cluster.added_hosts)) # only one valid peer found + assert 1 == len(self.cluster.added_hosts) # only one valid peer found # peersV1 del self.connection.peer_results[:] @@ -357,12 +382,12 @@ def test_change_ip(self): self.connection.local_results, self.connection.peer_results)) self.control_connection.refresh_node_list_and_token_map() # all peers are updated - self.assertEqual(0, len(self.cluster.added_hosts)) + assert 0 == len(self.cluster.added_hosts) assert self.cluster.metadata.get_host('192.168.1.5') assert self.cluster.metadata.get_host('192.168.1.6') - self.assertEqual(3, len(self.cluster.metadata.all_hosts())) + assert 3 == len(self.cluster.metadata.all_hosts()) def test_refresh_nodes_and_tokens_uses_preloaded_results_if_given(self): @@ -372,21 +397,21 @@ def test_refresh_nodes_and_tokens_uses_preloaded_results_if_given(self): preloaded_results = self._matching_schema_preloaded_results self.control_connection._refresh_node_list_and_token_map(self.connection, preloaded_results=preloaded_results) meta = self.cluster.metadata - self.assertEqual(meta.partitioner, 'Murmur3Partitioner') - self.assertEqual(meta.cluster_name, 'foocluster') + assert meta.partitioner == 'Murmur3Partitioner' + assert meta.cluster_name == 'foocluster' # check token map - self.assertEqual(sorted(meta.all_hosts()), sorted(meta.token_map.keys())) + assert sorted(meta.all_hosts()) == sorted(meta.token_map.keys()) for token_list in meta.token_map.values(): - self.assertEqual(3, len(token_list)) + assert 3 == len(token_list) # check datacenter/rack for host in meta.all_hosts(): - self.assertEqual(host.datacenter, "dc1") - self.assertEqual(host.rack, "rack1") + assert host.datacenter == "dc1" + assert host.rack == "rack1" # the connection should not have made any queries if given preloaded results - self.assertEqual(self.connection.wait_for_responses.call_count, 0) + assert self.connection.wait_for_responses.call_count == 0 def test_refresh_nodes_and_tokens_no_partitioner(self): """ @@ -396,8 +421,8 @@ def test_refresh_nodes_and_tokens_no_partitioner(self): self.connection.local_results[1][0][5] = None self.control_connection.refresh_node_list_and_token_map() meta = self.cluster.metadata - self.assertEqual(meta.partitioner, None) - self.assertEqual(meta.token_map, {}) + assert meta.partitioner == None + assert meta.token_map == {} def test_refresh_nodes_and_tokens_add_host(self): self.connection.peer_results[1].append( @@ -405,29 +430,30 @@ def test_refresh_nodes_and_tokens_add_host(self): ) self.cluster.scheduler.schedule = lambda delay, f, *args, **kwargs: f(*args, **kwargs) self.control_connection.refresh_node_list_and_token_map() - self.assertEqual(1, len(self.cluster.added_hosts)) - self.assertEqual(self.cluster.added_hosts[0].address, "192.168.1.3") - self.assertEqual(self.cluster.added_hosts[0].datacenter, "dc1") - self.assertEqual(self.cluster.added_hosts[0].rack, "rack1") - self.assertEqual(self.cluster.added_hosts[0].host_id, "uuid4") + assert 1 == len(self.cluster.added_hosts) + assert self.cluster.added_hosts[0].address == "192.168.1.3" + assert self.cluster.added_hosts[0].datacenter == "dc1" + assert self.cluster.added_hosts[0].rack == "rack1" + assert self.cluster.added_hosts[0].host_id == "uuid4" def test_refresh_nodes_and_tokens_remove_host(self): del self.connection.peer_results[1][1] self.control_connection.refresh_node_list_and_token_map() - self.assertEqual(1, len(self.cluster.metadata.removed_hosts)) - self.assertEqual(self.cluster.metadata.removed_hosts[0].address, "192.168.1.2") + assert 1 == len(self.cluster.metadata.removed_hosts) + assert self.cluster.metadata.removed_hosts[0].address == "192.168.1.2" def test_refresh_nodes_and_tokens_timeout(self): def bad_wait_for_responses(*args, **kwargs): - self.assertEqual(kwargs['timeout'], self.control_connection._timeout) + assert kwargs['timeout'] == self.control_connection._timeout raise OperationTimedOut() self.connection.wait_for_responses = bad_wait_for_responses self.control_connection.refresh_node_list_and_token_map() self.cluster.executor.submit.assert_called_with(self.control_connection._reconnect) - def test_refresh_schema_timeout(self): + @patch('cassandra.cluster.warn') + def test_refresh_schema_timeout(self, mocked_warn): def bad_wait_for_responses(*args, **kwargs): self.time.sleep(kwargs['timeout']) @@ -435,8 +461,9 @@ def bad_wait_for_responses(*args, **kwargs): self.connection.wait_for_responses = Mock(side_effect=bad_wait_for_responses) self.control_connection.refresh_schema() - self.assertEqual(self.connection.wait_for_responses.call_count, self.cluster.max_schema_agreement_wait / self.control_connection._timeout) - self.assertEqual(self.connection.wait_for_responses.call_args[1]['timeout'], self.control_connection._timeout) + assert self.connection.wait_for_responses.call_count == self.cluster.max_schema_agreement_wait / self.control_connection._timeout + assert self.connection.wait_for_responses.call_args[1]['timeout'] == self.control_connection._timeout + mocked_warn.assert_not_called() def test_handle_topology_change(self): event = { @@ -489,7 +516,7 @@ def test_handle_status_change(self): 'address': ('1.2.3.4', 9000) } self.control_connection._handle_status_change(event) - self.assertFalse(self.cluster.scheduler.schedule.called) + assert not self.cluster.scheduler.schedule.called # do the same with a known Host event = { @@ -498,7 +525,7 @@ def test_handle_status_change(self): } self.control_connection._handle_status_change(event) host = self.cluster.metadata.get_host(DefaultEndPoint('192.168.1.0')) - self.assertIs(host, self.cluster.down_host) + assert host is self.cluster.down_host def test_handle_schema_change(self): @@ -545,8 +572,8 @@ def test_refresh_disabled(self): # no call on schema refresh cc_no_schema_refresh._handle_schema_change(schema_event) - self.assertFalse(cluster.scheduler.schedule.called) - self.assertFalse(cluster.scheduler.schedule_unique.called) + assert not cluster.scheduler.schedule.called + assert not cluster.scheduler.schedule_unique.called # topo and status changes as normal cc_no_schema_refresh._handle_status_change(status_event) @@ -559,8 +586,8 @@ def test_refresh_disabled(self): # no call on topo refresh cc_no_topo_refresh._handle_topology_change(topo_event) - self.assertFalse(cluster.scheduler.schedule.called) - self.assertFalse(cluster.scheduler.schedule_unique.called) + assert not cluster.scheduler.schedule.called + assert not cluster.scheduler.schedule_unique.called # schema and status change refresh as normal cc_no_topo_refresh._handle_status_change(status_event) @@ -579,15 +606,15 @@ def test_refresh_nodes_and_tokens_add_host_detects_port(self): self.connection.local_results, self.connection.peer_results)) self.cluster.scheduler.schedule = lambda delay, f, *args, **kwargs: f(*args, **kwargs) self.control_connection.refresh_node_list_and_token_map() - self.assertEqual(1, len(self.cluster.added_hosts)) - self.assertEqual(self.cluster.added_hosts[0].endpoint.address, "192.168.1.3") - self.assertEqual(self.cluster.added_hosts[0].endpoint.port, 555) - self.assertEqual(self.cluster.added_hosts[0].broadcast_rpc_address, "192.168.1.3") - self.assertEqual(self.cluster.added_hosts[0].broadcast_rpc_port, 555) - self.assertEqual(self.cluster.added_hosts[0].broadcast_address, "10.0.0.3") - self.assertEqual(self.cluster.added_hosts[0].broadcast_port, 666) - self.assertEqual(self.cluster.added_hosts[0].datacenter, "dc1") - self.assertEqual(self.cluster.added_hosts[0].rack, "rack1") + assert 1 == len(self.cluster.added_hosts) + assert self.cluster.added_hosts[0].endpoint.address == "192.168.1.3" + assert self.cluster.added_hosts[0].endpoint.port == 555 + assert self.cluster.added_hosts[0].broadcast_rpc_address == "192.168.1.3" + assert self.cluster.added_hosts[0].broadcast_rpc_port == 555 + assert self.cluster.added_hosts[0].broadcast_address == "10.0.0.3" + assert self.cluster.added_hosts[0].broadcast_port == 666 + assert self.cluster.added_hosts[0].datacenter == "dc1" + assert self.cluster.added_hosts[0].rack == "rack1" def test_refresh_nodes_and_tokens_add_host_detects_invalid_port(self): del self.connection.peer_results[:] @@ -599,15 +626,15 @@ def test_refresh_nodes_and_tokens_add_host_detects_invalid_port(self): self.connection.local_results, self.connection.peer_results)) self.cluster.scheduler.schedule = lambda delay, f, *args, **kwargs: f(*args, **kwargs) self.control_connection.refresh_node_list_and_token_map() - self.assertEqual(1, len(self.cluster.added_hosts)) - self.assertEqual(self.cluster.added_hosts[0].endpoint.address, "192.168.1.3") - self.assertEqual(self.cluster.added_hosts[0].endpoint.port, 9042) # fallback default - self.assertEqual(self.cluster.added_hosts[0].broadcast_rpc_address, "192.168.1.3") - self.assertEqual(self.cluster.added_hosts[0].broadcast_rpc_port, None) - self.assertEqual(self.cluster.added_hosts[0].broadcast_address, "10.0.0.3") - self.assertEqual(self.cluster.added_hosts[0].broadcast_port, None) - self.assertEqual(self.cluster.added_hosts[0].datacenter, "dc1") - self.assertEqual(self.cluster.added_hosts[0].rack, "rack1") + assert 1 == len(self.cluster.added_hosts) + assert self.cluster.added_hosts[0].endpoint.address == "192.168.1.3" + assert self.cluster.added_hosts[0].endpoint.port == 9042 # fallback default + assert self.cluster.added_hosts[0].broadcast_rpc_address == "192.168.1.3" + assert self.cluster.added_hosts[0].broadcast_rpc_port == None + assert self.cluster.added_hosts[0].broadcast_address == "10.0.0.3" + assert self.cluster.added_hosts[0].broadcast_port == None + assert self.cluster.added_hosts[0].datacenter == "dc1" + assert self.cluster.added_hosts[0].rack == "rack1" class EventTimingTest(unittest.TestCase): @@ -644,5 +671,5 @@ def test_event_delay_timing(self): self.cluster.scheduler.mock_calls # Grabs the delay parameter from the scheduler invocation current_delay = self.cluster.scheduler.mock_calls[0][1][0] - self.assertLess(prior_delay, current_delay) + assert prior_delay < current_delay prior_delay = current_delay diff --git a/tests/unit/test_endpoints.py b/tests/unit/test_endpoints.py index b0841962ca..14fb8b5806 100644 --- a/tests/unit/test_endpoints.py +++ b/tests/unit/test_endpoints.py @@ -31,31 +31,19 @@ class SniEndPointTest(unittest.TestCase): def test_sni_endpoint_properties(self): endpoint = self.endpoint_factory.create_from_sni('test') - self.assertEqual(endpoint.address, 'proxy.datastax.com') - self.assertEqual(endpoint.port, 30002) - self.assertEqual(endpoint._server_name, 'test') - self.assertEqual(str(endpoint), 'proxy.datastax.com:30002:test') + assert endpoint.address == 'proxy.datastax.com' + assert endpoint.port == 30002 + assert endpoint._server_name == 'test' + assert str(endpoint) == 'proxy.datastax.com:30002:test' def test_endpoint_equality(self): - self.assertNotEqual( - DefaultEndPoint('10.0.0.1'), - self.endpoint_factory.create_from_sni('10.0.0.1') - ) - - self.assertEqual( - self.endpoint_factory.create_from_sni('10.0.0.1'), - self.endpoint_factory.create_from_sni('10.0.0.1') - ) - - self.assertNotEqual( - self.endpoint_factory.create_from_sni('10.0.0.1'), - self.endpoint_factory.create_from_sni('10.0.0.0') - ) - - self.assertNotEqual( - self.endpoint_factory.create_from_sni('10.0.0.1'), - SniEndPointFactory("proxy.datastax.com", 9999).create_from_sni('10.0.0.1') - ) + assert DefaultEndPoint('10.0.0.1') != self.endpoint_factory.create_from_sni('10.0.0.1') + + assert self.endpoint_factory.create_from_sni('10.0.0.1') == self.endpoint_factory.create_from_sni('10.0.0.1') + + assert self.endpoint_factory.create_from_sni('10.0.0.1') != self.endpoint_factory.create_from_sni('10.0.0.0') + + assert self.endpoint_factory.create_from_sni('10.0.0.1') != SniEndPointFactory("proxy.datastax.com", 9999).create_from_sni('10.0.0.1') def test_endpoint_resolve(self): ips = ['127.0.0.1', '127.0.0.2', '127.0.0.3'] @@ -64,4 +52,4 @@ def test_endpoint_resolve(self): endpoint = self.endpoint_factory.create_from_sni('test') for i in range(10): (address, _) = endpoint.resolve() - self.assertEqual(address, next(it)) + assert address == next(it) diff --git a/tests/unit/test_exception.py b/tests/unit/test_exception.py index b39b22239c..6bddd96a4b 100644 --- a/tests/unit/test_exception.py +++ b/tests/unit/test_exception.py @@ -37,17 +37,17 @@ def test_timeout_consistency(self): Verify that Timeout exception object translates consistency from input value to correct output string """ consistency_str = self.extract_consistency(repr(Timeout("Timeout Message", consistency=None))) - self.assertEqual(consistency_str, 'Not Set') + assert consistency_str == 'Not Set' for c in ConsistencyLevel.value_to_name.keys(): consistency_str = self.extract_consistency(repr(Timeout("Timeout Message", consistency=c))) - self.assertEqual(consistency_str, ConsistencyLevel.value_to_name[c]) + assert consistency_str == ConsistencyLevel.value_to_name[c] def test_unavailable_consistency(self): """ Verify that Unavailable exception object translates consistency from input value to correct output string """ consistency_str = self.extract_consistency(repr(Unavailable("Unavailable Message", consistency=None))) - self.assertEqual(consistency_str, 'Not Set') + assert consistency_str == 'Not Set' for c in ConsistencyLevel.value_to_name.keys(): consistency_str = self.extract_consistency(repr(Unavailable("Timeout Message", consistency=c))) - self.assertEqual(consistency_str, ConsistencyLevel.value_to_name[c]) + assert consistency_str == ConsistencyLevel.value_to_name[c] diff --git a/tests/unit/test_host_connection_pool.py b/tests/unit/test_host_connection_pool.py index b4cb067d2f..f92bb53785 100644 --- a/tests/unit/test_host_connection_pool.py +++ b/tests/unit/test_host_connection_pool.py @@ -14,6 +14,7 @@ from concurrent.futures import ThreadPoolExecutor import logging import time +import uuid from cassandra.protocol_features import ProtocolFeatures from cassandra.shard_info import _ShardingInfo @@ -24,9 +25,12 @@ from cassandra.cluster import Session, ShardAwareOptions from cassandra.connection import Connection -from cassandra.pool import HostConnection, HostConnectionPool +from cassandra.pool import HostConnection from cassandra.pool import Host, NoConnectionsAvailable from cassandra.policies import HostDistance, SimpleConvictionPolicy +import pytest + +from tests.unit.util import HashableMock LOGGER = logging.getLogger(__name__) @@ -37,53 +41,51 @@ class _PoolTests(unittest.TestCase): uses_single_connection = None def make_session(self): - session = NonCallableMagicMock(spec=Session, keyspace='foobarkeyspace') - session.cluster.get_core_connections_per_host.return_value = 1 - session.cluster.get_max_requests_per_connection.return_value = 1 - session.cluster.get_max_connections_per_host.return_value = 1 + session = NonCallableMagicMock(spec=Session, keyspace='foobarkeyspace', _trash=[]) return session def test_borrow_and_return(self): host = Mock(spec=Host, address='ip1') session = self.make_session() - conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False, max_request_id=100) + conn = HashableMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False, max_request_id=100) session.cluster.connection_factory.return_value = conn pool = self.PoolImpl(host, HostDistance.LOCAL, session) session.cluster.connection_factory.assert_called_once_with(host.endpoint, on_orphaned_stream_released=pool.on_orphaned_stream_released) c, request_id = pool.borrow_connection(timeout=0.01) - self.assertIs(c, conn) - self.assertEqual(1, conn.in_flight) + assert c is conn + assert 1 == conn.in_flight conn.set_keyspace_blocking.assert_called_once_with('foobarkeyspace') pool.return_connection(conn) - self.assertEqual(0, conn.in_flight) + assert 0 == conn.in_flight if not self.uses_single_connection: - self.assertNotIn(conn, pool._trash) + assert conn not in pool._trash def test_failed_wait_for_connection(self): host = Mock(spec=Host, address='ip1') session = self.make_session() - conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False, max_request_id=100) + conn = HashableMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False, max_request_id=100) session.cluster.connection_factory.return_value = conn pool = self.PoolImpl(host, HostDistance.LOCAL, session) session.cluster.connection_factory.assert_called_once_with(host.endpoint, on_orphaned_stream_released=pool.on_orphaned_stream_released) pool.borrow_connection(timeout=0.01) - self.assertEqual(1, conn.in_flight) + assert 1 == conn.in_flight conn.in_flight = conn.max_request_id # we're already at the max number of requests for this connection, # so we this should fail - self.assertRaises(NoConnectionsAvailable, pool.borrow_connection, 0) + with pytest.raises(NoConnectionsAvailable): + pool.borrow_connection(0) def test_successful_wait_for_connection(self): host = Mock(spec=Host, address='ip1') session = self.make_session() - conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False, max_request_id=100, + conn = HashableMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False, max_request_id=100, lock=Lock()) session.cluster.connection_factory.return_value = conn @@ -91,11 +93,11 @@ def test_successful_wait_for_connection(self): session.cluster.connection_factory.assert_called_once_with(host.endpoint, on_orphaned_stream_released=pool.on_orphaned_stream_released) pool.borrow_connection(timeout=0.01) - self.assertEqual(1, conn.in_flight) + assert 1 == conn.in_flight def get_second_conn(): c, request_id = pool.borrow_connection(1.0) - self.assertIs(conn, c) + assert conn is c pool.return_connection(c) t = Thread(target=get_second_conn) @@ -103,23 +105,20 @@ def get_second_conn(): pool.return_connection(conn) t.join() - self.assertEqual(0, conn.in_flight) + assert 0 == conn.in_flight def test_spawn_when_at_max(self): host = Mock(spec=Host, address='ip1') session = self.make_session() - conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False, max_request_id=100) + conn = HashableMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False, max_request_id=100) conn.max_request_id = 100 session.cluster.connection_factory.return_value = conn - # core conns = 1, max conns = 2 - session.cluster.get_max_connections_per_host.return_value = 2 - pool = self.PoolImpl(host, HostDistance.LOCAL, session) session.cluster.connection_factory.assert_called_once_with(host.endpoint, on_orphaned_stream_released=pool.on_orphaned_stream_released) pool.borrow_connection(timeout=0.01) - self.assertEqual(1, conn.in_flight) + assert 1 == conn.in_flight # make this conn full conn.in_flight = conn.max_request_id @@ -127,14 +126,15 @@ def test_spawn_when_at_max(self): # we don't care about making this borrow_connection call succeed for the # purposes of this test, as long as it results in a new connection # creation being scheduled - self.assertRaises(NoConnectionsAvailable, pool.borrow_connection, 0) + with pytest.raises(NoConnectionsAvailable): + pool.borrow_connection(0) if not self.uses_single_connection: session.submit.assert_called_once_with(pool._create_new_connection) def test_return_defunct_connection(self): host = Mock(spec=Host, address='ip1') session = self.make_session() - conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False, + conn = HashableMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False, max_request_id=100, signaled_error=False) session.cluster.connection_factory.return_value = conn @@ -148,13 +148,13 @@ def test_return_defunct_connection(self): pool.return_connection(conn) # the connection should be closed a new creation scheduled - self.assertTrue(session.submit.call_args) - self.assertFalse(pool.is_shutdown) + assert session.submit.call_args + assert not pool.is_shutdown def test_return_defunct_connection_on_down_host(self): host = Mock(spec=Host, address='ip1') session = self.make_session() - conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False, + conn = HashableMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False, max_request_id=100, signaled_error=False, orphaned_threshold_reached=False) session.cluster.connection_factory.return_value = conn @@ -170,20 +170,20 @@ def test_return_defunct_connection_on_down_host(self): pool.return_connection(conn) # the connection should be closed a new creation scheduled - self.assertTrue(conn.close.call_args) + assert conn.close.call_args if self.PoolImpl is HostConnection: # on shard aware implementation we use submit function regardless - self.assertTrue(host.signal_connection_failure.call_args) - self.assertTrue(session.submit.called) + assert host.signal_connection_failure.call_args + assert session.submit.called else: - self.assertFalse(session.submit.called) - self.assertTrue(session.cluster.signal_connection_failure.call_args) - self.assertTrue(pool.is_shutdown) + assert not session.submit.called + assert session.cluster.signal_connection_failure.call_args + assert pool.is_shutdown def test_return_closed_connection(self): host = Mock(spec=Host, address='ip1') session = self.make_session() - conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=True, max_request_id=100, + conn = HashableMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=True, max_request_id=100, signaled_error=False, orphaned_threshold_reached=False) session.cluster.connection_factory.return_value = conn @@ -197,79 +197,33 @@ def test_return_closed_connection(self): pool.return_connection(conn) # a new creation should be scheduled - self.assertTrue(session.submit.call_args) - self.assertFalse(pool.is_shutdown) + assert session.submit.call_args + assert not pool.is_shutdown def test_host_instantiations(self): """ Ensure Host fails if not initialized properly """ - self.assertRaises(ValueError, Host, None, None) - self.assertRaises(ValueError, Host, '127.0.0.1', None) - self.assertRaises(ValueError, Host, None, SimpleConvictionPolicy) + with pytest.raises(ValueError): + Host(None, None, host_id=uuid.uuid4()) + with pytest.raises(ValueError): + Host('127.0.0.1', None, host_id=uuid.uuid4()) + with pytest.raises(ValueError): + Host(None, SimpleConvictionPolicy, host_id=uuid.uuid4()) def test_host_equality(self): """ Test host equality has correct logic """ - a = Host('127.0.0.1', SimpleConvictionPolicy) - b = Host('127.0.0.1', SimpleConvictionPolicy) - c = Host('127.0.0.2', SimpleConvictionPolicy) - - self.assertEqual(a, b, 'Two Host instances should be equal when sharing.') - self.assertNotEqual(a, c, 'Two Host instances should NOT be equal when using two different addresses.') - self.assertNotEqual(b, c, 'Two Host instances should NOT be equal when using two different addresses.') - - -class HostConnectionPoolTests(_PoolTests): - __test__ = True - PoolImpl = HostConnectionPool - uses_single_connection = False - - def test_all_connections_trashed(self): - host = Mock(spec=Host, address='ip1') - session = self.make_session() - conn = NonCallableMagicMock(spec=Connection, in_flight=0, is_defunct=False, is_closed=False, max_request_id=100, - lock=Lock()) - session.cluster.connection_factory.return_value = conn - session.cluster.get_core_connections_per_host.return_value = 1 - - # manipulate the core connection setting so that we can - # trash the only connection - pool = self.PoolImpl(host, HostDistance.LOCAL, session) - session.cluster.get_core_connections_per_host.return_value = 0 - pool._maybe_trash_connection(conn) - session.cluster.get_core_connections_per_host.return_value = 1 - - submit_called = Event() - - def fire_event(*args, **kwargs): - submit_called.set() + a = Host('127.0.0.1', SimpleConvictionPolicy, host_id=uuid.uuid4()) + b = Host('127.0.0.1', SimpleConvictionPolicy, host_id=uuid.uuid4()) + c = Host('127.0.0.2', SimpleConvictionPolicy, host_id=uuid.uuid4()) - session.submit.side_effect = fire_event - - def get_conn(): - conn.reset_mock() - c, request_id = pool.borrow_connection(1.0) - self.assertIs(conn, c) - self.assertEqual(1, conn.in_flight) - conn.set_keyspace_blocking.assert_called_once_with('foobarkeyspace') - pool.return_connection(c) - - t = Thread(target=get_conn) - t.start() - - submit_called.wait() - self.assertEqual(1, pool._scheduled_for_creation) - session.submit.assert_called_once_with(pool._create_new_connection) - - # now run the create_new_connection call - pool._create_new_connection() - - t.join() - self.assertEqual(0, conn.in_flight) + assert a == b, 'Two Host instances should be equal when sharing.' + assert a != c, 'Two Host instances should NOT be equal when using two different addresses.' + assert b != c, 'Two Host instances should NOT be equal when using two different addresses.' class HostConnectionTests(_PoolTests): @@ -285,7 +239,8 @@ class MockSession(MagicMock): def __init__(self, *args, **kwargs): super(MockSession, self).__init__(*args, **kwargs) self.cluster = MagicMock() - self.cluster.executor = ThreadPoolExecutor(max_workers=2, initializer=self.executor_init) + self.connection_created = Event() + self.cluster.executor = ThreadPoolExecutor(max_workers=2) self.cluster.signal_connection_failure = lambda *args, **kwargs: False self.cluster.connection_factory = self.mock_connection_factory self.connection_counter = 0 @@ -296,32 +251,39 @@ def submit(self, fn, *args, **kwargs): return self.cluster.executor.submit(fn, *args, **kwargs) def mock_connection_factory(self, *args, **kwargs): - connection = MagicMock() + connection = HashableMock() connection.is_shutdown = False connection.is_defunct = False connection.is_closed = False - connection.features = ProtocolFeatures(shard_id=self.connection_counter, + connection.features = ProtocolFeatures(shard_id=self.connection_counter, sharding_info=_ShardingInfo(shard_id=1, shards_count=14, partitioner="", sharding_algorithm="", sharding_ignore_msb=0, shard_aware_port="", shard_aware_port_ssl="")) self.connection_counter += 1 + self.connection_created.set() return connection - def executor_init(self, *args): - time.sleep(0.5) - LOGGER.info("Future start: %s", args) - - for attempt_num in range(20): - LOGGER.info("Testing fast shutdown %d / 20 times", attempt_num + 1) + for attempt_num in range(3): + LOGGER.info("Testing fast shutdown %d / 3 times", attempt_num + 1) host = MagicMock() host.endpoint = "1.2.3.4" session = MockSession() pool = HostConnection(host=host, host_distance=HostDistance.REMOTE, session=session) LOGGER.info("Initialized pool %s", pool) + + # Wait for initial connection to be created (with timeout) + if not session.connection_created.wait(timeout=2.0): + pytest.fail("Initial connection failed to be created within 2 seconds") + LOGGER.info("Connections: %s", pool._connections) - time.sleep(0.5) + + # Shutdown the pool pool.shutdown() - time.sleep(3) - session.cluster.executor.shutdown() + + # Verify pool is shut down + assert pool.is_shutdown, "Pool should be marked as shutdown" + + # Cleanup executor with proper wait + session.cluster.executor.shutdown(wait=True) diff --git a/tests/unit/test_marshalling.py b/tests/unit/test_marshalling.py index 1fdbfa6a4b..e4b415ac69 100644 --- a/tests/unit/test_marshalling.py +++ b/tests/unit/test_marshalling.py @@ -75,10 +75,10 @@ (b'', 'MapType(AsciiType, BooleanType)', None), (b'', 'ListType(FloatType)', None), (b'', 'SetType(LongType)', None), - (b'\x00\x00', 'MapType(DecimalType, BooleanType)', OrderedMapSerializedKey(DecimalType, 0)), - (b'\x00\x00', 'ListType(FloatType)', []), - (b'\x00\x00', 'SetType(IntegerType)', sortedset()), - (b'\x00\x01\x00\x10\xafYC\xa3\xea<\x11\xe1\xabc\xc4,\x03"y\xf0', 'ListType(TimeUUIDType)', [UUID(bytes=b'\xafYC\xa3\xea<\x11\xe1\xabc\xc4,\x03"y\xf0')]), + (b'\x00\x00\x00\x00', 'MapType(DecimalType, BooleanType)', OrderedMapSerializedKey(DecimalType, 3)), + (b'\x00\x00\x00\x00', 'ListType(FloatType)', []), + (b'\x00\x00\x00\x00', 'SetType(IntegerType)', sortedset()), + (b'\x00\x00\x00\x01\x00\x00\x00\x10\xafYC\xa3\xea<\x11\xe1\xabc\xc4,\x03"y\xf0', 'ListType(TimeUUIDType)', [UUID(bytes=b'\xafYC\xa3\xea<\x11\xe1\xabc\xc4,\x03"y\xf0')]), (b'\x80\x00\x00\x01', 'SimpleDateType', Date(1)), (b'\x7f\xff\xff\xff', 'SimpleDateType', Date('1969-12-31')), (b'\x00\x00\x00\x00\x00\x00\x00\x01', 'TimeType', Time(1)), @@ -88,7 +88,7 @@ (b'\x80\x00', 'ShortType', -32768) ) -ordered_map_value = OrderedMapSerializedKey(UTF8Type, 2) +ordered_map_value = OrderedMapSerializedKey(UTF8Type, 3) ordered_map_value._insert(u'\u307fbob', 199) ordered_map_value._insert(u'', -1) ordered_map_value._insert(u'\\', 0) @@ -96,8 +96,8 @@ # these following entries work for me right now, but they're dependent on # vagaries of internal python ordering for unordered types marshalled_value_pairs_unsafe = ( - (b'\x00\x03\x00\x06\xe3\x81\xbfbob\x00\x04\x00\x00\x00\xc7\x00\x00\x00\x04\xff\xff\xff\xff\x00\x01\\\x00\x04\x00\x00\x00\x00', 'MapType(UTF8Type, Int32Type)', ordered_map_value), - (b'\x00\x02\x00\x08@\x01\x99\x99\x99\x99\x99\x9a\x00\x08@\x14\x00\x00\x00\x00\x00\x00', 'SetType(DoubleType)', sortedset([2.2, 5.0])), + (b'\x00\x00\x00\x03\x00\x00\x00\x06\xe3\x81\xbfbob\x00\x00\x00\x04\x00\x00\x00\xc7\x00\x00\x00\x00\x00\x00\x00\x04\xff\xff\xff\xff\x00\x00\x00\x01\\\x00\x00\x00\x04\x00\x00\x00\x00', 'MapType(UTF8Type, Int32Type)', ordered_map_value), + (b'\x00\x00\x00\x02\x00\x00\x00\x08@\x01\x99\x99\x99\x99\x99\x9a\x00\x00\x00\x08@\x14\x00\x00\x00\x00\x00\x00', 'SetType(DoubleType)', sortedset([2.2, 5.0])), (b'\x00', 'IntegerType', 0), ) @@ -111,35 +111,27 @@ class UnmarshalTest(unittest.TestCase): def test_unmarshalling(self): for serializedval, valtype, nativeval in marshalled_value_pairs: unmarshaller = lookup_casstype(valtype) - whatwegot = unmarshaller.from_binary(serializedval, 1) - self.assertEqual(whatwegot, nativeval, - msg='Unmarshaller for %s (%s) failed: unmarshal(%r) got %r instead of %r' - % (valtype, unmarshaller, serializedval, whatwegot, nativeval)) - self.assertEqual(type(whatwegot), type(nativeval), - msg='Unmarshaller for %s (%s) gave wrong type (%s instead of %s)' - % (valtype, unmarshaller, type(whatwegot), type(nativeval))) + whatwegot = unmarshaller.from_binary(serializedval, 3) + assert whatwegot == nativeval, 'Unmarshaller for %s (%s) failed: unmarshal(%r) got %r instead of %r' % (valtype, unmarshaller, serializedval, whatwegot, nativeval) + assert type(whatwegot) == type(nativeval), 'Unmarshaller for %s (%s) gave wrong type (%s instead of %s)' % (valtype, unmarshaller, type(whatwegot), type(nativeval)) def test_marshalling(self): for serializedval, valtype, nativeval in marshalled_value_pairs: marshaller = lookup_casstype(valtype) - whatwegot = marshaller.to_binary(nativeval, 1) - self.assertEqual(whatwegot, serializedval, - msg='Marshaller for %s (%s) failed: marshal(%r) got %r instead of %r' - % (valtype, marshaller, nativeval, whatwegot, serializedval)) - self.assertEqual(type(whatwegot), type(serializedval), - msg='Marshaller for %s (%s) gave wrong type (%s instead of %s)' - % (valtype, marshaller, type(whatwegot), type(serializedval))) + whatwegot = marshaller.to_binary(nativeval, 3) + assert whatwegot == serializedval, 'Marshaller for %s (%s) failed: marshal(%r) got %r instead of %r' % (valtype, marshaller, nativeval, whatwegot, serializedval) + assert type(whatwegot) == type(serializedval), 'Marshaller for %s (%s) gave wrong type (%s instead of %s)' % (valtype, marshaller, type(whatwegot), type(serializedval)) def test_date(self): # separate test because it will deserialize as datetime - self.assertEqual(DateType.from_binary(DateType.to_binary(date(2015, 11, 2), 1), 1), datetime(2015, 11, 2)) + assert DateType.from_binary(DateType.to_binary(date(2015, 11, 2), 3), 3) == datetime(2015, 11, 2) def test_decimal(self): # testing implicit numeric conversion # int, tuple(sign, digits, exp), float converted_types = (10001, (0, (1, 0, 0, 0, 0, 1), -3), 100.1, -87.629798) - for proto_ver in range(1, ProtocolVersion.MAX_SUPPORTED + 1): + for proto_ver in range(3, ProtocolVersion.MAX_SUPPORTED + 1): for n in converted_types: expected = Decimal(n) - self.assertEqual(DecimalType.from_binary(DecimalType.to_binary(n, proto_ver), proto_ver), expected) + assert DecimalType.from_binary(DecimalType.to_binary(n, proto_ver), proto_ver) == expected diff --git a/tests/unit/test_metadata.py b/tests/unit/test_metadata.py index bc5a93bf89..15cf283777 100644 --- a/tests/unit/test_metadata.py +++ b/tests/unit/test_metadata.py @@ -18,21 +18,26 @@ from unittest.mock import Mock import os import timeit +import uuid import cassandra from cassandra.cqltypes import strip_frozen from cassandra.marshal import uint16_unpack, uint16_pack from cassandra.metadata import (Murmur3Token, MD5Token, BytesToken, ReplicationStrategy, - NetworkTopologyStrategy, SimpleStrategy, + NetworkTopologyStrategy, LocalStrategy, protect_name, protect_names, protect_value, is_valid_name, UserType, KeyspaceMetadata, get_schema_parser, _UnknownStrategy, ColumnMetadata, TableMetadata, IndexMetadata, Function, Aggregate, - Metadata, TokenMap, ReplicationFactor) + Metadata, TokenMap, ReplicationFactor, + SchemaParserDSE68) from cassandra.policies import SimpleConvictionPolicy from cassandra.pool import Host +from cassandra.protocol import QueryMessage +from tests.util import assertCountEqual +import pytest log = logging.getLogger(__name__) @@ -42,38 +47,36 @@ class ReplicationFactorTest(unittest.TestCase): def test_replication_factor_parsing(self): rf = ReplicationFactor.create('3') - self.assertEqual(rf.all_replicas, 3) - self.assertEqual(rf.full_replicas, 3) - self.assertEqual(rf.transient_replicas, None) - self.assertEqual(str(rf), '3') + assert rf.all_replicas == 3 + assert rf.full_replicas == 3 + assert rf.transient_replicas == None + assert str(rf) == '3' rf = ReplicationFactor.create('3/1') - self.assertEqual(rf.all_replicas, 3) - self.assertEqual(rf.full_replicas, 2) - self.assertEqual(rf.transient_replicas, 1) - self.assertEqual(str(rf), '3/1') - - self.assertRaises(ValueError, ReplicationFactor.create, '3/') - self.assertRaises(ValueError, ReplicationFactor.create, 'a/1') - self.assertRaises(ValueError, ReplicationFactor.create, 'a') - self.assertRaises(ValueError, ReplicationFactor.create, '3/a') + assert rf.all_replicas == 3 + assert rf.full_replicas == 2 + assert rf.transient_replicas == 1 + assert str(rf) == '3/1' + + with pytest.raises(ValueError): + ReplicationFactor.create('3/') + with pytest.raises(ValueError): + ReplicationFactor.create('a/1') + with pytest.raises(ValueError): + ReplicationFactor.create('a') + with pytest.raises(ValueError): + ReplicationFactor.create('3/a') def test_replication_factor_equality(self): - self.assertEqual(ReplicationFactor.create('3/1'), ReplicationFactor.create('3/1')) - self.assertEqual(ReplicationFactor.create('3'), ReplicationFactor.create('3')) - self.assertNotEqual(ReplicationFactor.create('3'), ReplicationFactor.create('3/1')) - self.assertNotEqual(ReplicationFactor.create('3'), ReplicationFactor.create('3/1')) + assert ReplicationFactor.create('3/1') == ReplicationFactor.create('3/1') + assert ReplicationFactor.create('3') == ReplicationFactor.create('3') + assert ReplicationFactor.create('3') != ReplicationFactor.create('3/1') + assert ReplicationFactor.create('3') != ReplicationFactor.create('3/1') class StrategiesTest(unittest.TestCase): - @classmethod - def setUpClass(cls): - "Hook method for setting up class fixture before running tests in the class." - if not hasattr(cls, 'assertItemsEqual'): - cls.assertItemsEqual = cls.assertCountEqual - def test_replication_strategy(self): """ Basic code coverage testing that ensures different ReplicationStrategies @@ -82,72 +85,66 @@ def test_replication_strategy(self): rs = ReplicationStrategy() - self.assertEqual(rs.create('OldNetworkTopologyStrategy', None), _UnknownStrategy('OldNetworkTopologyStrategy', None)) + assert rs.create('OldNetworkTopologyStrategy', None) == _UnknownStrategy('OldNetworkTopologyStrategy', None) fake_options_map = {'options': 'map'} uks = rs.create('OldNetworkTopologyStrategy', fake_options_map) - self.assertEqual(uks, _UnknownStrategy('OldNetworkTopologyStrategy', fake_options_map)) - self.assertEqual(uks.make_token_replica_map({}, []), {}) + assert uks == _UnknownStrategy('OldNetworkTopologyStrategy', fake_options_map) + assert uks.make_token_replica_map({}, []) == {} fake_options_map = {'dc1': '3'} - self.assertIsInstance(rs.create('NetworkTopologyStrategy', fake_options_map), NetworkTopologyStrategy) - self.assertEqual(rs.create('NetworkTopologyStrategy', fake_options_map).dc_replication_factors, - NetworkTopologyStrategy(fake_options_map).dc_replication_factors) + assert isinstance(rs.create('NetworkTopologyStrategy', fake_options_map), NetworkTopologyStrategy) + assert rs.create('NetworkTopologyStrategy', fake_options_map).dc_replication_factors == NetworkTopologyStrategy(fake_options_map).dc_replication_factors fake_options_map = {'options': 'map'} - self.assertIsNone(rs.create('SimpleStrategy', fake_options_map)) + assert rs.create('NetworkTopologyStrategy', fake_options_map) is None fake_options_map = {'options': 'map'} - self.assertIsInstance(rs.create('LocalStrategy', fake_options_map), LocalStrategy) + assert isinstance(rs.create('LocalStrategy', fake_options_map), LocalStrategy) - fake_options_map = {'options': 'map', 'replication_factor': 3} - self.assertIsInstance(rs.create('SimpleStrategy', fake_options_map), SimpleStrategy) - self.assertEqual(rs.create('SimpleStrategy', fake_options_map).replication_factor, - SimpleStrategy(fake_options_map).replication_factor) + fake_options_map = {'dc1': 3} + assert isinstance(rs.create('NetworkTopologyStrategy', fake_options_map), NetworkTopologyStrategy) + assert rs.create('NetworkTopologyStrategy', fake_options_map).dc_replication_factors == NetworkTopologyStrategy(fake_options_map).dc_replication_factors - self.assertEqual(rs.create('xxxxxxxx', fake_options_map), _UnknownStrategy('xxxxxxxx', fake_options_map)) + assert rs.create('xxxxxxxx', fake_options_map) == _UnknownStrategy('xxxxxxxx', fake_options_map) - self.assertRaises(NotImplementedError, rs.make_token_replica_map, None, None) - self.assertRaises(NotImplementedError, rs.export_for_schema) + with pytest.raises(NotImplementedError): + rs.make_token_replica_map(None, None) + with pytest.raises(NotImplementedError): + rs.export_for_schema() def test_simple_replication_type_parsing(self): - """ Test equality between passing numeric and string replication factor for simple strategy """ + """ Test equality between passing numeric and string replication factor for NTS """ rs = ReplicationStrategy() - simple_int = rs.create('SimpleStrategy', {'replication_factor': 3}) - simple_str = rs.create('SimpleStrategy', {'replication_factor': '3'}) + nts_int = rs.create('NetworkTopologyStrategy', {'dc1': 3}) + nts_str = rs.create('NetworkTopologyStrategy', {'dc1': '3'}) - self.assertEqual(simple_int.export_for_schema(), simple_str.export_for_schema()) - self.assertEqual(simple_int, simple_str) + assert nts_int.export_for_schema() == nts_str.export_for_schema() + assert nts_int == nts_str # make token replica map ring = [MD5Token(0), MD5Token(1), MD5Token(2)] - hosts = [Host('dc1.{}'.format(host), SimpleConvictionPolicy) for host in range(3)] + hosts = [Host('dc1.{}'.format(host), SimpleConvictionPolicy, datacenter='dc1', rack='rack1', host_id=uuid.uuid4()) for host in range(3)] token_to_host = dict(zip(ring, hosts)) - self.assertEqual( - simple_int.make_token_replica_map(token_to_host, ring), - simple_str.make_token_replica_map(token_to_host, ring) - ) + assert nts_int.make_token_replica_map(token_to_host, ring) == nts_str.make_token_replica_map(token_to_host, ring) def test_transient_replication_parsing(self): - """ Test that we can PARSE a transient replication factor for SimpleStrategy """ + """ Test that we can PARSE a transient replication factor for NetworkTopologyStrategy """ rs = ReplicationStrategy() - simple_transient = rs.create('SimpleStrategy', {'replication_factor': '3/1'}) - self.assertEqual(simple_transient.replication_factor_info, ReplicationFactor(3, 1)) - self.assertEqual(simple_transient.replication_factor, 2) - self.assertIn("'replication_factor': '3/1'", simple_transient.export_for_schema()) + nts_transient = rs.create('NetworkTopologyStrategy', {'dc1': '3/1'}) + assert nts_transient.dc_replication_factors_info['dc1'] == ReplicationFactor(3, 1) + assert nts_transient.dc_replication_factors['dc1'] == 2 + assert "'dc1': '3/1'" in nts_transient.export_for_schema() - simple_str = rs.create('SimpleStrategy', {'replication_factor': '2'}) - self.assertNotEqual(simple_transient, simple_str) + nts_str = rs.create('NetworkTopologyStrategy', {'dc1': '2'}) + assert nts_transient != nts_str # make token replica map ring = [MD5Token(0), MD5Token(1), MD5Token(2)] - hosts = [Host('dc1.{}'.format(host), SimpleConvictionPolicy) for host in range(3)] + hosts = [Host('dc1.{}'.format(host), SimpleConvictionPolicy, datacenter='dc1', rack='rack1', host_id=uuid.uuid4()) for host in range(3)] token_to_host = dict(zip(ring, hosts)) - self.assertEqual( - simple_transient.make_token_replica_map(token_to_host, ring), - simple_str.make_token_replica_map(token_to_host, ring) - ) + assert nts_transient.make_token_replica_map(token_to_host, ring) == nts_str.make_token_replica_map(token_to_host, ring) def test_nts_replication_parsing(self): """ Test equality between passing numeric and string replication factor for NTS """ @@ -156,66 +153,60 @@ def test_nts_replication_parsing(self): nts_int = rs.create('NetworkTopologyStrategy', {'dc1': 3, 'dc2': 5}) nts_str = rs.create('NetworkTopologyStrategy', {'dc1': '3', 'dc2': '5'}) - self.assertEqual(nts_int.dc_replication_factors['dc1'], 3) - self.assertEqual(nts_str.dc_replication_factors['dc1'], 3) - self.assertEqual(nts_int.dc_replication_factors_info['dc1'], ReplicationFactor(3)) - self.assertEqual(nts_str.dc_replication_factors_info['dc1'], ReplicationFactor(3)) + assert nts_int.dc_replication_factors['dc1'] == 3 + assert nts_str.dc_replication_factors['dc1'] == 3 + assert nts_int.dc_replication_factors_info['dc1'] == ReplicationFactor(3) + assert nts_str.dc_replication_factors_info['dc1'] == ReplicationFactor(3) - self.assertEqual(nts_int.export_for_schema(), nts_str.export_for_schema()) - self.assertEqual(nts_int, nts_str) + assert nts_int.export_for_schema() == nts_str.export_for_schema() + assert nts_int == nts_str # make token replica map ring = [MD5Token(0), MD5Token(1), MD5Token(2)] - hosts = [Host('dc1.{}'.format(host), SimpleConvictionPolicy) for host in range(3)] + hosts = [Host('dc1.{}'.format(host), SimpleConvictionPolicy, host_id=uuid.uuid4()) for host in range(3)] token_to_host = dict(zip(ring, hosts)) - self.assertEqual( - nts_int.make_token_replica_map(token_to_host, ring), - nts_str.make_token_replica_map(token_to_host, ring) - ) + assert nts_int.make_token_replica_map(token_to_host, ring) == nts_str.make_token_replica_map(token_to_host, ring) def test_nts_transient_parsing(self): """ Test that we can PARSE a transient replication factor for NTS """ rs = ReplicationStrategy() nts_transient = rs.create('NetworkTopologyStrategy', {'dc1': '3/1', 'dc2': '5/1'}) - self.assertEqual(nts_transient.dc_replication_factors_info['dc1'], ReplicationFactor(3, 1)) - self.assertEqual(nts_transient.dc_replication_factors_info['dc2'], ReplicationFactor(5, 1)) - self.assertEqual(nts_transient.dc_replication_factors['dc1'], 2) - self.assertEqual(nts_transient.dc_replication_factors['dc2'], 4) - self.assertIn("'dc1': '3/1', 'dc2': '5/1'", nts_transient.export_for_schema()) + assert nts_transient.dc_replication_factors_info['dc1'] == ReplicationFactor(3, 1) + assert nts_transient.dc_replication_factors_info['dc2'] == ReplicationFactor(5, 1) + assert nts_transient.dc_replication_factors['dc1'] == 2 + assert nts_transient.dc_replication_factors['dc2'] == 4 + assert "'dc1': '3/1', 'dc2': '5/1'" in nts_transient.export_for_schema() nts_str = rs.create('NetworkTopologyStrategy', {'dc1': '3', 'dc2': '5'}) - self.assertNotEqual(nts_transient, nts_str) + assert nts_transient != nts_str # make token replica map ring = [MD5Token(0), MD5Token(1), MD5Token(2)] - hosts = [Host('dc1.{}'.format(host), SimpleConvictionPolicy) for host in range(3)] + hosts = [Host('dc1.{}'.format(host), SimpleConvictionPolicy, host_id=uuid.uuid4()) for host in range(3)] token_to_host = dict(zip(ring, hosts)) - self.assertEqual( - nts_transient.make_token_replica_map(token_to_host, ring), - nts_str.make_token_replica_map(token_to_host, ring) - ) + assert nts_transient.make_token_replica_map(token_to_host, ring) == nts_str.make_token_replica_map(token_to_host, ring) def test_nts_make_token_replica_map(self): token_to_host_owner = {} - dc1_1 = Host('dc1.1', SimpleConvictionPolicy) - dc1_2 = Host('dc1.2', SimpleConvictionPolicy) - dc1_3 = Host('dc1.3', SimpleConvictionPolicy) + dc1_1 = Host('dc1.1', SimpleConvictionPolicy, host_id=uuid.uuid4()) + dc1_2 = Host('dc1.2', SimpleConvictionPolicy, host_id=uuid.uuid4()) + dc1_3 = Host('dc1.3', SimpleConvictionPolicy, host_id=uuid.uuid4()) for host in (dc1_1, dc1_2, dc1_3): host.set_location_info('dc1', 'rack1') token_to_host_owner[MD5Token(0)] = dc1_1 token_to_host_owner[MD5Token(100)] = dc1_2 token_to_host_owner[MD5Token(200)] = dc1_3 - dc2_1 = Host('dc2.1', SimpleConvictionPolicy) - dc2_2 = Host('dc2.2', SimpleConvictionPolicy) + dc2_1 = Host('dc2.1', SimpleConvictionPolicy, host_id=uuid.uuid4()) + dc2_2 = Host('dc2.2', SimpleConvictionPolicy, host_id=uuid.uuid4()) dc2_1.set_location_info('dc2', 'rack1') dc2_2.set_location_info('dc2', 'rack1') token_to_host_owner[MD5Token(1)] = dc2_1 token_to_host_owner[MD5Token(101)] = dc2_2 - dc3_1 = Host('dc3.1', SimpleConvictionPolicy) + dc3_1 = Host('dc3.1', SimpleConvictionPolicy, host_id=uuid.uuid4()) dc3_1.set_location_info('dc3', 'rack3') token_to_host_owner[MD5Token(2)] = dc3_1 @@ -229,7 +220,7 @@ def test_nts_make_token_replica_map(self): nts = NetworkTopologyStrategy({'dc1': 2, 'dc2': 2, 'dc3': 1}) replica_map = nts.make_token_replica_map(token_to_host_owner, ring) - self.assertItemsEqual(replica_map[MD5Token(0)], (dc1_1, dc1_2, dc2_1, dc2_2, dc3_1)) + assertCountEqual(replica_map[MD5Token(0)], (dc1_1, dc1_2, dc2_1, dc2_2, dc3_1)) def test_nts_token_performance(self): """ @@ -250,7 +241,7 @@ def test_nts_token_performance(self): vnodes_per_host = 500 for i in range(dc1hostnum): - host = Host('dc1.{0}'.format(i), SimpleConvictionPolicy) + host = Host('dc1.{0}'.format(i), SimpleConvictionPolicy, host_id=uuid.uuid4()) host.set_location_info('dc1', "rack1") for vnode_num in range(vnodes_per_host): md5_token = MD5Token(current_token+vnode_num) @@ -268,16 +259,16 @@ def test_nts_token_performance(self): nts.make_token_replica_map(token_to_host_owner, ring) elapsed_bad = timeit.default_timer() - start_time difference = elapsed_bad - elapsed_base - self.assertTrue(difference < 1 and difference > -1) + assert difference < 1 and difference > -1 def test_nts_make_token_replica_map_multi_rack(self): token_to_host_owner = {} # (A) not enough distinct racks, first skipped is used - dc1_1 = Host('dc1.1', SimpleConvictionPolicy) - dc1_2 = Host('dc1.2', SimpleConvictionPolicy) - dc1_3 = Host('dc1.3', SimpleConvictionPolicy) - dc1_4 = Host('dc1.4', SimpleConvictionPolicy) + dc1_1 = Host('dc1.1', SimpleConvictionPolicy, host_id=uuid.uuid4()) + dc1_2 = Host('dc1.2', SimpleConvictionPolicy, host_id=uuid.uuid4()) + dc1_3 = Host('dc1.3', SimpleConvictionPolicy, host_id=uuid.uuid4()) + dc1_4 = Host('dc1.4', SimpleConvictionPolicy, host_id=uuid.uuid4()) dc1_1.set_location_info('dc1', 'rack1') dc1_2.set_location_info('dc1', 'rack1') dc1_3.set_location_info('dc1', 'rack2') @@ -288,9 +279,9 @@ def test_nts_make_token_replica_map_multi_rack(self): token_to_host_owner[MD5Token(300)] = dc1_4 # (B) distinct racks, but not contiguous - dc2_1 = Host('dc2.1', SimpleConvictionPolicy) - dc2_2 = Host('dc2.2', SimpleConvictionPolicy) - dc2_3 = Host('dc2.3', SimpleConvictionPolicy) + dc2_1 = Host('dc2.1', SimpleConvictionPolicy, host_id=uuid.uuid4()) + dc2_2 = Host('dc2.2', SimpleConvictionPolicy, host_id=uuid.uuid4()) + dc2_3 = Host('dc2.3', SimpleConvictionPolicy, host_id=uuid.uuid4()) dc2_1.set_location_info('dc2', 'rack1') dc2_2.set_location_info('dc2', 'rack1') dc2_3.set_location_info('dc2', 'rack2') @@ -310,27 +301,26 @@ def test_nts_make_token_replica_map_multi_rack(self): replica_map = nts.make_token_replica_map(token_to_host_owner, ring) token_replicas = replica_map[MD5Token(0)] - self.assertItemsEqual(token_replicas, (dc1_1, dc1_2, dc1_3, dc2_1, dc2_3)) + assertCountEqual(token_replicas, (dc1_1, dc1_2, dc1_3, dc2_1, dc2_3)) def test_nts_make_token_replica_map_empty_dc(self): - host = Host('1', SimpleConvictionPolicy) + host = Host('1', SimpleConvictionPolicy, host_id=uuid.uuid4()) host.set_location_info('dc1', 'rack1') token_to_host_owner = {MD5Token(0): host} ring = [MD5Token(0)] nts = NetworkTopologyStrategy({'dc1': 1, 'dc2': 0}) replica_map = nts.make_token_replica_map(token_to_host_owner, ring) - self.assertEqual(set(replica_map[MD5Token(0)]), set([host])) + assert set(replica_map[MD5Token(0)]) == set([host]) def test_nts_export_for_schema(self): strategy = NetworkTopologyStrategy({'dc1': '1', 'dc2': '2'}) - self.assertEqual("{'class': 'NetworkTopologyStrategy', 'dc1': '1', 'dc2': '2'}", - strategy.export_for_schema()) + assert "{'class': 'NetworkTopologyStrategy', 'dc1': '1', 'dc2': '2'}" == strategy.export_for_schema() def test_simple_strategy_make_token_replica_map(self): - host1 = Host('1', SimpleConvictionPolicy) - host2 = Host('2', SimpleConvictionPolicy) - host3 = Host('3', SimpleConvictionPolicy) + host1 = Host('1', SimpleConvictionPolicy, datacenter='dc1', rack='rack1', host_id=uuid.uuid4()) + host2 = Host('2', SimpleConvictionPolicy, datacenter='dc1', rack='rack1', host_id=uuid.uuid4()) + host3 = Host('3', SimpleConvictionPolicy, datacenter='dc1', rack='rack1', host_id=uuid.uuid4()) token_to_host_owner = { MD5Token(0): host1, MD5Token(100): host2, @@ -338,23 +328,23 @@ def test_simple_strategy_make_token_replica_map(self): } ring = [MD5Token(0), MD5Token(100), MD5Token(200)] - rf1_replicas = SimpleStrategy({'replication_factor': '1'}).make_token_replica_map(token_to_host_owner, ring) - self.assertItemsEqual(rf1_replicas[MD5Token(0)], [host1]) - self.assertItemsEqual(rf1_replicas[MD5Token(100)], [host2]) - self.assertItemsEqual(rf1_replicas[MD5Token(200)], [host3]) + rf1_replicas = NetworkTopologyStrategy({'dc1': '1'}).make_token_replica_map(token_to_host_owner, ring) + assertCountEqual(rf1_replicas[MD5Token(0)], [host1]) + assertCountEqual(rf1_replicas[MD5Token(100)], [host2]) + assertCountEqual(rf1_replicas[MD5Token(200)], [host3]) - rf2_replicas = SimpleStrategy({'replication_factor': '2'}).make_token_replica_map(token_to_host_owner, ring) - self.assertItemsEqual(rf2_replicas[MD5Token(0)], [host1, host2]) - self.assertItemsEqual(rf2_replicas[MD5Token(100)], [host2, host3]) - self.assertItemsEqual(rf2_replicas[MD5Token(200)], [host3, host1]) + rf2_replicas = NetworkTopologyStrategy({'dc1': '2'}).make_token_replica_map(token_to_host_owner, ring) + assertCountEqual(rf2_replicas[MD5Token(0)], [host1, host2]) + assertCountEqual(rf2_replicas[MD5Token(100)], [host2, host3]) + assertCountEqual(rf2_replicas[MD5Token(200)], [host3, host1]) - rf3_replicas = SimpleStrategy({'replication_factor': '3'}).make_token_replica_map(token_to_host_owner, ring) - self.assertItemsEqual(rf3_replicas[MD5Token(0)], [host1, host2, host3]) - self.assertItemsEqual(rf3_replicas[MD5Token(100)], [host2, host3, host1]) - self.assertItemsEqual(rf3_replicas[MD5Token(200)], [host3, host1, host2]) + rf3_replicas = NetworkTopologyStrategy({'dc1': '3'}).make_token_replica_map(token_to_host_owner, ring) + assertCountEqual(rf3_replicas[MD5Token(0)], [host1, host2, host3]) + assertCountEqual(rf3_replicas[MD5Token(100)], [host2, host3, host1]) + assertCountEqual(rf3_replicas[MD5Token(200)], [host3, host1, host2]) def test_ss_equals(self): - self.assertNotEqual(SimpleStrategy({'replication_factor': '1'}), NetworkTopologyStrategy({'dc1': 2})) + assert NetworkTopologyStrategy({'dc1': '1'}) != NetworkTopologyStrategy({'dc1': 2}) class NameEscapingTest(unittest.TestCase): @@ -363,84 +353,83 @@ def test_protect_name(self): """ Test cassandra.metadata.protect_name output """ - self.assertEqual(protect_name('tests'), 'tests') - self.assertEqual(protect_name('test\'s'), '"test\'s"') - self.assertEqual(protect_name('test\'s'), "\"test's\"") - self.assertEqual(protect_name('tests ?!@#$%^&*()'), '"tests ?!@#$%^&*()"') - self.assertEqual(protect_name('1'), '"1"') - self.assertEqual(protect_name('1test'), '"1test"') + assert protect_name('tests') == 'tests' + assert protect_name('test\'s') == '"test\'s"' + assert protect_name('test\'s') == "\"test's\"" + assert protect_name('tests ?!@#$%^&*()') == '"tests ?!@#$%^&*()"' + assert protect_name('1') == '"1"' + assert protect_name('1test') == '"1test"' def test_protect_names(self): """ Test cassandra.metadata.protect_names output """ - self.assertEqual(protect_names(['tests']), ['tests']) - self.assertEqual(protect_names( + assert protect_names(['tests']) == ['tests'] + assert protect_names( [ 'tests', 'test\'s', 'tests ?!@#$%^&*()', '1' - ]), - [ - 'tests', - "\"test's\"", - '"tests ?!@#$%^&*()"', - '"1"' - ]) + ]) == [ + 'tests', + "\"test's\"", + '"tests ?!@#$%^&*()"', + '"1"' + ] def test_protect_value(self): """ Test cassandra.metadata.protect_value output """ - self.assertEqual(protect_value(True), "true") - self.assertEqual(protect_value(False), "false") - self.assertEqual(protect_value(3.14), '3.14') - self.assertEqual(protect_value(3), '3') - self.assertEqual(protect_value('test'), "'test'") - self.assertEqual(protect_value('test\'s'), "'test''s'") - self.assertEqual(protect_value(None), 'NULL') + assert protect_value(True) == "true" + assert protect_value(False) == "false" + assert protect_value(3.14) == '3.14' + assert protect_value(3) == '3' + assert protect_value('test') == "'test'" + assert protect_value('test\'s') == "'test''s'" + assert protect_value(None) == 'NULL' def test_is_valid_name(self): """ Test cassandra.metadata.is_valid_name output """ - self.assertEqual(is_valid_name(None), False) - self.assertEqual(is_valid_name('test'), True) - self.assertEqual(is_valid_name('Test'), False) - self.assertEqual(is_valid_name('t_____1'), True) - self.assertEqual(is_valid_name('test1'), True) - self.assertEqual(is_valid_name('1test1'), False) + assert is_valid_name(None) == False + assert is_valid_name('test') == True + assert is_valid_name('Test') == False + assert is_valid_name('t_____1') == True + assert is_valid_name('test1') == True + assert is_valid_name('1test1') == False invalid_keywords = cassandra.metadata.cql_keywords - cassandra.metadata.cql_keywords_unreserved for keyword in invalid_keywords: - self.assertEqual(is_valid_name(keyword), False) + assert is_valid_name(keyword) == False class GetReplicasTest(unittest.TestCase): def _get_replicas(self, token_klass): tokens = [token_klass(i) for i in range(0, (2 ** 127 - 1), 2 ** 125)] - hosts = [Host("ip%d" % i, SimpleConvictionPolicy) for i in range(len(tokens))] + hosts = [Host("ip%d" % i, SimpleConvictionPolicy, datacenter="dc1", rack="rack1", host_id=uuid.uuid4()) for i in range(len(tokens))] token_to_primary_replica = dict(zip(tokens, hosts)) - keyspace = KeyspaceMetadata("ks", True, "SimpleStrategy", {"replication_factor": "1"}) + keyspace = KeyspaceMetadata("ks", True, "NetworkTopologyStrategy", {"dc1": "1"}) metadata = Mock(spec=Metadata, keyspaces={'ks': keyspace}) token_map = TokenMap(token_klass, token_to_primary_replica, tokens, metadata) # tokens match node tokens exactly for token, expected_host in zip(tokens, hosts): replicas = token_map.get_replicas("ks", token) - self.assertEqual(set(replicas), {expected_host}) + assert set(replicas) == {expected_host} # shift the tokens back by one for token, expected_host in zip(tokens, hosts): replicas = token_map.get_replicas("ks", token_klass(token.value - 1)) - self.assertEqual(set(replicas), {expected_host}) + assert set(replicas) == {expected_host} # shift the tokens forward by one for i, token in enumerate(tokens): replicas = token_map.get_replicas("ks", token_klass(token.value + 1)) expected_host = hosts[(i + 1) % len(hosts)] - self.assertEqual(set(replicas), {expected_host}) + assert set(replicas) == {expected_host} def test_murmur3_tokens(self): self._get_replicas(Murmur3Token) @@ -456,7 +445,7 @@ class Murmur3TokensTest(unittest.TestCase): def test_murmur3_init(self): murmur3_token = Murmur3Token(cassandra.metadata.MIN_LONG - 1) - self.assertEqual(str(murmur3_token), '') + assert str(murmur3_token) == '' def test_python_vs_c(self): from cassandra.murmur3 import _murmur3 as mm3_python @@ -467,7 +456,7 @@ def test_python_vs_c(self): for _ in range(iterations): for len in range(0, 32): # zero to one block plus full range of tail lengths key = os.urandom(len) - self.assertEqual(mm3_python(key), mm3_c(key)) + assert mm3_python(key) == mm3_c(key) except ImportError: raise unittest.SkipTest('The cmurmur3 extension is not available') @@ -484,64 +473,64 @@ def test_murmur3_c(self): raise unittest.SkipTest('The cmurmur3 extension is not available') def _verify_hash(self, fn): - self.assertEqual(fn(b'123'), -7468325962851647638) - self.assertEqual(fn(b'\x00\xff\x10\xfa\x99' * 10), 5837342703291459765) - self.assertEqual(fn(b'\xfe' * 8), -8927430733708461935) - self.assertEqual(fn(b'\x10' * 8), 1446172840243228796) - self.assertEqual(fn(str(cassandra.metadata.MAX_LONG).encode()), 7162290910810015547) + assert fn(b'123') == -7468325962851647638 + assert fn(b'\x00\xff\x10\xfa\x99' * 10) == 5837342703291459765 + assert fn(b'\xfe' * 8) == -8927430733708461935 + assert fn(b'\x10' * 8) == 1446172840243228796 + assert fn(str(cassandra.metadata.MAX_LONG).encode()) == 7162290910810015547 class MD5TokensTest(unittest.TestCase): def test_md5_tokens(self): md5_token = MD5Token(cassandra.metadata.MIN_LONG - 1) - self.assertEqual(md5_token.hash_fn('123'), 42767516990368493138776584305024125808) - self.assertEqual(md5_token.hash_fn(str(cassandra.metadata.MAX_LONG)), 28528976619278518853815276204542453639) - self.assertEqual(str(md5_token), '' % -9223372036854775809) + assert md5_token.hash_fn('123') == 42767516990368493138776584305024125808 + assert md5_token.hash_fn(str(cassandra.metadata.MAX_LONG)) == 28528976619278518853815276204542453639 + assert str(md5_token) == '' % -9223372036854775809 class BytesTokensTest(unittest.TestCase): def test_bytes_tokens(self): bytes_token = BytesToken(unhexlify(b'01')) - self.assertEqual(bytes_token.value, b'\x01') - self.assertEqual(str(bytes_token), "" % bytes_token.value) - self.assertEqual(bytes_token.hash_fn('123'), '123') - self.assertEqual(bytes_token.hash_fn(123), 123) - self.assertEqual(bytes_token.hash_fn(str(cassandra.metadata.MAX_LONG)), str(cassandra.metadata.MAX_LONG)) + assert bytes_token.value == b'\x01' + assert str(bytes_token) == "" % bytes_token.value + assert bytes_token.hash_fn('123') == '123' + assert bytes_token.hash_fn(123) == 123 + assert bytes_token.hash_fn(str(cassandra.metadata.MAX_LONG)) == str(cassandra.metadata.MAX_LONG) def test_from_string(self): from_unicode = BytesToken.from_string('0123456789abcdef') from_bin = BytesToken.from_string(b'0123456789abcdef') - self.assertEqual(from_unicode, from_bin) - self.assertIsInstance(from_unicode.value, bytes) - self.assertIsInstance(from_bin.value, bytes) + assert from_unicode == from_bin + assert isinstance(from_unicode.value, bytes) + assert isinstance(from_bin.value, bytes) def test_comparison(self): tok = BytesToken.from_string('0123456789abcdef') token_high_order = uint16_unpack(tok.value[0:2]) - self.assertLess(BytesToken(uint16_pack(token_high_order - 1)), tok) - self.assertGreater(BytesToken(uint16_pack(token_high_order + 1)), tok) + assert BytesToken(uint16_pack(token_high_order - 1)) < tok + assert BytesToken(uint16_pack(token_high_order + 1)) > tok def test_comparison_unicode(self): value = b'\'_-()"\xc2\xac' t0 = BytesToken(value) t1 = BytesToken.from_string('00') - self.assertGreater(t0, t1) - self.assertFalse(t0 < t1) + assert t0 > t1 + assert not t0 < t1 class KeyspaceMetadataTest(unittest.TestCase): def test_export_as_string_user_types(self): keyspace_name = 'test' - keyspace = KeyspaceMetadata(keyspace_name, True, 'SimpleStrategy', dict(replication_factor=3)) + keyspace = KeyspaceMetadata(keyspace_name, True, 'NetworkTopologyStrategy', dict(dc1=3)) keyspace.user_types['a'] = UserType(keyspace_name, 'a', ['one', 'two'], ['c', 'int']) keyspace.user_types['b'] = UserType(keyspace_name, 'b', ['one', 'two', 'three'], ['d', 'int', 'a']) keyspace.user_types['c'] = UserType(keyspace_name, 'c', ['one'], ['int']) keyspace.user_types['d'] = UserType(keyspace_name, 'd', ['one'], ['c']) - self.assertEqual("""CREATE KEYSPACE test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '3'} AND durable_writes = true; + assert """CREATE KEYSPACE test WITH replication = {'class': 'NetworkTopologyStrategy', 'dc1': '3'} AND durable_writes = true; CREATE TYPE test.c ( one int @@ -560,7 +549,7 @@ def test_export_as_string_user_types(self): one d, two int, three a -);""", keyspace.export_as_string()) +);""" == keyspace.export_as_string() class UserTypesTest(unittest.TestCase): @@ -568,17 +557,17 @@ class UserTypesTest(unittest.TestCase): def test_as_cql_query(self): field_types = ['varint', 'ascii', 'frozen>'] udt = UserType("ks1", "mytype", ["a", "b", "c"], field_types) - self.assertEqual("CREATE TYPE ks1.mytype (a varint, b ascii, c frozen>)", udt.as_cql_query(formatted=False)) + assert "CREATE TYPE ks1.mytype (a varint, b ascii, c frozen>)" == udt.as_cql_query(formatted=False) - self.assertEqual("""CREATE TYPE ks1.mytype ( + assert """CREATE TYPE ks1.mytype ( a varint, b ascii, c frozen> -);""", udt.export_as_string()) +);""" == udt.export_as_string() def test_as_cql_query_name_escaping(self): udt = UserType("MyKeyspace", "MyType", ["AbA", "keyspace"], ['ascii', 'ascii']) - self.assertEqual('CREATE TYPE "MyKeyspace"."MyType" ("AbA" ascii, "keyspace" ascii)', udt.as_cql_query(formatted=False)) + assert 'CREATE TYPE "MyKeyspace"."MyType" ("AbA" ascii, "keyspace" ascii)' == udt.as_cql_query(formatted=False) class UserDefinedFunctionTest(unittest.TestCase): @@ -594,7 +583,7 @@ def test_as_cql_query_removes_frozen(self): "LANGUAGE java " "AS $$return 0;$$" ) - self.assertEqual(expected_result, func.as_cql_query(formatted=False)) + assert expected_result == func.as_cql_query(formatted=False) class UserDefinedAggregateTest(unittest.TestCase): @@ -607,7 +596,7 @@ def test_as_cql_query_removes_frozen(self): "FINALFUNC finalfunc " "INITCOND (0)" ) - self.assertEqual(expected_result, aggregate.as_cql_query(formatted=False)) + assert expected_result == aggregate.as_cql_query(formatted=False) class IndexTest(unittest.TestCase): @@ -622,14 +611,43 @@ def test_build_index_as_cql(self): row = {'index_name': 'index_name_here', 'index_type': 'index_type_here'} index_meta = parser._build_index_metadata(column_meta, row) - self.assertEqual(index_meta.as_cql_query(), - 'CREATE INDEX index_name_here ON keyspace_name_here.table_name_here (column_name_here)') + assert index_meta.as_cql_query() == 'CREATE INDEX index_name_here ON keyspace_name_here.table_name_here (column_name_here)' row['index_options'] = '{ "class_name": "class_name_here" }' row['index_type'] = 'CUSTOM' index_meta = parser._build_index_metadata(column_meta, row) - self.assertEqual(index_meta.as_cql_query(), - "CREATE CUSTOM INDEX index_name_here ON keyspace_name_here.table_name_here (column_name_here) USING 'class_name_here'") + assert index_meta.as_cql_query() == "CREATE CUSTOM INDEX index_name_here ON keyspace_name_here.table_name_here (column_name_here) USING 'class_name_here'" + + +class SchemaParserLookupTests(unittest.TestCase): + + def test_reads_versions_from_system_local_when_missing(self): + connection = Mock() + + release_version_resp = Mock() + release_version_resp.column_names = ["release_version"] + release_version_resp.parsed_rows = [["4.0.0"]] + + dse_version_resp = Mock() + dse_version_resp.column_names = ["dse_version"] + dse_version_resp.parsed_rows = [["6.8.0"]] + + def mock_system_local(query, *args, **kwargs): + if not isinstance(query, QueryMessage): + raise RuntimeError("first argument should be a QueryMessage") + if "release_version" in query.query: + return (True, release_version_resp) + if "dse_version" in query.query: + return (True, dse_version_resp) + raise RuntimeError("unexpected query") + + connection.wait_for_response.side_effect = mock_system_local + + parser = get_schema_parser(connection, None, None, 0.1, None) + + assert isinstance(parser, SchemaParserDSE68) + message = connection.wait_for_response.call_args[0][0] + assert "system.local" in message.query class UnicodeIdentifiersTests(unittest.TestCase): @@ -644,7 +662,7 @@ class UnicodeIdentifiersTests(unittest.TestCase): name = b'\'_-()"\xc2\xac'.decode('utf-8') def test_keyspace_name(self): - km = KeyspaceMetadata(self.name, False, 'SimpleStrategy', {'replication_factor': 1}) + km = KeyspaceMetadata(self.name, False, 'NetworkTopologyStrategy', {'dc1': 1}) km.export_as_string() def test_table_name(self): @@ -728,63 +746,39 @@ def _function_with_kwargs(self, **kwargs): ) def test_non_monotonic(self): - self.assertNotIn( - 'MONOTONIC', - self._function_with_kwargs( - monotonic=False, - monotonic_on=() - ).export_as_string() - ) + assert 'MONOTONIC' not in self._function_with_kwargs( + monotonic=False, + monotonic_on=() + ).export_as_string() def test_monotonic_all(self): mono_function = self._function_with_kwargs( monotonic=True, monotonic_on=() ) - self.assertIn( - 'MONOTONIC LANG', - mono_function.as_cql_query(formatted=False) - ) - self.assertIn( - 'MONOTONIC\n LANG', - mono_function.as_cql_query(formatted=True) - ) + assert 'MONOTONIC LANG' in mono_function.as_cql_query(formatted=False) + assert 'MONOTONIC\n LANG' in mono_function.as_cql_query(formatted=True) def test_monotonic_one(self): mono_on_function = self._function_with_kwargs( monotonic=False, monotonic_on=('x',) ) - self.assertIn( - 'MONOTONIC ON x LANG', - mono_on_function.as_cql_query(formatted=False) - ) - self.assertIn( - 'MONOTONIC ON x\n LANG', - mono_on_function.as_cql_query(formatted=True) - ) + assert 'MONOTONIC ON x LANG' in mono_on_function.as_cql_query(formatted=False) + assert 'MONOTONIC ON x\n LANG' in mono_on_function.as_cql_query(formatted=True) def test_nondeterministic(self): - self.assertNotIn( - 'DETERMINISTIC', - self._function_with_kwargs( - deterministic=False - ).as_cql_query(formatted=False) - ) + assert 'DETERMINISTIC' not in self._function_with_kwargs( + deterministic=False + ).as_cql_query(formatted=False) def test_deterministic(self): - self.assertIn( - 'DETERMINISTIC', - self._function_with_kwargs( - deterministic=True - ).as_cql_query(formatted=False) - ) - self.assertIn( - 'DETERMINISTIC\n', - self._function_with_kwargs( - deterministic=True - ).as_cql_query(formatted=True) - ) + assert 'DETERMINISTIC' in self._function_with_kwargs( + deterministic=True + ).as_cql_query(formatted=False) + assert 'DETERMINISTIC\n' in self._function_with_kwargs( + deterministic=True + ).as_cql_query(formatted=True) class AggregateToCQLTests(unittest.TestCase): @@ -806,21 +800,16 @@ def _aggregate_with_kwargs(self, **kwargs): ) def test_nondeterministic(self): - self.assertNotIn( - 'DETERMINISTIC', - self._aggregate_with_kwargs( - deterministic=False - ).as_cql_query(formatted=True) - ) + assert 'DETERMINISTIC' not in self._aggregate_with_kwargs( + deterministic=False + ).as_cql_query(formatted=True) def test_deterministic(self): for formatted in (True, False): query = self._aggregate_with_kwargs( deterministic=True ).as_cql_query(formatted=formatted) - self.assertTrue(query.endswith('DETERMINISTIC'), - msg="'DETERMINISTIC' not found in {}".format(query) - ) + assert query.endswith('DETERMINISTIC'), "'DETERMINISTIC' not found in {}".format(query) class HostsTests(unittest.TestCase): @@ -829,15 +818,15 @@ def test_iterate_all_hosts_and_modify(self): PYTHON-572 """ metadata = Metadata() - metadata.add_or_return_host(Host('dc1.1', SimpleConvictionPolicy)) - metadata.add_or_return_host(Host('dc1.2', SimpleConvictionPolicy)) + metadata.add_or_return_host(Host('dc1.1', SimpleConvictionPolicy, host_id=uuid.uuid4())) + metadata.add_or_return_host(Host('dc1.2', SimpleConvictionPolicy, host_id=uuid.uuid4())) - self.assertEqual(len(metadata.all_hosts()), 2) + assert len(metadata.all_hosts()) == 2 for host in metadata.all_hosts(): # this would previously raise in Py3 metadata.remove_host(host) - self.assertEqual(len(metadata.all_hosts()), 0) + assert len(metadata.all_hosts()) == 0 class MetadataHelpersTest(unittest.TestCase): @@ -856,4 +845,4 @@ def test_strip_frozen(self): ] for argument, expected_result in argument_to_expected_results: result = strip_frozen(argument) - self.assertEqual(result, expected_result, "strip_frozen() arg: {}".format(argument)) + assert result == expected_result, "strip_frozen() arg: {}".format(argument) diff --git a/tests/unit/test_metrics.py b/tests/unit/test_metrics.py new file mode 100644 index 0000000000..07e851188c --- /dev/null +++ b/tests/unit/test_metrics.py @@ -0,0 +1,386 @@ +# Copyright ScyllaDB, Inc. +# +# 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 +# +# http://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. + +""" +Unit tests for the self-contained metrics module. +""" + +import threading +import unittest + +from cassandra.metrics import ( + IntStat, Stat, PmfStat, StatsCollection, + collection, init, getStats, _stats_registry, _registry_lock +) + + +class IntStatTest(unittest.TestCase): + """Tests for IntStat class.""" + + def test_initial_value(self): + stat = IntStat('test_counter') + self.assertEqual(stat.value, 0) + self.assertEqual(int(stat), 0) + + def test_increment(self): + stat = IntStat('test_counter') + stat += 1 + self.assertEqual(stat.value, 1) + stat += 5 + self.assertEqual(stat.value, 6) + + def test_thread_safety(self): + stat = IntStat('test_counter') + num_threads = 10 + increments_per_thread = 1000 + + def increment(): + nonlocal stat + for _ in range(increments_per_thread): + stat += 1 + + threads = [threading.Thread(target=increment) for _ in range(num_threads)] + for t in threads: + t.start() + for t in threads: + t.join() + + self.assertEqual(stat.value, num_threads * increments_per_thread) + + def test_repr(self): + stat = IntStat('my_counter') + stat += 42 + self.assertEqual(repr(stat), "IntStat(my_counter=42)") + + def test_equality(self): + stat = IntStat('test') + stat += 5 + self.assertEqual(stat, 5) + self.assertEqual(5, stat) + self.assertNotEqual(stat, 3) + self.assertNotEqual(stat, 10) + + def test_comparison_operators(self): + stat = IntStat('test') + stat += 5 + self.assertTrue(stat > 0) + self.assertTrue(stat >= 5) + self.assertTrue(stat < 10) + self.assertTrue(stat <= 5) + self.assertFalse(stat > 5) + self.assertFalse(stat < 5) + + def test_comparison_with_intstat(self): + stat1 = IntStat('test1') + stat2 = IntStat('test2') + stat1 += 5 + stat2 += 5 + self.assertEqual(stat1, stat2) + stat2 += 1 + self.assertNotEqual(stat1, stat2) + self.assertTrue(stat1 < stat2) + self.assertTrue(stat2 > stat1) + + +class StatTest(unittest.TestCase): + """Tests for Stat (gauge) class.""" + + def test_basic_gauge(self): + counter = [0] + stat = Stat('test_gauge', lambda: counter[0]) + + self.assertEqual(stat.value, 0) + counter[0] = 10 + self.assertEqual(stat.value, 10) + counter[0] = 42 + self.assertEqual(stat.value, 42) + + def test_repr(self): + stat = Stat('my_gauge', lambda: 123) + self.assertEqual(repr(stat), "Stat(my_gauge=123)") + + +class PmfStatTest(unittest.TestCase): + """Tests for PmfStat class.""" + + def test_empty_stats(self): + stat = PmfStat('test_timer') + stats = stat._get_stats() + + self.assertEqual(stats['count'], 0) + self.assertEqual(stats['min'], 0.0) + self.assertEqual(stats['max'], 0.0) + self.assertEqual(stats['mean'], 0.0) + self.assertEqual(stats['stddev'], 0.0) + self.assertEqual(stats['median'], 0.0) + + def test_single_value(self): + stat = PmfStat('test_timer') + stat.addValue(10.0) + stats = stat._get_stats() + + self.assertEqual(stats['count'], 1) + self.assertEqual(stats['min'], 10.0) + self.assertEqual(stats['max'], 10.0) + self.assertEqual(stats['mean'], 10.0) + self.assertEqual(stats['stddev'], 0.0) + self.assertEqual(stats['median'], 10.0) + + def test_multiple_values(self): + stat = PmfStat('test_timer') + for v in [1, 2, 3, 4, 5]: + stat.addValue(v) + stats = stat._get_stats() + + self.assertEqual(stats['count'], 5) + self.assertEqual(stats['min'], 1.0) + self.assertEqual(stats['max'], 5.0) + self.assertEqual(stats['mean'], 3.0) + self.assertEqual(stats['median'], 3.0) + + def test_dict_like_access(self): + stat = PmfStat('test_timer') + stat.addValue(5.0) + + self.assertEqual(stat['count'], 1) + self.assertEqual(stat['mean'], 5.0) + self.assertIn('count', stat.keys()) + self.assertIn('mean', stat.keys()) + + def test_percentiles(self): + stat = PmfStat('test_timer') + # Add values 1-100 + for v in range(1, 101): + stat.addValue(v) + stats = stat._get_stats() + + self.assertEqual(stats['count'], 100) + self.assertEqual(stats['min'], 1.0) + self.assertEqual(stats['max'], 100.0) + # Median should be around 50 + self.assertAlmostEqual(stats['median'], 50.5, delta=1) + # 75th percentile should be around 75 + self.assertAlmostEqual(stats['75percentile'], 75.25, delta=1) + # 95th percentile should be around 95 + self.assertAlmostEqual(stats['95percentile'], 95.05, delta=1) + + def test_thread_safety(self): + stat = PmfStat('test_timer') + num_threads = 10 + values_per_thread = 100 + + def add_values(): + for i in range(values_per_thread): + stat.addValue(i) + + threads = [threading.Thread(target=add_values) for _ in range(num_threads)] + for t in threads: + t.start() + for t in threads: + t.join() + + stats = stat._get_stats() + self.assertEqual(stats['count'], num_threads * values_per_thread) + + +class StatsCollectionTest(unittest.TestCase): + """Tests for StatsCollection class.""" + + def test_access_stats_by_attribute(self): + int_stat = IntStat('errors') + pmf_stat = PmfStat('latency') + + coll = StatsCollection('test', int_stat, pmf_stat) + + self.assertIs(coll.errors, int_stat) + self.assertIs(coll.latency, pmf_stat) + + def test_augmented_assignment(self): + int_stat = IntStat('errors') + coll = StatsCollection('test', int_stat) + + coll.errors += 1 + self.assertEqual(int_stat.value, 1) + coll.errors += 5 + self.assertEqual(int_stat.value, 6) + + def test_get_stats_dict(self): + int_stat = IntStat('errors') + int_stat += 3 + pmf_stat = PmfStat('latency') + pmf_stat.addValue(10.0) + gauge = Stat('connections', lambda: 5) + + coll = StatsCollection('test', int_stat, pmf_stat, gauge) + stats_dict = coll._get_stats_dict() + + self.assertEqual(stats_dict['errors'], 3) + self.assertEqual(stats_dict['connections'], 5) + self.assertIsInstance(stats_dict['latency'], dict) + self.assertEqual(stats_dict['latency']['count'], 1) + self.assertEqual(stats_dict['latency']['mean'], 10.0) + + def test_nonexistent_attribute(self): + coll = StatsCollection('test', IntStat('errors')) + with self.assertRaises(AttributeError): + _ = coll.nonexistent + + def test_cannot_set_new_attribute(self): + coll = StatsCollection('test', IntStat('errors')) + with self.assertRaises(AttributeError): + coll.new_attr = 123 + + +class CollectionFunctionTest(unittest.TestCase): + """Tests for the collection() function.""" + + def setUp(self): + # Clean up registry before each test + with _registry_lock: + keys_to_remove = [k for k in _stats_registry.keys() + if k.startswith('test_')] + for k in keys_to_remove: + del _stats_registry[k] + + def test_registers_in_global_registry(self): + coll = collection('test_coll', IntStat('counter')) + + with _registry_lock: + self.assertIn('test_coll', _stats_registry) + self.assertIs(_stats_registry['test_coll'], coll) + + def test_get_stats_returns_dict(self): + int_stat = IntStat('counter') + int_stat += 10 + collection('test_stats', int_stat) + + stats = getStats() + self.assertIn('test_stats', stats) + self.assertEqual(stats['test_stats']['counter'], 10) + + +class InitFunctionTest(unittest.TestCase): + """Tests for the init() function.""" + + def setUp(self): + # Clean up registry before each test + with _registry_lock: + keys_to_remove = [k for k in _stats_registry.keys() + if k.startswith('test') or k == 'request'] + for k in keys_to_remove: + del _stats_registry[k] + + def test_creates_instance_stats(self): + class Analyzer: + requests = PmfStat('request size') + errors = IntStat('errors') + + def __init__(self): + init(self, '/test_analyzer') + + analyzer = Analyzer() + + # Instance should have its own stats + self.assertIsInstance(analyzer.requests, PmfStat) + self.assertIsInstance(analyzer.errors, IntStat) + + # Should be different from class-level stats + self.assertIsNot(analyzer.requests, Analyzer.requests) + self.assertIsNot(analyzer.errors, Analyzer.errors) + + def test_instance_stats_are_independent(self): + class Analyzer: + errors = IntStat('errors') + + def __init__(self): + init(self, '/test_analyzer') + + a1 = Analyzer() + a2 = Analyzer() + + a1.errors += 5 + a2.errors += 10 + + self.assertEqual(a1.errors.value, 5) + self.assertEqual(a2.errors.value, 10) + + def test_strips_leading_slash(self): + class Analyzer: + errors = IntStat('errors') + + def __init__(self): + init(self, '/test_path') + + Analyzer() + + with _registry_lock: + self.assertIn('test_path', _stats_registry) + self.assertNotIn('/test_path', _stats_registry) + + +class MetricsShutdownTest(unittest.TestCase): + """Tests for Metrics shutdown functionality.""" + + def setUp(self): + # Clean up registry before each test + with _registry_lock: + keys_to_remove = [k for k in _stats_registry.keys() + if k.startswith('cassandra')] + for k in keys_to_remove: + del _stats_registry[k] + + def test_shutdown_removes_from_registry(self): + import weakref + from cassandra.metrics import Metrics + + class MockCluster: + def __init__(self): + self.metadata = type('obj', (object,), {'all_hosts': lambda: []})() + self.sessions = [] + + cluster = MockCluster() + proxy = weakref.proxy(cluster) + + metrics = Metrics(proxy) + stats_name = metrics.stats_name + + with _registry_lock: + self.assertIn(stats_name, _stats_registry) + + metrics.shutdown() + + with _registry_lock: + self.assertNotIn(stats_name, _stats_registry) + + def test_shutdown_is_idempotent(self): + import weakref + from cassandra.metrics import Metrics + + class MockCluster: + def __init__(self): + self.metadata = type('obj', (object,), {'all_hosts': lambda: []})() + self.sessions = [] + + cluster = MockCluster() + proxy = weakref.proxy(cluster) + + metrics = Metrics(proxy) + + # Should not raise even if called multiple times + metrics.shutdown() + metrics.shutdown() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_orderedmap.py b/tests/unit/test_orderedmap.py index 5d99fc74a8..156bbd5f30 100644 --- a/tests/unit/test_orderedmap.py +++ b/tests/unit/test_orderedmap.py @@ -16,6 +16,8 @@ from cassandra.util import OrderedMap, OrderedMapSerializedKey from cassandra.cqltypes import EMPTY, UTF8Type, lookup_casstype +from tests.util import assertListEqual +import pytest class OrderedMapTest(unittest.TestCase): def test_init(self): @@ -23,17 +25,17 @@ def test_init(self): b = OrderedMap([('one', 1), ('three', 3), ('two', 2)]) c = OrderedMap(a) builtin = {'one': 1, 'two': 2, 'three': 3} - self.assertEqual(a, b) - self.assertEqual(a, c) - self.assertEqual(a, builtin) - self.assertEqual(OrderedMap([(1, 1), (1, 2)]), {1: 2}) + assert a == b + assert a == c + assert a == builtin + assert OrderedMap([(1, 1), (1, 2)]) == {1: 2} d = OrderedMap({'': 3}, key1='v1', key2='v2') - self.assertEqual(d[''], 3) - self.assertEqual(d['key1'], 'v1') - self.assertEqual(d['key2'], 'v2') + assert d[''] == 3 + assert d['key1'] == 'v1' + assert d['key2'] == 'v2' - with self.assertRaises(TypeError): + with pytest.raises(TypeError): OrderedMap('too', 'many', 'args') def test_contains(self): @@ -44,41 +46,41 @@ def test_contains(self): om = OrderedMap(zip(keys, range(len(keys)))) for k in keys: - self.assertTrue(k in om) - self.assertFalse(k not in om) + assert k in om + assert not k not in om - self.assertTrue('notthere' not in om) - self.assertFalse('notthere' in om) + assert 'notthere' not in om + assert not 'notthere' in om def test_keys(self): keys = ['first', 'middle', 'last'] om = OrderedMap(zip(keys, range(len(keys)))) - self.assertListEqual(list(om.keys()), keys) + assertListEqual(list(om.keys()), keys) def test_values(self): keys = ['first', 'middle', 'last'] values = list(range(len(keys))) om = OrderedMap(zip(keys, values)) - self.assertListEqual(list(om.values()), values) + assertListEqual(list(om.values()), values) def test_items(self): keys = ['first', 'middle', 'last'] items = list(zip(keys, range(len(keys)))) om = OrderedMap(items) - self.assertListEqual(list(om.items()), items) + assertListEqual(list(om.items()), items) def test_get(self): keys = ['first', 'middle', 'last'] om = OrderedMap(zip(keys, range(len(keys)))) for v, k in enumerate(keys): - self.assertEqual(om.get(k), v) - - self.assertEqual(om.get('notthere', 'default'), 'default') - self.assertIsNone(om.get('notthere')) + assert om.get(k) == v + + assert om.get('notthere', 'default') == 'default' + assert om.get('notthere') is None def test_equal(self): d1 = {'one': 1} @@ -87,26 +89,26 @@ def test_equal(self): om12 = OrderedMap([('one', 1), ('two', 2)]) om21 = OrderedMap([('two', 2), ('one', 1)]) - self.assertEqual(om1, d1) - self.assertEqual(om12, d12) - self.assertEqual(om21, d12) - self.assertNotEqual(om1, om12) - self.assertNotEqual(om12, om1) - self.assertNotEqual(om12, om21) - self.assertNotEqual(om1, d12) - self.assertNotEqual(om12, d1) - self.assertNotEqual(om1, EMPTY) + assert om1 == d1 + assert om12 == d12 + assert om21 == d12 + assert om1 != om12 + assert om12 != om1 + assert om12 != om21 + assert om1 != d12 + assert om12 != d1 + assert om1 != EMPTY - self.assertFalse(OrderedMap([('three', 3), ('four', 4)]) == d12) + assert not OrderedMap([('three', 3), ('four', 4)]) == d12 def test_getitem(self): keys = ['first', 'middle', 'last'] om = OrderedMap(zip(keys, range(len(keys)))) for v, k in enumerate(keys): - self.assertEqual(om[k], v) - - with self.assertRaises(KeyError): + assert om[k] == v + + with pytest.raises(KeyError): om['notthere'] def test_iter(self): @@ -116,16 +118,17 @@ def test_iter(self): om = OrderedMap(items) itr = iter(om) - self.assertEqual(sum([1 for _ in itr]), len(keys)) - self.assertRaises(StopIteration, next, itr) + assert sum([1 for _ in itr]) == len(keys) + with pytest.raises(StopIteration): + next(itr) - self.assertEqual(list(iter(om)), keys) - self.assertEqual(list(om.items()), items) - self.assertEqual(list(om.values()), values) + assert list(iter(om)) == keys + assert list(om.items()) == items + assert list(om.values()) == values def test_len(self): - self.assertEqual(len(OrderedMap()), 0) - self.assertEqual(len(OrderedMap([(1, 1)])), 1) + assert len(OrderedMap()) == 0 + assert len(OrderedMap([(1, 1)])) == 1 def test_mutable_keys(self): d = {'1': 1} @@ -136,35 +139,36 @@ def test_strings(self): # changes in 3.x d = {'map': 'inner'} s = set([1, 2, 3]) - self.assertEqual(repr(OrderedMap([('two', 2), ('one', 1), (d, 'value'), (s, 'another')])), - "OrderedMap([('two', 2), ('one', 1), (%r, 'value'), (%r, 'another')])" % (d, s)) + assert repr(OrderedMap([('two', 2), ('one', 1), (d, 'value'), (s, 'another')])) == "OrderedMap([('two', 2), ('one', 1), (%r, 'value'), (%r, 'another')])" % (d, s) - self.assertEqual(str(OrderedMap([('two', 2), ('one', 1), (d, 'value'), (s, 'another')])), - "{'two': 2, 'one': 1, %r: 'value', %r: 'another'}" % (d, s)) + assert str(OrderedMap([('two', 2), ('one', 1), (d, 'value'), (s, 'another')])) == "{'two': 2, 'one': 1, %r: 'value', %r: 'another'}" % (d, s) def test_popitem(self): item = (1, 2) om = OrderedMap((item,)) - self.assertEqual(om.popitem(), item) - self.assertRaises(KeyError, om.popitem) + assert om.popitem() == item + with pytest.raises(KeyError): + om.popitem() def test_delitem(self): om = OrderedMap({1: 1, 2: 2}) - self.assertRaises(KeyError, om.__delitem__, 3) + with pytest.raises(KeyError): + om.__delitem__(3) del om[1] - self.assertEqual(om, {2: 2}) + assert om == {2: 2} del om[2] - self.assertFalse(om) + assert not om - self.assertRaises(KeyError, om.__delitem__, 1) + with pytest.raises(KeyError): + om.__delitem__(1) class OrderedMapSerializedKeyTest(unittest.TestCase): def test_init(self): - om = OrderedMapSerializedKey(UTF8Type, 2) - self.assertEqual(om, {}) + om = OrderedMapSerializedKey(UTF8Type, 3) + assert om == {} def test_normalized_lookup(self): key_type = lookup_casstype('MapType(UTF8Type, Int32Type)') @@ -177,6 +181,6 @@ def test_normalized_lookup(self): # type lookup is normalized by key_type # PYTHON-231 - self.assertIs(om[{'one': 1}], om[{u'one': 1}]) - self.assertIs(om[{'two': 2}], om[{u'two': 2}]) - self.assertIsNot(om[{'one': 1}], om[{'two': 2}]) + assert om[{'one': 1}] is om[{u'one': 1}] + assert om[{'two': 2}] is om[{u'two': 2}] + assert om[{'one': 1}] is not om[{'two': 2}] diff --git a/tests/unit/test_parameter_binding.py b/tests/unit/test_parameter_binding.py index 78f3898e01..5416ac461d 100644 --- a/tests/unit/test_parameter_binding.py +++ b/tests/unit/test_parameter_binding.py @@ -13,6 +13,7 @@ # limitations under the License. import unittest +import pytest from cassandra.encoder import Encoder from cassandra.protocol import ColumnMetadata @@ -21,36 +22,38 @@ from cassandra.cqltypes import Int32Type from cassandra.util import OrderedDict +from tests.util import assertListEqual + class ParamBindingTest(unittest.TestCase): def test_bind_sequence(self): result = bind_params("%s %s %s", (1, "a", 2.0), Encoder()) - self.assertEqual(result, "1 'a' 2.0") + assert result == "1 'a' 2.0" def test_bind_map(self): result = bind_params("%(a)s %(b)s %(c)s", dict(a=1, b="a", c=2.0), Encoder()) - self.assertEqual(result, "1 'a' 2.0") + assert result == "1 'a' 2.0" def test_sequence_param(self): result = bind_params("%s", (ValueSequence((1, "a", 2.0)),), Encoder()) - self.assertEqual(result, "(1, 'a', 2.0)") + assert result == "(1, 'a', 2.0)" def test_generator_param(self): result = bind_params("%s", ((i for i in range(3)),), Encoder()) - self.assertEqual(result, "[0, 1, 2]") + assert result == "[0, 1, 2]" def test_none_param(self): result = bind_params("%s", (None,), Encoder()) - self.assertEqual(result, "NULL") + assert result == "NULL" def test_list_collection(self): result = bind_params("%s", (['a', 'b', 'c'],), Encoder()) - self.assertEqual(result, "['a', 'b', 'c']") + assert result == "['a', 'b', 'c']" def test_set_collection(self): result = bind_params("%s", (set(['a', 'b']),), Encoder()) - self.assertIn(result, ("{'a', 'b'}", "{'b', 'a'}")) + assert result in ("{'a', 'b'}", "{'b', 'a'}") def test_map_collection(self): vals = OrderedDict() @@ -58,20 +61,19 @@ def test_map_collection(self): vals['b'] = 'b' vals['c'] = 'c' result = bind_params("%s", (vals,), Encoder()) - self.assertEqual(result, "{'a': 'a', 'b': 'b', 'c': 'c'}") + assert result == "{'a': 'a', 'b': 'b', 'c': 'c'}" def test_quote_escaping(self): result = bind_params("%s", ("""'ef''ef"ef""ef'""",), Encoder()) - self.assertEqual(result, """'''ef''''ef"ef""ef'''""") + assert result == """'''ef''''ef"ef""ef'''""" def test_float_precision(self): f = 3.4028234663852886e+38 - self.assertEqual(float(bind_params("%s", (f,), Encoder())), f) - + assert float(bind_params("%s", (f,), Encoder())) == f -class BoundStatementTestV1(unittest.TestCase): +class BoundStatementTestV3(unittest.TestCase): - protocol_version = 1 + protocol_version = 3 @classmethod def setUpClass(cls): @@ -90,25 +92,21 @@ def setUpClass(cls): def test_invalid_argument_type(self): values = (0, 0, 0, 'string not int') - try: + with pytest.raises(TypeError) as exc: self.bound.bind(values) - except TypeError as e: - self.assertIn('v0', str(e)) - self.assertIn('Int32Type', str(e)) - self.assertIn('str', str(e)) - else: - self.fail('Passed invalid type but exception was not thrown') + e = exc.value + assert 'v0' in str(e) + assert 'Int32Type' in str(e) + assert 'str' in str(e) values = (['1', '2'], 0, 0, 0) - try: + with pytest.raises(TypeError) as exc: self.bound.bind(values) - except TypeError as e: - self.assertIn('rk0', str(e)) - self.assertIn('Int32Type', str(e)) - self.assertIn('list', str(e)) - else: - self.fail('Passed invalid type but exception was not thrown') + e = exc.value + assert 'rk0' in str(e) + assert 'Int32Type' in str(e) + assert 'list' in str(e) def test_inherit_fetch_size(self): keyspace = 'keyspace1' @@ -129,29 +127,35 @@ def test_inherit_fetch_size(self): result_metadata_id=None) prepared_statement.fetch_size = 1234 bound_statement = BoundStatement(prepared_statement=prepared_statement) - self.assertEqual(1234, bound_statement.fetch_size) + assert 1234 == bound_statement.fetch_size def test_too_few_parameters_for_routing_key(self): - self.assertRaises(ValueError, self.prepared.bind, (1,)) + with pytest.raises(ValueError): + self.prepared.bind((1,)) bound = self.prepared.bind((1, 2)) - self.assertEqual(bound.keyspace, 'keyspace') + assert bound.keyspace == 'keyspace' def test_dict_missing_routing_key(self): - self.assertRaises(KeyError, self.bound.bind, {'rk0': 0, 'ck0': 0, 'v0': 0}) - self.assertRaises(KeyError, self.bound.bind, {'rk1': 0, 'ck0': 0, 'v0': 0}) + with pytest.raises(KeyError): + self.bound.bind({'rk0': 0, 'ck0': 0, 'v0': 0}) + with pytest.raises(KeyError): + self.bound.bind({'rk1': 0, 'ck0': 0, 'v0': 0}) def test_missing_value(self): - self.assertRaises(KeyError, self.bound.bind, {'rk0': 0, 'rk1': 0, 'ck0': 0}) + with pytest.raises(KeyError): + self.bound.bind({'rk0': 0, 'rk1': 0, 'ck0': 0}) def test_extra_value(self): self.bound.bind({'rk0': 0, 'rk1': 0, 'ck0': 0, 'v0': 0, 'should_not_be_here': 123}) # okay to have extra keys in dict - self.assertEqual(self.bound.values, [b'\x00' * 4] * 4) # four encoded zeros - self.assertRaises(ValueError, self.bound.bind, (0, 0, 0, 0, 123)) + assert self.bound.values == [b'\x00' * 4] * 4 # four encoded zeros + with pytest.raises(ValueError): + self.bound.bind((0, 0, 0, 0, 123)) def test_values_none(self): # should have values - self.assertRaises(ValueError, self.bound.bind, None) + with pytest.raises(ValueError): + self.bound.bind(None) # prepared statement with no values prepared_statement = PreparedStatement(column_metadata=[], @@ -163,55 +167,51 @@ def test_values_none(self): result_metadata=None, result_metadata_id=None) bound = prepared_statement.bind(None) - self.assertListEqual(bound.values, []) + assertListEqual(bound.values, []) def test_bind_none(self): self.bound.bind({'rk0': 0, 'rk1': 0, 'ck0': 0, 'v0': None}) - self.assertEqual(self.bound.values[-1], None) + assert self.bound.values[-1] == None old_values = self.bound.values self.bound.bind((0, 0, 0, None)) - self.assertIsNot(self.bound.values, old_values) - self.assertEqual(self.bound.values[-1], None) + assert self.bound.values is not old_values + assert self.bound.values[-1] == None def test_unset_value(self): - self.assertRaises(ValueError, self.bound.bind, {'rk0': 0, 'rk1': 0, 'ck0': 0, 'v0': UNSET_VALUE}) - self.assertRaises(ValueError, self.bound.bind, (0, 0, 0, UNSET_VALUE)) - - -class BoundStatementTestV2(BoundStatementTestV1): - protocol_version = 2 - - -class BoundStatementTestV3(BoundStatementTestV1): - protocol_version = 3 + with pytest.raises(ValueError): + self.bound.bind({'rk0': 0, 'rk1': 0, 'ck0': 0, 'v0': UNSET_VALUE}) + with pytest.raises(ValueError): + self.bound.bind((0, 0, 0, UNSET_VALUE)) -class BoundStatementTestV4(BoundStatementTestV1): +class BoundStatementTestV4(BoundStatementTestV3): protocol_version = 4 def test_dict_missing_routing_key(self): # in v4 it implicitly binds UNSET_VALUE for missing items, # UNSET_VALUE is ValueError for routing keys - self.assertRaises(ValueError, self.bound.bind, {'rk0': 0, 'ck0': 0, 'v0': 0}) - self.assertRaises(ValueError, self.bound.bind, {'rk1': 0, 'ck0': 0, 'v0': 0}) + with pytest.raises(ValueError): + self.bound.bind({'rk0': 0, 'ck0': 0, 'v0': 0}) + with pytest.raises(ValueError): + self.bound.bind({'rk1': 0, 'ck0': 0, 'v0': 0}) def test_missing_value(self): # in v4 missing values are UNSET_VALUE self.bound.bind({'rk0': 0, 'rk1': 0, 'ck0': 0}) - self.assertEqual(self.bound.values[-1], UNSET_VALUE) + assert self.bound.values[-1] == UNSET_VALUE old_values = self.bound.values self.bound.bind((0, 0, 0)) - self.assertIsNot(self.bound.values, old_values) - self.assertEqual(self.bound.values[-1], UNSET_VALUE) + assert self.bound.values is not old_values + assert self.bound.values[-1] == UNSET_VALUE def test_unset_value(self): self.bound.bind({'rk0': 0, 'rk1': 0, 'ck0': 0, 'v0': UNSET_VALUE}) - self.assertEqual(self.bound.values[-1], UNSET_VALUE) + assert self.bound.values[-1] == UNSET_VALUE self.bound.bind((0, 0, 0, UNSET_VALUE)) - self.assertEqual(self.bound.values[-1], UNSET_VALUE) + assert self.bound.values[-1] == UNSET_VALUE class BoundStatementTestV5(BoundStatementTestV4): diff --git a/tests/unit/test_policies.py b/tests/unit/test_policies.py index e7757aedfc..6142af1aa1 100644 --- a/tests/unit/test_policies.py +++ b/tests/unit/test_policies.py @@ -11,12 +11,13 @@ # 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 random import unittest from itertools import islice, cycle from unittest.mock import Mock, patch, call from random import randint +import uuid import pytest from _thread import LockType import sys @@ -36,6 +37,7 @@ from cassandra.connection import DefaultEndPoint, UnixSocketEndPoint from cassandra.pool import Host from cassandra.query import Statement +from cassandra.tablets import Tablets, Tablet class LoadBalancingPolicyTest(unittest.TestCase): @@ -45,19 +47,27 @@ def test_non_implemented(self): """ policy = LoadBalancingPolicy() - host = Host(DefaultEndPoint("ip1"), SimpleConvictionPolicy) + host = Host(DefaultEndPoint("ip1"), SimpleConvictionPolicy, host_id=uuid.uuid4()) host.set_location_info("dc1", "rack1") - self.assertRaises(NotImplementedError, policy.distance, host) - self.assertRaises(NotImplementedError, policy.populate, None, host) - self.assertRaises(NotImplementedError, policy.make_query_plan) - self.assertRaises(NotImplementedError, policy.on_up, host) - self.assertRaises(NotImplementedError, policy.on_down, host) - self.assertRaises(NotImplementedError, policy.on_add, host) - self.assertRaises(NotImplementedError, policy.on_remove, host) + with pytest.raises(NotImplementedError): + policy.distance(host) + with pytest.raises(NotImplementedError): + policy.populate(None, host) + with pytest.raises(NotImplementedError): + policy.make_query_plan() + with pytest.raises(NotImplementedError): + policy.on_up(host) + with pytest.raises(NotImplementedError): + policy.on_down(host) + with pytest.raises(NotImplementedError): + policy.on_add(host) + with pytest.raises(NotImplementedError): + policy.on_remove(host) def test_instance_check(self): - self.assertRaises(TypeError, Cluster, load_balancing_policy=RoundRobinPolicy) + with pytest.raises(TypeError): + Cluster(load_balancing_policy=RoundRobinPolicy) class RoundRobinPolicyTest(unittest.TestCase): @@ -67,7 +77,7 @@ def test_basic(self): policy = RoundRobinPolicy() policy.populate(None, hosts) qplan = list(policy.make_query_plan()) - self.assertEqual(sorted(qplan), hosts) + assert sorted(qplan) == hosts def test_multiple_query_plans(self): hosts = [0, 1, 2, 3] @@ -75,13 +85,13 @@ def test_multiple_query_plans(self): policy.populate(None, hosts) for i in range(20): qplan = list(policy.make_query_plan()) - self.assertEqual(sorted(qplan), hosts) + assert sorted(qplan) == hosts def test_single_host(self): policy = RoundRobinPolicy() policy.populate(None, [0]) qplan = list(policy.make_query_plan()) - self.assertEqual(qplan, [0]) + assert qplan == [0] def test_status_updates(self): hosts = [0, 1, 2, 3] @@ -92,7 +102,7 @@ def test_status_updates(self): policy.on_up(4) policy.on_add(5) qplan = list(policy.make_query_plan()) - self.assertEqual(sorted(qplan), [2, 3, 4, 5]) + assert sorted(qplan) == [2, 3, 4, 5] def test_thread_safety(self): hosts = range(100) @@ -102,7 +112,7 @@ def test_thread_safety(self): def check_query_plan(): for i in range(100): qplan = list(policy.make_query_plan()) - self.assertEqual(sorted(qplan), list(hosts)) + assert sorted(qplan) == list(hosts) threads = [Thread(target=check_query_plan) for i in range(4)] for t in threads: @@ -161,8 +171,7 @@ def host_down(): else: sys.setswitchinterval(original_interval) - if errors: - self.fail("Saw errors: %s" % (errors,)) + assert not errors, "Saw errors: %s" % (errors,) def test_no_live_nodes(self): """ @@ -176,7 +185,7 @@ def test_no_live_nodes(self): policy.on_down(i) qplan = list(policy.make_query_plan()) - self.assertEqual(qplan, []) + assert qplan == [] @pytest.mark.parametrize("policy_specialization, constructor_args", [(DCAwareRoundRobinPolicy, ("dc1", )), (RackAwareRoundRobinPolicy, ("dc1", "rack1"))]) class TestRackOrDCAwareRoundRobinPolicy: @@ -184,21 +193,23 @@ class TestRackOrDCAwareRoundRobinPolicy: def test_no_remote(self, policy_specialization, constructor_args): hosts = [] for i in range(2): - h = Host(DefaultEndPoint(i), SimpleConvictionPolicy) + h = Host(DefaultEndPoint(i), SimpleConvictionPolicy, host_id=uuid.uuid4()) h.set_location_info("dc1", "rack2") hosts.append(h) for i in range(2): - h = Host(DefaultEndPoint(i + 2), SimpleConvictionPolicy) + h = Host(DefaultEndPoint(i + 2), SimpleConvictionPolicy, host_id=uuid.uuid4()) h.set_location_info("dc1", "rack1") hosts.append(h) + random.shuffle(hosts) + policy = policy_specialization(*constructor_args) policy.populate(None, hosts) qplan = list(policy.make_query_plan()) assert sorted(qplan) == sorted(hosts) def test_with_remotes(self, policy_specialization, constructor_args): - hosts = [Host(DefaultEndPoint(i), SimpleConvictionPolicy) for i in range(6)] + hosts = [Host(DefaultEndPoint(i), SimpleConvictionPolicy, host_id=uuid.uuid4()) for i in range(6)] for h in hosts[:2]: h.set_location_info("dc1", "rack1") for h in hosts[2:4]: @@ -206,6 +217,8 @@ def test_with_remotes(self, policy_specialization, constructor_args): for h in hosts[4:]: h.set_location_info("dc2", "rack1") + random.shuffle(hosts) + local_rack_hosts = set(h for h in hosts if h.datacenter == "dc1" and h.rack == "rack1") local_hosts = set(h for h in hosts if h.datacenter == "dc1" and h.rack != "rack1") remote_hosts = set(h for h in hosts if h.datacenter != "dc1") @@ -251,7 +264,7 @@ def test_get_distance(self, policy_specialization, constructor_args): policy = policy_specialization(*constructor_args, used_hosts_per_remote_dc=0) # same dc, same rack - host = Host(DefaultEndPoint("ip1"), SimpleConvictionPolicy) + host = Host(DefaultEndPoint("ip1"), SimpleConvictionPolicy, host_id=uuid.uuid4()) host.set_location_info("dc1", "rack1") policy.populate(Mock(), [host]) @@ -261,14 +274,14 @@ def test_get_distance(self, policy_specialization, constructor_args): assert policy.distance(host) == HostDistance.LOCAL_RACK # same dc different rack - host = Host(DefaultEndPoint("ip1"), SimpleConvictionPolicy) + host = Host(DefaultEndPoint("ip1"), SimpleConvictionPolicy, host_id=uuid.uuid4()) host.set_location_info("dc1", "rack2") policy.populate(Mock(), [host]) assert policy.distance(host) == HostDistance.LOCAL # used_hosts_per_remote_dc is set to 0, so ignore it - remote_host = Host(DefaultEndPoint("ip2"), SimpleConvictionPolicy) + remote_host = Host(DefaultEndPoint("ip2"), SimpleConvictionPolicy, host_id=uuid.uuid4()) remote_host.set_location_info("dc2", "rack1") assert policy.distance(remote_host) == HostDistance.IGNORED @@ -282,14 +295,14 @@ def test_get_distance(self, policy_specialization, constructor_args): # since used_hosts_per_remote_dc is set to 1, only the first # remote host in dc2 will be REMOTE, the rest are IGNORED - second_remote_host = Host(DefaultEndPoint("ip3"), SimpleConvictionPolicy) + second_remote_host = Host(DefaultEndPoint("ip3"), SimpleConvictionPolicy, host_id=uuid.uuid4()) second_remote_host.set_location_info("dc2", "rack1") policy.populate(Mock(), [host, remote_host, second_remote_host]) distances = set([policy.distance(remote_host), policy.distance(second_remote_host)]) assert distances == set([HostDistance.REMOTE, HostDistance.IGNORED]) def test_status_updates(self, policy_specialization, constructor_args): - hosts = [Host(DefaultEndPoint(i), SimpleConvictionPolicy) for i in range(5)] + hosts = [Host(DefaultEndPoint(i), SimpleConvictionPolicy, host_id=uuid.uuid4()) for i in range(5)] for h in hosts[:2]: h.set_location_info("dc1", "rack1") for h in hosts[2:4]: @@ -302,11 +315,11 @@ def test_status_updates(self, policy_specialization, constructor_args): policy.on_down(hosts[0]) policy.on_remove(hosts[2]) - new_local_host = Host(DefaultEndPoint(5), SimpleConvictionPolicy) + new_local_host = Host(DefaultEndPoint(5), SimpleConvictionPolicy, host_id=uuid.uuid4()) new_local_host.set_location_info("dc1", "rack1") policy.on_up(new_local_host) - new_remote_host = Host(DefaultEndPoint(6), SimpleConvictionPolicy) + new_remote_host = Host(DefaultEndPoint(6), SimpleConvictionPolicy, host_id=uuid.uuid4()) new_remote_host.set_location_info("dc9000", "rack1") policy.on_add(new_remote_host) @@ -331,7 +344,7 @@ def test_status_updates(self, policy_specialization, constructor_args): assert qplan == [] def test_modification_during_generation(self, policy_specialization, constructor_args): - hosts = [Host(DefaultEndPoint(i), SimpleConvictionPolicy) for i in range(4)] + hosts = [Host(DefaultEndPoint(i), SimpleConvictionPolicy, host_id=uuid.uuid4()) for i in range(4)] for h in hosts[:2]: h.set_location_info("dc1", "rack1") for h in hosts[2:]: @@ -345,7 +358,7 @@ def test_modification_during_generation(self, policy_specialization, constructor # approach that changes specific things during known phases of the # generator. - new_host = Host(DefaultEndPoint(4), SimpleConvictionPolicy) + new_host = Host(DefaultEndPoint(4), SimpleConvictionPolicy, host_id=uuid.uuid4()) new_host.set_location_info("dc1", "rack1") # new local before iteration @@ -456,7 +469,7 @@ def test_modification_during_generation(self, policy_specialization, constructor policy.on_up(hosts[2]) policy.on_up(hosts[3]) - another_host = Host(DefaultEndPoint(5), SimpleConvictionPolicy) + another_host = Host(DefaultEndPoint(5), SimpleConvictionPolicy, host_id=uuid.uuid4()) another_host.set_location_info("dc3", "rack1") new_host.set_location_info("dc3", "rack1") @@ -490,7 +503,7 @@ def test_no_live_nodes(self, policy_specialization, constructor_args): hosts = [] for i in range(4): - h = Host(DefaultEndPoint(i), SimpleConvictionPolicy) + h = Host(DefaultEndPoint(i), SimpleConvictionPolicy, host_id=uuid.uuid4()) h.set_location_info("dc1", "rack1") hosts.append(h) @@ -515,7 +528,7 @@ def test_no_nodes(self, policy_specialization, constructor_args): assert qplan == [] def test_wrong_dc(self, policy_specialization, constructor_args): - hosts = [Host(DefaultEndPoint(i), SimpleConvictionPolicy) for i in range(3)] + hosts = [Host(DefaultEndPoint(i), SimpleConvictionPolicy, host_id=uuid.uuid4()) for i in range(3)] for h in hosts[:3]: h.set_location_info("dc2", "rack2") @@ -527,9 +540,9 @@ def test_wrong_dc(self, policy_specialization, constructor_args): class DCAwareRoundRobinPolicyTest(unittest.TestCase): def test_default_dc(self): - host_local = Host(DefaultEndPoint(1), SimpleConvictionPolicy, 'local') - host_remote = Host(DefaultEndPoint(2), SimpleConvictionPolicy, 'remote') - host_none = Host(DefaultEndPoint(1), SimpleConvictionPolicy) + host_local = Host(DefaultEndPoint(1), SimpleConvictionPolicy, 'local', host_id=uuid.uuid4()) + host_remote = Host(DefaultEndPoint(2), SimpleConvictionPolicy, 'remote', host_id=uuid.uuid4()) + host_none = Host(DefaultEndPoint(1), SimpleConvictionPolicy, host_id=uuid.uuid4()) # contact point is '1' cluster = Mock(endpoints_resolved=[DefaultEndPoint(1)]) @@ -543,15 +556,6 @@ def test_default_dc(self): assert policy.local_dc != host_remote.datacenter assert policy.local_dc == host_local.datacenter - # contact DC second - policy = DCAwareRoundRobinPolicy() - policy.populate(cluster, [host_none]) - assert not policy.local_dc - policy.on_add(host_remote) - policy.on_add(host_local) - assert policy.local_dc != host_remote.datacenter - assert policy.local_dc == host_local.datacenter - # no DC policy = DCAwareRoundRobinPolicy() policy.populate(cluster, [host_none]) @@ -564,15 +568,16 @@ def test_default_dc(self): policy.populate(cluster, [host_none]) assert not policy.local_dc policy.on_add(host_remote) - assert not policy.local_dc + assert policy.local_dc class TokenAwarePolicyTest(unittest.TestCase): def test_wrap_round_robin(self): cluster = Mock(spec=Cluster) cluster.metadata = Mock(spec=Metadata) - cluster.control_connection._tablets_routing_v1 = False - hosts = [Host(DefaultEndPoint(str(i)), SimpleConvictionPolicy) for i in range(4)] + cluster.metadata._tablets = Mock(spec=Tablets) + cluster.metadata._tablets.get_tablet_for_key.return_value = None + hosts = [Host(DefaultEndPoint(str(i)), SimpleConvictionPolicy, host_id=uuid.uuid4()) for i in range(4)] for host in hosts: host.set_up() @@ -591,20 +596,21 @@ def get_replicas(keyspace, packed_key): replicas = get_replicas(None, struct.pack('>i', i)) other = set(h for h in hosts if h not in replicas) - self.assertEqual(replicas, qplan[:2]) - self.assertEqual(other, set(qplan[2:])) + assert sorted(replicas) == sorted(qplan[:2]) + assert other == set(qplan[2:]) # Should use the secondary policy for i in range(4): qplan = list(policy.make_query_plan()) - self.assertEqual(set(qplan), set(hosts)) + assert sorted(set(qplan)) == sorted(set(hosts)) def test_wrap_dc_aware(self): cluster = Mock(spec=Cluster) cluster.metadata = Mock(spec=Metadata) - cluster.control_connection._tablets_routing_v1 = False - hosts = [Host(DefaultEndPoint(str(i)), SimpleConvictionPolicy) for i in range(4)] + cluster.metadata._tablets = Mock(spec=Tablets) + cluster.metadata._tablets.get_tablet_for_key.return_value = None + hosts = [Host(DefaultEndPoint(str(i)), SimpleConvictionPolicy, host_id=uuid.uuid4()) for i in range(4)] for host in hosts: host.set_up() for h in hosts[:2]: @@ -622,7 +628,7 @@ def get_replicas(keyspace, packed_key): cluster.metadata.get_replicas.side_effect = get_replicas - policy = TokenAwarePolicy(DCAwareRoundRobinPolicy("dc1", used_hosts_per_remote_dc=1)) + policy = TokenAwarePolicy(DCAwareRoundRobinPolicy("dc1", used_hosts_per_remote_dc=2)) policy.populate(cluster, hosts) for i in range(4): @@ -631,17 +637,78 @@ def get_replicas(keyspace, packed_key): replicas = get_replicas(None, struct.pack('>i', i)) # first should be the only local replica - self.assertIn(qplan[0], replicas) - self.assertEqual(qplan[0].datacenter, "dc1") + assert qplan[0] in replicas + assert qplan[0].datacenter == "dc1" + + # then the replica from remote DC + assert qplan[1] in replicas + assert qplan[1].datacenter == "dc2" + + # then non-replica from local DC + assert qplan[2] not in replicas + assert qplan[2].datacenter == "dc1" - # then the local non-replica - self.assertNotIn(qplan[1], replicas) - self.assertEqual(qplan[1].datacenter, "dc1") + # and only then non-replica from remote DC + assert qplan[3] not in replicas + assert qplan[3].datacenter == "dc2" - # then one of the remotes (used_hosts_per_remote_dc is 1, so we - # shouldn't see two remotes) - self.assertEqual(qplan[2].datacenter, "dc2") - self.assertEqual(3, len(qplan)) + assert 4 == len(qplan) + + def test_wrap_rack_aware(self): + cluster = Mock(spec=Cluster) + cluster.metadata = Mock(spec=Metadata) + cluster.metadata._tablets = Mock(spec=Tablets) + cluster.metadata._tablets.get_tablet_for_key.return_value = None + hosts = [Host(DefaultEndPoint(str(i)), SimpleConvictionPolicy, host_id=uuid.uuid4()) for i in range(8)] + for host in hosts: + host.set_up() + hosts[0].set_location_info("dc1", "rack1") + hosts[1].set_location_info("dc1", "rack2") + hosts[2].set_location_info("dc2", "rack1") + hosts[3].set_location_info("dc2", "rack2") + hosts[4].set_location_info("dc1", "rack1") + hosts[5].set_location_info("dc1", "rack2") + hosts[6].set_location_info("dc2", "rack1") + hosts[7].set_location_info("dc2", "rack2") + + def get_replicas(keyspace, packed_key): + index = struct.unpack('>i', packed_key)[0] + # return one node from each DC + if index % 2 == 0: + return [hosts[0], hosts[1], hosts[2], hosts[3]] + else: + return [hosts[4], hosts[5], hosts[6], hosts[7]] + + cluster.metadata.get_replicas.side_effect = get_replicas + + policy = TokenAwarePolicy(RackAwareRoundRobinPolicy("dc1", "rack1", used_hosts_per_remote_dc=4)) + policy.populate(cluster, hosts) + + for i in range(4): + query = Statement(routing_key=struct.pack('>i', i), keyspace='keyspace_name') + qplan = list(policy.make_query_plan(None, query)) + replicas = get_replicas(None, struct.pack('>i', i)) + + print(qplan) + print(replicas) + + # first should be replica from local rack local dc + assert qplan[0] in replicas + assert qplan[0].datacenter == "dc1" + assert qplan[0].rack == "rack1" + + # second should be replica from remote rack local dc + assert qplan[1] in replicas + assert qplan[1].datacenter == "dc1" + assert qplan[1].rack == "rack2" + + # third and forth should be replica from the remote dcs + assert qplan[2] in replicas + assert qplan[2].datacenter == "dc2" + assert qplan[3] in replicas + assert qplan[3].datacenter == "dc2" + + assert 8 == len(qplan) class FakeCluster: def __init__(self): @@ -656,40 +723,40 @@ def test_get_distance(self): """ policy = TokenAwarePolicy(DCAwareRoundRobinPolicy("dc1", used_hosts_per_remote_dc=0)) - host = Host(DefaultEndPoint("ip1"), SimpleConvictionPolicy) + host = Host(DefaultEndPoint("ip1"), SimpleConvictionPolicy, host_id=uuid.uuid4()) host.set_location_info("dc1", "rack1") policy.populate(self.FakeCluster(), [host]) - self.assertEqual(policy.distance(host), HostDistance.LOCAL) + assert policy.distance(host) == HostDistance.LOCAL # used_hosts_per_remote_dc is set to 0, so ignore it - remote_host = Host(DefaultEndPoint("ip2"), SimpleConvictionPolicy) + remote_host = Host(DefaultEndPoint("ip2"), SimpleConvictionPolicy, host_id=uuid.uuid4()) remote_host.set_location_info("dc2", "rack1") - self.assertEqual(policy.distance(remote_host), HostDistance.IGNORED) + assert policy.distance(remote_host) == HostDistance.IGNORED # dc2 isn't registered in the policy's live_hosts dict policy._child_policy.used_hosts_per_remote_dc = 1 - self.assertEqual(policy.distance(remote_host), HostDistance.IGNORED) + assert policy.distance(remote_host) == HostDistance.IGNORED # make sure the policy has both dcs registered policy.populate(self.FakeCluster(), [host, remote_host]) - self.assertEqual(policy.distance(remote_host), HostDistance.REMOTE) + assert policy.distance(remote_host) == HostDistance.REMOTE # since used_hosts_per_remote_dc is set to 1, only the first # remote host in dc2 will be REMOTE, the rest are IGNORED - second_remote_host = Host(DefaultEndPoint("ip3"), SimpleConvictionPolicy) + second_remote_host = Host(DefaultEndPoint("ip3"), SimpleConvictionPolicy, host_id=uuid.uuid4()) second_remote_host.set_location_info("dc2", "rack1") policy.populate(self.FakeCluster(), [host, remote_host, second_remote_host]) distances = set([policy.distance(remote_host), policy.distance(second_remote_host)]) - self.assertEqual(distances, set([HostDistance.REMOTE, HostDistance.IGNORED])) + assert distances == set([HostDistance.REMOTE, HostDistance.IGNORED]) def test_status_updates(self): """ Same test as DCAwareRoundRobinPolicyTest.test_status_updates() """ - hosts = [Host(DefaultEndPoint(i), SimpleConvictionPolicy) for i in range(4)] + hosts = [Host(DefaultEndPoint(i), SimpleConvictionPolicy, host_id=uuid.uuid4()) for i in range(4)] for h in hosts[:2]: h.set_location_info("dc1", "rack1") for h in hosts[2:]: @@ -700,42 +767,43 @@ def test_status_updates(self): policy.on_down(hosts[0]) policy.on_remove(hosts[2]) - new_local_host = Host(DefaultEndPoint(4), SimpleConvictionPolicy) + new_local_host = Host(DefaultEndPoint(4), SimpleConvictionPolicy, host_id=uuid.uuid4()) new_local_host.set_location_info("dc1", "rack1") policy.on_up(new_local_host) - new_remote_host = Host(DefaultEndPoint(5), SimpleConvictionPolicy) + new_remote_host = Host(DefaultEndPoint(5), SimpleConvictionPolicy, host_id=uuid.uuid4()) new_remote_host.set_location_info("dc9000", "rack1") policy.on_add(new_remote_host) # we now have two local hosts and two remote hosts in separate dcs qplan = list(policy.make_query_plan()) - self.assertEqual(set(qplan[:2]), set([hosts[1], new_local_host])) - self.assertEqual(set(qplan[2:]), set([hosts[3], new_remote_host])) + assert set(qplan[:2]) == set([hosts[1], new_local_host]) + assert set(qplan[2:]) == set([hosts[3], new_remote_host]) # since we have hosts in dc9000, the distance shouldn't be IGNORED - self.assertEqual(policy.distance(new_remote_host), HostDistance.REMOTE) + assert policy.distance(new_remote_host) == HostDistance.REMOTE policy.on_down(new_local_host) policy.on_down(hosts[1]) qplan = list(policy.make_query_plan()) - self.assertEqual(set(qplan), set([hosts[3], new_remote_host])) + assert set(qplan) == set([hosts[3], new_remote_host]) policy.on_down(new_remote_host) policy.on_down(hosts[3]) qplan = list(policy.make_query_plan()) - self.assertEqual(qplan, []) + assert qplan == [] def test_statement_keyspace(self): - hosts = [Host(DefaultEndPoint(str(i)), SimpleConvictionPolicy) for i in range(4)] + hosts = [Host(DefaultEndPoint(str(i)), SimpleConvictionPolicy, host_id=uuid.uuid4()) for i in range(4)] for host in hosts: host.set_up() cluster = Mock(spec=Cluster) cluster.metadata = Mock(spec=Metadata) - cluster.control_connection._tablets_routing_v1 = False + cluster.metadata._tablets = Mock(spec=Tablets) replicas = hosts[2:] cluster.metadata.get_replicas.return_value = replicas + cluster.metadata._tablets.get_tablet_for_key.return_value = None child_policy = Mock() child_policy.make_query_plan.return_value = hosts @@ -749,8 +817,8 @@ def test_statement_keyspace(self): routing_key = 'routing_key' query = Statement(routing_key=routing_key) qplan = list(policy.make_query_plan(keyspace, query)) - self.assertEqual(hosts, qplan) - self.assertEqual(cluster.metadata.get_replicas.call_count, 0) + assert hosts == qplan + assert cluster.metadata.get_replicas.call_count == 0 child_policy.make_query_plan.assert_called_once_with(keyspace, query) # working keyspace, no statement @@ -759,7 +827,7 @@ def test_statement_keyspace(self): routing_key = 'routing_key' query = Statement(routing_key=routing_key) qplan = list(policy.make_query_plan(keyspace, query)) - self.assertEqual(replicas + hosts[:2], qplan) + assert replicas + hosts[:2] == qplan cluster.metadata.get_replicas.assert_called_with(keyspace, routing_key) # statement keyspace, no working @@ -769,7 +837,7 @@ def test_statement_keyspace(self): routing_key = 'routing_key' query = Statement(routing_key=routing_key, keyspace=statement_keyspace) qplan = list(policy.make_query_plan(working_keyspace, query)) - self.assertEqual(replicas + hosts[:2], qplan) + assert replicas + hosts[:2] == qplan cluster.metadata.get_replicas.assert_called_with(statement_keyspace, routing_key) # both keyspaces set, statement keyspace used for routing @@ -779,7 +847,7 @@ def test_statement_keyspace(self): routing_key = 'routing_key' query = Statement(routing_key=routing_key, keyspace=statement_keyspace) qplan = list(policy.make_query_plan(working_keyspace, query)) - self.assertEqual(replicas + hosts[:2], qplan) + assert replicas + hosts[:2] == qplan cluster.metadata.get_replicas.assert_called_with(statement_keyspace, routing_key) def test_shuffles_if_given_keyspace_and_routing_key(self): @@ -792,7 +860,8 @@ def test_shuffles_if_given_keyspace_and_routing_key(self): @test_category policy """ - self._assert_shuffle(keyspace='keyspace', routing_key='routing_key') + self._assert_shuffle(cluster=self._prepare_cluster_with_vnodes(), keyspace='keyspace', routing_key='routing_key') + self._assert_shuffle(cluster=self._prepare_cluster_with_tablets(), keyspace='keyspace', routing_key='routing_key') def test_no_shuffle_if_given_no_keyspace(self): """ @@ -803,7 +872,8 @@ def test_no_shuffle_if_given_no_keyspace(self): @test_category policy """ - self._assert_shuffle(keyspace=None, routing_key='routing_key') + self._assert_shuffle(cluster=self._prepare_cluster_with_vnodes(), keyspace=None, routing_key='routing_key') + self._assert_shuffle(cluster=self._prepare_cluster_with_tablets(), keyspace=None, routing_key='routing_key') def test_no_shuffle_if_given_no_routing_key(self): """ @@ -814,20 +884,37 @@ def test_no_shuffle_if_given_no_routing_key(self): @test_category policy """ - self._assert_shuffle(keyspace='keyspace', routing_key=None) + self._assert_shuffle(cluster=self._prepare_cluster_with_vnodes(), keyspace='keyspace', routing_key=None) + self._assert_shuffle(cluster=self._prepare_cluster_with_tablets(), keyspace='keyspace', routing_key=None) - @patch('cassandra.policies.shuffle') - def _assert_shuffle(self, patched_shuffle, keyspace, routing_key): - hosts = [Host(DefaultEndPoint(str(i)), SimpleConvictionPolicy) for i in range(4)] + def _prepare_cluster_with_vnodes(self): + hosts = [Host(DefaultEndPoint(str(i)), SimpleConvictionPolicy, host_id=uuid.uuid4()) for i in range(4)] for host in hosts: host.set_up() - cluster = Mock(spec=Cluster) cluster.metadata = Mock(spec=Metadata) - cluster.control_connection._tablets_routing_v1 = False - replicas = hosts[2:] - cluster.metadata.get_replicas.return_value = replicas + cluster.metadata._tablets = Mock(spec=Tablets) + cluster.metadata.all_hosts.return_value = hosts + cluster.metadata.get_replicas.return_value = hosts[2:] + cluster.metadata._tablets.get_tablet_for_key.return_value = None + return cluster + + def _prepare_cluster_with_tablets(self): + hosts = [Host(DefaultEndPoint(str(i)), SimpleConvictionPolicy, host_id=uuid.uuid4()) for i in range(4)] + for host in hosts: + host.set_up() + cluster = Mock(spec=Cluster) + cluster.metadata = Mock(spec=Metadata) + cluster.metadata._tablets = Mock(spec=Tablets) + cluster.metadata.all_hosts.return_value = hosts + cluster.metadata.get_replicas.return_value = hosts[2:] + cluster.metadata._tablets.get_tablet_for_key.return_value = Tablet(replicas=[(h.host_id, 0) for h in hosts[2:]]) + return cluster + @patch('cassandra.policies.shuffle') + def _assert_shuffle(self, patched_shuffle, cluster, keyspace, routing_key): + hosts = cluster.metadata.all_hosts() + replicas = cluster.metadata.get_replicas() child_policy = Mock() child_policy.make_query_plan.return_value = hosts child_policy.distance.return_value = HostDistance.LOCAL @@ -835,20 +922,26 @@ def _assert_shuffle(self, patched_shuffle, keyspace, routing_key): policy = TokenAwarePolicy(child_policy, shuffle_replicas=True) policy.populate(cluster, hosts) + is_tablets = cluster.metadata._tablets.get_tablet_for_key() is not None + cluster.metadata.get_replicas.reset_mock() child_policy.make_query_plan.reset_mock() query = Statement(routing_key=routing_key) qplan = list(policy.make_query_plan(keyspace, query)) if keyspace is None or routing_key is None: - self.assertEqual(hosts, qplan) - self.assertEqual(cluster.metadata.get_replicas.call_count, 0) + assert hosts == qplan + assert cluster.metadata.get_replicas.call_count == 0 child_policy.make_query_plan.assert_called_once_with(keyspace, query) - self.assertEqual(patched_shuffle.call_count, 0) + assert patched_shuffle.call_count == 0 else: - self.assertEqual(set(replicas), set(qplan[:2])) - self.assertEqual(hosts[:2], qplan[2:]) - child_policy.make_query_plan.assert_called_once_with(keyspace, query) - self.assertEqual(patched_shuffle.call_count, 1) + assert set(replicas) == set(qplan[:2]) + assert hosts[:2] == qplan[2:] + if is_tablets: + child_policy.make_query_plan.assert_called_with(keyspace, query) + assert child_policy.make_query_plan.call_count == 2 + else: + child_policy.make_query_plan.assert_called_once_with(keyspace, query) + assert patched_shuffle.call_count == 1 class ConvictionPolicyTest(unittest.TestCase): @@ -858,8 +951,10 @@ def test_not_implemented(self): """ conviction_policy = ConvictionPolicy(1) - self.assertRaises(NotImplementedError, conviction_policy.add_failure, 1) - self.assertRaises(NotImplementedError, conviction_policy.reset) + with pytest.raises(NotImplementedError): + conviction_policy.add_failure(1) + with pytest.raises(NotImplementedError): + conviction_policy.reset() class SimpleConvictionPolicyTest(unittest.TestCase): @@ -869,8 +964,8 @@ def test_basic_responses(self): """ conviction_policy = SimpleConvictionPolicy(1) - self.assertEqual(conviction_policy.add_failure(1), True) - self.assertEqual(conviction_policy.reset(), None) + assert conviction_policy.add_failure(1) == True + assert conviction_policy.reset() == None class ReconnectionPolicyTest(unittest.TestCase): @@ -880,7 +975,8 @@ def test_basic_responses(self): """ policy = ReconnectionPolicy() - self.assertRaises(NotImplementedError, policy.new_schedule) + with pytest.raises(NotImplementedError): + policy.new_schedule() class ConstantReconnectionPolicyTest(unittest.TestCase): @@ -890,20 +986,21 @@ def test_bad_vals(self): Test initialization values """ - self.assertRaises(ValueError, ConstantReconnectionPolicy, -1, 0) + with pytest.raises(ValueError): + ConstantReconnectionPolicy(-1, 0) def test_schedule(self): """ Test ConstantReconnectionPolicy schedule """ - delay = 2 + configured_delay = 2 max_attempts = 100 - policy = ConstantReconnectionPolicy(delay=delay, max_attempts=max_attempts) + policy = ConstantReconnectionPolicy(delay=configured_delay, max_attempts=max_attempts) schedule = list(policy.new_schedule()) - self.assertEqual(len(schedule), max_attempts) + assert len(schedule) == max_attempts for i, delay in enumerate(schedule): - self.assertEqual(delay, delay) + assert delay == configured_delay def test_schedule_negative_max_attempts(self): """ @@ -913,11 +1010,8 @@ def test_schedule_negative_max_attempts(self): delay = 2 max_attempts = -100 - try: + with pytest.raises(ValueError): ConstantReconnectionPolicy(delay=delay, max_attempts=max_attempts) - self.fail('max_attempts should throw ValueError when negative') - except ValueError: - pass def test_schedule_infinite_attempts(self): delay = 2 @@ -925,19 +1019,23 @@ def test_schedule_infinite_attempts(self): crp = ConstantReconnectionPolicy(delay=delay, max_attempts=max_attempts) # this is infinite. we'll just verify one more than default for _, d in zip(range(65), crp.new_schedule()): - self.assertEqual(d, delay) + assert d == delay class ExponentialReconnectionPolicyTest(unittest.TestCase): def _assert_between(self, value, min, max): - self.assertTrue(min <= value <= max) + assert min <= value <= max def test_bad_vals(self): - self.assertRaises(ValueError, ExponentialReconnectionPolicy, -1, 0) - self.assertRaises(ValueError, ExponentialReconnectionPolicy, 0, -1) - self.assertRaises(ValueError, ExponentialReconnectionPolicy, 9000, 1) - self.assertRaises(ValueError, ExponentialReconnectionPolicy, 1, 2, -1) + with pytest.raises(ValueError): + ExponentialReconnectionPolicy(-1, 0) + with pytest.raises(ValueError): + ExponentialReconnectionPolicy(0, -1) + with pytest.raises(ValueError): + ExponentialReconnectionPolicy(9000, 1) + with pytest.raises(ValueError): + ExponentialReconnectionPolicy(1, 2, -1) def test_schedule_no_max(self): base_delay = 2.0 @@ -947,7 +1045,7 @@ def test_schedule_no_max(self): sched_slice = list(islice(policy.new_schedule(), 0, test_iter)) self._assert_between(sched_slice[0], base_delay*0.85, base_delay*1.15) self._assert_between(sched_slice[-1], max_delay*0.85, max_delay*1.15) - self.assertEqual(len(sched_slice), test_iter) + assert len(sched_slice) == test_iter def test_schedule_with_max(self): base_delay = 2.0 @@ -955,7 +1053,7 @@ def test_schedule_with_max(self): max_attempts = 64 policy = ExponentialReconnectionPolicy(base_delay=base_delay, max_delay=max_delay, max_attempts=max_attempts) schedule = list(policy.new_schedule()) - self.assertEqual(len(schedule), max_attempts) + assert len(schedule) == max_attempts for i, delay in enumerate(schedule): if i == 0: self._assert_between(delay, base_delay*0.85, base_delay*1.15) @@ -972,7 +1070,7 @@ def test_schedule_exactly_one_attempt(self): policy = ExponentialReconnectionPolicy( base_delay=base_delay, max_delay=max_delay, max_attempts=max_attempts ) - self.assertEqual(len(list(policy.new_schedule())), 1) + assert len(list(policy.new_schedule())) == 1 def test_schedule_overflow(self): """ @@ -996,7 +1094,7 @@ def test_schedule_overflow(self): policy = ExponentialReconnectionPolicy(base_delay=base_delay, max_delay=max_delay, max_attempts=max_attempts) schedule = list(policy.new_schedule()) for number in schedule: - self.assertLessEqual(number, sys.float_info.max) + assert number <= sys.float_info.max def test_schedule_with_jitter(self): """ @@ -1030,29 +1128,29 @@ def test_read_timeout(self): retry, consistency = policy.on_read_timeout( query=None, consistency=ONE, required_responses=1, received_responses=2, data_retrieved=True, retry_num=1) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None # if we didn't get enough responses, rethrow retry, consistency = policy.on_read_timeout( query=None, consistency=ONE, required_responses=2, received_responses=1, data_retrieved=True, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None # if we got enough responses, but also got a data response, rethrow retry, consistency = policy.on_read_timeout( query=None, consistency=ONE, required_responses=2, received_responses=2, data_retrieved=True, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None # we got enough responses but no data response, so retry retry, consistency = policy.on_read_timeout( query=None, consistency=ONE, required_responses=2, received_responses=2, data_retrieved=False, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETRY) - self.assertEqual(consistency, ONE) + assert retry == RetryPolicy.RETRY + assert consistency == ONE def test_write_timeout(self): policy = RetryPolicy() @@ -1061,22 +1159,22 @@ def test_write_timeout(self): retry, consistency = policy.on_write_timeout( query=None, consistency=ONE, write_type=WriteType.SIMPLE, required_responses=1, received_responses=2, retry_num=1) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None # if it's not a BATCH_LOG write, don't retry it retry, consistency = policy.on_write_timeout( query=None, consistency=ONE, write_type=WriteType.SIMPLE, required_responses=1, received_responses=2, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None # retry BATCH_LOG writes regardless of received responses retry, consistency = policy.on_write_timeout( query=None, consistency=ONE, write_type=WriteType.BATCH_LOG, required_responses=10000, received_responses=1, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETRY) - self.assertEqual(consistency, ONE) + assert retry == RetryPolicy.RETRY + assert consistency == ONE def test_unavailable(self): """ @@ -1087,20 +1185,20 @@ def test_unavailable(self): retry, consistency = policy.on_unavailable( query=None, consistency=ONE, required_replicas=1, alive_replicas=2, retry_num=1) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None retry, consistency = policy.on_unavailable( query=None, consistency=ONE, required_replicas=1, alive_replicas=2, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETRY_NEXT_HOST) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETRY_NEXT_HOST + assert consistency == None retry, consistency = policy.on_unavailable( query=None, consistency=ONE, required_replicas=10000, alive_replicas=1, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETRY_NEXT_HOST) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETRY_NEXT_HOST + assert consistency == None class FallthroughRetryPolicyTest(unittest.TestCase): @@ -1115,26 +1213,26 @@ def test_read_timeout(self): retry, consistency = policy.on_read_timeout( query=None, consistency=ONE, required_responses=1, received_responses=2, data_retrieved=True, retry_num=1) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None retry, consistency = policy.on_read_timeout( query=None, consistency=ONE, required_responses=2, received_responses=1, data_retrieved=True, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None retry, consistency = policy.on_read_timeout( query=None, consistency=ONE, required_responses=2, received_responses=2, data_retrieved=True, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None retry, consistency = policy.on_read_timeout( query=None, consistency=ONE, required_responses=2, received_responses=2, data_retrieved=False, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None def test_write_timeout(self): policy = FallthroughRetryPolicy() @@ -1142,20 +1240,20 @@ def test_write_timeout(self): retry, consistency = policy.on_write_timeout( query=None, consistency=ONE, write_type=WriteType.SIMPLE, required_responses=1, received_responses=2, retry_num=1) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None retry, consistency = policy.on_write_timeout( query=None, consistency=ONE, write_type=WriteType.SIMPLE, required_responses=1, received_responses=2, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None retry, consistency = policy.on_write_timeout( query=None, consistency=ONE, write_type=WriteType.BATCH_LOG, required_responses=10000, received_responses=1, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None def test_unavailable(self): policy = FallthroughRetryPolicy() @@ -1163,20 +1261,20 @@ def test_unavailable(self): retry, consistency = policy.on_unavailable( query=None, consistency=ONE, required_replicas=1, alive_replicas=2, retry_num=1) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None retry, consistency = policy.on_unavailable( query=None, consistency=ONE, required_replicas=1, alive_replicas=2, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None retry, consistency = policy.on_unavailable( query=None, consistency=ONE, required_replicas=10000, alive_replicas=1, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None class DowngradingConsistencyRetryPolicyTest(unittest.TestCase): @@ -1188,50 +1286,50 @@ def test_read_timeout(self): retry, consistency = policy.on_read_timeout( query=None, consistency=ONE, required_responses=1, received_responses=2, data_retrieved=True, retry_num=1) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None # if we didn't get enough responses, retry at a lower consistency retry, consistency = policy.on_read_timeout( query=None, consistency=ONE, required_responses=4, received_responses=3, data_retrieved=True, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETRY) - self.assertEqual(consistency, ConsistencyLevel.THREE) + assert retry == RetryPolicy.RETRY + assert consistency == ConsistencyLevel.THREE # if we didn't get enough responses, retry at a lower consistency retry, consistency = policy.on_read_timeout( query=None, consistency=ONE, required_responses=3, received_responses=2, data_retrieved=True, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETRY) - self.assertEqual(consistency, ConsistencyLevel.TWO) + assert retry == RetryPolicy.RETRY + assert consistency == ConsistencyLevel.TWO # retry consistency level goes down based on the # of recv'd responses retry, consistency = policy.on_read_timeout( query=None, consistency=ONE, required_responses=3, received_responses=1, data_retrieved=True, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETRY) - self.assertEqual(consistency, ConsistencyLevel.ONE) + assert retry == RetryPolicy.RETRY + assert consistency == ConsistencyLevel.ONE # if we got no responses, rethrow retry, consistency = policy.on_read_timeout( query=None, consistency=ONE, required_responses=3, received_responses=0, data_retrieved=True, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None # if we got enough response but no data, retry retry, consistency = policy.on_read_timeout( query=None, consistency=ONE, required_responses=3, received_responses=3, data_retrieved=False, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETRY) - self.assertEqual(consistency, ONE) + assert retry == RetryPolicy.RETRY + assert consistency == ONE # if we got enough responses, but also got a data response, rethrow retry, consistency = policy.on_read_timeout( query=None, consistency=ONE, required_responses=2, received_responses=2, data_retrieved=True, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None def test_write_timeout(self): policy = DowngradingConsistencyRetryPolicy() @@ -1240,41 +1338,41 @@ def test_write_timeout(self): retry, consistency = policy.on_write_timeout( query=None, consistency=ONE, write_type=WriteType.SIMPLE, required_responses=1, received_responses=2, retry_num=1) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None for write_type in (WriteType.SIMPLE, WriteType.BATCH, WriteType.COUNTER): # ignore failures if at least one response (replica persisted) retry, consistency = policy.on_write_timeout( query=None, consistency=ONE, write_type=write_type, required_responses=1, received_responses=2, retry_num=0) - self.assertEqual(retry, RetryPolicy.IGNORE) + assert retry == RetryPolicy.IGNORE # retrhow if we can't be sure we have a replica retry, consistency = policy.on_write_timeout( query=None, consistency=ONE, write_type=write_type, required_responses=1, received_responses=0, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETHROW) + assert retry == RetryPolicy.RETHROW # downgrade consistency level on unlogged batch writes retry, consistency = policy.on_write_timeout( query=None, consistency=ONE, write_type=WriteType.UNLOGGED_BATCH, required_responses=3, received_responses=1, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETRY) - self.assertEqual(consistency, ConsistencyLevel.ONE) + assert retry == RetryPolicy.RETRY + assert consistency == ConsistencyLevel.ONE # retry batch log writes at the same consistency level retry, consistency = policy.on_write_timeout( query=None, consistency=ONE, write_type=WriteType.BATCH_LOG, required_responses=3, received_responses=1, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETRY) - self.assertEqual(consistency, ONE) + assert retry == RetryPolicy.RETRY + assert consistency == ONE # timeout on an unknown write_type retry, consistency = policy.on_write_timeout( query=None, consistency=ONE, write_type=None, required_responses=1, received_responses=2, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None def test_unavailable(self): policy = DowngradingConsistencyRetryPolicy() @@ -1282,14 +1380,14 @@ def test_unavailable(self): # if this is the second or greater attempt, rethrow retry, consistency = policy.on_unavailable( query=None, consistency=ONE, required_replicas=3, alive_replicas=1, retry_num=1) - self.assertEqual(retry, RetryPolicy.RETHROW) - self.assertEqual(consistency, None) + assert retry == RetryPolicy.RETHROW + assert consistency == None # downgrade consistency on unavailable exceptions retry, consistency = policy.on_unavailable( query=None, consistency=ONE, required_replicas=3, alive_replicas=1, retry_num=0) - self.assertEqual(retry, RetryPolicy.RETRY) - self.assertEqual(consistency, ConsistencyLevel.ONE) + assert retry == RetryPolicy.RETRY + assert consistency == ConsistencyLevel.ONE class ExponentialRetryPolicyTest(unittest.TestCase): @@ -1315,24 +1413,24 @@ class WhiteListRoundRobinPolicyTest(unittest.TestCase): def test_hosts_with_hostname(self): hosts = ['localhost'] policy = WhiteListRoundRobinPolicy(hosts) - host = Host(DefaultEndPoint("127.0.0.1"), SimpleConvictionPolicy) + host = Host(DefaultEndPoint("127.0.0.1"), SimpleConvictionPolicy, host_id=uuid.uuid4()) policy.populate(None, [host]) qplan = list(policy.make_query_plan()) - self.assertEqual(sorted(qplan), [host]) + assert sorted(qplan) == [host] - self.assertEqual(policy.distance(host), HostDistance.LOCAL) + assert policy.distance(host) == HostDistance.LOCAL def test_hosts_with_socket_hostname(self): hosts = [UnixSocketEndPoint('/tmp/scylla-workdir/cql.m')] policy = WhiteListRoundRobinPolicy(hosts) - host = Host(UnixSocketEndPoint('/tmp/scylla-workdir/cql.m'), SimpleConvictionPolicy) + host = Host(UnixSocketEndPoint('/tmp/scylla-workdir/cql.m'), SimpleConvictionPolicy, host_id=uuid.uuid4()) policy.populate(None, [host]) qplan = list(policy.make_query_plan()) - self.assertEqual(sorted(qplan), [host]) + assert sorted(qplan) == [host] - self.assertEqual(policy.distance(host), HostDistance.LOCAL) + assert policy.distance(host) == HostDistance.LOCAL class AddressTranslatorTest(unittest.TestCase): @@ -1345,8 +1443,8 @@ def test_ec2_multi_region_translator(self, *_): ec2t = EC2MultiRegionTranslator() addr = '127.0.0.1' translated = ec2t.translate(addr) - self.assertIsNot(translated, addr) # verifies that the resolver path is followed - self.assertEqual(translated, addr) # and that it resolves to the same address + assert translated is not addr # verifies that the resolver path is followed + assert translated == addr # and that it resolves to the same address class HostFilterPolicyInitTest(unittest.TestCase): @@ -1356,8 +1454,8 @@ def setUp(self): Mock(name='predicate')) def _check_init(self, hfp): - self.assertIs(hfp._child_policy, self.child_policy) - self.assertIsInstance(hfp._hosts_lock, LockType) + assert hfp._child_policy is self.child_policy + assert isinstance(hfp._hosts_lock, LockType) # we can't use a simple assertIs because we wrap the function arg0, arg1 = Mock(name='arg0'), Mock(name='arg1') @@ -1380,7 +1478,7 @@ def test_immutable_predicate(self): expected_message_regex = "can't set attribute" hfp = HostFilterPolicy(child_policy=Mock(name='child_policy'), predicate=Mock(name='predicate')) - with self.assertRaisesRegex(AttributeError, expected_message_regex): + with pytest.raises(AttributeError, match=expected_message_regex): hfp.predicate = object() @@ -1408,7 +1506,7 @@ def _check_host_triggered_method(self, policy, name): # method calls the child policy's method... child_policy_method.assert_called_once_with(arg, kw=kwarg) # and returns its return value - self.assertIs(result, child_policy_method.return_value) + assert result is child_policy_method.return_value def test_defer_on_up_to_child_policy(self): self._check_host_triggered_method(self.passthrough_hfp, 'on_up') @@ -1452,14 +1550,12 @@ def setUp(self): child_policy=Mock(name='child_policy', distance=Mock(name='distance')), predicate=lambda host: host.address == 'acceptme' ) - self.ignored_host = Host(DefaultEndPoint('ignoreme'), conviction_policy_factory=Mock()) - self.accepted_host = Host(DefaultEndPoint('acceptme'), conviction_policy_factory=Mock()) + self.ignored_host = Host(DefaultEndPoint('ignoreme'), conviction_policy_factory=Mock(), host_id=uuid.uuid4()) + self.accepted_host = Host(DefaultEndPoint('acceptme'), conviction_policy_factory=Mock(), host_id=uuid.uuid4()) def test_ignored_with_filter(self): - self.assertEqual(self.hfp.distance(self.ignored_host), - HostDistance.IGNORED) - self.assertNotEqual(self.hfp.distance(self.accepted_host), - HostDistance.IGNORED) + assert self.hfp.distance(self.ignored_host) == HostDistance.IGNORED + assert self.hfp.distance(self.accepted_host) != HostDistance.IGNORED def test_accepted_filter_defers_to_child_policy(self): self.hfp._child_policy.distance.side_effect = distances = Mock(), Mock() @@ -1467,9 +1563,9 @@ def test_accepted_filter_defers_to_child_policy(self): # getting the distance for an ignored host shouldn't affect subsequent results self.hfp.distance(self.ignored_host) # first call of _child_policy with count() side effect - self.assertEqual(self.hfp.distance(self.accepted_host), distances[0]) + assert self.hfp.distance(self.accepted_host) == distances[0] # second call of _child_policy with count() side effect - self.assertEqual(self.hfp.distance(self.accepted_host), distances[1]) + assert self.hfp.distance(self.accepted_host) == distances[1] class HostFilterPolicyPopulateTest(unittest.TestCase): @@ -1496,10 +1592,7 @@ def test_child_is_populated_with_filtered_hosts(self): ['acceptme0', 'acceptme1']) hfp.populate(mock_cluster, hosts) hfp._child_policy.populate.assert_called_once() - self.assertEqual( - hfp._child_policy.populate.call_args[1]['hosts'], - ['acceptme0', 'acceptme1'] - ) + assert hfp._child_policy.populate.call_args[1]['hosts'] == ['acceptme0', 'acceptme1'] class HostFilterPolicyQueryPlanTest(unittest.TestCase): @@ -1523,12 +1616,11 @@ def test_query_plan_deferred_to_child(self): working_keyspace=working_keyspace, query=query ) - self.assertEqual(qp, hfp._child_policy.make_query_plan.return_value) + assert qp == hfp._child_policy.make_query_plan.return_value def test_wrap_token_aware(self): cluster = Mock(spec=Cluster) - cluster.control_connection._tablets_routing_v1 = False - hosts = [Host(DefaultEndPoint("127.0.0.{}".format(i)), SimpleConvictionPolicy) for i in range(1, 6)] + hosts = [Host(DefaultEndPoint("127.0.0.{}".format(i)), SimpleConvictionPolicy, host_id=uuid.uuid4()) for i in range(1, 6)] for host in hosts: host.set_up() @@ -1536,6 +1628,8 @@ def get_replicas(keyspace, packed_key): return hosts[:2] cluster.metadata.get_replicas.side_effect = get_replicas + cluster.metadata._tablets = Mock(spec=Tablets) + cluster.metadata._tablets.get_tablet_for_key.return_value = None child_policy = TokenAwarePolicy(RoundRobinPolicy()) @@ -1553,13 +1647,13 @@ def get_replicas(keyspace, packed_key): query_plan = hfp.make_query_plan("keyspace", mocked_query) # First the not filtered replica, and then the rest of the allowed hosts ordered query_plan = list(query_plan) - self.assertEqual(query_plan[0], Host(DefaultEndPoint("127.0.0.2"), SimpleConvictionPolicy)) - self.assertEqual(set(query_plan[1:]),{Host(DefaultEndPoint("127.0.0.3"), SimpleConvictionPolicy), - Host(DefaultEndPoint("127.0.0.5"), SimpleConvictionPolicy)}) + assert query_plan[0] == Host(DefaultEndPoint("127.0.0.2"), SimpleConvictionPolicy, host_id=uuid.uuid4()) + assert set(query_plan[1:]) == {Host(DefaultEndPoint("127.0.0.3"), SimpleConvictionPolicy, host_id=uuid.uuid4()), + Host(DefaultEndPoint("127.0.0.5"), SimpleConvictionPolicy, host_id=uuid.uuid4())} def test_create_whitelist(self): cluster = Mock(spec=Cluster) - hosts = [Host(DefaultEndPoint("127.0.0.{}".format(i)), SimpleConvictionPolicy) for i in range(1, 6)] + hosts = [Host(DefaultEndPoint("127.0.0.{}".format(i)), SimpleConvictionPolicy, host_id=uuid.uuid4()) for i in range(1, 6)] for host in hosts: host.set_up() @@ -1577,5 +1671,5 @@ def test_create_whitelist(self): mocked_query = Mock() query_plan = hfp.make_query_plan("keyspace", mocked_query) # Only the filtered replicas should be allowed - self.assertEqual(set(query_plan), {Host(DefaultEndPoint("127.0.0.1"), SimpleConvictionPolicy), - Host(DefaultEndPoint("127.0.0.4"), SimpleConvictionPolicy)}) + assert set(query_plan) == {Host(DefaultEndPoint("127.0.0.1"), SimpleConvictionPolicy, host_id=uuid.uuid4()), + Host(DefaultEndPoint("127.0.0.4"), SimpleConvictionPolicy, host_id=uuid.uuid4())} diff --git a/tests/unit/test_protocol.py b/tests/unit/test_protocol.py index 907f62f2bb..9704811239 100644 --- a/tests/unit/test_protocol.py +++ b/tests/unit/test_protocol.py @@ -26,6 +26,7 @@ from cassandra.query import BatchType from cassandra.marshal import uint32_unpack from cassandra.cluster import ContinuousPagingOptions +import pytest class MessageTest(unittest.TestCase): @@ -88,46 +89,7 @@ def test_query_message(self): self._check_calls(io, [(b'\x00\x00\x00\x01',), (b'a',), (b'\x00\x03',), (b'\x00\x00\x00\x00',)]) def _check_calls(self, io, expected): - self.assertEqual( - tuple(c[1] for c in io.write.mock_calls), - tuple(expected) - ) - - def test_continuous_paging(self): - """ - Test to check continuous paging throws an Exception if it's not supported and the correct valuesa - are written to the buffer if the option is enabled. - - @since DSE 2.0b3 GRAPH 1.0b1 - @jira_ticket PYTHON-694 - @expected_result the values are correctly written - - @test_category connection - """ - max_pages = 4 - max_pages_per_second = 3 - continuous_paging_options = ContinuousPagingOptions(max_pages=max_pages, - max_pages_per_second=max_pages_per_second) - message = QueryMessage("a", 3, continuous_paging_options=continuous_paging_options) - io = Mock() - for version in [version for version in ProtocolVersion.SUPPORTED_VERSIONS - if not ProtocolVersion.has_continuous_paging_support(version)]: - self.assertRaises(UnsupportedOperation, message.send_body, io, version) - - io.reset_mock() - message.send_body(io, ProtocolVersion.DSE_V1) - - # continuous paging adds two write calls to the buffer - self.assertEqual(len(io.write.mock_calls), 6) - # Check that the appropriate flag is set to True - self.assertEqual(uint32_unpack(io.write.mock_calls[3][1][0]) & _WITH_SERIAL_CONSISTENCY_FLAG, 0) - self.assertEqual(uint32_unpack(io.write.mock_calls[3][1][0]) & _PAGE_SIZE_FLAG, 0) - self.assertEqual(uint32_unpack(io.write.mock_calls[3][1][0]) & _WITH_PAGING_STATE_FLAG, 0) - self.assertEqual(uint32_unpack(io.write.mock_calls[3][1][0]) & _PAGING_OPTIONS_FLAG, _PAGING_OPTIONS_FLAG) - - # Test max_pages and max_pages_per_second are correctly written - self.assertEqual(uint32_unpack(io.write.mock_calls[4][1][0]), max_pages) - self.assertEqual(uint32_unpack(io.write.mock_calls[5][1][0]), max_pages_per_second) + assert tuple(c[1] for c in io.write.mock_calls) == tuple(expected) def test_prepare_flag(self): """ @@ -144,9 +106,9 @@ def test_prepare_flag(self): for version in ProtocolVersion.SUPPORTED_VERSIONS: message.send_body(io, version) if ProtocolVersion.uses_prepare_flags(version): - self.assertEqual(len(io.write.mock_calls), 3) + assert len(io.write.mock_calls) == 3 else: - self.assertEqual(len(io.write.mock_calls), 2) + assert len(io.write.mock_calls) == 2 io.reset_mock() def test_prepare_flag_with_keyspace(self): @@ -164,7 +126,7 @@ def test_prepare_flag_with_keyspace(self): (b'ks',), ]) else: - with self.assertRaises(UnsupportedOperation): + with pytest.raises(UnsupportedOperation): message.send_body(io, version) io.reset_mock() @@ -172,7 +134,7 @@ def test_keyspace_flag_raises_before_v5(self): keyspace_message = QueryMessage('a', consistency_level=3, keyspace='ks') io = Mock(name='io') - with self.assertRaisesRegex(UnsupportedOperation, 'Keyspaces.*set'): + with pytest.raises(UnsupportedOperation, match='Keyspaces.*set'): keyspace_message.send_body(io, protocol_version=4) io.assert_not_called() diff --git a/tests/unit/test_protocol_features.py b/tests/unit/test_protocol_features.py index bcf874f68f..895c384f7e 100644 --- a/tests/unit/test_protocol_features.py +++ b/tests/unit/test_protocol_features.py @@ -1,7 +1,4 @@ -try: - import unittest2 as unittest -except ImportError: - import unittest # noqa +import unittest import logging @@ -22,6 +19,6 @@ class OptionsHolder(object): protocol_features = ProtocolFeatures.parse_from_supported(OptionsHolder().options) - self.assertEqual(protocol_features.rate_limit_error, 123) - self.assertEqual(protocol_features.shard_id, 0) - self.assertEqual(protocol_features.sharding_info, None) + assert protocol_features.rate_limit_error == 123 + assert protocol_features.shard_id == 0 + assert protocol_features.sharding_info is None diff --git a/tests/unit/test_query.py b/tests/unit/test_query.py index 8a3f00fa9d..6b0ebe690e 100644 --- a/tests/unit/test_query.py +++ b/tests/unit/test_query.py @@ -14,7 +14,7 @@ import unittest -from cassandra.query import BatchStatement, SimpleStatement +from cassandra.query import BatchStatement, PreparedStatement, SimpleStatement class BatchStatementTest(unittest.TestCase): @@ -30,26 +30,26 @@ def test_clear(self): batch = BatchStatement() batch.add(ss) - self.assertTrue(batch._statements_and_parameters) - self.assertEqual(batch.keyspace, keyspace) - self.assertEqual(batch.routing_key, routing_key) - self.assertEqual(batch.custom_payload, custom_payload) + assert batch._statements_and_parameters + assert batch.keyspace == keyspace + assert batch.routing_key == routing_key + assert batch.custom_payload == custom_payload batch.clear() - self.assertFalse(batch._statements_and_parameters) - self.assertIsNone(batch.keyspace) - self.assertIsNone(batch.routing_key) - self.assertFalse(batch.custom_payload) + assert not batch._statements_and_parameters + assert batch.keyspace is None + assert batch.routing_key is None + assert not batch.custom_payload batch.add(ss) def test_clear_empty(self): batch = BatchStatement() batch.clear() - self.assertFalse(batch._statements_and_parameters) - self.assertIsNone(batch.keyspace) - self.assertIsNone(batch.routing_key) - self.assertFalse(batch.custom_payload) + assert not batch._statements_and_parameters + assert batch.keyspace is None + assert batch.routing_key is None + assert not batch.custom_payload batch.add('something') @@ -60,11 +60,58 @@ def test_add_all(self): batch.add_all(statements, parameters) bound_statements = [t[1] for t in batch._statements_and_parameters] str_parameters = [str(i) for i in range(10)] - self.assertEqual(bound_statements, str_parameters) + assert bound_statements == str_parameters def test_len(self): for n in 0, 10, 100: batch = BatchStatement() batch.add_all(statements=['%s'] * n, parameters=[(i,) for i in range(n)]) - self.assertEqual(len(batch), n) + assert len(batch) == n + + def _make_prepared_statement(self, is_lwt=False): + return PreparedStatement( + column_metadata=[], + query_id=b"query-id", + routing_key_indexes=[], + query="INSERT INTO test.table (id) VALUES (1)", + keyspace=None, + protocol_version=4, + result_metadata=[], + result_metadata_id=None, + is_lwt=is_lwt, + ) + + def test_is_lwt_false_for_non_lwt_statements(self): + batch = BatchStatement() + batch.add(self._make_prepared_statement(is_lwt=False)) + batch.add(self._make_prepared_statement(is_lwt=False).bind(())) + batch.add(SimpleStatement("INSERT INTO test.table (id) VALUES (3)")) + batch.add("INSERT INTO test.table (id) VALUES (4)") + assert batch.is_lwt() is False + + def test_is_lwt_propagates_from_statements(self): + batch = BatchStatement() + batch.add(self._make_prepared_statement(is_lwt=False)) + assert batch.is_lwt() is False + + batch.add(self._make_prepared_statement(is_lwt=True)) + assert batch.is_lwt() is True + + bound_lwt = self._make_prepared_statement(is_lwt=True).bind(()) + batch_with_bound = BatchStatement() + batch_with_bound.add(bound_lwt) + assert batch_with_bound.is_lwt() is True + + class LwtSimpleStatement(SimpleStatement): + def __init__(self): + super(LwtSimpleStatement, self).__init__( + "INSERT INTO test.table (id) VALUES (2) IF NOT EXISTS" + ) + + def is_lwt(self): + return True + + batch_with_simple = BatchStatement() + batch_with_simple.add(LwtSimpleStatement()) + assert batch_with_simple.is_lwt() is True diff --git a/tests/unit/test_response_future.py b/tests/unit/test_response_future.py index 8226cea440..9673b0d634 100644 --- a/tests/unit/test_response_future.py +++ b/tests/unit/test_response_future.py @@ -19,18 +19,20 @@ from unittest.mock import Mock, MagicMock, ANY from cassandra import ConsistencyLevel, Unavailable, SchemaTargetType, SchemaChangeType, OperationTimedOut -from cassandra.cluster import Session, ResponseFuture, NoHostAvailable, ProtocolVersion +from cassandra.cluster import Session, ResponseFuture, NoHostAvailable, ProtocolVersion, ControlConnectionQueryFallback from cassandra.connection import Connection, ConnectionException from cassandra.protocol import (ReadTimeoutErrorMessage, WriteTimeoutErrorMessage, UnavailableErrorMessage, ResultMessage, QueryMessage, OverloadedErrorMessage, IsBootstrappingErrorMessage, - PreparedQueryNotFound, PrepareMessage, + PreparedQueryNotFound, PrepareMessage, ServerError, RESULT_KIND_ROWS, RESULT_KIND_SET_KEYSPACE, RESULT_KIND_SCHEMA_CHANGE, RESULT_KIND_PREPARED, ProtocolHandler) from cassandra.policies import RetryPolicy, ExponentialBackoffRetryPolicy from cassandra.pool import NoConnectionsAvailable from cassandra.query import SimpleStatement +from tests.util import assertEqual, assertIsInstance +import pytest class ResponseFutureTests(unittest.TestCase): @@ -39,6 +41,7 @@ def make_basic_session(self): s = Mock(spec=Session) s.row_factory = lambda col_names, rows: [(col_names, rows)] s.cluster.control_connection._tablets_routing_v1 = False + s.cluster.allow_control_connection_query_fallback = ControlConnectionQueryFallback.Disabled return s def make_pool(self): @@ -47,6 +50,22 @@ def make_pool(self): pool.borrow_connection.return_value = [Mock(), Mock()] return pool + def make_control_connection(self): + connection = Mock(spec=Connection) + connection.endpoint = 'control-host' + connection.lock = RLock() + connection.in_flight = 0 + connection.max_request_id = 100 + connection.request_ids = deque() + connection._requests = {} + connection.orphaned_request_ids = set() + connection.orphaned_threshold = 75 + connection.orphaned_threshold_reached = False + connection.is_control_connection = True + connection.get_request_id.return_value = 7 + connection.send_msg.return_value = 128 + return connection + def make_session(self): session = self.make_basic_session() session.cluster._default_load_balancing_policy.make_query_plan.return_value = ['ip1', 'ip2'] @@ -81,7 +100,7 @@ def test_result_message(self): expected_result = (object(), object()) rf._set_result(None, None, None, self.make_mock_response(expected_result[0], expected_result[1])) result = rf.result()[0] - self.assertEqual(result, expected_result) + assert result == expected_result def test_unknown_result_class(self): session = self.make_session() @@ -92,7 +111,8 @@ def test_unknown_result_class(self): rf = self.make_response_future(session) rf.send_request() rf._set_result(None, None, None, object()) - self.assertRaises(ConnectionException, rf.result) + with pytest.raises(ConnectionException): + rf.result() def test_set_keyspace_result(self): session = self.make_session() @@ -104,7 +124,7 @@ def test_set_keyspace_result(self): results="keyspace1") rf._set_result(None, None, None, result) rf._set_keyspace_completed({}) - self.assertFalse(rf.result()) + assert not rf.result() def test_schema_change_result(self): session = self.make_session() @@ -126,7 +146,7 @@ def test_other_result_message_kind(self): rf.send_request() result = Mock(spec=ResultMessage, kind=999, results=[1, 2, 3]) rf._set_result(None, None, None, result) - self.assertEqual(rf.result()[0], result) + assert rf.result()[0] == result def test_heartbeat_defunct_deadlock(self): """ @@ -139,6 +159,8 @@ def test_heartbeat_defunct_deadlock(self): connection = MagicMock(spec=Connection) connection._requests = {} + connection.in_flight = 5 + connection.orphaned_request_ids = set() pool = Mock() pool.is_shutdown = False @@ -159,7 +181,10 @@ def test_heartbeat_defunct_deadlock(self): # Simulate ResponseFuture timing out rf._on_timeout() - self.assertRaisesRegex(OperationTimedOut, "Connection defunct by heartbeat", rf.result) + with pytest.raises(OperationTimedOut, match="Connection defunct by heartbeat") as exc_info: + rf.result() + assert exc_info.value.timeout == 1 + assert exc_info.value.in_flight == 5 def test_read_timeout_error_message(self): session = self.make_session() @@ -173,7 +198,8 @@ def test_read_timeout_error_message(self): "received_responses":1, "consistency": 1}) rf._set_result(None, None, None, result) - self.assertRaises(Exception, rf.result) + with pytest.raises(Exception): + rf.result() def test_write_timeout_error_message(self): session = self.make_session() @@ -186,7 +212,8 @@ def test_write_timeout_error_message(self): result = Mock(spec=WriteTimeoutErrorMessage, info={"write_type": 1, "required_responses":2, "received_responses":1, "consistency": 1}) rf._set_result(None, None, None, result) - self.assertRaises(Exception, rf.result) + with pytest.raises(Exception): + rf.result() def test_unavailable_error_message(self): session = self.make_session() @@ -201,7 +228,8 @@ def test_unavailable_error_message(self): result = Mock(spec=UnavailableErrorMessage, info={"required_replicas":2, "alive_replicas": 1, "consistency": 1}) rf._set_result(None, None, None, result) - self.assertRaises(Exception, rf.result) + with pytest.raises(Exception): + rf.result() def test_request_error_with_prepare_message(self): session = self.make_session() @@ -216,14 +244,14 @@ def test_request_error_with_prepare_message(self): result = Mock(spec=OverloadedErrorMessage) result.to_exception.return_value = result rf._set_result(None, None, None, result) - self.assertIsInstance(rf._final_exception, OverloadedErrorMessage) + assert isinstance(rf._final_exception, OverloadedErrorMessage) rf = ResponseFuture(session, message, query, 1, retry_policy=retry_policy) rf._query_retries = 1 rf.send_request() result = Mock(spec=ConnectionException) rf._set_result(None, None, None, result) - self.assertIsInstance(rf._final_exception, ConnectionException) + assert isinstance(rf._final_exception, ConnectionException) def test_retry_policy_says_ignore(self): session = self.make_session() @@ -237,7 +265,7 @@ def test_retry_policy_says_ignore(self): result = Mock(spec=UnavailableErrorMessage, info={}) rf._set_result(None, None, None, result) - self.assertFalse(rf.result()) + assert not rf.result() def test_retry_policy_says_retry(self): session = self.make_session() @@ -264,7 +292,7 @@ def test_retry_policy_says_retry(self): rf._set_result(host, None, None, result) rf.session.cluster.scheduler.schedule.assert_called_once_with(ANY, rf._retry_task, True, host) - self.assertEqual(1, rf._query_retries) + assert 1 == rf._query_retries connection = Mock(spec=Connection) pool.borrow_connection.return_value = (connection, 2) @@ -292,7 +320,7 @@ def test_retry_with_different_host(self): rf.session._pools.get.assert_called_once_with('ip1') pool.borrow_connection.assert_called_once_with(timeout=ANY, routing_key=ANY, keyspace=ANY, table=ANY) connection.send_msg.assert_called_once_with(rf.message, 1, cb=ANY, encoder=ProtocolHandler.encode_message, decoder=ProtocolHandler.decode_message, result_metadata=[]) - self.assertEqual(ConsistencyLevel.QUORUM, rf.message.consistency_level) + assert ConsistencyLevel.QUORUM == rf.message.consistency_level result = Mock(spec=OverloadedErrorMessage, info={}) host = Mock() @@ -300,7 +328,7 @@ def test_retry_with_different_host(self): rf.session.cluster.scheduler.schedule.assert_called_once_with(ANY, rf._retry_task, False, host) # query_retries does get incremented for Overloaded/Bootstrapping errors (since 3.18) - self.assertEqual(1, rf._query_retries) + assert 1 == rf._query_retries connection = Mock(spec=Connection) pool.borrow_connection.return_value = (connection, 2) @@ -313,7 +341,7 @@ def test_retry_with_different_host(self): connection.send_msg.assert_called_with(rf.message, 2, cb=ANY, encoder=ProtocolHandler.encode_message, decoder=ProtocolHandler.decode_message, result_metadata=[]) # the consistency level should be the same - self.assertEqual(ConsistencyLevel.QUORUM, rf.message.consistency_level) + assert ConsistencyLevel.QUORUM == rf.message.consistency_level def test_all_retries_fail(self): session = self.make_session() @@ -344,7 +372,8 @@ def test_all_retries_fail(self): rf.session.cluster.scheduler.schedule.assert_called_with(ANY, rf._retry_task, False, host) rf._retry_task(False, host) - self.assertRaises(NoHostAvailable, rf.result) + with pytest.raises(NoHostAvailable): + rf.result() def test_exponential_retry_policy_fail(self): session = self.make_session() @@ -376,7 +405,270 @@ def test_all_pools_shutdown(self): rf = ResponseFuture(session, Mock(), Mock(), 1) rf.send_request() - self.assertRaises(NoHostAvailable, rf.result) + with pytest.raises(NoHostAvailable): + rf.result() + + def test_control_connection_fallback_disabled_by_default(self): + session = self.make_basic_session() + session.cluster._default_load_balancing_policy.make_query_plan.return_value = ['ip1'] + session._pools = {} + connection = self.make_control_connection() + session.cluster.control_connection._connection = connection + + rf = self.make_response_future(session) + rf.send_request() + + connection.send_msg.assert_not_called() + with pytest.raises(NoHostAvailable): + rf.result() + + def test_control_connection_fallback_updates_connection_keyspace(self): + session = self.make_basic_session() + session.cluster.allow_control_connection_query_fallback = ControlConnectionQueryFallback.Fallback + session.cluster._default_load_balancing_policy.make_query_plan.return_value = ['ip1'] + session._pools = {} + + def set_keyspace_for_all_pools(keyspace, callback): + session.keyspace = keyspace + callback({}) + + session._set_keyspace_for_all_pools.side_effect = set_keyspace_for_all_pools + + connection = self.make_control_connection() + connection.keyspace = 'oldks' + session.cluster.control_connection._connection = connection + control_host = Mock(endpoint=connection.endpoint) + session.cluster.get_control_connection_host.return_value = control_host + + rf = self.make_response_future(session) + assert rf.send_request() + + result = Mock(spec=ResultMessage, kind=RESULT_KIND_SET_KEYSPACE, new_keyspace='newks') + connection.send_msg.call_args[1]['cb'](result) + + assert connection.keyspace == 'newks' + assert session.keyspace == 'newks' + assert rf.result().current_rows == [] + + def test_control_connection_fallback_when_no_usable_pools(self): + session = self.make_basic_session() + session.cluster.allow_control_connection_query_fallback = ControlConnectionQueryFallback.SkipPoolCreation + session.cluster._default_load_balancing_policy.make_query_plan.return_value = ['ip1', 'ip2'] + session._pools = {} + connection = self.make_control_connection() + session.cluster.control_connection._connection = connection + control_host = Mock(endpoint=connection.endpoint) + session.cluster.get_control_connection_host.return_value = control_host + + rf = self.make_response_future(session) + assert rf.send_request() + + connection.send_msg.assert_called_once_with( + rf.message, 7, cb=ANY, encoder=ProtocolHandler.encode_message, + decoder=ProtocolHandler.decode_message, result_metadata=[]) + assert connection.in_flight == 1 + assert rf.attempted_hosts == [control_host] + + cb = connection.send_msg.call_args[1]['cb'] + expected_result = (object(), object()) + cb(self.make_mock_response(expected_result[0], expected_result[1])) + + assert connection.in_flight == 0 + assert rf.result()[0] == expected_result + + def test_control_connection_fallback_retries_after_server_error(self): + session = self.make_basic_session() + session.cluster.allow_control_connection_query_fallback = ControlConnectionQueryFallback.Fallback + session.cluster._default_load_balancing_policy.make_query_plan.return_value = ['ip1'] + session._pools = {} + connection = self.make_control_connection() + connection.get_request_id.side_effect = [7, 8] + session.cluster.control_connection._connection = connection + control_host = Mock(endpoint=connection.endpoint) + session.cluster.get_control_connection_host.return_value = control_host + + rf = self.make_response_future(session) + assert rf.send_request() + + first_response = Mock(spec=ServerError, info={}) + first_response.summary = 'boom' + first_response.to_exception.return_value = first_response + connection.send_msg.call_args[1]['cb'](first_response) + + rf.session.cluster.scheduler.schedule.assert_called_once_with(ANY, rf._retry_task, False, control_host) + + # The retry decision must come from the future state, not the live connection reference. + rf._connection = Mock(is_control_connection=False) + + rf._retry_task(False, control_host) + + assert connection.send_msg.call_count == 2 + assert connection.send_msg.call_args_list[1][0][0] is rf.message + assert connection.send_msg.call_args_list[1][0][1] == 8 + assert rf.attempted_hosts == [control_host, control_host] + + expected_result = (object(), object()) + connection.send_msg.call_args_list[1][1]['cb']( + self.make_mock_response(expected_result[0], expected_result[1])) + + assert connection.in_flight == 0 + assert rf.result()[0] == expected_result + + def test_control_connection_fallback_fetches_next_page(self): + session = self.make_basic_session() + session.cluster.allow_control_connection_query_fallback = ControlConnectionQueryFallback.Fallback + session.cluster._default_load_balancing_policy.make_query_plan.return_value = ['ip1'] + session._pools = {} + connection = self.make_control_connection() + connection.get_request_id.side_effect = [7, 8] + session.cluster.control_connection._connection = connection + control_host = Mock(endpoint=connection.endpoint) + session.cluster.get_control_connection_host.return_value = control_host + + rf = self.make_response_future(session) + assert rf.send_request() + + first_response = self.make_mock_response(['col'], [(1,)]) + first_response.paging_state = b'next-page' + connection.send_msg.call_args[1]['cb'](first_response) + + assert rf.result().current_rows == [(['col'], [(1,)])] + assert rf.has_more_pages + + rf.start_fetching_next_page() + + assert connection.send_msg.call_count == 2 + assert connection.send_msg.call_args_list[1][0][0] is rf.message + assert connection.send_msg.call_args_list[1][0][1] == 8 + assert rf.message.paging_state == b'next-page' + + second_response = self.make_mock_response(['col'], [(2,)]) + connection.send_msg.call_args_list[1][1]['cb'](second_response) + + assert connection.in_flight == 0 + assert rf.result().current_rows == [(['col'], [(2,)])] + + def test_control_connection_fallback_reprepares_prepared_statement(self): + session = self.make_basic_session() + session.cluster.allow_control_connection_query_fallback = ControlConnectionQueryFallback.Fallback + session.cluster.protocol_version = ProtocolVersion.V4 + session.cluster._default_load_balancing_policy.make_query_plan.return_value = ['ip1'] + session._pools = {} + session.submit.side_effect = lambda fn, *args, **kwargs: fn(*args, **kwargs) + + query_id = b'a' * 16 + prepared_statement = Mock( + query_id=query_id, + query_string="SELECT * FROM foobar", + keyspace="FooKeyspace", + result_metadata=[], + result_metadata_id=None) + session.cluster._prepared_statements = {query_id: prepared_statement} + + connection = self.make_control_connection() + connection.keyspace = "FooKeyspace" + connection.get_request_id.side_effect = [7, 8, 9] + session.cluster.control_connection._connection = connection + control_host = Mock(endpoint=connection.endpoint) + session.cluster.get_control_connection_host.return_value = control_host + + rf = self.make_response_future(session) + rf.prepared_statement = prepared_statement + assert rf.send_request() + + missing = Mock(spec=PreparedQueryNotFound, info=query_id) + connection.send_msg.call_args_list[0][1]['cb'](missing) + + assert connection.send_msg.call_count == 2 + prepare_message = connection.send_msg.call_args_list[1][0][0] + assert isinstance(prepare_message, PrepareMessage) + assert prepare_message.query == "SELECT * FROM foobar" + assert connection.send_msg.call_args_list[1][0][1] == 8 + + prepared_response = Mock( + spec=ResultMessage, + kind=RESULT_KIND_PREPARED, + query_id=query_id, + column_metadata=[], + result_metadata_id=None) + connection.send_msg.call_args_list[1][1]['cb'](prepared_response) + + assert connection.send_msg.call_count == 3 + assert connection.send_msg.call_args_list[2][0][0] is rf.message + assert connection.send_msg.call_args_list[2][0][1] == 9 + + expected_result = (['col'], [(1,)]) + connection.send_msg.call_args_list[2][1]['cb']( + self.make_mock_response(expected_result[0], expected_result[1])) + + assert connection.in_flight == 0 + assert rf.result()[0] == expected_result + + def test_control_connection_fallback_not_used_when_pool_can_serve(self): + session = self.make_basic_session() + session.cluster.allow_control_connection_query_fallback = ControlConnectionQueryFallback.Fallback + session.cluster._default_load_balancing_policy.make_query_plan.return_value = ['ip1'] + pool = Mock(is_shutdown=False) + pool.borrow_connection.side_effect = NoConnectionsAvailable() + session._pools = {'ip1': pool} + connection = self.make_control_connection() + session.cluster.control_connection._connection = connection + + rf = self.make_response_future(session) + rf.send_request() + + connection.send_msg.assert_not_called() + with pytest.raises(NoHostAvailable): + rf.result() + + def test_control_connection_fallback_orphans_stream_on_timeout(self): + session = self.make_basic_session() + session.cluster.allow_control_connection_query_fallback = ControlConnectionQueryFallback.Fallback + session.cluster._default_load_balancing_policy.make_query_plan.return_value = ['ip1'] + session._pools = {} + connection = self.make_control_connection() + session.cluster.control_connection._connection = connection + + def send_msg(message, request_id, cb, **kwargs): + connection._requests[request_id] = (cb, kwargs.get('decoder'), kwargs.get('result_metadata')) + return 128 + + connection.send_msg.side_effect = send_msg + + rf = self.make_response_future(session) + rf.send_request() + rf._on_timeout() + + assert 7 in connection.orphaned_request_ids + assert connection.in_flight == 1 + with pytest.raises(OperationTimedOut): + rf.result() + + def test_control_connection_fallback_timeout_without_metadata_host_uses_connection_endpoint(self): + session = self.make_basic_session() + session.cluster.allow_control_connection_query_fallback = ControlConnectionQueryFallback.Fallback + session.cluster._default_load_balancing_policy.make_query_plan.return_value = [] + session._pools = {} + session.cluster.get_control_connection_host.return_value = None + connection = self.make_control_connection() + session.cluster.control_connection._connection = connection + + def send_msg(message, request_id, cb, **kwargs): + connection._requests[request_id] = (cb, kwargs.get('decoder'), kwargs.get('result_metadata')) + return 128 + + connection.send_msg.side_effect = send_msg + + rf = self.make_response_future(session) + assert rf.send_request() + rf._on_timeout() + + with pytest.raises(OperationTimedOut) as exc_info: + rf.result() + + assert exc_info.value.errors == { + 'control-host': 'Client request timeout. See Session.execute[_async](timeout)' + } def test_first_pool_shutdown(self): session = self.make_basic_session() @@ -395,7 +687,7 @@ def test_first_pool_shutdown(self): rf._set_result(None, None, None, self.make_mock_response(expected_result[0], expected_result[1])) result = rf.result()[0] - self.assertEqual(result, expected_result) + assert result == expected_result def test_timeout_getting_connection_from_pool(self): session = self.make_basic_session() @@ -418,10 +710,10 @@ def test_timeout_getting_connection_from_pool(self): expected_result = (object(), object()) rf._set_result(None, None, None, self.make_mock_response(expected_result[0], expected_result[1])) - self.assertEqual(rf.result()[0], expected_result) + assert rf.result()[0] == expected_result # make sure the exception is recorded correctly - self.assertEqual(rf._errors, {'ip1': exc}) + assert rf._errors == {'ip1': exc} def test_callback(self): session = self.make_session() @@ -437,12 +729,12 @@ def test_callback(self): rf._set_result(None, None, None, self.make_mock_response(expected_result[0], expected_result[1])) result = rf.result()[0] - self.assertEqual(result, expected_result) + assert result == expected_result callback.assert_called_once_with([expected_result], arg, **kwargs) # this should get called immediately now that the result is set - rf.add_callback(self.assertEqual, [expected_result]) + rf.add_callback(assertEqual, [expected_result]) def test_errback(self): session = self.make_session() @@ -457,16 +749,17 @@ def test_errback(self): rf._query_retries = 1 rf.send_request() - rf.add_errback(self.assertIsInstance, Exception) + rf.add_errback(assertIsInstance, Exception) result = Mock(spec=UnavailableErrorMessage, info={"required_replicas":2, "alive_replicas": 1, "consistency": 1}) result.to_exception.return_value = Exception() rf._set_result(None, None, None, result) - self.assertRaises(Exception, rf.result) + with pytest.raises(Exception): + rf.result() # this should get called immediately now that the error is set - rf.add_errback(self.assertIsInstance, Exception) + rf.add_errback(assertIsInstance, Exception) def test_multiple_callbacks(self): session = self.make_session() @@ -487,7 +780,7 @@ def test_multiple_callbacks(self): rf._set_result(None, None, None, self.make_mock_response(expected_result[0], expected_result[1])) result = rf.result()[0] - self.assertEqual(result, expected_result) + assert result == expected_result callback.assert_called_once_with([expected_result], arg, **kwargs) callback2.assert_called_once_with([expected_result], arg2, **kwargs2) @@ -521,7 +814,8 @@ def test_multiple_errbacks(self): result.to_exception.return_value = expected_exception rf._set_result(None, None, None, result) rf._event.set() - self.assertRaises(Exception, rf.result) + with pytest.raises(Exception): + rf.result() callback.assert_called_once_with(expected_exception, arg, **kwargs) callback2.assert_called_once_with(expected_exception, arg2, **kwargs2) @@ -537,14 +831,15 @@ def test_add_callbacks(self): rf.send_request() rf.add_callbacks( - callback=self.assertEqual, callback_args=([{'col': 'val'}],), - errback=self.assertIsInstance, errback_args=(Exception,)) + callback=assertEqual, callback_args=([{'col': 'val'}],), + errback=assertIsInstance, errback_args=(Exception,)) result = Mock(spec=UnavailableErrorMessage, info={"required_replicas":2, "alive_replicas": 1, "consistency": 1}) result.to_exception.return_value = Exception() rf._set_result(None, None, None, result) - self.assertRaises(Exception, rf.result) + with pytest.raises(Exception): + rf.result() # test callback rf = ResponseFuture(session, message, query, 1) @@ -556,10 +851,10 @@ def test_add_callbacks(self): kwargs = {'one': 1, 'two': 2} rf.add_callbacks( callback=callback, callback_args=(arg,), callback_kwargs=kwargs, - errback=self.assertIsInstance, errback_args=(Exception,)) + errback=assertIsInstance, errback_args=(Exception,)) rf._set_result(None, None, None, self.make_mock_response(expected_result[0], expected_result[1])) - self.assertEqual(rf.result()[0], expected_result) + assert rf.result()[0] == expected_result callback.assert_called_once_with([expected_result], arg, **kwargs) @@ -582,11 +877,11 @@ def test_prepared_query_not_found(self): result = Mock(spec=PreparedQueryNotFound, info='a' * 16) rf._set_result(None, None, None, result) - self.assertTrue(session.submit.call_args) + assert session.submit.call_args args, kwargs = session.submit.call_args - self.assertEqual(rf._reprepare, args[-5]) - self.assertIsInstance(args[-4], PrepareMessage) - self.assertEqual(args[-4].query, "SELECT * FROM foobar") + assert rf._reprepare == args[-5] + assert isinstance(args[-4], PrepareMessage) + assert args[-4].query == "SELECT * FROM foobar" def test_prepared_query_not_found_bad_keyspace(self): session = self.make_session() @@ -606,7 +901,8 @@ def test_prepared_query_not_found_bad_keyspace(self): result = Mock(spec=PreparedQueryNotFound, info='a' * 16) rf._set_result(None, None, None, result) - self.assertRaises(ValueError, rf.result) + with pytest.raises(ValueError): + rf.result() def test_repeat_orig_query_after_succesful_reprepare(self): query_id = b'abc123' # Just a random binary string so we don't hit id mismatch exception @@ -640,7 +936,7 @@ def test_timeout_does_not_release_stream_id(self): pool = self.make_pool() session._pools.get.return_value = pool connection = Mock(spec=Connection, lock=RLock(), _requests={}, request_ids=deque(), - orphaned_request_ids=set(), orphaned_threshold=256) + orphaned_request_ids=set(), orphaned_threshold=256, in_flight=3) pool.borrow_connection.return_value = (connection, 1) rf = self.make_response_future(session) @@ -650,7 +946,65 @@ def test_timeout_does_not_release_stream_id(self): rf._on_timeout() pool.return_connection.assert_called_once_with(connection, stream_was_orphaned=True) - self.assertRaisesRegex(OperationTimedOut, "Client request timeout", rf.result) + with pytest.raises(OperationTimedOut, match="Client request timeout") as exc_info: + rf.result() + assert exc_info.value.timeout == 1 + assert exc_info.value.in_flight == 3 assert len(connection.request_ids) == 0, \ "Request IDs should be empty but it's not: {}".format(connection.request_ids) + + def test_single_host_query_plan_exhausted_after_one_retry(self): + """ + Test that when a specific host is provided, the query plan is properly + exhausted after one attempt and doesn't cause infinite retries. + + This test reproduces the issue where providing a single host in the query plan + (via the host parameter) would cause infinite retries on server errors because + the query_plan was a list instead of an iterator. + """ + session = self.make_basic_session() + pool = self.make_pool() + session._pools.get.return_value = pool + + # Create a specific host + specific_host = Mock() + + connection = Mock(spec=Connection) + pool.borrow_connection.return_value = (connection, 1) + + query = SimpleStatement("INSERT INTO foo (a, b) VALUES (1, 2)") + message = QueryMessage(query=query, consistency_level=ConsistencyLevel.ONE) + + # Create ResponseFuture with a specific host (this is the key to reproducing the bug) + rf = ResponseFuture(session, message, query, 1, host=specific_host) + rf.send_request() + + # Verify initial request was sent + rf.session._pools.get.assert_called_once_with(specific_host) + pool.borrow_connection.assert_called_once_with(timeout=ANY, routing_key=ANY, keyspace=ANY, table=ANY) + connection.send_msg.assert_called_once_with(rf.message, 1, cb=ANY, encoder=ProtocolHandler.encode_message, decoder=ProtocolHandler.decode_message, result_metadata=[]) + + # Simulate a ServerError response (which triggers RETRY_NEXT_HOST by default) + result = Mock(spec=ServerError, info={}) + result.to_exception.return_value = result + rf._set_result(specific_host, None, None, result) + + # The retry should be scheduled + rf.session.cluster.scheduler.schedule.assert_called_once_with(ANY, rf._retry_task, False, specific_host) + assert 1 == rf._query_retries + + # Reset mocks to track next calls + pool.borrow_connection.reset_mock() + connection.send_msg.reset_mock() + + # Now simulate the retry task executing + # The bug would cause this to succeed and retry again infinitely + # The fix ensures the iterator is exhausted after the first try + rf._retry_task(False, specific_host) + + # After the retry, send_request should be called but the query_plan iterator + # should be exhausted, so no new request should be sent + # Instead, it should set a NoHostAvailable exception + assert rf._final_exception is not None + assert isinstance(rf._final_exception, NoHostAvailable) diff --git a/tests/unit/test_resultset.py b/tests/unit/test_resultset.py index 7ff6352394..80e9c21ff9 100644 --- a/tests/unit/test_resultset.py +++ b/tests/unit/test_resultset.py @@ -18,6 +18,9 @@ from cassandra.cluster import ResultSet from cassandra.query import named_tuple_factory, dict_factory, tuple_factory +from tests.util import assertListEqual +import pytest + class ResultSetTests(unittest.TestCase): @@ -25,7 +28,7 @@ def test_iter_non_paged(self): expected = list(range(10)) rs = ResultSet(Mock(has_more_pages=False), expected) itr = iter(rs) - self.assertListEqual(list(itr), expected) + assertListEqual(list(itr), expected) def test_iter_paged(self): expected = list(range(10)) @@ -35,7 +38,7 @@ def test_iter_paged(self): itr = iter(rs) # this is brittle, depends on internal impl details. Would like to find a better way type(response_future).has_more_pages = PropertyMock(side_effect=(True, True, False)) # after init to avoid side effects being consumed by init - self.assertListEqual(list(itr), expected) + assertListEqual(list(itr), expected) def test_iter_paged_with_empty_pages(self): expected = list(range(10)) @@ -48,15 +51,15 @@ def test_iter_paged_with_empty_pages(self): ] rs = ResultSet(response_future, []) itr = iter(rs) - self.assertListEqual(list(itr), expected) + assertListEqual(list(itr), expected) def test_list_non_paged(self): # list access on RS for backwards-compatibility expected = list(range(10)) rs = ResultSet(Mock(has_more_pages=False), expected) for i in range(10): - self.assertEqual(rs[i], expected[i]) - self.assertEqual(list(rs), expected) + assert rs[i] == expected[i] + assert list(rs) == expected def test_list_paged(self): # list access on RS for backwards-compatibility @@ -66,16 +69,16 @@ def test_list_paged(self): rs = ResultSet(response_future, expected[:5]) # this is brittle, depends on internal impl details. Would like to find a better way type(response_future).has_more_pages = PropertyMock(side_effect=(True, True, True, False)) # First two True are consumed on check entering list mode - self.assertEqual(rs[9], expected[9]) - self.assertEqual(list(rs), expected) + assert rs[9] == expected[9] + assert list(rs) == expected def test_has_more_pages(self): response_future = Mock() response_future.has_more_pages.side_effect = PropertyMock(side_effect=(True, False)) rs = ResultSet(response_future, []) type(response_future).has_more_pages = PropertyMock(side_effect=(True, False)) # after init to avoid side effects being consumed by init - self.assertTrue(rs.has_more_pages) - self.assertFalse(rs.has_more_pages) + assert rs.has_more_pages + assert not rs.has_more_pages def test_iterate_then_index(self): # RuntimeError if indexing with no pages @@ -83,15 +86,15 @@ def test_iterate_then_index(self): rs = ResultSet(Mock(has_more_pages=False), expected) itr = iter(rs) # before consuming - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): rs[0] list(itr) # after consuming - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): rs[0] - self.assertFalse(rs) - self.assertFalse(list(rs)) + assert not rs + assert not list(rs) # RuntimeError if indexing during or after pages response_future = Mock(has_more_pages=True, _continuous_paging_session=None) @@ -100,17 +103,17 @@ def test_iterate_then_index(self): type(response_future).has_more_pages = PropertyMock(side_effect=(True, False)) itr = iter(rs) # before consuming - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): rs[0] for row in itr: # while consuming - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): rs[0] # after consuming - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): rs[0] - self.assertFalse(rs) - self.assertFalse(list(rs)) + assert not rs + assert not list(rs) def test_index_list_mode(self): # no pages @@ -118,13 +121,13 @@ def test_index_list_mode(self): rs = ResultSet(Mock(has_more_pages=False), expected) # index access before iteration causes list to be materialized - self.assertEqual(rs[0], expected[0]) + assert rs[0] == expected[0] # resusable iteration - self.assertListEqual(list(rs), expected) - self.assertListEqual(list(rs), expected) + assertListEqual(list(rs), expected) + assertListEqual(list(rs), expected) - self.assertTrue(rs) + assert rs # pages response_future = Mock(has_more_pages=True, _continuous_paging_session=None) @@ -133,13 +136,13 @@ def test_index_list_mode(self): # this is brittle, depends on internal impl details. Would like to find a better way type(response_future).has_more_pages = PropertyMock(side_effect=(True, True, True, False)) # First two True are consumed on check entering list mode # index access before iteration causes list to be materialized - self.assertEqual(rs[0], expected[0]) - self.assertEqual(rs[9], expected[9]) + assert rs[0] == expected[0] + assert rs[9] == expected[9] # resusable iteration - self.assertListEqual(list(rs), expected) - self.assertListEqual(list(rs), expected) + assertListEqual(list(rs), expected) + assertListEqual(list(rs), expected) - self.assertTrue(rs) + assert rs def test_eq(self): # no pages @@ -147,12 +150,12 @@ def test_eq(self): rs = ResultSet(Mock(has_more_pages=False), expected) # eq before iteration causes list to be materialized - self.assertEqual(rs, expected) + assert rs == expected # results can be iterated or indexed once we're materialized - self.assertListEqual(list(rs), expected) - self.assertEqual(rs[9], expected[9]) - self.assertTrue(rs) + assertListEqual(list(rs), expected) + assert rs[9] == expected[9] + assert rs # pages response_future = Mock(has_more_pages=True, _continuous_paging_session=None) @@ -160,56 +163,56 @@ def test_eq(self): rs = ResultSet(response_future, expected[:5]) type(response_future).has_more_pages = PropertyMock(side_effect=(True, True, True, False)) # eq before iteration causes list to be materialized - self.assertEqual(rs, expected) + assert rs == expected # results can be iterated or indexed once we're materialized - self.assertListEqual(list(rs), expected) - self.assertEqual(rs[9], expected[9]) - self.assertTrue(rs) + assertListEqual(list(rs), expected) + assert rs[9] == expected[9] + assert rs def test_bool(self): - self.assertFalse(ResultSet(Mock(has_more_pages=False), [])) - self.assertTrue(ResultSet(Mock(has_more_pages=False), [1])) + assert not ResultSet(Mock(has_more_pages=False), []) + assert ResultSet(Mock(has_more_pages=False), [1]) def test_was_applied(self): # unknown row factory raises - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): ResultSet(Mock(), []).was_applied response_future = Mock(row_factory=named_tuple_factory) # no row - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): ResultSet(response_future, []).was_applied # too many rows - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): ResultSet(response_future, [tuple(), tuple()]).was_applied # various internal row factories for row_factory in (named_tuple_factory, tuple_factory): for applied in (True, False): rs = ResultSet(Mock(row_factory=row_factory), [(applied,)]) - self.assertEqual(rs.was_applied, applied) + assert rs.was_applied == applied row_factory = dict_factory for applied in (True, False): rs = ResultSet(Mock(row_factory=row_factory), [{'[applied]': applied}]) - self.assertEqual(rs.was_applied, applied) + assert rs.was_applied == applied def test_one(self): # no pages first, second = Mock(), Mock() rs = ResultSet(Mock(has_more_pages=False), [first, second]) - self.assertEqual(rs.one(), first) + assert rs.one() == first def test_all(self): first, second = Mock(), Mock() rs1 = ResultSet(Mock(has_more_pages=False), [first, second]) rs2 = ResultSet(Mock(has_more_pages=False), [first, second]) - self.assertEqual(rs1.all(), list(rs2)) + assert rs1.all() == list(rs2) @patch('cassandra.cluster.warn') def test_indexing_deprecation(self, mocked_warn): @@ -217,9 +220,8 @@ def test_indexing_deprecation(self, mocked_warn): # pre-Py3.0 for some reason first, second = Mock(), Mock() rs = ResultSet(Mock(has_more_pages=False), [first, second]) - self.assertEqual(rs[0], first) - self.assertEqual(len(mocked_warn.mock_calls), 1) + assert rs[0] == first + assert len(mocked_warn.mock_calls) == 1 index_warning_args = tuple(mocked_warn.mock_calls[0])[1] - self.assertIn('indexing support will be removed in 4.0', - str(index_warning_args[0])) - self.assertIs(index_warning_args[1], DeprecationWarning) + assert 'indexing support will be removed in 4.0' in str(index_warning_args[0]) + assert index_warning_args[1] is DeprecationWarning diff --git a/tests/unit/test_row_factories.py b/tests/unit/test_row_factories.py index 70691ad8fd..7787f1d271 100644 --- a/tests/unit/test_row_factories.py +++ b/tests/unit/test_row_factories.py @@ -61,13 +61,13 @@ def test_creation_warning_on_long_column_list(self): with warnings.catch_warnings(record=True) as w: rows = named_tuple_factory(self.long_colnames, self.long_rows) - self.assertEqual(len(w), 1) + assert len(w) == 1 warning = w[0] - self.assertIn('pseudo_namedtuple_factory', str(warning)) - self.assertIn('3.7', str(warning)) + assert 'pseudo_namedtuple_factory' in str(warning) + assert '3.7' in str(warning) for r in rows: - self.assertEqual(r.col0, self.long_rows[0][0]) + assert r.col0 == self.long_rows[0][0] def test_creation_no_warning_on_short_column_list(self): """ @@ -81,7 +81,7 @@ def test_creation_no_warning_on_short_column_list(self): """ with warnings.catch_warnings(record=True) as w: rows = named_tuple_factory(self.short_colnames, self.short_rows) - self.assertEqual(len(w), 0) + assert len(w) == 0 # check that this is a real namedtuple - self.assertTrue(hasattr(rows[0], '_fields')) - self.assertIsInstance(rows[0], tuple) + assert hasattr(rows[0], '_fields') + assert isinstance(rows[0], tuple) diff --git a/tests/unit/test_segment.py b/tests/unit/test_segment.py index 0d0f146c16..bfc273db05 100644 --- a/tests/unit/test_segment.py +++ b/tests/unit/test_segment.py @@ -19,6 +19,7 @@ from cassandra import DriverException from cassandra.segment import Segment, CrcException from cassandra.connection import segment_codec_no_compression, segment_codec_lz4 +import pytest def to_bits(b): @@ -50,10 +51,8 @@ def _header_to_bits(data): def test_encode_uncompressed_header(self): buffer = BytesIO() segment_codec_no_compression.encode_header(buffer, len(self.small_msg), -1, True) - self.assertEqual(buffer.tell(), 6) - self.assertEqual( - self._header_to_bits(buffer.getvalue()), - "00000000000110010" + "1" + "000000") + assert buffer.tell() == 6 + assert self._header_to_bits(buffer.getvalue()) == "00000000000110010" + "1" + "000000" @unittest.skipUnless(segment_codec_lz4, ' lz4 not installed') def test_encode_compressed_header(self): @@ -61,45 +60,37 @@ def test_encode_compressed_header(self): compressed_length = len(segment_codec_lz4.compress(self.small_msg)) segment_codec_lz4.encode_header(buffer, compressed_length, len(self.small_msg), True) - self.assertEqual(buffer.tell(), 8) - self.assertEqual( - self._header_to_bits(buffer.getvalue()), - "{:017b}".format(compressed_length) + "00000000000110010" + "1" + "00000") + assert buffer.tell() == 8 + assert self._header_to_bits(buffer.getvalue()) == "{:017b}".format(compressed_length) + "00000000000110010" + "1" + "00000" def test_encode_uncompressed_header_with_max_payload(self): buffer = BytesIO() segment_codec_no_compression.encode_header(buffer, len(self.max_msg), -1, True) - self.assertEqual(buffer.tell(), 6) - self.assertEqual( - self._header_to_bits(buffer.getvalue()), - "11111111111111111" + "1" + "000000") + assert buffer.tell() == 6 + assert self._header_to_bits(buffer.getvalue()) == "11111111111111111" + "1" + "000000" def test_encode_header_fails_if_payload_too_big(self): buffer = BytesIO() for codec in [c for c in [segment_codec_no_compression, segment_codec_lz4] if c is not None]: - with self.assertRaises(DriverException): + with pytest.raises(DriverException): codec.encode_header(buffer, len(self.large_msg), -1, False) def test_encode_uncompressed_header_not_self_contained_msg(self): buffer = BytesIO() # simulate the first chunk with the max size segment_codec_no_compression.encode_header(buffer, len(self.max_msg), -1, False) - self.assertEqual(buffer.tell(), 6) - self.assertEqual( - self._header_to_bits(buffer.getvalue()), - ("11111111111111111" - "0" # not self contained - "000000")) + assert buffer.tell() == 6 + assert self._header_to_bits(buffer.getvalue()) == ("11111111111111111" + "0" # not self contained + "000000") @unittest.skipUnless(segment_codec_lz4, ' lz4 not installed') def test_encode_compressed_header_with_max_payload(self): buffer = BytesIO() compressed_length = len(segment_codec_lz4.compress(self.max_msg)) segment_codec_lz4.encode_header(buffer, compressed_length, len(self.max_msg), True) - self.assertEqual(buffer.tell(), 8) - self.assertEqual( - self._header_to_bits(buffer.getvalue()), - "{:017b}".format(compressed_length) + "11111111111111111" + "1" + "00000") + assert buffer.tell() == 8 + assert self._header_to_bits(buffer.getvalue()) == "{:017b}".format(compressed_length) + "11111111111111111" + "1" + "00000" @unittest.skipUnless(segment_codec_lz4, ' lz4 not installed') def test_encode_compressed_header_not_self_contained_msg(self): @@ -107,22 +98,20 @@ def test_encode_compressed_header_not_self_contained_msg(self): # simulate the first chunk with the max size compressed_length = len(segment_codec_lz4.compress(self.max_msg)) segment_codec_lz4.encode_header(buffer, compressed_length, len(self.max_msg), False) - self.assertEqual(buffer.tell(), 8) - self.assertEqual( - self._header_to_bits(buffer.getvalue()), - ("{:017b}".format(compressed_length) + - "11111111111111111" - "0" # not self contained - "00000")) + assert buffer.tell() == 8 + assert self._header_to_bits(buffer.getvalue()) == ("{:017b}".format(compressed_length) + + "11111111111111111" + "0" # not self contained + "00000") def test_decode_uncompressed_header(self): buffer = BytesIO() segment_codec_no_compression.encode_header(buffer, len(self.small_msg), -1, True) buffer.seek(0) header = segment_codec_no_compression.decode_header(buffer) - self.assertEqual(header.uncompressed_payload_length, -1) - self.assertEqual(header.payload_length, len(self.small_msg)) - self.assertEqual(header.is_self_contained, True) + assert header.uncompressed_payload_length == -1 + assert header.payload_length == len(self.small_msg) + assert header.is_self_contained == True @unittest.skipUnless(segment_codec_lz4, ' lz4 not installed') def test_decode_compressed_header(self): @@ -131,9 +120,9 @@ def test_decode_compressed_header(self): segment_codec_lz4.encode_header(buffer, compressed_length, len(self.small_msg), True) buffer.seek(0) header = segment_codec_lz4.decode_header(buffer) - self.assertEqual(header.uncompressed_payload_length, len(self.small_msg)) - self.assertEqual(header.payload_length, compressed_length) - self.assertEqual(header.is_self_contained, True) + assert header.uncompressed_payload_length == len(self.small_msg) + assert header.payload_length == compressed_length + assert header.is_self_contained == True def test_decode_header_fails_if_corrupted(self): buffer = BytesIO() @@ -143,7 +132,7 @@ def test_decode_header_fails_if_corrupted(self): buffer.write(b'0') buffer.seek(0) - with self.assertRaises(CrcException): + with pytest.raises(CrcException): segment_codec_no_compression.decode_header(buffer) def test_decode_uncompressed_self_contained_segment(self): @@ -154,10 +143,10 @@ def test_decode_uncompressed_self_contained_segment(self): header = segment_codec_no_compression.decode_header(buffer) segment = segment_codec_no_compression.decode(buffer, header) - self.assertEqual(header.is_self_contained, True) - self.assertEqual(header.uncompressed_payload_length, -1) - self.assertEqual(header.payload_length, len(self.small_msg)) - self.assertEqual(segment.payload, self.small_msg) + assert header.is_self_contained == True + assert header.uncompressed_payload_length == -1 + assert header.payload_length == len(self.small_msg) + assert segment.payload == self.small_msg @unittest.skipUnless(segment_codec_lz4, ' lz4 not installed') def test_decode_compressed_self_contained_segment(self): @@ -168,10 +157,10 @@ def test_decode_compressed_self_contained_segment(self): header = segment_codec_lz4.decode_header(buffer) segment = segment_codec_lz4.decode(buffer, header) - self.assertEqual(header.is_self_contained, True) - self.assertEqual(header.uncompressed_payload_length, len(self.small_msg)) - self.assertGreater(header.uncompressed_payload_length, header.payload_length) - self.assertEqual(segment.payload, self.small_msg) + assert header.is_self_contained == True + assert header.uncompressed_payload_length == len(self.small_msg) + assert header.uncompressed_payload_length > header.payload_length + assert segment.payload == self.small_msg def test_decode_multi_segments(self): buffer = BytesIO() @@ -186,9 +175,9 @@ def test_decode_multi_segments(self): headers.append(segment_codec_no_compression.decode_header(buffer)) segments.append(segment_codec_no_compression.decode(buffer, headers[1])) - self.assertTrue(all([h.is_self_contained is False for h in headers])) + assert all([h.is_self_contained is False for h in headers]) decoded_msg = segments[0].payload + segments[1].payload - self.assertEqual(decoded_msg, self.large_msg) + assert decoded_msg == self.large_msg @unittest.skipUnless(segment_codec_lz4, ' lz4 not installed') def test_decode_fails_if_corrupted(self): @@ -198,7 +187,7 @@ def test_decode_fails_if_corrupted(self): buffer.write(b'0') buffer.seek(0) header = segment_codec_lz4.decode_header(buffer) - with self.assertRaises(CrcException): + with pytest.raises(CrcException): segment_codec_lz4.decode(buffer, header) @unittest.skipUnless(segment_codec_lz4, ' lz4 not installed') @@ -208,6 +197,6 @@ def test_decode_tiny_msg_not_compressed(self): buffer.seek(0) header = segment_codec_lz4.decode_header(buffer) segment = segment_codec_lz4.decode(buffer, header) - self.assertEqual(header.uncompressed_payload_length, 0) - self.assertEqual(header.payload_length, 1) - self.assertEqual(segment.payload, b'b') + assert header.uncompressed_payload_length == 0 + assert header.payload_length == 1 + assert segment.payload == b'b' diff --git a/tests/unit/test_session_schema_agreement.py b/tests/unit/test_session_schema_agreement.py new file mode 100644 index 0000000000..ffad687fcc --- /dev/null +++ b/tests/unit/test_session_schema_agreement.py @@ -0,0 +1,204 @@ +from datetime import timedelta +from types import SimpleNamespace +from unittest.mock import Mock +import uuid + +import pytest + +import cassandra.cluster as cluster_module +from cassandra.connection import ConnectionBusy +from cassandra.cluster import ControlConnection, Session, ResultSet +from cassandra.policies import HostDistance, SimpleConvictionPolicy +from cassandra.pool import Host +from cassandra.util import maybe_add_timeout_to_query + + +class FakeTime: + def __init__(self): + self.clock = 0 + + def time(self): + return self.clock + + def sleep(self, amount): + self.clock += amount + + +class MockPool: + def __init__(self, host): + self.host = host + self.is_shutdown = False + + +class MockSchemaVersionFuture: + def __init__(self, outcome, auto_complete=True): + self._outcome = outcome + self._auto_complete = auto_complete + self._delivered = False + self._callback_state = None + self._col_names = ("schema_version",) + self._col_types = None + self.has_more_pages = False + self._continuous_paging_session = None + + def _deliver(self): + if self._delivered or self._callback_state is None: + return + + self._delivered = True + callback, errback, callback_args, callback_kwargs, errback_args, errback_kwargs = self._callback_state + if isinstance(self._outcome, Exception): + errback(self._outcome, *errback_args, **errback_kwargs) + else: + row = SimpleNamespace(schema_version=self._outcome) + callback([row], *callback_args, **callback_kwargs) + + def add_callbacks(self, callback, errback, + callback_args=(), callback_kwargs=None, + errback_args=(), errback_kwargs=None): + self._callback_state = ( + callback, + errback, + callback_args, + callback_kwargs or {}, + errback_args, + errback_kwargs or {}, + ) + if self._auto_complete: + self._deliver() + return self + + def complete(self): + self._deliver() + + def result(self): + if isinstance(self._outcome, Exception): + raise self._outcome + return ResultSet(self, [SimpleNamespace(schema_version=self._outcome)]) + + +def _host_query_count(session, target_host): + return sum(1 for call in session.execute_async.call_args_list if call.kwargs.get("host") is target_host) + + +def _new_session(schema_versions, distances=None, metadata_request_timeout=timedelta(seconds=2), timeout=2.0): + hosts = [] + connections = {} + distance_map = {} + + if distances is None: + distances = [HostDistance.LOCAL] * len(schema_versions) + + for index, schema_version in enumerate(schema_versions): + host = Host("127.0.0.%d" % (index + 1), SimpleConvictionPolicy, host_id=uuid.uuid4()) + host.set_up() + hosts.append(host) + distance_map[host] = distances[index] + + cluster = SimpleNamespace( + max_schema_agreement_wait=10, + control_connection=SimpleNamespace( + _timeout=timeout, + _metadata_request_timeout=metadata_request_timeout, + ), + ) + + session = Session.__new__(Session) + session.cluster = cluster + session._profile_manager = SimpleNamespace(distance=lambda host: distance_map.get(host, HostDistance.LOCAL)) + session._pools = {} + session.is_shutdown = False + + for host, schema_version in zip(hosts, schema_versions): + connection = Mock(endpoint=host.endpoint) + connection.future_outcomes = [schema_version] + session._pools[host] = MockPool(host) + connections[host] = connection + + def execute_async(query, parameters=None, trace=False, + custom_payload=None, execution_profile=None, + paging_state=None, timeout=None, host=None, execute_as=None): + connection = connections[host] + outcome = connection.future_outcomes.pop(0) if len(connection.future_outcomes) > 1 else connection.future_outcomes[0] + return MockSchemaVersionFuture(outcome) + + session.execute_async = Mock(side_effect=execute_async) + + return session, hosts, connections + + +def test_wait_for_schema_agreement_retries_with_module_time(monkeypatch): + session, hosts, connections = _new_session(["a", "b"]) + clock = FakeTime() + monkeypatch.setattr(cluster_module, "time", clock) + connections[hosts[1]].future_outcomes = ["b", "a"] + + assert session.wait_for_schema_agreement(wait_time=1) + assert clock.clock == pytest.approx(0.2) + for host in hosts: + assert _host_query_count(session, host) == 2 + + +@pytest.mark.parametrize("wait_time", [0, -1]) +def test_wait_for_schema_agreement_rejects_non_positive_wait_time(wait_time): + session, _, _ = _new_session(["a"]) + + with pytest.raises(ValueError, match="wait_time must be greater than 0"): + session.wait_for_schema_agreement(wait_time=wait_time) + + assert session.execute_async.call_count == 0 + + +def test_wait_for_schema_agreement_returns_false_when_no_hosts_match_scope(monkeypatch): + session, _, _ = _new_session(["a"], distances=[HostDistance.IGNORED]) + clock = FakeTime() + monkeypatch.setattr(cluster_module, "time", clock) + + assert session.wait_for_schema_agreement(wait_time=1) is False + assert session.execute_async.call_count == 0 + assert clock.clock == pytest.approx(1.0) + + +def test_wait_for_schema_agreement_uses_host_targeted_session_queries(): + session, hosts, _ = _new_session(["a", "a"]) + + assert session.wait_for_schema_agreement(wait_time=0.1) + + expected_query = maybe_add_timeout_to_query( + ControlConnection._SELECT_SCHEMA_LOCAL, + timedelta(seconds=2), + ) + assert session.execute_async.call_count == 2 + assert [call.args[0] for call in session.execute_async.call_args_list] == [expected_query, expected_query] + assert [call.kwargs["host"] for call in session.execute_async.call_args_list] == hosts + for call in session.execute_async.call_args_list: + assert 0 < call.kwargs["timeout"] <= 0.1 + + +def test_wait_for_schema_agreement_retries_after_host_targeted_query_error(monkeypatch): + session, hosts, connections = _new_session(["a", "a"]) + clock = FakeTime() + monkeypatch.setattr(cluster_module, "time", clock) + connections[hosts[1]].future_outcomes = [ConnectionBusy("connection overloaded"), "a"] + + assert session.wait_for_schema_agreement(wait_time=1) + assert clock.clock == pytest.approx(0.2) + for host in hosts: + assert _host_query_count(session, host) == 2 + + +def test_wait_for_schema_agreement_queries_hosts_in_order_under_one_deadline(monkeypatch): + session, hosts, _ = _new_session(["a", "a", "a"]) + clock = FakeTime() + monkeypatch.setattr(cluster_module, "time", clock) + + def execute_async(query, parameters=None, trace=False, + custom_payload=None, execution_profile=None, + paging_state=None, timeout=None, host=None, execute_as=None): + clock.sleep(0.01) + return MockSchemaVersionFuture("a") + + session.execute_async = Mock(side_effect=execute_async) + + assert session.wait_for_schema_agreement(wait_time=1) + assert [call.kwargs["host"] for call in session.execute_async.call_args_list] == hosts diff --git a/tests/unit/test_shard_aware.py b/tests/unit/test_shard_aware.py index fe7b95edba..4b4c2c138d 100644 --- a/tests/unit/test_shard_aware.py +++ b/tests/unit/test_shard_aware.py @@ -12,13 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -try: - import unittest2 as unittest -except ImportError: - import unittest # noqa +import unittest import logging -from mock import MagicMock +import time +from unittest.mock import MagicMock from concurrent.futures import ThreadPoolExecutor from cassandra.cluster import ShardAwareOptions @@ -30,6 +28,45 @@ LOGGER = logging.getLogger(__name__) +class MockSession(MagicMock): + is_shutdown = False + keyspace = "ks1" + + def __init__(self, is_ssl=False, *args, **kwargs): + super(MockSession, self).__init__(*args, **kwargs) + self.cluster = MagicMock() + if is_ssl: + self.cluster.ssl_options = {'some_ssl_options': True} + else: + self.cluster.ssl_options = None + self.cluster.shard_aware_options = ShardAwareOptions() + self.cluster.executor = ThreadPoolExecutor(max_workers=2) + self.cluster.signal_connection_failure = lambda *args, **kwargs: False + self.cluster.connection_factory = self.mock_connection_factory + self.connection_counter = 0 + self.futures = [] + + def submit(self, fn, *args, **kwargs): + logging.info("Scheduling %s with args: %s, kwargs: %s", fn, args, kwargs) + if not self.is_shutdown: + f = self.cluster.executor.submit(fn, *args, **kwargs) + self.futures += [f] + return f + + def mock_connection_factory(self, *args, **kwargs): + connection = MagicMock() + connection.is_shutdown = False + connection.is_defunct = False + connection.is_closed = False + connection.orphaned_threshold_reached = False + connection.endpoint = args[0] + sharding_info = ShardingInfo(shard_id=1, shards_count=4, partitioner="", sharding_algorithm="", sharding_ignore_msb=0, shard_aware_port=19042, shard_aware_port_ssl=19045) + connection.features = ProtocolFeatures(shard_id=kwargs.get('shard_id', self.connection_counter), sharding_info=sharding_info) + self.connection_counter += 1 + + return connection + + class TestShardAware(unittest.TestCase): def test_parsing_and_calculating_shard_id(self): """ @@ -46,70 +83,70 @@ class OptionsHolder(object): } shard_id, shard_info = ProtocolFeatures.parse_sharding_info(OptionsHolder().options) - self.assertEqual(shard_id, 1) - self.assertEqual(shard_info.shard_id_from_token(Murmur3Token.from_key(b"a").value), 4) - self.assertEqual(shard_info.shard_id_from_token(Murmur3Token.from_key(b"b").value), 6) - self.assertEqual(shard_info.shard_id_from_token(Murmur3Token.from_key(b"c").value), 6) - self.assertEqual(shard_info.shard_id_from_token(Murmur3Token.from_key(b"e").value), 4) - self.assertEqual(shard_info.shard_id_from_token(Murmur3Token.from_key(b"100000").value), 2) + assert shard_id == 1 + assert shard_info.shard_id_from_token(Murmur3Token.from_key(b"a").value) == 4 + assert shard_info.shard_id_from_token(Murmur3Token.from_key(b"b").value) == 6 + assert shard_info.shard_id_from_token(Murmur3Token.from_key(b"c").value) == 6 + assert shard_info.shard_id_from_token(Murmur3Token.from_key(b"e").value) == 4 + assert shard_info.shard_id_from_token(Murmur3Token.from_key(b"100000").value) == 2 def test_advanced_shard_aware_port(self): """ Test that on given a `shard_aware_port` on the OPTIONS message (ShardInfo class) the next connections would be open using this port """ - class MockSession(MagicMock): - is_shutdown = False - keyspace = "ks1" - - def __init__(self, is_ssl=False, *args, **kwargs): - super(MockSession, self).__init__(*args, **kwargs) - self.cluster = MagicMock() - if is_ssl: - self.cluster.ssl_options = {'some_ssl_options': True} - else: - self.cluster.ssl_options = None - self.cluster.shard_aware_options = ShardAwareOptions() - self.cluster.executor = ThreadPoolExecutor(max_workers=2) - self.cluster.signal_connection_failure = lambda *args, **kwargs: False - self.cluster.connection_factory = self.mock_connection_factory - self.connection_counter = 0 - self.futures = [] - - def submit(self, fn, *args, **kwargs): - logging.info("Scheduling %s with args: %s, kwargs: %s", fn, args, kwargs) - if not self.is_shutdown: - f = self.cluster.executor.submit(fn, *args, **kwargs) - self.futures += [f] - return f - - def mock_connection_factory(self, *args, **kwargs): - connection = MagicMock() - connection.is_shutdown = False - connection.is_defunct = False - connection.is_closed = False - connection.orphaned_threshold_reached = False - connection.endpoint = args[0] - sharding_info = ShardingInfo(shard_id=1, shards_count=4, partitioner="", sharding_algorithm="", sharding_ignore_msb=0, shard_aware_port=19042, shard_aware_port_ssl=19045) - connection.features = ProtocolFeatures(shard_id=kwargs.get('shard_id', self.connection_counter), sharding_info=sharding_info) - self.connection_counter += 1 - - return connection - host = MagicMock() host.endpoint = DefaultEndPoint("1.2.3.4") for port, is_ssl in [(19042, False), (19045, True)]: session = MockSession(is_ssl=is_ssl) pool = HostConnection(host=host, host_distance=HostDistance.REMOTE, session=session) - for f in session.futures: - f.result() - assert len(pool._connections) == 4 - for shard_id, connection in pool._connections.items(): - assert connection.features.shard_id == shard_id - if shard_id == 0: - assert connection.endpoint == DefaultEndPoint("1.2.3.4") - else: - assert connection.endpoint == DefaultEndPoint("1.2.3.4", port=port) - - session.cluster.executor.shutdown(wait=True) + try: + for f in session.futures: + f.result() + assert len(pool._connections) == 4 + for shard_id, connection in pool._connections.items(): + assert connection.features.shard_id == shard_id + if shard_id == 0: + assert connection.endpoint == DefaultEndPoint("1.2.3.4") + else: + assert connection.endpoint == DefaultEndPoint("1.2.3.4", port=port) + finally: + session.cluster.executor.shutdown(wait=True) + + def test_advanced_shard_aware_cooldown(self): + """ + `disable_advanced_shard_aware` must suppress the shard-aware endpoint for + the duration of the cool-down window, then automatically restore it once + the deadline has passed. The hard-disable flag must suppress the endpoint + unconditionally. + """ + host = MagicMock() + host.endpoint = DefaultEndPoint("1.2.3.4") + session = MockSession(is_ssl=False) + + pool = HostConnection(host=host, host_distance=HostDistance.REMOTE, session=session) + for f in session.futures: + f.result() + + try: + # Baseline: shard-aware port is returned. + endpoint = pool._get_shard_aware_endpoint() + assert endpoint is not None + assert endpoint.port == 19042 + + # During the cool-down window `_get_shard_aware_endpoint` must return None. + pool.disable_advanced_shard_aware(600) + assert pool._get_shard_aware_endpoint() is None + + # Once the deadline has passed, the shard-aware port must be used again. + pool.advanced_shardaware_block_until = time.time() - 1 + endpoint = pool._get_shard_aware_endpoint() + assert endpoint is not None + assert endpoint.port == 19042 + + # The hard-disable flag must suppress the endpoint regardless of the timer. + session.cluster.shard_aware_options.disable_shardaware_port = True + assert pool._get_shard_aware_endpoint() is None + finally: + session.cluster.executor.shutdown(wait=True) diff --git a/tests/unit/test_sortedset.py b/tests/unit/test_sortedset.py index 49c3658df8..071907d53e 100644 --- a/tests/unit/test_sortedset.py +++ b/tests/unit/test_sortedset.py @@ -13,10 +13,13 @@ # limitations under the License. import unittest +import pytest from cassandra.util import sortedset from cassandra.cqltypes import EMPTY +from tests.util import assertListEqual + from datetime import datetime from itertools import permutations @@ -25,11 +28,11 @@ def test_init(self): input = [5, 4, 3, 2, 1, 1, 1] expected = sorted(set(input)) ss = sortedset(input) - self.assertEqual(len(ss), len(expected)) - self.assertEqual(list(ss), expected) + assert len(ss) == len(expected) + assert list(ss) == expected def test_repr(self): - self.assertEqual(repr(sortedset([1, 2, 3, 4])), "SortedSet([1, 2, 3, 4])") + assert repr(sortedset([1, 2, 3, 4])) == "SortedSet([1, 2, 3, 4])" def test_contains(self): input = [5, 4, 3, 2, 1, 1, 1] @@ -37,24 +40,24 @@ def test_contains(self): ss = sortedset(input) for i in expected: - self.assertTrue(i in ss) - self.assertFalse(i not in ss) + assert i in ss + assert not i not in ss hi = max(expected)+1 lo = min(expected)-1 - self.assertFalse(hi in ss) - self.assertFalse(lo in ss) + assert not hi in ss + assert not lo in ss def test_mutable_contents(self): ba = bytearray(b'some data here') ss = sortedset([ba, ba]) - self.assertEqual(list(ss), [ba]) + assert list(ss) == [ba] def test_clear(self): ss = sortedset([1, 2, 3]) ss.clear() - self.assertEqual(len(ss), 0) + assert len(ss) == 0 def test_equal(self): s1 = set([1]) @@ -62,15 +65,15 @@ def test_equal(self): ss1 = sortedset(s1) ss12 = sortedset(s12) - self.assertEqual(ss1, s1) - self.assertEqual(ss12, s12) - self.assertEqual(ss12, s12) - self.assertEqual(ss1.__eq__(None), NotImplemented) - self.assertNotEqual(ss1, ss12) - self.assertNotEqual(ss12, ss1) - self.assertNotEqual(ss1, s12) - self.assertNotEqual(ss12, s1) - self.assertNotEqual(ss1, EMPTY) + assert ss1 == s1 + assert ss12 == s12 + assert ss12 == s12 + assert ss1.__eq__(None) == NotImplemented + assert ss1 != ss12 + assert ss12 != ss1 + assert ss1 != s12 + assert ss12 != s1 + assert ss1 != EMPTY def test_copy(self): class comparable(object): @@ -80,9 +83,9 @@ def __lt__(self, other): o = comparable() ss = sortedset([comparable(), o]) ss2 = ss.copy() - self.assertNotEqual(id(ss), id(ss2)) - self.assertTrue(o in ss) - self.assertTrue(o in ss2) + assert id(ss) != id(ss2) + assert o in ss + assert o in ss2 def test_isdisjoint(self): # set, ss @@ -92,25 +95,25 @@ def test_isdisjoint(self): ss13 = sortedset([1, 3]) ss3 = sortedset([3]) # s ss disjoint - self.assertTrue(s2.isdisjoint(ss1)) - self.assertTrue(s2.isdisjoint(ss13)) + assert s2.isdisjoint(ss1) + assert s2.isdisjoint(ss13) # s ss not disjoint - self.assertFalse(s12.isdisjoint(ss1)) - self.assertFalse(s12.isdisjoint(ss13)) + assert not s12.isdisjoint(ss1) + assert not s12.isdisjoint(ss13) # ss s disjoint - self.assertTrue(ss1.isdisjoint(s2)) - self.assertTrue(ss13.isdisjoint(s2)) + assert ss1.isdisjoint(s2) + assert ss13.isdisjoint(s2) # ss s not disjoint - self.assertFalse(ss1.isdisjoint(s12)) - self.assertFalse(ss13.isdisjoint(s12)) + assert not ss1.isdisjoint(s12) + assert not ss13.isdisjoint(s12) # ss ss disjoint - self.assertTrue(ss1.isdisjoint(ss3)) - self.assertTrue(ss3.isdisjoint(ss1)) + assert ss1.isdisjoint(ss3) + assert ss3.isdisjoint(ss1) # ss ss not disjoint - self.assertFalse(ss1.isdisjoint(ss13)) - self.assertFalse(ss13.isdisjoint(ss1)) - self.assertFalse(ss3.isdisjoint(ss13)) - self.assertFalse(ss13.isdisjoint(ss3)) + assert not ss1.isdisjoint(ss13) + assert not ss13.isdisjoint(ss1) + assert not ss3.isdisjoint(ss13) + assert not ss13.isdisjoint(ss3) def test_issubset(self): s12 = set([1, 2]) @@ -118,13 +121,13 @@ def test_issubset(self): ss13 = sortedset([1, 3]) ss3 = sortedset([3]) - self.assertTrue(ss1.issubset(s12)) - self.assertTrue(ss1.issubset(ss13)) + assert ss1.issubset(s12) + assert ss1.issubset(ss13) - self.assertFalse(ss1.issubset(ss3)) - self.assertFalse(ss13.issubset(ss3)) - self.assertFalse(ss13.issubset(ss1)) - self.assertFalse(ss13.issubset(s12)) + assert not ss1.issubset(ss3) + assert not ss13.issubset(ss3) + assert not ss13.issubset(ss1) + assert not ss13.issubset(s12) def test_issuperset(self): s12 = set([1, 2]) @@ -132,253 +135,253 @@ def test_issuperset(self): ss13 = sortedset([1, 3]) ss3 = sortedset([3]) - self.assertTrue(s12.issuperset(ss1)) - self.assertTrue(ss13.issuperset(ss3)) - self.assertTrue(ss13.issuperset(ss13)) + assert s12.issuperset(ss1) + assert ss13.issuperset(ss3) + assert ss13.issuperset(ss13) - self.assertFalse(s12.issuperset(ss13)) - self.assertFalse(ss1.issuperset(ss3)) - self.assertFalse(ss1.issuperset(ss13)) + assert not s12.issuperset(ss13) + assert not ss1.issuperset(ss3) + assert not ss1.issuperset(ss13) def test_union(self): s1 = set([1]) ss12 = sortedset([1, 2]) ss23 = sortedset([2, 3]) - self.assertEqual(sortedset().union(s1), sortedset([1])) - self.assertEqual(ss12.union(s1), sortedset([1, 2])) - self.assertEqual(ss12.union(ss23), sortedset([1, 2, 3])) - self.assertEqual(ss23.union(ss12), sortedset([1, 2, 3])) - self.assertEqual(ss23.union(s1), sortedset([1, 2, 3])) + assert sortedset().union(s1) == sortedset([1]) + assert ss12.union(s1) == sortedset([1, 2]) + assert ss12.union(ss23) == sortedset([1, 2, 3]) + assert ss23.union(ss12) == sortedset([1, 2, 3]) + assert ss23.union(s1) == sortedset([1, 2, 3]) def test_intersection(self): s12 = set([1, 2]) ss23 = sortedset([2, 3]) - self.assertEqual(s12.intersection(ss23), set([2])) - self.assertEqual(ss23.intersection(s12), sortedset([2])) - self.assertEqual(ss23.intersection(s12, [2], (2,)), sortedset([2])) - self.assertEqual(ss23.intersection(s12, [900], (2,)), sortedset()) + assert s12.intersection(ss23) == set([2]) + assert ss23.intersection(s12) == sortedset([2]) + assert ss23.intersection(s12, [2], (2,)) == sortedset([2]) + assert ss23.intersection(s12, [900], (2,)) == sortedset() def test_difference(self): s1 = set([1]) ss12 = sortedset([1, 2]) ss23 = sortedset([2, 3]) - self.assertEqual(sortedset().difference(s1), sortedset()) - self.assertEqual(ss12.difference(s1), sortedset([2])) - self.assertEqual(ss12.difference(ss23), sortedset([1])) - self.assertEqual(ss23.difference(ss12), sortedset([3])) - self.assertEqual(ss23.difference(s1), sortedset([2, 3])) + assert sortedset().difference(s1) == sortedset() + assert ss12.difference(s1) == sortedset([2]) + assert ss12.difference(ss23) == sortedset([1]) + assert ss23.difference(ss12) == sortedset([3]) + assert ss23.difference(s1) == sortedset([2, 3]) def test_symmetric_difference(self): s = set([1, 3, 5]) ss = sortedset([2, 3, 4]) ss2 = sortedset([5, 6, 7]) - self.assertEqual(ss.symmetric_difference(s), sortedset([1, 2, 4, 5])) - self.assertFalse(ss.symmetric_difference(ss)) - self.assertEqual(ss.symmetric_difference(s), sortedset([1, 2, 4, 5])) - self.assertEqual(ss2.symmetric_difference(ss), sortedset([2, 3, 4, 5, 6, 7])) + assert ss.symmetric_difference(s) == sortedset([1, 2, 4, 5]) + assert not ss.symmetric_difference(ss) + assert ss.symmetric_difference(s) == sortedset([1, 2, 4, 5]) + assert ss2.symmetric_difference(ss) == sortedset([2, 3, 4, 5, 6, 7]) def test_pop(self): ss = sortedset([2, 1]) - self.assertEqual(ss.pop(), 2) - self.assertEqual(ss.pop(), 1) - try: + assert ss.pop() == 2 + assert ss.pop() == 1 + with pytest.raises((KeyError, IndexError)): ss.pop() - self.fail("Error not thrown") - except (KeyError, IndexError) as e: - pass + def test_remove(self): ss = sortedset([2, 1]) - self.assertEqual(len(ss), 2) - self.assertRaises(KeyError, ss.remove, 3) - self.assertEqual(len(ss), 2) + assert len(ss) == 2 + with pytest.raises(KeyError): + ss.remove(3) + assert len(ss) == 2 ss.remove(1) - self.assertEqual(len(ss), 1) + assert len(ss) == 1 ss.remove(2) - self.assertFalse(ss) - self.assertRaises(KeyError, ss.remove, 2) - self.assertFalse(ss) + assert not ss + with pytest.raises(KeyError): + ss.remove(2) + assert not ss def test_getitem(self): ss = sortedset(range(3)) for i in range(len(ss)): - self.assertEqual(ss[i], i) - with self.assertRaises(IndexError): + assert ss[i] == i + with pytest.raises(IndexError): ss[len(ss)] def test_delitem(self): expected = [1,2,3,4] ss = sortedset(expected) for i in range(len(ss)): - self.assertListEqual(list(ss), expected[i:]) + assertListEqual(list(ss), expected[i:]) del ss[0] - with self.assertRaises(IndexError): + with pytest.raises(IndexError): ss[0] def test_delslice(self): expected = [1, 2, 3, 4, 5] ss = sortedset(expected) del ss[1:3] - self.assertListEqual(list(ss), [1, 4, 5]) + assertListEqual(list(ss), [1, 4, 5]) del ss[-1:] - self.assertListEqual(list(ss), [1, 4]) + assertListEqual(list(ss), [1, 4]) del ss[1:] - self.assertListEqual(list(ss), [1]) + assertListEqual(list(ss), [1]) del ss[:] - self.assertFalse(ss) - with self.assertRaises(IndexError): + assert not ss + with pytest.raises(IndexError): del ss[0] def test_reversed(self): expected = range(10) - self.assertListEqual(list(reversed(sortedset(expected))), list(reversed(expected))) + assertListEqual(list(reversed(sortedset(expected))), list(reversed(expected))) def test_operators(self): ss1 = sortedset([1]) ss12 = sortedset([1, 2]) # __ne__ - self.assertFalse(ss12 != ss12) - self.assertFalse(ss12 != sortedset([1, 2])) - self.assertTrue(ss12 != sortedset()) + assert not ss12 != ss12 + assert not ss12 != sortedset([1, 2]) + assert ss12 != sortedset() # __le__ - self.assertTrue(ss1 <= ss12) - self.assertTrue(ss12 <= ss12) - self.assertFalse(ss12 <= ss1) + assert ss1 <= ss12 + assert ss12 <= ss12 + assert not ss12 <= ss1 # __lt__ - self.assertTrue(ss1 < ss12) - self.assertFalse(ss12 < ss12) - self.assertFalse(ss12 < ss1) + assert ss1 < ss12 + assert not ss12 < ss12 + assert not ss12 < ss1 # __ge__ - self.assertFalse(ss1 >= ss12) - self.assertTrue(ss12 >= ss12) - self.assertTrue(ss12 >= ss1) + assert not ss1 >= ss12 + assert ss12 >= ss12 + assert ss12 >= ss1 # __gt__ - self.assertFalse(ss1 > ss12) - self.assertFalse(ss12 > ss12) - self.assertTrue(ss12 > ss1) + assert not ss1 > ss12 + assert not ss12 > ss12 + assert ss12 > ss1 # __and__ - self.assertEqual(ss1 & ss12, ss1) - self.assertEqual(ss12 & ss12, ss12) - self.assertEqual(ss12 & set(), sortedset()) + assert ss1 & ss12 == ss1 + assert ss12 & ss12 == ss12 + assert ss12 & set() == sortedset() # __iand__ tmp = sortedset(ss12) tmp &= ss1 - self.assertEqual(tmp, ss1) + assert tmp == ss1 tmp = sortedset(ss1) tmp &= ss12 - self.assertEqual(tmp, ss1) + assert tmp == ss1 tmp = sortedset(ss12) tmp &= ss12 - self.assertEqual(tmp, ss12) + assert tmp == ss12 tmp = sortedset(ss12) tmp &= set() - self.assertEqual(tmp, sortedset()) + assert tmp == sortedset() # __rand__ - self.assertEqual(set([1]) & ss12, ss1) + assert set([1]) & ss12 == ss1 # __or__ - self.assertEqual(ss1 | ss12, ss12) - self.assertEqual(ss12 | ss12, ss12) - self.assertEqual(ss12 | set(), ss12) - self.assertEqual(sortedset() | ss1 | ss12, ss12) + assert ss1 | ss12 == ss12 + assert ss12 | ss12 == ss12 + assert ss12 | set() == ss12 + assert sortedset() | ss1 | ss12 == ss12 # __ior__ tmp = sortedset(ss1) tmp |= ss12 - self.assertEqual(tmp, ss12) + assert tmp == ss12 tmp = sortedset(ss12) tmp |= ss12 - self.assertEqual(tmp, ss12) + assert tmp == ss12 tmp = sortedset(ss12) tmp |= set() - self.assertEqual(tmp, ss12) + assert tmp == ss12 tmp = sortedset() tmp |= ss1 tmp |= ss12 - self.assertEqual(tmp, ss12) + assert tmp == ss12 # __ror__ - self.assertEqual(set([1]) | ss12, ss12) + assert set([1]) | ss12 == ss12 # __sub__ - self.assertEqual(ss1 - ss12, set()) - self.assertEqual(ss12 - ss12, set()) - self.assertEqual(ss12 - set(), ss12) - self.assertEqual(ss12 - ss1, sortedset([2])) + assert ss1 - ss12 == set() + assert ss12 - ss12 == set() + assert ss12 - set() == ss12 + assert ss12 - ss1 == sortedset([2]) # __isub__ tmp = sortedset(ss1) tmp -= ss12 - self.assertEqual(tmp, set()) + assert tmp == set() tmp = sortedset(ss12) tmp -= ss12 - self.assertEqual(tmp, set()) + assert tmp == set() tmp = sortedset(ss12) tmp -= set() - self.assertEqual(tmp, ss12) + assert tmp == ss12 tmp = sortedset(ss12) tmp -= ss1 - self.assertEqual(tmp, sortedset([2])) + assert tmp == sortedset([2]) # __rsub__ - self.assertEqual(set((1,2,3)) - ss12, set((3,))) + assert set((1,2,3)) - ss12 == set((3,)) # __xor__ - self.assertEqual(ss1 ^ ss12, set([2])) - self.assertEqual(ss12 ^ ss1, set([2])) - self.assertEqual(ss12 ^ ss12, set()) - self.assertEqual(ss12 ^ set(), ss12) + assert ss1 ^ ss12 == set([2]) + assert ss12 ^ ss1 == set([2]) + assert ss12 ^ ss12 == set() + assert ss12 ^ set() == ss12 # __ixor__ tmp = sortedset(ss1) tmp ^= ss12 - self.assertEqual(tmp, set([2])) + assert tmp == set([2]) tmp = sortedset(ss12) tmp ^= ss1 - self.assertEqual(tmp, set([2])) + assert tmp == set([2]) tmp = sortedset(ss12) tmp ^= ss12 - self.assertEqual(tmp, set()) + assert tmp == set() tmp = sortedset(ss12) tmp ^= set() - self.assertEqual(tmp, ss12) + assert tmp == ss12 # __rxor__ - self.assertEqual(set([1, 2]) ^ ss1, (set([2]))) + assert set([1, 2]) ^ ss1 == (set([2])) def test_reduce_pickle(self): ss = sortedset((4,3,2,1)) import pickle s = pickle.dumps(ss) - self.assertEqual(pickle.loads(s), ss) + assert pickle.loads(s) == ss def _test_uncomparable_types(self, items): for perm in permutations(items): ss = sortedset(perm) s = set(perm) - self.assertEqual(s, ss) - self.assertEqual(ss, ss.union(s)) + assert s == ss + assert ss == ss.union(s) for x in range(len(ss)): subset = set(s) for _ in range(x): subset.pop() - self.assertEqual(ss.difference(subset), s.difference(subset)) - self.assertEqual(ss.intersection(subset), s.intersection(subset)) + assert ss.difference(subset) == s.difference(subset) + assert ss.intersection(subset) == s.intersection(subset) for x in ss: - self.assertIn(x, ss) + assert x in ss ss.remove(x) - self.assertNotIn(x, ss) + assert x not in ss def test_uncomparable_types_with_tuples(self): # PYTHON-1087 - make set handle uncomparable types diff --git a/tests/unit/test_tablets.py b/tests/unit/test_tablets.py index 3bbba06918..7a40e7de4d 100644 --- a/tests/unit/test_tablets.py +++ b/tests/unit/test_tablets.py @@ -4,11 +4,11 @@ class TabletsTest(unittest.TestCase): def compare_ranges(self, tablets, ranges): - self.assertEqual(len(tablets), len(ranges)) + assert len(tablets) == len(ranges) for idx, tablet in enumerate(tablets): - self.assertEqual(tablet.first_token, ranges[idx][0], "First token is not correct in tablet: {}".format(tablet)) - self.assertEqual(tablet.last_token, ranges[idx][1], "Last token is not correct in tablet: {}".format(tablet)) + assert tablet.first_token == ranges[idx][0], "First token is not correct in tablet: {}".format(tablet) + assert tablet.last_token == ranges[idx][1], "Last token is not correct in tablet: {}".format(tablet) def test_add_tablet_to_empty_tablets(self): tablets = Tablets({("test_ks", "test_tb"): []}) @@ -86,3 +86,41 @@ def test_add_tablet_intersecting_with_last(self): self.compare_ranges(tablets_list, [(-8611686018427387905, -7917529027641081857), (-5011686018427387905, -2987529027641081857)]) + + +class GetTabletForKeyTest(unittest.TestCase): + """Tests for Tablets.get_tablet_for_key.""" + + def test_found(self): + t1 = Tablet(0, 100, [("host1", 0)]) + t2 = Tablet(100, 200, [("host2", 0)]) + t3 = Tablet(200, 300, [("host3", 0)]) + tablets = Tablets({("ks", "tb"): [t1, t2, t3]}) + + class Token: + def __init__(self, v): + self.value = v + + result = tablets.get_tablet_for_key("ks", "tb", Token(150)) + self.assertIs(result, t2) + + def test_not_found_empty(self): + tablets = Tablets({}) + + class Token: + def __init__(self, v): + self.value = v + + self.assertIsNone(tablets.get_tablet_for_key("ks", "tb", Token(50))) + + def test_not_found_outside_range(self): + t1 = Tablet(100, 200, [("host1", 0)]) + tablets = Tablets({("ks", "tb"): [t1]}) + + class Token: + def __init__(self, v): + self.value = v + + # Token value 50 is not > first_token (100) of the tablet whose + # last_token (200) is >= 50, so no match. + self.assertIsNone(tablets.get_tablet_for_key("ks", "tb", Token(50))) diff --git a/tests/unit/test_time_util.py b/tests/unit/test_time_util.py index 2605992d1c..d87a3fe2ad 100644 --- a/tests/unit/test_time_util.py +++ b/tests/unit/test_time_util.py @@ -20,20 +20,34 @@ import datetime import time import uuid +import pytest class TimeUtilTest(unittest.TestCase): def test_datetime_from_timestamp(self): - self.assertEqual(util.datetime_from_timestamp(0), datetime.datetime(1970, 1, 1)) + assert util.datetime_from_timestamp(0) == datetime.datetime(1970, 1, 1) # large negative; test PYTHON-110 workaround for windows - self.assertEqual(util.datetime_from_timestamp(-62135596800), datetime.datetime(1, 1, 1)) - self.assertEqual(util.datetime_from_timestamp(-62135596199), datetime.datetime(1, 1, 1, 0, 10, 1)) + assert util.datetime_from_timestamp(-62135596800) == datetime.datetime(1, 1, 1) + assert util.datetime_from_timestamp(-62135596199) == datetime.datetime(1, 1, 1, 0, 10, 1) - self.assertEqual(util.datetime_from_timestamp(253402300799), datetime.datetime(9999, 12, 31, 23, 59, 59)) + assert util.datetime_from_timestamp(253402300799) == datetime.datetime(9999, 12, 31, 23, 59, 59) - self.assertEqual(util.datetime_from_timestamp(0.123456), datetime.datetime(1970, 1, 1, 0, 0, 0, 123456)) + assert util.datetime_from_timestamp(0.123456) == datetime.datetime(1970, 1, 1, 0, 0, 0, 123456) - self.assertEqual(util.datetime_from_timestamp(2177403010.123456), datetime.datetime(2038, 12, 31, 10, 10, 10, 123456)) + assert util.datetime_from_timestamp(2177403010.123456) == datetime.datetime(2038, 12, 31, 10, 10, 10, 123456) + + def test_datetime_from_ms_timestamp(self): + # epoch + assert util.datetime_from_ms_timestamp(0) == datetime.datetime(1970, 1, 1) + # positive with millisecond precision + assert util.datetime_from_ms_timestamp(1000) == datetime.datetime(1970, 1, 1, 0, 0, 1) + assert util.datetime_from_ms_timestamp(1454781157123) == datetime.datetime(2016, 2, 6, 17, 52, 37, 123000) + # large positive far from epoch (GH-532) - must not lose precision + assert util.datetime_from_ms_timestamp(10413792000001) == datetime.datetime(2300, 1, 1, 0, 0, 0, 1000) + # negative timestamp + assert util.datetime_from_ms_timestamp(-770172256000) == datetime.datetime(1945, 8, 5, 23, 15, 44) + # large negative with millisecond precision + assert util.datetime_from_ms_timestamp(-11676095999999) == datetime.datetime(1600, 1, 1, 0, 0, 0, 1000) def test_times_from_uuid1(self): node = uuid.getnode() @@ -41,11 +55,11 @@ def test_times_from_uuid1(self): u = uuid.uuid1(node, 0) t = util.unix_time_from_uuid1(u) - self.assertAlmostEqual(now, t, 2) + assert now == pytest.approx(t, abs=1e-2) dt = util.datetime_from_uuid1(u) t = calendar.timegm(dt.timetuple()) + dt.microsecond / 1e6 - self.assertAlmostEqual(now, t, 2) + assert now == pytest.approx(t, abs=1e-2) def test_uuid_from_time(self): t = time.time() @@ -54,42 +68,42 @@ def test_uuid_from_time(self): u = util.uuid_from_time(t, node, seq) # using AlmostEqual because time precision is different for # some platforms - self.assertAlmostEqual(util.unix_time_from_uuid1(u), t, 4) - self.assertEqual(u.node, node) - self.assertEqual(u.clock_seq, seq) + assert util.unix_time_from_uuid1(u) == pytest.approx(t, abs=1e-4) + assert u.node == node + assert u.clock_seq == seq # random node u1 = util.uuid_from_time(t, clock_seq=seq) u2 = util.uuid_from_time(t, clock_seq=seq) - self.assertAlmostEqual(util.unix_time_from_uuid1(u1), t, 4) - self.assertAlmostEqual(util.unix_time_from_uuid1(u2), t, 4) - self.assertEqual(u.clock_seq, seq) + assert util.unix_time_from_uuid1(u1) == pytest.approx(t, abs=1e-4) + assert util.unix_time_from_uuid1(u2) == pytest.approx(t, abs=1e-4) + assert u.clock_seq == seq # not impossible, but we shouldn't get the same value twice - self.assertNotEqual(u1.node, u2.node) + assert u1.node != u2.node # random seq u1 = util.uuid_from_time(t, node=node) u2 = util.uuid_from_time(t, node=node) - self.assertAlmostEqual(util.unix_time_from_uuid1(u1), t, 4) - self.assertAlmostEqual(util.unix_time_from_uuid1(u2), t, 4) - self.assertEqual(u.node, node) + assert util.unix_time_from_uuid1(u1) == pytest.approx(t, abs=1e-4) + assert util.unix_time_from_uuid1(u2) == pytest.approx(t, abs=1e-4) + assert u.node == node # not impossible, but we shouldn't get the same value twice - self.assertNotEqual(u1.clock_seq, u2.clock_seq) + assert u1.clock_seq != u2.clock_seq # node too large - with self.assertRaises(ValueError): + with pytest.raises(ValueError): u = util.uuid_from_time(t, node=2 ** 48) # clock_seq too large - with self.assertRaises(ValueError): + with pytest.raises(ValueError): u = util.uuid_from_time(t, clock_seq=0x4000) # construct from datetime dt = util.datetime_from_timestamp(t) u = util.uuid_from_time(dt, node, seq) - self.assertAlmostEqual(util.unix_time_from_uuid1(u), t, 4) - self.assertEqual(u.node, node) - self.assertEqual(u.clock_seq, seq) + assert util.unix_time_from_uuid1(u) == pytest.approx(t, abs=1e-4) + assert u.node == node + assert u.clock_seq == seq # 0 1 2 3 # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 @@ -106,7 +120,7 @@ def test_min_uuid(self): u = util.min_uuid_from_time(0) # cassandra does a signed comparison of the remaining bytes for i in range(8, 16): - self.assertEqual(marshal.int8_unpack(u.bytes[i:i + 1]), -128) + assert marshal.int8_unpack(u.bytes[i:i + 1]) == -128 def test_max_uuid(self): u = util.max_uuid_from_time(0) @@ -114,6 +128,6 @@ def test_max_uuid(self): # the first non-time byte has the variant in it # This byte is always negative, but should be the smallest negative # number with high-order bits '10' - self.assertEqual(marshal.int8_unpack(u.bytes[8:9]), -65) + assert marshal.int8_unpack(u.bytes[8:9]) == -65 for i in range(9, 16): - self.assertEqual(marshal.int8_unpack(u.bytes[i:i + 1]), 127) + assert marshal.int8_unpack(u.bytes[i:i + 1]) == 127 diff --git a/tests/unit/test_timestamps.py b/tests/unit/test_timestamps.py index 151c004c90..8ef747d515 100644 --- a/tests/unit/test_timestamps.py +++ b/tests/unit/test_timestamps.py @@ -16,7 +16,9 @@ from unittest import mock from cassandra import timestamps +from tests.util import assertRegex from threading import Thread, Lock +import pytest class _TimestampTestMixin(object): @@ -42,10 +44,10 @@ def _call_and_check_results(self, for expected in expected_timestamps: actual = tsg() if expected is not None: - self.assertEqual(actual, expected) + assert actual == expected # assert we patched timestamps.time.time correctly - with self.assertRaises(StopIteration): + with pytest.raises(StopIteration): tsg() @@ -102,9 +104,9 @@ def setUp(self): def assertLastCallArgRegex(self, call, pattern): last_warn_args, last_warn_kwargs = call - self.assertEqual(len(last_warn_args), 1) - self.assertEqual(len(last_warn_kwargs), 0) - self.assertRegex(last_warn_args[0], pattern) + assert len(last_warn_args) == 1 + assert len(last_warn_kwargs) == 0 + assertRegex(last_warn_args[0], pattern) def test_basic_log_content(self): """ @@ -124,10 +126,10 @@ def test_basic_log_content(self): tsg._last_warn = 12 tsg._next_timestamp(20, tsg.last) - self.assertEqual(len(self.patched_timestamp_log.warning.call_args_list), 0) + assert len(self.patched_timestamp_log.warning.call_args_list) == 0 tsg._next_timestamp(16, tsg.last) - self.assertEqual(len(self.patched_timestamp_log.warning.call_args_list), 1) + assert len(self.patched_timestamp_log.warning.call_args_list) == 1 self.assertLastCallArgRegex( self.patched_timestamp_log.warning.call_args, r'Clock skew detected:.*\b16\b.*\b4\b.*\b20\b' @@ -147,7 +149,7 @@ def test_disable_logging(self): no_warn_tsg.last = 100 no_warn_tsg._next_timestamp(99, no_warn_tsg.last) - self.assertEqual(len(self.patched_timestamp_log.warning.call_args_list), 0) + assert len(self.patched_timestamp_log.warning.call_args_list) == 0 def test_warning_threshold_respected_no_logging(self): """ @@ -164,7 +166,7 @@ def test_warning_threshold_respected_no_logging(self): ) tsg.last, tsg._last_warn = 100, 97 tsg._next_timestamp(98, tsg.last) - self.assertEqual(len(self.patched_timestamp_log.warning.call_args_list), 0) + assert len(self.patched_timestamp_log.warning.call_args_list) == 0 def test_warning_threshold_respected_logs(self): """ @@ -182,7 +184,7 @@ def test_warning_threshold_respected_logs(self): ) tsg.last, tsg._last_warn = 100, 97 tsg._next_timestamp(98, tsg.last) - self.assertEqual(len(self.patched_timestamp_log.warning.call_args_list), 1) + assert len(self.patched_timestamp_log.warning.call_args_list) == 1 def test_warning_interval_respected_no_logging(self): """ @@ -200,10 +202,10 @@ def test_warning_interval_respected_no_logging(self): ) tsg.last = 100 tsg._next_timestamp(70, tsg.last) - self.assertEqual(len(self.patched_timestamp_log.warning.call_args_list), 1) + assert len(self.patched_timestamp_log.warning.call_args_list) == 1 tsg._next_timestamp(71, tsg.last) - self.assertEqual(len(self.patched_timestamp_log.warning.call_args_list), 1) + assert len(self.patched_timestamp_log.warning.call_args_list) == 1 def test_warning_interval_respected_logs(self): """ @@ -222,10 +224,10 @@ def test_warning_interval_respected_logs(self): ) tsg.last = 100 tsg._next_timestamp(70, tsg.last) - self.assertEqual(len(self.patched_timestamp_log.warning.call_args_list), 1) + assert len(self.patched_timestamp_log.warning.call_args_list) == 1 tsg._next_timestamp(72, tsg.last) - self.assertEqual(len(self.patched_timestamp_log.warning.call_args_list), 2) + assert len(self.patched_timestamp_log.warning.call_args_list) == 2 class TestTimestampGeneratorMultipleThreads(unittest.TestCase): @@ -266,6 +268,6 @@ def request_time(): for t in threads: t.join() - self.assertEqual(len(generated_timestamps), num_threads * timestamp_to_generate) + assert len(generated_timestamps) == num_threads * timestamp_to_generate for i, timestamp in enumerate(sorted(generated_timestamps)): - self.assertEqual(int(i + 1e6), timestamp) + assert int(i + 1e6) == timestamp diff --git a/tests/unit/test_types.py b/tests/unit/test_types.py index aba11d4ced..11aab2748d 100644 --- a/tests/unit/test_types.py +++ b/tests/unit/test_types.py @@ -24,7 +24,7 @@ from cassandra.cqltypes import ( CassandraType, DateRangeType, DateType, DecimalType, EmptyValue, LongType, SetType, UTF8Type, - cql_typename, int8_pack, int64_pack, lookup_casstype, + cql_typename, int8_pack, int64_pack, int64_unpack, lookup_casstype, lookup_casstype_simple, parse_casstype_args, int32_pack, Int32Type, ListType, MapType, VectorType, FloatType @@ -45,6 +45,7 @@ datetime_from_timestamp ) from tests.unit.util import check_sequence_consistency +import pytest class TypeTests(unittest.TestCase): @@ -54,83 +55,84 @@ def test_lookup_casstype_simple(self): Ensure lookup_casstype_simple returns the correct classes """ - self.assertEqual(lookup_casstype_simple('AsciiType'), cassandra.cqltypes.AsciiType) - self.assertEqual(lookup_casstype_simple('LongType'), cassandra.cqltypes.LongType) - self.assertEqual(lookup_casstype_simple('BytesType'), cassandra.cqltypes.BytesType) - self.assertEqual(lookup_casstype_simple('BooleanType'), cassandra.cqltypes.BooleanType) - self.assertEqual(lookup_casstype_simple('CounterColumnType'), cassandra.cqltypes.CounterColumnType) - self.assertEqual(lookup_casstype_simple('DecimalType'), cassandra.cqltypes.DecimalType) - self.assertEqual(lookup_casstype_simple('DoubleType'), cassandra.cqltypes.DoubleType) - self.assertEqual(lookup_casstype_simple('FloatType'), cassandra.cqltypes.FloatType) - self.assertEqual(lookup_casstype_simple('InetAddressType'), cassandra.cqltypes.InetAddressType) - self.assertEqual(lookup_casstype_simple('Int32Type'), cassandra.cqltypes.Int32Type) - self.assertEqual(lookup_casstype_simple('UTF8Type'), cassandra.cqltypes.UTF8Type) - self.assertEqual(lookup_casstype_simple('DateType'), cassandra.cqltypes.DateType) - self.assertEqual(lookup_casstype_simple('SimpleDateType'), cassandra.cqltypes.SimpleDateType) - self.assertEqual(lookup_casstype_simple('ByteType'), cassandra.cqltypes.ByteType) - self.assertEqual(lookup_casstype_simple('ShortType'), cassandra.cqltypes.ShortType) - self.assertEqual(lookup_casstype_simple('TimeUUIDType'), cassandra.cqltypes.TimeUUIDType) - self.assertEqual(lookup_casstype_simple('TimeType'), cassandra.cqltypes.TimeType) - self.assertEqual(lookup_casstype_simple('UUIDType'), cassandra.cqltypes.UUIDType) - self.assertEqual(lookup_casstype_simple('IntegerType'), cassandra.cqltypes.IntegerType) - self.assertEqual(lookup_casstype_simple('MapType'), cassandra.cqltypes.MapType) - self.assertEqual(lookup_casstype_simple('ListType'), cassandra.cqltypes.ListType) - self.assertEqual(lookup_casstype_simple('SetType'), cassandra.cqltypes.SetType) - self.assertEqual(lookup_casstype_simple('CompositeType'), cassandra.cqltypes.CompositeType) - self.assertEqual(lookup_casstype_simple('ColumnToCollectionType'), cassandra.cqltypes.ColumnToCollectionType) - self.assertEqual(lookup_casstype_simple('ReversedType'), cassandra.cqltypes.ReversedType) - self.assertEqual(lookup_casstype_simple('DurationType'), cassandra.cqltypes.DurationType) - self.assertEqual(lookup_casstype_simple('DateRangeType'), cassandra.cqltypes.DateRangeType) - - self.assertEqual(str(lookup_casstype_simple('unknown')), str(cassandra.cqltypes.mkUnrecognizedType('unknown'))) + assert lookup_casstype_simple('AsciiType') == cassandra.cqltypes.AsciiType + assert lookup_casstype_simple('LongType') == cassandra.cqltypes.LongType + assert lookup_casstype_simple('BytesType') == cassandra.cqltypes.BytesType + assert lookup_casstype_simple('BooleanType') == cassandra.cqltypes.BooleanType + assert lookup_casstype_simple('CounterColumnType') == cassandra.cqltypes.CounterColumnType + assert lookup_casstype_simple('DecimalType') == cassandra.cqltypes.DecimalType + assert lookup_casstype_simple('DoubleType') == cassandra.cqltypes.DoubleType + assert lookup_casstype_simple('FloatType') == cassandra.cqltypes.FloatType + assert lookup_casstype_simple('InetAddressType') == cassandra.cqltypes.InetAddressType + assert lookup_casstype_simple('Int32Type') == cassandra.cqltypes.Int32Type + assert lookup_casstype_simple('UTF8Type') == cassandra.cqltypes.UTF8Type + assert lookup_casstype_simple('DateType') == cassandra.cqltypes.DateType + assert lookup_casstype_simple('SimpleDateType') == cassandra.cqltypes.SimpleDateType + assert lookup_casstype_simple('ByteType') == cassandra.cqltypes.ByteType + assert lookup_casstype_simple('ShortType') == cassandra.cqltypes.ShortType + assert lookup_casstype_simple('TimeUUIDType') == cassandra.cqltypes.TimeUUIDType + assert lookup_casstype_simple('TimeType') == cassandra.cqltypes.TimeType + assert lookup_casstype_simple('UUIDType') == cassandra.cqltypes.UUIDType + assert lookup_casstype_simple('IntegerType') == cassandra.cqltypes.IntegerType + assert lookup_casstype_simple('MapType') == cassandra.cqltypes.MapType + assert lookup_casstype_simple('ListType') == cassandra.cqltypes.ListType + assert lookup_casstype_simple('SetType') == cassandra.cqltypes.SetType + assert lookup_casstype_simple('CompositeType') == cassandra.cqltypes.CompositeType + assert lookup_casstype_simple('ColumnToCollectionType') == cassandra.cqltypes.ColumnToCollectionType + assert lookup_casstype_simple('ReversedType') == cassandra.cqltypes.ReversedType + assert lookup_casstype_simple('DurationType') == cassandra.cqltypes.DurationType + assert lookup_casstype_simple('DateRangeType') == cassandra.cqltypes.DateRangeType + + assert str(lookup_casstype_simple('unknown')) == str(cassandra.cqltypes.mkUnrecognizedType('unknown')) def test_lookup_casstype(self): """ Ensure lookup_casstype returns the correct classes """ - self.assertEqual(lookup_casstype('AsciiType'), cassandra.cqltypes.AsciiType) - self.assertEqual(lookup_casstype('LongType'), cassandra.cqltypes.LongType) - self.assertEqual(lookup_casstype('BytesType'), cassandra.cqltypes.BytesType) - self.assertEqual(lookup_casstype('BooleanType'), cassandra.cqltypes.BooleanType) - self.assertEqual(lookup_casstype('CounterColumnType'), cassandra.cqltypes.CounterColumnType) - self.assertEqual(lookup_casstype('DateType'), cassandra.cqltypes.DateType) - self.assertEqual(lookup_casstype('DecimalType'), cassandra.cqltypes.DecimalType) - self.assertEqual(lookup_casstype('DoubleType'), cassandra.cqltypes.DoubleType) - self.assertEqual(lookup_casstype('FloatType'), cassandra.cqltypes.FloatType) - self.assertEqual(lookup_casstype('InetAddressType'), cassandra.cqltypes.InetAddressType) - self.assertEqual(lookup_casstype('Int32Type'), cassandra.cqltypes.Int32Type) - self.assertEqual(lookup_casstype('UTF8Type'), cassandra.cqltypes.UTF8Type) - self.assertEqual(lookup_casstype('DateType'), cassandra.cqltypes.DateType) - self.assertEqual(lookup_casstype('TimeType'), cassandra.cqltypes.TimeType) - self.assertEqual(lookup_casstype('ByteType'), cassandra.cqltypes.ByteType) - self.assertEqual(lookup_casstype('ShortType'), cassandra.cqltypes.ShortType) - self.assertEqual(lookup_casstype('TimeUUIDType'), cassandra.cqltypes.TimeUUIDType) - self.assertEqual(lookup_casstype('UUIDType'), cassandra.cqltypes.UUIDType) - self.assertEqual(lookup_casstype('IntegerType'), cassandra.cqltypes.IntegerType) - self.assertEqual(lookup_casstype('MapType'), cassandra.cqltypes.MapType) - self.assertEqual(lookup_casstype('ListType'), cassandra.cqltypes.ListType) - self.assertEqual(lookup_casstype('SetType'), cassandra.cqltypes.SetType) - self.assertEqual(lookup_casstype('CompositeType'), cassandra.cqltypes.CompositeType) - self.assertEqual(lookup_casstype('ColumnToCollectionType'), cassandra.cqltypes.ColumnToCollectionType) - self.assertEqual(lookup_casstype('ReversedType'), cassandra.cqltypes.ReversedType) - self.assertEqual(lookup_casstype('DurationType'), cassandra.cqltypes.DurationType) - self.assertEqual(lookup_casstype('DateRangeType'), cassandra.cqltypes.DateRangeType) - - self.assertEqual(str(lookup_casstype('unknown')), str(cassandra.cqltypes.mkUnrecognizedType('unknown'))) - - self.assertRaises(ValueError, lookup_casstype, 'AsciiType~') + assert lookup_casstype('AsciiType') == cassandra.cqltypes.AsciiType + assert lookup_casstype('LongType') == cassandra.cqltypes.LongType + assert lookup_casstype('BytesType') == cassandra.cqltypes.BytesType + assert lookup_casstype('BooleanType') == cassandra.cqltypes.BooleanType + assert lookup_casstype('CounterColumnType') == cassandra.cqltypes.CounterColumnType + assert lookup_casstype('DateType') == cassandra.cqltypes.DateType + assert lookup_casstype('DecimalType') == cassandra.cqltypes.DecimalType + assert lookup_casstype('DoubleType') == cassandra.cqltypes.DoubleType + assert lookup_casstype('FloatType') == cassandra.cqltypes.FloatType + assert lookup_casstype('InetAddressType') == cassandra.cqltypes.InetAddressType + assert lookup_casstype('Int32Type') == cassandra.cqltypes.Int32Type + assert lookup_casstype('UTF8Type') == cassandra.cqltypes.UTF8Type + assert lookup_casstype('DateType') == cassandra.cqltypes.DateType + assert lookup_casstype('TimeType') == cassandra.cqltypes.TimeType + assert lookup_casstype('ByteType') == cassandra.cqltypes.ByteType + assert lookup_casstype('ShortType') == cassandra.cqltypes.ShortType + assert lookup_casstype('TimeUUIDType') == cassandra.cqltypes.TimeUUIDType + assert lookup_casstype('UUIDType') == cassandra.cqltypes.UUIDType + assert lookup_casstype('IntegerType') == cassandra.cqltypes.IntegerType + assert lookup_casstype('MapType') == cassandra.cqltypes.MapType + assert lookup_casstype('ListType') == cassandra.cqltypes.ListType + assert lookup_casstype('SetType') == cassandra.cqltypes.SetType + assert lookup_casstype('CompositeType') == cassandra.cqltypes.CompositeType + assert lookup_casstype('ColumnToCollectionType') == cassandra.cqltypes.ColumnToCollectionType + assert lookup_casstype('ReversedType') == cassandra.cqltypes.ReversedType + assert lookup_casstype('DurationType') == cassandra.cqltypes.DurationType + assert lookup_casstype('DateRangeType') == cassandra.cqltypes.DateRangeType + + assert str(lookup_casstype('unknown')) == str(cassandra.cqltypes.mkUnrecognizedType('unknown')) + + # With the fast-path for simple type names (no parens), malformed names + # like 'AsciiType~' create unrecognized types instead of raising ValueError + assert str(lookup_casstype('AsciiType~')) == str(cassandra.cqltypes.mkUnrecognizedType('AsciiType~')) def test_casstype_parameterized(self): - self.assertEqual(LongType.cass_parameterized_type_with(()), 'LongType') - self.assertEqual(LongType.cass_parameterized_type_with((), full=True), 'org.apache.cassandra.db.marshal.LongType') - self.assertEqual(SetType.cass_parameterized_type_with([DecimalType], full=True), 'org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.DecimalType)') + assert LongType.cass_parameterized_type_with(()) == 'LongType' + assert LongType.cass_parameterized_type_with((), full=True) == 'org.apache.cassandra.db.marshal.LongType' + assert SetType.cass_parameterized_type_with([DecimalType], full=True) == 'org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.DecimalType)' - self.assertEqual(LongType.cql_parameterized_type(), 'bigint') + assert LongType.cql_parameterized_type() == 'bigint' subtypes = (cassandra.cqltypes.UTF8Type, cassandra.cqltypes.UTF8Type) - self.assertEqual('map', - cassandra.cqltypes.MapType.apply_parameters(subtypes).cql_parameterized_type()) + assert 'map' == cassandra.cqltypes.MapType.apply_parameters(subtypes).cql_parameterized_type() def test_datetype_from_string(self): # Ensure all formats can be parsed, without exception @@ -143,18 +145,18 @@ def test_cql_typename(self): Smoke test cql_typename """ - self.assertEqual(cql_typename('DateType'), 'timestamp') - self.assertEqual(cql_typename('org.apache.cassandra.db.marshal.ListType(IntegerType)'), 'list') + assert cql_typename('DateType') == 'timestamp' + assert cql_typename('org.apache.cassandra.db.marshal.ListType(IntegerType)') == 'list' def test_named_tuple_colname_substitution(self): colnames = ("func(abc)", "[applied]", "func(func(abc))", "foo_bar", "foo_bar_") rows = [(1, 2, 3, 4, 5)] result = named_tuple_factory(colnames, rows)[0] - self.assertEqual(result[0], result.func_abc) - self.assertEqual(result[1], result.applied) - self.assertEqual(result[2], result.func_func_abc) - self.assertEqual(result[3], result.foo_bar) - self.assertEqual(result[4], result.foo_bar_) + assert result[0] == result.func_abc + assert result[1] == result.applied + assert result[2] == result.func_func_abc + assert result[3] == result.foo_bar + assert result[4] == result.foo_bar_ def test_parse_casstype_args(self): class FooType(CassandraType): @@ -178,36 +180,36 @@ class BarType(FooType): '7a6970:org.apache.cassandra.db.marshal.UTF8Type', ')'))) - self.assertEqual(FooType, ctype.__class__) + assert FooType == ctype.__class__ - self.assertEqual(UTF8Type, ctype.subtypes[0]) + assert UTF8Type == ctype.subtypes[0] # middle subtype should be a BarType instance with its own subtypes and names - self.assertIsInstance(ctype.subtypes[1], BarType) - self.assertEqual([UTF8Type], ctype.subtypes[1].subtypes) - self.assertEqual([b"address"], ctype.subtypes[1].names) + assert isinstance(ctype.subtypes[1], BarType) + assert [UTF8Type] == ctype.subtypes[1].subtypes + assert [b"address"] == ctype.subtypes[1].names - self.assertEqual(UTF8Type, ctype.subtypes[2]) - self.assertEqual([b'city', None, b'zip'], ctype.names) + assert UTF8Type == ctype.subtypes[2] + assert [b'city', None, b'zip'] == ctype.names def test_parse_casstype_vector(self): ctype = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 3)") - self.assertTrue(issubclass(ctype, VectorType)) - self.assertEqual(3, ctype.vector_size) - self.assertEqual(FloatType, ctype.subtype) + assert issubclass(ctype, VectorType) + assert 3 == ctype.vector_size + assert FloatType == ctype.subtype def test_parse_casstype_vector_of_vectors(self): inner_type = "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 4)" ctype = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(%s, 3)" % (inner_type)) - self.assertTrue(issubclass(ctype, VectorType)) - self.assertEqual(3, ctype.vector_size) + assert issubclass(ctype, VectorType) + assert 3 == ctype.vector_size sub_ctype = ctype.subtype - self.assertTrue(issubclass(sub_ctype, VectorType)) - self.assertEqual(4, sub_ctype.vector_size) - self.assertEqual(FloatType, sub_ctype.subtype) + assert issubclass(sub_ctype, VectorType) + assert 4 == sub_ctype.vector_size + assert FloatType == sub_ctype.subtype def test_empty_value(self): - self.assertEqual(str(EmptyValue()), 'EMPTY') + assert str(EmptyValue()) == 'EMPTY' def test_datetype(self): now_time_seconds = time.time() @@ -217,28 +219,43 @@ def test_datetype(self): now_timestamp = now_time_seconds * 1e3 # same results serialized - self.assertEqual(DateType.serialize(now_datetime, 0), DateType.serialize(now_timestamp, 0)) + assert DateType.serialize(now_datetime, 0) == DateType.serialize(now_timestamp, 0) # deserialize # epoc expected = 0 - self.assertEqual(DateType.deserialize(int64_pack(1000 * expected), 0), datetime.datetime.fromtimestamp(expected, tz=datetime.timezone.utc).replace(tzinfo=None)) + assert DateType.deserialize(int64_pack(1000 * expected), 0) == datetime.datetime.fromtimestamp(expected, tz=datetime.timezone.utc).replace(tzinfo=None) # beyond 32b expected = 2 ** 33 - self.assertEqual(DateType.deserialize(int64_pack(1000 * expected), 0), datetime.datetime(2242, 3, 16, 12, 56, 32, tzinfo=datetime.timezone.utc).replace(tzinfo=None)) + assert DateType.deserialize(int64_pack(1000 * expected), 0) == datetime.datetime(2242, 3, 16, 12, 56, 32, tzinfo=datetime.timezone.utc).replace(tzinfo=None) # less than epoc (PYTHON-119) expected = -770172256 - self.assertEqual(DateType.deserialize(int64_pack(1000 * expected), 0), datetime.datetime(1945, 8, 5, 23, 15, 44, tzinfo=datetime.timezone.utc).replace(tzinfo=None)) + assert DateType.deserialize(int64_pack(1000 * expected), 0) == datetime.datetime(1945, 8, 5, 23, 15, 44, tzinfo=datetime.timezone.utc).replace(tzinfo=None) # work around rounding difference among Python versions (PYTHON-230) expected = 1424817268.274 - self.assertEqual(DateType.deserialize(int64_pack(int(1000 * expected)), 0), datetime.datetime(2015, 2, 24, 22, 34, 28, 274000, tzinfo=datetime.timezone.utc).replace(tzinfo=None)) + assert DateType.deserialize(int64_pack(int(1000 * expected)), 0) == datetime.datetime(2015, 2, 24, 22, 34, 28, 274000, tzinfo=datetime.timezone.utc).replace(tzinfo=None) # Large date overflow (PYTHON-452) expected = 2177403010.123 - self.assertEqual(DateType.deserialize(int64_pack(int(1000 * expected)), 0), datetime.datetime(2038, 12, 31, 10, 10, 10, 123000, tzinfo=datetime.timezone.utc).replace(tzinfo=None)) + assert DateType.deserialize(int64_pack(int(1000 * expected)), 0) == datetime.datetime(2038, 12, 31, 10, 10, 10, 123000, tzinfo=datetime.timezone.utc).replace(tzinfo=None) + + # Large timestamp precision (GH-532) - timestamps far from epoch must + # not lose precision due to floating-point conversions. + # 2300-01-01 00:00:00.001 UTC + ts_ms = 10413792000001 + deserialized = DateType.deserialize(int64_pack(ts_ms), 0) + assert deserialized == datetime.datetime(2300, 1, 1, 0, 0, 0, 1000) + # Round-trip: serialize the deserialized datetime back to milliseconds + assert int64_unpack(DateType.serialize(deserialized, 0)) == ts_ms + + # Negative large timestamp: 1600-01-01 00:00:00.001 UTC + ts_ms_neg = -11676096000000 + 1 # -11676095999999 + deserialized_neg = DateType.deserialize(int64_pack(ts_ms_neg), 0) + assert deserialized_neg == datetime.datetime(1600, 1, 1, 0, 0, 0, 1000) + assert int64_unpack(DateType.serialize(deserialized_neg, 0)) == ts_ms_neg def test_collection_null_support(self): """ @@ -253,16 +270,10 @@ def test_collection_null_support(self): int32_pack(4) + # size of item2 int32_pack(42) # item2 ) - self.assertEqual( - [None, 42], - int_list.deserialize(value, 3) - ) + assert [None, 42] == int_list.deserialize(value, 3) set_list = SetType.apply_parameters([Int32Type]) - self.assertEqual( - {None, 42}, - set(set_list.deserialize(value, 3)) - ) + assert {None, 42} == set(set_list.deserialize(value, 3)) value = ( int32_pack(2) + # num items @@ -275,49 +286,47 @@ def test_collection_null_support(self): ) map_list = MapType.apply_parameters([Int32Type, Int32Type]) - self.assertEqual( - [(42, None), (None, 42)], - map_list.deserialize(value, 3)._items # OrderedMapSerializedKey - ) + + assert [(42, None), (None, 42)] == map_list.deserialize(value, 3)._items # OrderedMapSerializedKey def test_write_read_string(self): with tempfile.TemporaryFile() as f: value = u'test' write_string(f, value) f.seek(0) - self.assertEqual(read_string(f), value) + assert read_string(f) == value def test_write_read_longstring(self): with tempfile.TemporaryFile() as f: value = u'test' write_longstring(f, value) f.seek(0) - self.assertEqual(read_longstring(f), value) + assert read_longstring(f) == value def test_write_read_stringmap(self): with tempfile.TemporaryFile() as f: value = {'key': 'value'} write_stringmap(f, value) f.seek(0) - self.assertEqual(read_stringmap(f), value) + assert read_stringmap(f) == value def test_write_read_inet(self): with tempfile.TemporaryFile() as f: value = ('192.168.1.1', 9042) write_inet(f, value) f.seek(0) - self.assertEqual(read_inet(f), value) + assert read_inet(f) == value with tempfile.TemporaryFile() as f: value = ('2001:db8:0:f101::1', 9042) write_inet(f, value) f.seek(0) - self.assertEqual(read_inet(f), value) + assert read_inet(f) == value def test_cql_quote(self): - self.assertEqual(cql_quote(u'test'), "'test'") - self.assertEqual(cql_quote('test'), "'test'") - self.assertEqual(cql_quote(0), '0') + assert cql_quote(u'test') == "'test'" + assert cql_quote('test') == "'test'" + assert cql_quote(0) == '0' class VectorTests(unittest.TestCase): @@ -328,31 +337,31 @@ def _normalize_set(self, val): def _round_trip_compare_fn(self, first, second): if isinstance(first, float): - self.assertAlmostEqual(first, second, places=5) + assert first == pytest.approx(second, rel=1e-5) elif isinstance(first, list): - self.assertEqual(len(first), len(second)) + assert len(first) == len(second) for (felem, selem) in zip(first, second): self._round_trip_compare_fn(felem, selem) elif isinstance(first, set) or isinstance(first, frozenset): - self.assertEqual(len(first), len(second)) + assert len(first) == len(second) first_norm = self._normalize_set(first) second_norm = self._normalize_set(second) - self.assertEqual(first_norm, second_norm) + assert first_norm == second_norm elif isinstance(first, dict): for ((fk,fv), (sk,sv)) in zip(first.items(), second.items()): self._round_trip_compare_fn(fk, sk) self._round_trip_compare_fn(fv, sv) else: - self.assertEqual(first,second) + assert first == second def _round_trip_test(self, data, ctype_str): ctype = parse_casstype_args(ctype_str) data_bytes = ctype.serialize(data, 0) serialized_size = ctype.subtype.serial_size() if serialized_size: - self.assertEqual(serialized_size * len(data), len(data_bytes)) + assert serialized_size * len(data) == len(data_bytes) result = ctype.deserialize(data_bytes, 0) - self.assertEqual(len(data), len(result)) + assert len(data) == len(result) for idx in range(0,len(data)): self._round_trip_compare_fn(data[idx], result[idx]) @@ -460,60 +469,60 @@ def test_round_trip_vector_of_vectors(self): def test_cql_parameterized_type(self): # Base vector functionality ctype = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 4)") - self.assertEqual(ctype.cql_parameterized_type(), "org.apache.cassandra.db.marshal.VectorType") + assert ctype.cql_parameterized_type() == "org.apache.cassandra.db.marshal.VectorType" # Test vector-of-vectors inner_type = "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 4)" ctype = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(%s, 3)" % (inner_type)) inner_parsed_type = "org.apache.cassandra.db.marshal.VectorType" - self.assertEqual(ctype.cql_parameterized_type(), "org.apache.cassandra.db.marshal.VectorType<%s, 3>" % (inner_parsed_type)) + assert ctype.cql_parameterized_type() == "org.apache.cassandra.db.marshal.VectorType<%s, 3>" % (inner_parsed_type) def test_serialization_fixed_size_too_small(self): ctype = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 5)") - with self.assertRaisesRegex(ValueError, "Expected sequence of size 5 for vector of type float and dimension 5, observed sequence of length 4"): + with pytest.raises(ValueError, match="Expected sequence of size 5 for vector of type float and dimension 5, observed sequence of length 4"): ctype.serialize([1.2, 3.4, 5.6, 7.8], 0) def test_serialization_fixed_size_too_big(self): ctype = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 4)") - with self.assertRaisesRegex(ValueError, "Expected sequence of size 4 for vector of type float and dimension 4, observed sequence of length 5"): + with pytest.raises(ValueError, match="Expected sequence of size 4 for vector of type float and dimension 4, observed sequence of length 5"): ctype.serialize([1.2, 3.4, 5.6, 7.8, 9.10], 0) def test_serialization_variable_size_too_small(self): ctype = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.IntegerType, 5)") - with self.assertRaisesRegex(ValueError, "Expected sequence of size 5 for vector of type varint and dimension 5, observed sequence of length 4"): + with pytest.raises(ValueError, match="Expected sequence of size 5 for vector of type varint and dimension 5, observed sequence of length 4"): ctype.serialize([1, 2, 3, 4], 0) def test_serialization_variable_size_too_big(self): ctype = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.IntegerType, 4)") - with self.assertRaisesRegex(ValueError, "Expected sequence of size 4 for vector of type varint and dimension 4, observed sequence of length 5"): + with pytest.raises(ValueError, match="Expected sequence of size 4 for vector of type varint and dimension 4, observed sequence of length 5"): ctype.serialize([1, 2, 3, 4, 5], 0) def test_deserialization_fixed_size_too_small(self): ctype_four = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 4)") ctype_four_bytes = ctype_four.serialize([1.2, 3.4, 5.6, 7.8], 0) ctype_five = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 5)") - with self.assertRaisesRegex(ValueError, "Expected vector of type float and dimension 5 to have serialized size 20; observed serialized size of 16 instead"): + with pytest.raises(ValueError, match="Expected vector of type float and dimension 5 to have serialized size 20; observed serialized size of 16 instead"): ctype_five.deserialize(ctype_four_bytes, 0) def test_deserialization_fixed_size_too_big(self): ctype_five = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 5)") ctype_five_bytes = ctype_five.serialize([1.2, 3.4, 5.6, 7.8, 9.10], 0) ctype_four = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 4)") - with self.assertRaisesRegex(ValueError, "Expected vector of type float and dimension 4 to have serialized size 16; observed serialized size of 20 instead"): + with pytest.raises(ValueError, match="Expected vector of type float and dimension 4 to have serialized size 16; observed serialized size of 20 instead"): ctype_four.deserialize(ctype_five_bytes, 0) def test_deserialization_variable_size_too_small(self): ctype_four = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.IntegerType, 4)") ctype_four_bytes = ctype_four.serialize([1, 2, 3, 4], 0) ctype_five = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.IntegerType, 5)") - with self.assertRaisesRegex(ValueError, "Error reading additional data during vector deserialization after successfully adding 4 elements"): + with pytest.raises(ValueError, match="Error reading additional data during vector deserialization after successfully adding 4 elements"): ctype_five.deserialize(ctype_four_bytes, 0) def test_deserialization_variable_size_too_big(self): ctype_five = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.IntegerType, 5)") ctype_five_bytes = ctype_five.serialize([1, 2, 3, 4, 5], 0) ctype_four = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.IntegerType, 4)") - with self.assertRaisesRegex(ValueError, "Additional bytes remaining after vector deserialization completed"): + with pytest.raises(ValueError, match="Additional bytes remaining after vector deserialization completed"): ctype_four.deserialize(ctype_five_bytes, 0) @@ -553,7 +562,7 @@ def test_month_rounding_creation_failure(self): dr = DateRange(OPEN_BOUND, DateRangeBound(feb_stamp, DateRangePrecision.MONTH)) dt = datetime_from_timestamp(dr.upper_bound.milliseconds / 1000) - self.assertEqual(dt.day, 28) + assert dt.day == 28 # Leap year feb_stamp_leap_year = ms_timestamp_from_datetime( @@ -562,32 +571,29 @@ def test_month_rounding_creation_failure(self): dr = DateRange(OPEN_BOUND, DateRangeBound(feb_stamp_leap_year, DateRangePrecision.MONTH)) dt = datetime_from_timestamp(dr.upper_bound.milliseconds / 1000) - self.assertEqual(dt.day, 29) + assert dt.day == 29 def test_decode_precision(self): - self.assertEqual(DateRangeType._decode_precision(6), 'MILLISECOND') + assert DateRangeType._decode_precision(6) == 'MILLISECOND' def test_decode_precision_error(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): DateRangeType._decode_precision(-1) def test_encode_precision(self): - self.assertEqual(DateRangeType._encode_precision('SECOND'), 5) + assert DateRangeType._encode_precision('SECOND') == 5 def test_encode_precision_error(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): DateRangeType._encode_precision('INVALID') def test_deserialize_single_value(self): serialized = (int8_pack(0) + int64_pack(self.timestamp) + int8_pack(3)) - self.assertEqual( - DateRangeType.deserialize(serialized, 5), - util.DateRange(value=util.DateRangeBound( - value=datetime.datetime(2017, 2, 1, 15, 42, 12, 404000), - precision='HOUR') - ) + assert DateRangeType.deserialize(serialized, 5) == util.DateRange(value=util.DateRangeBound( + value=datetime.datetime(2017, 2, 1, 15, 42, 12, 404000), + precision='HOUR') ) def test_deserialize_closed_range(self): @@ -596,17 +602,14 @@ def test_deserialize_closed_range(self): int8_pack(2) + int64_pack(self.timestamp) + int8_pack(6)) - self.assertEqual( - DateRangeType.deserialize(serialized, 5), - util.DateRange( - lower_bound=util.DateRangeBound( - value=datetime.datetime(2017, 2, 1, 0, 0), - precision='DAY' - ), - upper_bound=util.DateRangeBound( - value=datetime.datetime(2017, 2, 1, 15, 42, 12, 404000), - precision='MILLISECOND' - ) + assert DateRangeType.deserialize(serialized, 5) == util.DateRange( + lower_bound=util.DateRangeBound( + value=datetime.datetime(2017, 2, 1, 0, 0), + precision='DAY' + ), + upper_bound=util.DateRangeBound( + value=datetime.datetime(2017, 2, 1, 15, 42, 12, 404000), + precision='MILLISECOND' ) ) @@ -615,15 +618,12 @@ def test_deserialize_open_high(self): int64_pack(self.timestamp) + int8_pack(3)) deserialized = DateRangeType.deserialize(serialized, 5) - self.assertEqual( - deserialized, - util.DateRange( - lower_bound=util.DateRangeBound( - value=datetime.datetime(2017, 2, 1, 15, 0), - precision='HOUR' - ), - upper_bound=util.OPEN_BOUND - ) + assert deserialized == util.DateRange( + lower_bound=util.DateRangeBound( + value=datetime.datetime(2017, 2, 1, 15, 0), + precision='HOUR' + ), + upper_bound=util.OPEN_BOUND ) def test_deserialize_open_low(self): @@ -631,35 +631,26 @@ def test_deserialize_open_low(self): int64_pack(self.timestamp) + int8_pack(4)) deserialized = DateRangeType.deserialize(serialized, 5) - self.assertEqual( - deserialized, - util.DateRange( - lower_bound=util.OPEN_BOUND, - upper_bound=util.DateRangeBound( - value=datetime.datetime(2017, 2, 1, 15, 42, 20, 1000), - precision='MINUTE' - ) + assert deserialized == util.DateRange( + lower_bound=util.OPEN_BOUND, + upper_bound=util.DateRangeBound( + value=datetime.datetime(2017, 2, 1, 15, 42, 20, 1000), + precision='MINUTE' ) ) def test_deserialize_single_open(self): - self.assertEqual( - util.DateRange(value=util.OPEN_BOUND), - DateRangeType.deserialize(int8_pack(5), 5) - ) + assert util.DateRange(value=util.OPEN_BOUND) == DateRangeType.deserialize(int8_pack(5), 5) def test_serialize_single_value(self): serialized = (int8_pack(0) + int64_pack(self.timestamp) + int8_pack(5)) deserialized = DateRangeType.deserialize(serialized, 5) - self.assertEqual( - deserialized, - util.DateRange( - value=util.DateRangeBound( - value=datetime.datetime(2017, 2, 1, 15, 42, 12), - precision='SECOND' - ) + assert deserialized == util.DateRange( + value=util.DateRangeBound( + value=datetime.datetime(2017, 2, 1, 15, 42, 12), + precision='SECOND' ) ) @@ -670,17 +661,14 @@ def test_serialize_closed_range(self): int64_pack(self.timestamp) + int8_pack(0)) deserialized = DateRangeType.deserialize(serialized, 5) - self.assertEqual( - deserialized, - util.DateRange( - lower_bound=util.DateRangeBound( - value=datetime.datetime(2017, 2, 1, 15, 42, 12), - precision='SECOND' - ), - upper_bound=util.DateRangeBound( - value=datetime.datetime(2017, 12, 31), - precision='YEAR' - ) + assert deserialized == util.DateRange( + lower_bound=util.DateRangeBound( + value=datetime.datetime(2017, 2, 1, 15, 42, 12), + precision='SECOND' + ), + upper_bound=util.DateRangeBound( + value=datetime.datetime(2017, 12, 31), + precision='YEAR' ) ) @@ -689,15 +677,12 @@ def test_serialize_open_high(self): int64_pack(self.timestamp) + int8_pack(2)) deserialized = DateRangeType.deserialize(serialized, 5) - self.assertEqual( - deserialized, - util.DateRange( - lower_bound=util.DateRangeBound( - value=datetime.datetime(2017, 2, 1), - precision='DAY' - ), - upper_bound=util.OPEN_BOUND - ) + assert deserialized == util.DateRange( + lower_bound=util.DateRangeBound( + value=datetime.datetime(2017, 2, 1), + precision='DAY' + ), + upper_bound=util.OPEN_BOUND ) def test_serialize_open_low(self): @@ -705,57 +690,50 @@ def test_serialize_open_low(self): int64_pack(self.timestamp) + int8_pack(3)) deserialized = DateRangeType.deserialize(serialized, 5) - self.assertEqual( - deserialized, - util.DateRange( - lower_bound=util.DateRangeBound( - value=datetime.datetime(2017, 2, 1, 15), - precision='HOUR' - ), - upper_bound=util.OPEN_BOUND - ) + assert deserialized == util.DateRange( + lower_bound=util.DateRangeBound( + value=datetime.datetime(2017, 2, 1, 15), + precision='HOUR' + ), + upper_bound=util.OPEN_BOUND ) def test_deserialize_both_open(self): serialized = (int8_pack(4)) deserialized = DateRangeType.deserialize(serialized, 5) - self.assertEqual( - deserialized, - util.DateRange( - lower_bound=util.OPEN_BOUND, - upper_bound=util.OPEN_BOUND - ) + assert deserialized == util.DateRange( + lower_bound=util.OPEN_BOUND, + upper_bound=util.OPEN_BOUND ) def test_serialize_single_open(self): serialized = DateRangeType.serialize(util.DateRange( value=util.OPEN_BOUND, ), 5) - self.assertEqual(int8_pack(5), serialized) + assert int8_pack(5) == serialized def test_serialize_both_open(self): serialized = DateRangeType.serialize(util.DateRange( lower_bound=util.OPEN_BOUND, upper_bound=util.OPEN_BOUND ), 5) - self.assertEqual(int8_pack(4), serialized) + assert int8_pack(4) == serialized def test_failure_to_serialize_no_value_object(self): - self.assertRaises(ValueError, DateRangeType.serialize, object(), 5) + with pytest.raises(ValueError): + DateRangeType.serialize(object(), 5) def test_failure_to_serialize_no_bounds_object(self): class no_bounds_object(object): value = lower_bound = None - self.assertRaises(ValueError, DateRangeType.serialize, no_bounds_object, 5) + with pytest.raises(ValueError): + DateRangeType.serialize(no_bounds_object, 5) def test_serialized_value_round_trip(self): vals = [b'\x01\x00\x00\x01%\xe9a\xf9\xd1\x06\x00\x00\x01v\xbb>o\xff\x00', b'\x01\x00\x00\x00\xdcm\x03-\xd1\x06\x00\x00\x01v\xbb>o\xff\x00'] for serialized in vals: - self.assertEqual( - serialized, - DateRangeType.serialize(DateRangeType.deserialize(serialized, 0), 0) - ) + assert serialized == DateRangeType.serialize(DateRangeType.deserialize(serialized, 0), 0) def test_serialize_zero_datetime(self): """ @@ -826,8 +804,8 @@ def test_deserialize_date_range_milliseconds(self): upper_value = self.starting_upper_value + i dr = DateRange(DateRangeBound(lower_value, DateRangePrecision.MILLISECOND), DateRangeBound(upper_value, DateRangePrecision.MILLISECOND)) - self.assertEqual(lower_value, dr.lower_bound.milliseconds) - self.assertEqual(upper_value, dr.upper_bound.milliseconds) + assert lower_value == dr.lower_bound.milliseconds + assert upper_value == dr.upper_bound.milliseconds def test_deserialize_date_range_seconds(self): """ @@ -852,9 +830,9 @@ def truncate_last_figures(number, n=3): dr = DateRange(DateRangeBound(lower_value, DateRangePrecision.SECOND), DateRangeBound(upper_value, DateRangePrecision.SECOND)) - self.assertEqual(truncate_last_figures(lower_value), dr.lower_bound.milliseconds) + assert truncate_last_figures(lower_value) == dr.lower_bound.milliseconds upper_value = truncate_last_figures(upper_value) + 999 - self.assertEqual(upper_value, dr.upper_bound.milliseconds) + assert upper_value == dr.upper_bound.milliseconds def test_deserialize_date_range_minutes(self): """ @@ -1021,9 +999,9 @@ def truncate_date(number): DateRangeBound(upper_value, precision)) # We verify that rounded value corresponds with what we would expect - self.assertEqual(truncate_date(lower_value), dr.lower_bound.milliseconds) + assert truncate_date(lower_value) == dr.lower_bound.milliseconds upper_value = round_up_truncated_upper_value(truncate_date(upper_value)) - self.assertEqual(upper_value, dr.upper_bound.milliseconds) + assert upper_value == dr.upper_bound.milliseconds class TestOrdering(unittest.TestCase): @@ -1040,14 +1018,14 @@ def test_host_order(self): @test_category data_types """ - hosts = [Host(addr, SimpleConvictionPolicy) for addr in + hosts = [Host(addr, SimpleConvictionPolicy, host_id=uuid.uuid4()) for addr in ("127.0.0.1", "127.0.0.2", "127.0.0.3", "127.0.0.4")] - hosts_equal = [Host(addr, SimpleConvictionPolicy) for addr in + hosts_equal = [Host(addr, SimpleConvictionPolicy, host_id=uuid.uuid4()) for addr in ("127.0.0.1", "127.0.0.1")] - hosts_equal_conviction = [Host("127.0.0.1", SimpleConvictionPolicy), Host("127.0.0.1", ConvictionPolicy)] - check_sequence_consistency(self, hosts) - check_sequence_consistency(self, hosts_equal, equal=True) - check_sequence_consistency(self, hosts_equal_conviction, equal=True) + hosts_equal_conviction = [Host("127.0.0.1", SimpleConvictionPolicy, host_id=uuid.uuid4()), Host("127.0.0.1", ConvictionPolicy, host_id=uuid.uuid4())] + check_sequence_consistency(hosts) + check_sequence_consistency(hosts_equal, equal=True) + check_sequence_consistency(hosts_equal_conviction, equal=True) def test_date_order(self): """ @@ -1061,8 +1039,8 @@ def test_date_order(self): """ dates_from_string = [Date("2017-01-01"), Date("2017-01-05"), Date("2017-01-09"), Date("2017-01-13")] dates_from_string_equal = [Date("2017-01-01"), Date("2017-01-01")] - check_sequence_consistency(self, dates_from_string) - check_sequence_consistency(self, dates_from_string_equal, equal=True) + check_sequence_consistency(dates_from_string) + check_sequence_consistency(dates_from_string_equal, equal=True) date_format = "%Y-%m-%d" @@ -1072,15 +1050,15 @@ def test_date_order(self): for dtstr in ("2017-01-02", "2017-01-06", "2017-01-10", "2017-01-14") ] dates_from_value_equal = [Date(1), Date(1)] - check_sequence_consistency(self, dates_from_value) - check_sequence_consistency(self, dates_from_value_equal, equal=True) + check_sequence_consistency(dates_from_value) + check_sequence_consistency(dates_from_value_equal, equal=True) dates_from_datetime = [Date(datetime.datetime.strptime(dtstr, date_format)) for dtstr in ("2017-01-03", "2017-01-07", "2017-01-11", "2017-01-15")] dates_from_datetime_equal = [Date(datetime.datetime.strptime("2017-01-01", date_format)), Date(datetime.datetime.strptime("2017-01-01", date_format))] - check_sequence_consistency(self, dates_from_datetime) - check_sequence_consistency(self, dates_from_datetime_equal, equal=True) + check_sequence_consistency(dates_from_datetime) + check_sequence_consistency(dates_from_datetime_equal, equal=True) dates_from_date = [ Date(datetime.datetime.strptime(dtstr, date_format).date()) for dtstr in @@ -1089,10 +1067,10 @@ def test_date_order(self): dates_from_date_equal = [datetime.datetime.strptime(dtstr, date_format) for dtstr in ("2017-01-09", "2017-01-9")] - check_sequence_consistency(self, dates_from_date) - check_sequence_consistency(self, dates_from_date_equal, equal=True) + check_sequence_consistency(dates_from_date) + check_sequence_consistency(dates_from_date_equal, equal=True) - check_sequence_consistency(self, self._shuffle_lists(dates_from_string, dates_from_value, + check_sequence_consistency(self._shuffle_lists(dates_from_string, dates_from_value, dates_from_datetime, dates_from_date)) def test_timer_order(self): @@ -1107,23 +1085,23 @@ def test_timer_order(self): """ time_from_int = [Time(1000), Time(4000), Time(7000), Time(10000)] time_from_int_equal = [Time(1), Time(1)] - check_sequence_consistency(self, time_from_int) - check_sequence_consistency(self, time_from_int_equal, equal=True) + check_sequence_consistency(time_from_int) + check_sequence_consistency(time_from_int_equal, equal=True) time_from_datetime = [Time(datetime.time(hour=0, minute=0, second=0, microsecond=us)) for us in (2, 5, 8, 11)] time_from_datetime_equal = [Time(datetime.time(hour=0, minute=0, second=0, microsecond=us)) for us in (1, 1)] - check_sequence_consistency(self, time_from_datetime) - check_sequence_consistency(self, time_from_datetime_equal, equal=True) + check_sequence_consistency(time_from_datetime) + check_sequence_consistency(time_from_datetime_equal, equal=True) time_from_string = [Time("00:00:00.000003000"), Time("00:00:00.000006000"), Time("00:00:00.000009000"), Time("00:00:00.000012000")] time_from_string_equal = [Time("00:00:00.000004000"), Time("00:00:00.000004000")] - check_sequence_consistency(self, time_from_string) - check_sequence_consistency(self, time_from_string_equal, equal=True) + check_sequence_consistency(time_from_string) + check_sequence_consistency(time_from_string_equal, equal=True) - check_sequence_consistency(self, self._shuffle_lists(time_from_int, time_from_datetime, time_from_string)) + check_sequence_consistency(self._shuffle_lists(time_from_int, time_from_datetime, time_from_string)) def test_token_order(self): """ @@ -1137,5 +1115,5 @@ def test_token_order(self): """ tokens = [Token(1), Token(2), Token(3), Token(4)] tokens_equal = [Token(1), Token(1)] - check_sequence_consistency(self, tokens) - check_sequence_consistency(self, tokens_equal, equal=True) + check_sequence_consistency(tokens) + check_sequence_consistency(tokens_equal, equal=True) diff --git a/tests/unit/test_util_types.py b/tests/unit/test_util_types.py index a2551ba20b..4a115affbc 100644 --- a/tests/unit/test_util_types.py +++ b/tests/unit/test_util_types.py @@ -16,6 +16,7 @@ import datetime from cassandra.util import Date, Time, Duration, Version, maybe_add_timeout_to_query +import pytest class DateTests(unittest.TestCase): @@ -23,57 +24,57 @@ class DateTests(unittest.TestCase): def test_from_datetime(self): expected_date = datetime.date(1492, 10, 12) d = Date(expected_date) - self.assertEqual(str(d), str(expected_date)) + assert str(d) == str(expected_date) def test_from_string(self): expected_date = datetime.date(1492, 10, 12) d = Date(expected_date) sd = Date('1492-10-12') - self.assertEqual(sd, d) + assert sd == d sd = Date('+1492-10-12') - self.assertEqual(sd, d) + assert sd == d def test_from_date(self): expected_date = datetime.date(1492, 10, 12) d = Date(expected_date) - self.assertEqual(d.date(), expected_date) + assert d.date() == expected_date def test_from_days(self): sd = Date(0) - self.assertEqual(sd, Date(datetime.date(1970, 1, 1))) + assert sd == Date(datetime.date(1970, 1, 1)) sd = Date(-1) - self.assertEqual(sd, Date(datetime.date(1969, 12, 31))) + assert sd == Date(datetime.date(1969, 12, 31)) sd = Date(1) - self.assertEqual(sd, Date(datetime.date(1970, 1, 2))) + assert sd == Date(datetime.date(1970, 1, 2)) def test_limits(self): min_builtin = Date(datetime.date(1, 1, 1)) max_builtin = Date(datetime.date(9999, 12, 31)) - self.assertEqual(Date(min_builtin.days_from_epoch), min_builtin) - self.assertEqual(Date(max_builtin.days_from_epoch), max_builtin) + assert Date(min_builtin.days_from_epoch) == min_builtin + assert Date(max_builtin.days_from_epoch) == max_builtin # just proving we can construct with on offset outside buildin range - self.assertEqual(Date(min_builtin.days_from_epoch - 1).days_from_epoch, - min_builtin.days_from_epoch - 1) - self.assertEqual(Date(max_builtin.days_from_epoch + 1).days_from_epoch, - max_builtin.days_from_epoch + 1) + assert Date(min_builtin.days_from_epoch - 1).days_from_epoch == min_builtin.days_from_epoch - 1 + assert Date(max_builtin.days_from_epoch + 1).days_from_epoch == max_builtin.days_from_epoch + 1 def test_invalid_init(self): - self.assertRaises(ValueError, Date, '-1999-10-10') - self.assertRaises(TypeError, Date, 1.234) + with pytest.raises(ValueError): + Date('-1999-10-10') + with pytest.raises(TypeError): + Date(1.234) def test_str(self): date_str = '2015-03-16' - self.assertEqual(str(Date(date_str)), date_str) + assert str(Date(date_str)) == date_str def test_out_of_range(self): - self.assertEqual(str(Date(2932897)), '2932897') - self.assertEqual(repr(Date(1)), 'Date(1)') + assert str(Date(2932897)) == '2932897' + assert repr(Date(1)) == 'Date(1)' def test_equals(self): - self.assertEqual(Date(1234), 1234) - self.assertEqual(Date(1), datetime.date(1970, 1, 2)) - self.assertFalse(Date(2932897) == datetime.date(9999, 12, 31)) # date can't represent year > 9999 - self.assertEqual(Date(2932897), 2932897) + assert Date(1234) == 1234 + assert Date(1) == datetime.date(1970, 1, 2) + assert not Date(2932897) == datetime.date(9999, 12, 31) # date can't represent year > 9999 + assert Date(2932897) == 2932897 class TimeTests(unittest.TestCase): @@ -86,31 +87,31 @@ def test_units_from_string(self): one_hour = 60 * one_minute tt = Time('00:00:00.000000001') - self.assertEqual(tt.nanosecond_time, 1) + assert tt.nanosecond_time == 1 tt = Time('00:00:00.000001') - self.assertEqual(tt.nanosecond_time, one_micro) + assert tt.nanosecond_time == one_micro tt = Time('00:00:00.001') - self.assertEqual(tt.nanosecond_time, one_milli) + assert tt.nanosecond_time == one_milli tt = Time('00:00:01') - self.assertEqual(tt.nanosecond_time, one_second) + assert tt.nanosecond_time == one_second tt = Time('00:01:00') - self.assertEqual(tt.nanosecond_time, one_minute) + assert tt.nanosecond_time == one_minute tt = Time('01:00:00') - self.assertEqual(tt.nanosecond_time, one_hour) + assert tt.nanosecond_time == one_hour tt = Time('01:00:00.') - self.assertEqual(tt.nanosecond_time, one_hour) + assert tt.nanosecond_time == one_hour tt = Time('23:59:59.123456') - self.assertEqual(tt.nanosecond_time, 23 * one_hour + 59 * one_minute + 59 * one_second + 123 * one_milli + 456 * one_micro) + assert tt.nanosecond_time == 23 * one_hour + 59 * one_minute + 59 * one_second + 123 * one_milli + 456 * one_micro tt = Time('23:59:59.1234567') - self.assertEqual(tt.nanosecond_time, 23 * one_hour + 59 * one_minute + 59 * one_second + 123 * one_milli + 456 * one_micro + 700) + assert tt.nanosecond_time == 23 * one_hour + 59 * one_minute + 59 * one_second + 123 * one_milli + 456 * one_micro + 700 tt = Time('23:59:59.12345678') - self.assertEqual(tt.nanosecond_time, 23 * one_hour + 59 * one_minute + 59 * one_second + 123 * one_milli + 456 * one_micro + 780) + assert tt.nanosecond_time == 23 * one_hour + 59 * one_minute + 59 * one_second + 123 * one_milli + 456 * one_micro + 780 tt = Time('23:59:59.123456789') - self.assertEqual(tt.nanosecond_time, 23 * one_hour + 59 * one_minute + 59 * one_second + 123 * one_milli + 456 * one_micro + 789) + assert tt.nanosecond_time == 23 * one_hour + 59 * one_minute + 59 * one_second + 123 * one_milli + 456 * one_micro + 789 def test_micro_precision(self): Time('23:59:59.1') @@ -121,32 +122,36 @@ def test_micro_precision(self): def test_from_int(self): tt = Time(12345678) - self.assertEqual(tt.nanosecond_time, 12345678) + assert tt.nanosecond_time == 12345678 def test_from_time(self): expected_time = datetime.time(12, 1, 2, 3) tt = Time(expected_time) - self.assertEqual(tt, expected_time) + assert tt == expected_time def test_as_time(self): expected_time = datetime.time(12, 1, 2, 3) tt = Time(expected_time) - self.assertEqual(tt.time(), expected_time) + assert tt.time() == expected_time def test_equals(self): # util.Time self equality - self.assertEqual(Time(1234), Time(1234)) + assert Time(1234) == Time(1234) def test_str_repr(self): time_str = '12:13:14.123456789' - self.assertEqual(str(Time(time_str)), time_str) - self.assertEqual(repr(Time(1)), 'Time(1)') + assert str(Time(time_str)) == time_str + assert repr(Time(1)) == 'Time(1)' def test_invalid_init(self): - self.assertRaises(ValueError, Time, '1999-10-10 11:11:11.1234') - self.assertRaises(TypeError, Time, 1.234) - self.assertRaises(ValueError, Time, 123456789000000) - self.assertRaises(TypeError, Time, datetime.datetime(2004, 12, 23, 11, 11, 1)) + with pytest.raises(ValueError): + Time('1999-10-10 11:11:11.1234') + with pytest.raises(TypeError): + Time(1.234) + with pytest.raises(ValueError): + Time(123456789000000) + with pytest.raises(TypeError): + Time(datetime.datetime(2004, 12, 23, 11, 11, 1)) class DurationTests(unittest.TestCase): @@ -154,53 +159,53 @@ class DurationTests(unittest.TestCase): def test_valid_format(self): valid = Duration(1, 1, 1) - self.assertEqual(valid.months, 1) - self.assertEqual(valid.days, 1) - self.assertEqual(valid.nanoseconds, 1) + assert valid.months == 1 + assert valid.days == 1 + assert valid.nanoseconds == 1 valid = Duration(nanoseconds=100000) - self.assertEqual(valid.months, 0) - self.assertEqual(valid.days, 0) - self.assertEqual(valid.nanoseconds, 100000) + assert valid.months == 0 + assert valid.days == 0 + assert valid.nanoseconds == 100000 valid = Duration() - self.assertEqual(valid.months, 0) - self.assertEqual(valid.days, 0) - self.assertEqual(valid.nanoseconds, 0) + assert valid.months == 0 + assert valid.days == 0 + assert valid.nanoseconds == 0 valid = Duration(-10, -21, -1000) - self.assertEqual(valid.months, -10) - self.assertEqual(valid.days, -21) - self.assertEqual(valid.nanoseconds, -1000) + assert valid.months == -10 + assert valid.days == -21 + assert valid.nanoseconds == -1000 def test_equality(self): first = Duration(1, 1, 1) second = Duration(-1, 1, 1) - self.assertNotEqual(first, second) + assert first != second first = Duration(1, 1, 1) second = Duration(1, 1, 1) - self.assertEqual(first, second) + assert first == second first = Duration() second = Duration(0, 0, 0) - self.assertEqual(first, second) + assert first == second first = Duration(1000, 10000, 2345345) second = Duration(1000, 10000, 2345345) - self.assertEqual(first, second) + assert first == second first = Duration(12, 0 , 100) second = Duration(nanoseconds=100, months=12) - self.assertEqual(first, second) + assert first == second def test_str(self): - self.assertEqual(str(Duration(1, 1, 1)), "1mo1d1ns") - self.assertEqual(str(Duration(1, 1, -1)), "-1mo1d1ns") - self.assertEqual(str(Duration(1, 1, 1000000000000000)), "1mo1d1000000000000000ns") - self.assertEqual(str(Duration(52, 23, 564564)), "52mo23d564564ns") + assert str(Duration(1, 1, 1)) == "1mo1d1ns" + assert str(Duration(1, 1, -1)) == "-1mo1d1ns" + assert str(Duration(1, 1, 1000000000000000)) == "1mo1d1000000000000000ns" + assert str(Duration(52, 23, 564564)) == "52mo23d564564ns" class VersionTests(unittest.TestCase): @@ -223,79 +228,73 @@ def test_version_parsing(self): for str_version, expected_result in versions: v = Version(str_version) - self.assertEqual(str_version, str(v)) - self.assertEqual(v.major, expected_result[0]) - self.assertEqual(v.minor, expected_result[1]) - self.assertEqual(v.patch, expected_result[2]) - self.assertEqual(v.build, expected_result[3]) - self.assertEqual(v.prerelease, expected_result[4]) + assert str_version == str(v) + assert v.major == expected_result[0] + assert v.minor == expected_result[1] + assert v.patch == expected_result[2] + assert v.build == expected_result[3] + assert v.prerelease == expected_result[4] # not supported version formats - with self.assertRaises(ValueError): + with pytest.raises(ValueError): Version('test.1.0') def test_version_compare(self): # just tests a bunch of versions # major wins - self.assertTrue(Version('3.3.0') > Version('2.5.0')) - self.assertTrue(Version('3.3.0') > Version('2.5.0.66')) - self.assertTrue(Version('3.3.0') > Version('2.5.21')) + assert Version('3.3.0') > Version('2.5.0') + assert Version('3.3.0') > Version('2.5.0.66') + assert Version('3.3.0') > Version('2.5.21') # minor wins - self.assertTrue(Version('2.3.0') > Version('2.2.0')) - self.assertTrue(Version('2.3.0') > Version('2.2.7')) - self.assertTrue(Version('2.3.0') > Version('2.2.7.9')) + assert Version('2.3.0') > Version('2.2.0') + assert Version('2.3.0') > Version('2.2.7') + assert Version('2.3.0') > Version('2.2.7.9') # patch wins - self.assertTrue(Version('2.3.1') > Version('2.3.0')) - self.assertTrue(Version('2.3.1') > Version('2.3.0.4post0')) - self.assertTrue(Version('2.3.1') > Version('2.3.0.44')) + assert Version('2.3.1') > Version('2.3.0') + assert Version('2.3.1') > Version('2.3.0.4post0') + assert Version('2.3.1') > Version('2.3.0.44') # various - self.assertTrue(Version('2.3.0.1') > Version('2.3.0.0')) - self.assertTrue(Version('2.3.0.680') > Version('2.3.0.670')) - self.assertTrue(Version('2.3.0.681') > Version('2.3.0.680')) - self.assertTrue(Version('2.3.0.1build0') > Version('2.3.0.1')) # 4th part fallback to str cmp - self.assertTrue(Version('2.3.0.build0') > Version('2.3.0.1')) # 4th part fallback to str cmp - self.assertTrue(Version('2.3.0') < Version('2.3.0.build')) - - self.assertTrue(Version('4-a') <= Version('4.0.0')) - self.assertTrue(Version('4-a') <= Version('4.0-alpha1')) - self.assertTrue(Version('4-a') <= Version('4.0-beta1')) - self.assertTrue(Version('4.0.0') >= Version('4.0.0')) - self.assertTrue(Version('4.0.0.421') >= Version('4.0.0')) - self.assertTrue(Version('4.0.1') >= Version('4.0.0')) - self.assertTrue(Version('2.3.0') == Version('2.3.0')) - self.assertTrue(Version('2.3.32') == Version('2.3.32')) - self.assertTrue(Version('2.3.32') == Version('2.3.32.0')) - self.assertTrue(Version('2.3.0.build') == Version('2.3.0.build')) - - self.assertTrue(Version('4') == Version('4.0.0')) - self.assertTrue(Version('4.0') == Version('4.0.0.0')) - self.assertTrue(Version('4.0') > Version('3.9.3')) - - self.assertTrue(Version('4.0') > Version('4.0-SNAPSHOT')) - self.assertTrue(Version('4.0-SNAPSHOT') == Version('4.0-SNAPSHOT')) - self.assertTrue(Version('4.0.0-SNAPSHOT') == Version('4.0-SNAPSHOT')) - self.assertTrue(Version('4.0.0-SNAPSHOT') == Version('4.0.0-SNAPSHOT')) - self.assertTrue(Version('4.0.0.build5-SNAPSHOT') == Version('4.0.0.build5-SNAPSHOT')) - self.assertTrue(Version('4.1-SNAPSHOT') > Version('4.0-SNAPSHOT')) - self.assertTrue(Version('4.0.1-SNAPSHOT') > Version('4.0.0-SNAPSHOT')) - self.assertTrue(Version('4.0.0.build6-SNAPSHOT') > Version('4.0.0.build5-SNAPSHOT')) - self.assertTrue(Version('4.0-SNAPSHOT2') > Version('4.0-SNAPSHOT1')) - self.assertTrue(Version('4.0-SNAPSHOT2') > Version('4.0.0-SNAPSHOT1')) - - self.assertTrue(Version('4.0.0-alpha1-SNAPSHOT') > Version('4.0.0-SNAPSHOT')) + assert Version('2.3.0.1') > Version('2.3.0.0') + assert Version('2.3.0.680') > Version('2.3.0.670') + assert Version('2.3.0.681') > Version('2.3.0.680') + assert Version('2.3.0.1build0') > Version('2.3.0.1') # 4th part fallback to str cmp + assert Version('2.3.0.build0') > Version('2.3.0.1') # 4th part fallback to str cmp + assert Version('2.3.0') < Version('2.3.0.build') + + assert Version('4-a') <= Version('4.0.0') + assert Version('4-a') <= Version('4.0-alpha1') + assert Version('4-a') <= Version('4.0-beta1') + assert Version('4.0.0') >= Version('4.0.0') + assert Version('4.0.0.421') >= Version('4.0.0') + assert Version('4.0.1') >= Version('4.0.0') + assert Version('2.3.0') == Version('2.3.0') + assert Version('2.3.32') == Version('2.3.32') + assert Version('2.3.32') == Version('2.3.32.0') + assert Version('2.3.0.build') == Version('2.3.0.build') + + assert Version('4') == Version('4.0.0') + assert Version('4.0') == Version('4.0.0.0') + assert Version('4.0') > Version('3.9.3') + + assert Version('4.0') > Version('4.0-SNAPSHOT') + assert Version('4.0-SNAPSHOT') == Version('4.0-SNAPSHOT') + assert Version('4.0.0-SNAPSHOT') == Version('4.0-SNAPSHOT') + assert Version('4.0.0-SNAPSHOT') == Version('4.0.0-SNAPSHOT') + assert Version('4.0.0.build5-SNAPSHOT') == Version('4.0.0.build5-SNAPSHOT') + assert Version('4.1-SNAPSHOT') > Version('4.0-SNAPSHOT') + assert Version('4.0.1-SNAPSHOT') > Version('4.0.0-SNAPSHOT') + assert Version('4.0.0.build6-SNAPSHOT') > Version('4.0.0.build5-SNAPSHOT') + assert Version('4.0-SNAPSHOT2') > Version('4.0-SNAPSHOT1') + assert Version('4.0-SNAPSHOT2') > Version('4.0.0-SNAPSHOT1') + + assert Version('4.0.0-alpha1-SNAPSHOT') > Version('4.0.0-SNAPSHOT') class FunctionTests(unittest.TestCase): def test_maybe_add_timeout_to_query(self): - self.assertEqual( - "SELECT * FROM HOSTS", - maybe_add_timeout_to_query("SELECT * FROM HOSTS", None) - ) - self.assertEqual( - "SELECT * FROM HOSTS USING TIMEOUT 1000ms", - maybe_add_timeout_to_query("SELECT * FROM HOSTS", datetime.timedelta(seconds=1)) - ) + assert "SELECT * FROM HOSTS" == maybe_add_timeout_to_query("SELECT * FROM HOSTS", None) + assert "SELECT * FROM HOSTS USING TIMEOUT 1000ms" == maybe_add_timeout_to_query("SELECT * FROM HOSTS", datetime.timedelta(seconds=1)) diff --git a/tests/unit/util.py b/tests/unit/util.py index e57fa6c3ee..042f07fb99 100644 --- a/tests/unit/util.py +++ b/tests/unit/util.py @@ -9,22 +9,29 @@ # 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. +from unittest.mock import NonCallableMagicMock -def check_sequence_consistency(unit_test, ordered_sequence, equal=False): +def check_sequence_consistency(ordered_sequence, equal=False): for i, el in enumerate(ordered_sequence): for previous in ordered_sequence[:i]: - _check_order_consistency(unit_test, previous, el, equal) + _check_order_consistency(previous, el, equal) for posterior in ordered_sequence[i + 1:]: - _check_order_consistency(unit_test, el, posterior, equal) + _check_order_consistency(el, posterior, equal) -def _check_order_consistency(unit_test, smaller, bigger, equal=False): - unit_test.assertLessEqual(smaller, bigger) - unit_test.assertGreaterEqual(bigger, smaller) +def _check_order_consistency(smaller, bigger, equal=False): + assert smaller <= bigger + assert bigger >= smaller if equal: - unit_test.assertEqual(smaller, bigger) + assert smaller == bigger else: - unit_test.assertNotEqual(smaller, bigger) - unit_test.assertLess(smaller, bigger) - unit_test.assertGreater(bigger, smaller) + assert smaller != bigger + assert smaller < bigger + assert bigger > smaller + + +class HashableMock(NonCallableMagicMock): + + def __hash__(self): + return id(self) \ No newline at end of file diff --git a/tests/util.py b/tests/util.py index 5c7ac2416f..2439e20fd5 100644 --- a/tests/util.py +++ b/tests/util.py @@ -11,9 +11,13 @@ # 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 time from functools import wraps +import re +import unittest +import difflib +import pytest def wait_until(condition, delay, max_attempts): @@ -72,3 +76,38 @@ def wrapper(*args, **kwargs): func(*args, **kwargs) return wrapper return decorator + +def assertRegex(text: str, pattern: str): + assert re.search(pattern, text) + +unittest_test_case = unittest.TestCase() + +def assertSequenceEqual(a, b, seq_type = None): + unittest_test_case.assertSequenceEqual(a, b, seq_type=seq_type) + +def assertDictEqual(a, b): + assertSequenceEqual(a, b, seq_type=dict) + +def assertListEqual(a, b): + assertSequenceEqual(a, b, seq_type=list) + +def assertSetEqual(a, b): + assertSequenceEqual(a, b, seq_type=set) + +def assertCountEqual(a, b): + unittest_test_case.assertCountEqual(a, b) + +def assertEqual(a, b): + assert a == b + +def assert_startswith_diff(text, prefix): + if not text.startswith(prefix): + prefix_lines = prefix.split('\n') + diff_string = '\n'.join(difflib.unified_diff(prefix_lines, + text.split('\n')[:len(prefix_lines)], + 'EXPECTED', 'RECEIVED', + lineterm='')) + pytest.fail(diff_string) + +def assertIsInstance(a, b): + assert isinstance(a, b)