Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
69d1a14
Refactor: Split Cursor into a shared CursorBase and the sync Cursor
bgunebakan Oct 19, 2026
6e33ea5
Refactor: Move CursorBase and the parameter helpers into _cursor_base.py
bgunebakan Oct 19, 2026
e4e0838
Refactor: Split Connection into a shared ConnectionBase and the sync …
bgunebakan Oct 19, 2026
0b2c47f
Refactor: Move ConnectionBase into _connection_base.py
bgunebakan Oct 19, 2026
4b2b863
Tests: Cover ConnectionBase._lowest_version directly
bgunebakan Oct 19, 2026
8bf3dbf
Add experimental asyncio client for CrateDB
bgunebakan Oct 22, 2026
e88495c
Merge branch 'main' into bilal/async-http-transport-for-crateclientaio
bgunebakan Oct 22, 2026
55f628c
Add workflow job to test CrateDB client without aiohttp dependency
bgunebakan Oct 22, 2026
65768a0
Add TLS utility functions for CA certificate resolution and integrate…
bgunebakan Oct 22, 2026
af9a98b
Add asynchronous HTTP client implementation
bgunebakan Oct 26, 2026
874be65
Add unit tests for CA certificate resolution and SSL context handling
bgunebakan Oct 26, 2026
b2032df
Add aiohttp support and fake CrateDB server for testing
bgunebakan Oct 26, 2026
709b802
Add comprehensive unit tests for asynchronous HTTP client functionality
bgunebakan Oct 26, 2026
7edbe9d
Remove deprecated tests
bgunebakan Oct 26, 2026
cecc420
Fix SSL pipeline failure
bgunebakan Oct 26, 2026
6a66d28
fix duplicated code
bgunebakan Oct 26, 2026
3ed7f72
Merge branch 'bilal/refactor-add-shared-base-classes-for-cursor-and-c…
bgunebakan Oct 27, 2026
3d6c483
Refactor ConnectionBase and Connection classes to support generic cur…
bgunebakan Oct 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,33 @@ jobs:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
fail_ci_if_error: true

without-aiohttp:
name: "Without aiohttp"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false

- name: Set up uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.14"
activate-environment: true
version: "latest"

- name: Setup env
run: uv pip install -e .

- name: Check that the async client is optional
run: |
python -c "import crate.client"
python - <<'PYTHON'
try:
import crate.client.aio
except ImportError as ex:
assert "requires aiohttp" in str(ex), ex
else:
raise AssertionError("crate.client.aio imported without aiohttp")
PYTHON
17 changes: 16 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,13 @@ dependencies = [

[dependency-groups]
dev = [
"aiohttp>=3.12,<4",
"certifi",
"coverage",
"mypy<2.4",
"pytest<10",
"pytest-asyncio<2",
"pytest-timeout<3",
"pytz",
"ruff<0.17",
]
Expand Down Expand Up @@ -102,9 +105,16 @@ install_types = true
namespace_packages = true
non_interactive = true

[[tool.mypy.overrides]]
module = ["crate.client.aio.*", "crate.client._tls"]
disallow_untyped_defs = true
disallow_incomplete_defs = true


[tool.pytest.ini_options]
addopts = "-rA --verbosity=3 --doctest-modules"
asyncio_mode = "strict"
asyncio_default_fixture_loop_scope = "function"
doctest_optionflags = "NORMALIZE_WHITESPACE ELLIPSIS"
minversion = "2.0"
testpaths = [
Expand Down Expand Up @@ -148,6 +158,11 @@ lint.select = [
"YTT",
]

lint.extend-select = [
# flake8-async: blocking calls in async functions
"ASYNC",
]

lint.extend-ignore = [
# Unnecessary variable assignment before `return` statement
"RET504",
Expand All @@ -174,7 +189,7 @@ lint.per-file-ignores."tests/*" = [
"S310", # False positive; it's a https url
"S311", # Standard pseudo-random generators are not suitable for cryptographic purposes
]
lint.per-file-ignores."src/crate/client/{connection.py,http.py,_pool.py,_response.py}" = [
lint.per-file-ignores."src/crate/client/{connection.py,http.py,_connection_base.py,_pool.py,_response.py,aio/__init__.py,aio/_http.py}" = [
"A004", # Import `ConnectionError` is shadowing a Python builtin
"A005", # Import `ConnectionError` is shadowing a Python builtin
]
Expand Down
109 changes: 109 additions & 0 deletions src/crate/client/_connection_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# -*- coding: utf-8; -*-
#
# Licensed to CRATE Technology GmbH ("Crate") under one or more contributor
# license agreements. See the NOTICE file distributed with this work for
# additional information regarding copyright ownership. Crate licenses
# this file to you under the Apache License, Version 2.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.
#
# However, if you have executed another commercial license agreement
# with Crate these terms will supersede the license and you may use the
# software solely pursuant to the terms of the relevant commercial agreement.
import typing as t

from verlib2 import Version

from ._cursor_base import CursorBase
from .exceptions import ConnectionError, ProgrammingError

CursorT = t.TypeVar("CursorT", bound=CursorBase)


class ConnectionBase(t.Generic[CursorT]):
"""
Connection state shared by the sync and async connections.

Public methods that do IO, or that are coroutines on the async
connection, live on the subclasses.
"""

client: t.Any

@property
def _cursor_class(self) -> t.Type[CursorT]:
"""The class `cursor()` creates."""
raise NotImplementedError

@property
def _blob_container_class(self) -> t.Type[t.Any]:
"""The class `get_blob_container()` creates."""
raise NotImplementedError

def __init__(self, converter=None, time_zone=None):
self._converter = converter
self.time_zone = time_zone
self._closed = False

def cursor(self, **kwargs) -> CursorT:
"""
Return a new Cursor Object using the connection.
"""
converter = kwargs.pop("converter", self._converter)
time_zone = kwargs.pop("time_zone", self.time_zone)
self._check_open()
return self._cursor_class(
connection=self,
converter=converter,
time_zone=time_zone,
)

def get_blob_container(self, container_name):
"""Retrieve a BlobContainer for `container_name`

:param container_name: the name of the BLOB container.
:returns: a :class:ContainerObject
"""
return self._blob_container_class(container_name, self)

def _check_open(self):
if self._closed:
raise ProgrammingError("Connection closed")

@staticmethod
def _lowest_version(
versions: t.Iterable[t.Union[str, BaseException]],
) -> Version:
"""
Return the lowest of the versions reported by the servers.
"""
lowest = None
connection_errors = []
for version in versions:
if isinstance(version, ConnectionError):
connection_errors.append(version)
continue
if isinstance(version, ValueError):
continue
if isinstance(version, BaseException):
raise version
try:
parsed = Version(version)
except ValueError:
continue
if not lowest or parsed < lowest:
lowest = parsed
if connection_errors and not lowest:
raise ConnectionError("; ".join(str(e) for e in connection_errors))
return lowest or Version("0.0.0")

def __repr__(self):
return f"<{self.__class__.__qualname__} {self.client!r}>"
Loading
Loading