Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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,_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
1 change: 1 addition & 0 deletions src/crate/client/_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
)

SRV_UNAVAILABLE_STATUSES = {502, 503, 504, 509}
MAX_REDIRECTS = 10
PRESERVE_ACTIVE_SERVER_EXCEPTIONS = {ConnectionResetError, BrokenPipeError}


Expand Down
65 changes: 65 additions & 0 deletions src/crate/client/_tls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# -*- 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 os
import ssl
import typing as t

from urllib3.util.ssl_ import create_urllib3_context


def resolve_ca_cert(ca_cert: t.Optional[str]) -> t.Optional[str]:
"""
Return the CA bundle to use, falling back to ``REQUESTS_CA_BUNDLE``.
"""
ca_cert = ca_cert or os.environ.get("REQUESTS_CA_BUNDLE", None)
if ca_cert and not os.path.exists(ca_cert):
# Sanity check
raise IOError('CA bundle file "{}" does not exist.'.format(ca_cert))
return ca_cert


def make_ssl_context(
verify_ssl_cert: bool = True,
ca_cert: t.Optional[str] = None,
cert_file: t.Optional[str] = None,
key_file: t.Optional[str] = None,
ssl_relax_minimum_version: bool = False,
) -> ssl.SSLContext:
"""
Build the `SSLContext` that urllib3 builds for the same options.
"""
context = create_urllib3_context(
cert_reqs=ssl.CERT_REQUIRED if verify_ssl_cert else ssl.CERT_NONE,
ssl_minimum_version=(
ssl.TLSVersion.MINIMUM_SUPPORTED
if ssl_relax_minimum_version
else None
),
)
ca_cert = resolve_ca_cert(ca_cert)
if ca_cert:
context.load_verify_locations(cafile=ca_cert)
else:
context.load_default_certs()
if cert_file:
context.load_cert_chain(cert_file, key_file)
return context
35 changes: 35 additions & 0 deletions src/crate/client/aio/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# -*- 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.

"""
Experimental asyncio client for CrateDB, over CrateDB's HTTP endpoint.

It is not public yet: the API may change without notice until it is
documented.
"""

try:
import aiohttp # noqa: F401
except ImportError as ex: # pragma: no cover
raise ImportError(
"crate.client.aio requires aiohttp. "
'Install it with: pip install "aiohttp>=3.12,<4"'
) from ex
7 changes: 2 additions & 5 deletions src/crate/client/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@


import logging
import os
import ssl
import threading
import typing as t
Expand Down Expand Up @@ -74,6 +73,7 @@
redirect_target,
should_preserve_server,
)
from crate.client._tls import resolve_ca_cert
from crate.client.exceptions import (
ConnectionError,
DigestNotFoundException,
Expand Down Expand Up @@ -194,10 +194,7 @@ def _pool_kw_args(
timeout=None,
pool_size=None,
):
ca_cert = ca_cert or os.environ.get("REQUESTS_CA_BUNDLE", None)
if ca_cert and not os.path.exists(ca_cert):
# Sanity check
raise IOError('CA bundle file "{}" does not exist.'.format(ca_cert))
ca_cert = resolve_ca_cert(ca_cert)

kw = {
"ca_certs": ca_cert,
Expand Down
Loading