Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
876ecc8
Update pyproject.toml
surister Oct 14, 2025
3651e56
Add deps and metadata
surister Oct 14, 2025
45d8140
Add proper error formating to `BlobException`
surister Oct 14, 2025
15d53ec
Migrate test_cursor to pytest format.
surister Oct 14, 2025
c8dfa3a
Migrate several http tests
surister Oct 21, 2025
d854bbf
Rename `REQUEST` to `REQUEST_PATH` and move it to conftest.py with `f…
surister Oct 23, 2025
6c0c8b6
Migrate more tests
surister Oct 23, 2025
aa7794f
fix typo
surister Oct 23, 2025
8f46388
Clean imports
surister Oct 23, 2025
3bc6cff
Add specific tests for serialization
surister Oct 23, 2025
8cc192d
Add `temp_env` util function to temporarily set env variables with a …
surister Oct 23, 2025
4a2fbf8
Migrate client ca tests
surister Oct 23, 2025
b8a79fc
Migrate more tests
surister Oct 23, 2025
bf2903a
Migrate test_connection
surister Oct 23, 2025
6db454d
refactor fail_sometimes
surister Oct 24, 2025
010fcfa
Remove temp_env util function
surister Oct 24, 2025
08e9401
Migrate test_http and fix some test_cursor
surister Oct 24, 2025
1548b03
Fix linting
surister Oct 24, 2025
1cc2986
Fix CI
surister Oct 24, 2025
39ad8c9
Fix CI
surister Oct 24, 2025
0516259
Fix tests
surister Oct 24, 2025
65255bc
Renew default __repr__ implementation
surister Oct 27, 2025
10bfaec
Add more tests
surister Oct 27, 2025
7427104
Add more tests for cursors
surister Nov 11, 2025
7a914be
Add test super_len
surister Dec 3, 2025
62d57d5
Add more tests
surister Dec 9, 2025
dea1e6e
Update pyproject.toml
surister Oct 14, 2025
82831cf
Add deps and metadata
surister Oct 14, 2025
13cba1d
Add proper error formating to `BlobException`
surister Oct 14, 2025
e516df5
Migrate test_cursor to pytest format.
surister Oct 14, 2025
6fdf2f3
Migrate several http tests
surister Oct 21, 2025
05828dd
Rename `REQUEST` to `REQUEST_PATH` and move it to conftest.py with `f…
surister Oct 23, 2025
84cc663
Migrate more tests
surister Oct 23, 2025
3586e37
fix typo
surister Oct 23, 2025
5c89231
Clean imports
surister Oct 23, 2025
fcaae2b
Add specific tests for serialization
surister Oct 23, 2025
e23e95c
Add `temp_env` util function to temporarily set env variables with a …
surister Oct 23, 2025
cf69bc8
Migrate client ca tests
surister Oct 23, 2025
4f0912e
Migrate more tests
surister Oct 23, 2025
49b4a83
Migrate test_connection
surister Oct 23, 2025
9b50cd2
refactor fail_sometimes
surister Oct 24, 2025
596fdbc
Remove temp_env util function
surister Oct 24, 2025
82dfa7c
Migrate test_http and fix some test_cursor
surister Oct 24, 2025
8bd3de3
Fix linting
surister Oct 24, 2025
41a2bcc
Fix CI
surister Oct 24, 2025
fd2aaaa
Fix CI
surister Oct 24, 2025
5dba2b6
Fix tests
surister Oct 24, 2025
a96903e
Renew default __repr__ implementation
surister Oct 27, 2025
2556826
Add more tests
surister Oct 27, 2025
8562976
Add more tests for cursors
surister Nov 11, 2025
c704280
Add test super_len
surister Dec 3, 2025
c4da897
Add more tests
surister Dec 9, 2025
abdf3db
Merge remote-tracking branch 'origin/feat/modernize-project' into fea…
surister Dec 9, 2025
c4371fc
Add test for update pool kwags
surister Dec 9, 2025
980bbc5
Add test for cursor description
surister Dec 9, 2025
5096b3e
Add test for cursor rowcount
surister Dec 9, 2025
a4a6b37
fix format
surister Dec 9, 2025
c4832b5
Add tests for blobs
surister Dec 9, 2025
833f36d
add changelog entry
surister Dec 9, 2025
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
24 changes: 12 additions & 12 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,32 +49,32 @@ jobs:
- name: Set up uv
uses: astral-sh/setup-uv@v6
with:
cache-dependency-glob: |
setup.py
cache-suffix: ${{ matrix.python-version }}
enable-cache: true
version: "latest"

- name: Setup env
run: uv sync
- name: Invoke tests
run: |

# Propagate build matrix information.
./devtools/setup_ci.sh

# Bootstrap environment.
source bootstrap.sh


# Run linter.
uv run ruff check .

# Run type testing
uv run mypy

# Report about the test matrix slot.
echo "Invoking tests with CrateDB ${CRATEDB_VERSION}"

# Run linter.
poe lint
uv run coverage run -m pytest

# Run tests.
coverage run bin/test -vvv

# Set the stage for uploading the coverage report.
coverage xml
uv run coverage xml

# https://github.com/codecov/codecov-action
- name: Upload coverage results to Codecov
Expand Down
7 changes: 7 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ Changes for crate

Unreleased
==========
- Modernize project to latest practices:
* Use `pyproject.toml` instead of `setup.py`.
* Use hatchling instead of buildout to build project.
* Use pytest instead of unittests and zope runner (decreasing dependencies).
* Make `uv` the recommended tool for local development.
* Simplify versioning when creating a new release.
* Increase the number of tests and overall coverage.

2025/01/30 2.0.0
================
Expand Down
134 changes: 94 additions & 40 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,55 @@
[build-system]
requires = ["hatchling >= 1.26"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/crate"]

[tool.hatch.version]
path = "src/crate/client/__init__.py"

[project]
name = "crate-python"
dynamic = ["version"]
description = "CrateDB Python Client"
authors = [{ name = "Crate.io", email = "office@crate.io" }]
requires-python = ">=3.10"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the code is still compatible down to Python 3.6?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have two users running 2.7, so perhaps even lower than 3.6

readme = "README.rst"
license = { file = "LICENSE"}
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Database",
]
dependencies = [
"orjson>=3.11.3",
"urllib3>=2.5.0",
]

[dependency-groups]
dev = [
"certifi>=2025.10.5",
"coverage>=7.11.0",
"mypy>=1.18.2",
"pytest>=8.4.2",
"pytz>=2025.2",
"ruff>=0.14.2",
"setuptools>=80.9.0",
"stopit>=1.1.2",
"verlib2>=0.3.1",
]


[tool.mypy]
mypy_path = "src"
packages = [
Expand All @@ -18,65 +70,67 @@ non_interactive = true
line-length = 80

extend-exclude = [
"/example_*",
"/example_*",
]

lint.select = [
# Builtins
"A",
# Bugbear
"B",
# comprehensions
"C4",
# Pycodestyle
"E",
# eradicate
"ERA",
# Pyflakes
"F",
# isort
"I",
# pandas-vet
"PD",
# return
"RET",
# Bandit
"S",
# print
"T20",
"W",
# flake8-2020
"YTT",
# Builtins
"A",
# Bugbear
"B",
# comprehensions
"C4",
# Pycodestyle
"E",
# eradicate
"ERA",
# Pyflakes
"F",
# isort
"I",
# pandas-vet
"PD",
# return
"RET",
# Bandit
"S",
# print
"T20",
"W",
# flake8-2020
"YTT",
]

lint.extend-ignore = [
# Unnecessary variable assignment before `return` statement
"RET504",
# Unnecessary `elif` after `return` statement
"RET505",
# Unnecessary variable assignment before `return` statement
"RET504",
# Unnecessary `elif` after `return` statement
"RET505",
]

lint.per-file-ignores."example_*" = [
"ERA001", # Found commented-out code
"T201", # Allow `print`
"ERA001", # Found commented-out code
"T201", # Allow `print`
]
lint.per-file-ignores."devtools/*" = [
"T201", # Allow `print`
"T201", # Allow `print`
]
lint.per-file-ignores."examples/*" = [
"ERA001", # Found commented-out code
"T201", # Allow `print`
"ERA001", # Found commented-out code
"T201", # Allow `print`
]
lint.per-file-ignores."tests/*" = [
"S106", # Possible hardcoded password assigned to argument: "password"
"S311", # Standard pseudo-random generators are not suitable for cryptographic purposes
"S101", # Asserts.
"S105", # Possible hardcoded password assigned to: "password"
"S106", # Possible hardcoded password assigned to argument: "password"
"S311", # Standard pseudo-random generators are not suitable for cryptographic purposes
]
lint.per-file-ignores."src/crate/client/{connection.py,http.py}" = [
"A004", # Import `ConnectionError` is shadowing a Python builtin
"A005", # Import `ConnectionError` is shadowing a Python builtin
"A004", # Import `ConnectionError` is shadowing a Python builtin
"A005", # Import `ConnectionError` is shadowing a Python builtin
]
lint.per-file-ignores."tests/client/test_http.py" = [
"A004", # Import `ConnectionError` is shadowing a Python builtin
"A004", # Import `ConnectionError` is shadowing a Python builtin
]


Expand Down
2 changes: 1 addition & 1 deletion src/crate/client/blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def put(self, f, digest=None):
read.
:return:
The hex digest of the uploaded blob if not provided in the call.
Otherwise a boolean indicating if the blob has been newly created.
Otherwise, a boolean indicating if the blob has been newly created.
"""

if digest:
Expand Down
2 changes: 1 addition & 1 deletion src/crate/client/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ def _lowest_server_version(self):
return lowest or Version("0.0.0")

def __repr__(self):
return "<Connection {0}>".format(repr(self.client))
return f"<{self.__class__.__qualname__} {self.client!r}>"

def __enter__(self):
return self
Expand Down
5 changes: 4 additions & 1 deletion src/crate/client/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,10 @@ def _convert_rows(self):

# Process result rows with conversion.
for row in self._result["rows"]:
yield [convert(value) for convert, value in zip(converters, row)]
yield [
convert(value)
for convert, value in zip(converters, row, strict=False)
]

@property
def time_zone(self):
Expand Down
2 changes: 1 addition & 1 deletion src/crate/client/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def __init__(self, table, digest):
self.digest = digest

def __str__(self):
return "{table}/{digest}".format(table=self.table, digest=self.digest)
return f"{self.__class__.__qualname__}('{self.table}/{self.digest})'"


class DigestNotFoundException(BlobException):
Expand Down
6 changes: 5 additions & 1 deletion src/crate/client/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,10 @@ def _pool_kw_args(
return kw


def _remove_certs_for_non_https(server, kwargs):
def _remove_certs_for_non_https(server: str, kwargs: dict) -> dict:
"""
Removes certificates for http requests.
"""
if server.lower().startswith("https"):
return kwargs
used_ssl_args = SSL_ONLY_ARGS & set(kwargs.keys())
Expand Down Expand Up @@ -435,6 +438,7 @@ def __init__(
if servers and not username:
try:
url = urlparse(servers[0])

if url.username is not None:
username = url.username
if url.password is not None:
Expand Down
54 changes: 54 additions & 0 deletions tests/client/test_blob.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from io import BytesIO
from unittest.mock import MagicMock

import pytest

from crate.client.blob import BlobContainer


def test_container():
"""Verify a container can be instantiated."""
expected_name = "somename"
container = BlobContainer(expected_name, MagicMock())
assert container.container_name == expected_name


def test_container_digest():
digester = BlobContainer("", MagicMock())._compute_digest

# sha1 of some_data.
some_data, expected_digest = (
b"some_data_123456",
"51bea75c0f26998083ef3717a489f2dc05818e8d",
)
result = digester(BytesIO(some_data))
assert result == expected_digest

with pytest.raises(AttributeError):
digester("someundigestabledata")


def test_container_put():
"""Test the logic of container put method"""
some_data, expected_digest = (
b"some_data_123456",
"51bea75c0f26998083ef3717a489f2dc05818e8d",
)
expected_container_name = "somename"
m = MagicMock()
m.client.blob_put = MagicMock()
container = BlobContainer(expected_container_name, m)

result = container.put(BytesIO(some_data))
assert result == expected_digest

new_digest = "asdfn"
data = BytesIO(some_data)
result = container.put(data, digest=new_digest)
assert isinstance(result, MagicMock)
assert m.client.blob_put.call_count == 2
assert m.client.blob_put.call_args.args == (
expected_container_name,
new_digest,
data,
)
Loading
Loading