Compare commits
1
Commits
v2.0.3
..
aebddb60a2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aebddb60a2 |
@@ -17,7 +17,7 @@ on:
|
|||||||
env:
|
env:
|
||||||
REGISTRY: gitea.lille-vemmelund.dk
|
REGISTRY: gitea.lille-vemmelund.dk
|
||||||
REGISTRY_USER: ci-bot
|
REGISTRY_USER: ci-bot
|
||||||
IMAGE: gitea.lille-vemmelund.dk/lillevemmelund/python-repositories-ci
|
IMAGE: gitea.lille-vemmelund.dk/brian/python-repositories-ci
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
build-and-push:
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
CI_RUNNER_TOKEN: ${{ secrets.CI_RUNNER_TOKEN }}
|
CI_RUNNER_TOKEN: ${{ secrets.CI_RUNNER_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "$CI_RUNNER_TOKEN" > /tmp/uv_token
|
echo "$CI_RUNNER_TOKEN" > /tmp/uv_token
|
||||||
docker build --network=host -f docker/ci/Dockerfile \
|
docker build -f docker/ci/Dockerfile \
|
||||||
--secret id=uv_token,src=/tmp/uv_token \
|
--secret id=uv_token,src=/tmp/uv_token \
|
||||||
-t "${IMAGE}:latest" \
|
-t "${IMAGE}:latest" \
|
||||||
.
|
.
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ RUN apt-get update \
|
|||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
|
docker.io \
|
||||||
git \
|
git \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY --from=docker:27-cli /usr/local/bin/docker /usr/local/bin/docker
|
|
||||||
COPY --from=ghcr.io/astral-sh/uv:0.7.0 /uv /usr/local/bin/uv
|
COPY --from=ghcr.io/astral-sh/uv:0.7.0 /uv /usr/local/bin/uv
|
||||||
|
|
||||||
ENV UV_PYTHON_INSTALL_DIR=/opt/uv-python \
|
ENV UV_PYTHON_INSTALL_DIR=/opt/uv-python \
|
||||||
|
|||||||
+5
-5
@@ -14,10 +14,10 @@ Built from [`docker/ci/Dockerfile`](../docker/ci/Dockerfile):
|
|||||||
- Docker CLI (integration tests via testcontainers)
|
- Docker CLI (integration tests via testcontainers)
|
||||||
- Dev dependencies from `uv.lock` (`uv sync --all-extras --no-install-project`)
|
- Dev dependencies from `uv.lock` (`uv sync --all-extras --no-install-project`)
|
||||||
|
|
||||||
Published to the Gitea container registry (owner segment must be lowercase for Docker):
|
Published to the Gitea container registry:
|
||||||
|
|
||||||
- `gitea.lille-vemmelund.dk/lillevemmelund/python-repositories-ci:latest`
|
- `gitea.lille-vemmelund.dk/brian/python-repositories-ci:latest`
|
||||||
- `gitea.lille-vemmelund.dk/lillevemmelund/python-repositories-ci:YYYYMMDDHHmm` (timestamped rollback tag)
|
- `gitea.lille-vemmelund.dk/brian/python-repositories-ci:YYYYMMDDHHmm` (timestamped rollback tag)
|
||||||
|
|
||||||
## Rebuild triggers
|
## Rebuild triggers
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ homelab-platform
|
|||||||
[`platform/gitea-runners/values.yaml`](https://gitea.lille-vemmelund.dk/LilleVemmelund/homelab-platform/src/branch/main/platform/gitea-runners/values.yaml):
|
[`platform/gitea-runners/values.yaml`](https://gitea.lille-vemmelund.dk/LilleVemmelund/homelab-platform/src/branch/main/platform/gitea-runners/values.yaml):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
python-repositories-ci:docker://gitea.lille-vemmelund.dk/lillevemmelund/python-repositories-ci:latest
|
python-repositories-ci:docker://gitea.lille-vemmelund.dk/brian/python-repositories-ci:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
After label changes, roll runner pods so they re-register with Gitea.
|
After label changes, roll runner pods so they re-register with Gitea.
|
||||||
@@ -137,7 +137,7 @@ bash scripts/ci/build-ci-image.sh --push
|
|||||||
```
|
```
|
||||||
|
|
||||||
Run from the repo root on the branch you want to test. Runners pull
|
Run from the repo root on the branch you want to test. Runners pull
|
||||||
`gitea.lille-vemmelund.dk/lillevemmelund/python-repositories-ci:latest` from the registry;
|
`gitea.lille-vemmelund.dk/brian/python-repositories-ci:latest` from the registry;
|
||||||
they do not care which git branch built it.
|
they do not care which git branch built it.
|
||||||
|
|
||||||
Override registry settings if needed:
|
Override registry settings if needed:
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "python-repositories"
|
name = "python-repositories"
|
||||||
version = "2.0.3"
|
version = "2.0.2"
|
||||||
description = "Various python repository interfaces exposed as a python package."
|
description = "Various python repository interfaces exposed as a python package."
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Brian Bjarke Jensen", email = "schnitzelen@gmail.com" }
|
{ name = "Brian Bjarke Jensen", email = "schnitzelen@gmail.com" }
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class ConnectionAwareAdapter(ConnectionAwareInterface, ContextAwareInterface):
|
|||||||
self._invalidate_health_cache()
|
self._invalidate_health_cache()
|
||||||
return
|
return
|
||||||
if self._is_client_ready() and self.is_connected():
|
if self._is_client_ready() and self.is_connected():
|
||||||
self.logger.info("Already connected", connection_name=self.connection_name)
|
self.logger.info(f"Already connected to {self.connection_name}")
|
||||||
return
|
return
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
self._establish_connection()
|
self._establish_connection()
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class MinioAdapter(ObjectRepositoryInterface, ConnectionAwareAdapter):
|
|||||||
raise ConnectionError(
|
raise ConnectionError(
|
||||||
f"Bucket '{bucket}' does not exist on Minio at {endpoint}"
|
f"Bucket '{bucket}' does not exist on Minio at {endpoint}"
|
||||||
)
|
)
|
||||||
self.logger.info("Creating bucket", bucket=bucket)
|
self.logger.info(f"Creating bucket '{bucket}'")
|
||||||
client.make_bucket(bucket)
|
client.make_bucket(bucket)
|
||||||
self._client = client
|
self._client = client
|
||||||
self._bucket_name = bucket
|
self._bucket_name = bucket
|
||||||
@@ -138,7 +138,7 @@ class MinioAdapter(ObjectRepositoryInterface, ConnectionAwareAdapter):
|
|||||||
content_type=content_type,
|
content_type=content_type,
|
||||||
)
|
)
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
"Put object", object_name=object_name, bucket=self._bucket_name
|
f"Put object '{object_name}' into bucket '{self._bucket_name}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(self, object_name: str) -> BytesIO | None:
|
def get(self, object_name: str) -> BytesIO | None:
|
||||||
@@ -163,15 +163,13 @@ class MinioAdapter(ObjectRepositoryInterface, ConnectionAwareAdapter):
|
|||||||
buffer.write(chunk)
|
buffer.write(chunk)
|
||||||
buffer.seek(0)
|
buffer.seek(0)
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
"Got object", object_name=object_name, bucket=self._bucket_name
|
f"Got object '{object_name}' from bucket '{self._bucket_name}'"
|
||||||
)
|
)
|
||||||
return buffer
|
return buffer
|
||||||
except minio.S3Error as exc:
|
except minio.S3Error as exc:
|
||||||
if exc.code == "NoSuchKey":
|
if exc.code == "NoSuchKey":
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
"Object not found",
|
f"Object '{object_name}' not found in bucket '{self._bucket_name}'"
|
||||||
object_name=object_name,
|
|
||||||
bucket=self._bucket_name,
|
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
raise
|
raise
|
||||||
@@ -195,7 +193,7 @@ class MinioAdapter(ObjectRepositoryInterface, ConnectionAwareAdapter):
|
|||||||
object_name=object_name,
|
object_name=object_name,
|
||||||
)
|
)
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
"Deleted object", object_name=object_name, bucket=self._bucket_name
|
f"Deleted object '{object_name}' from bucket '{self._bucket_name}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
def list_objects(self, prefix: str = "") -> list[str]:
|
def list_objects(self, prefix: str = "") -> list[str]:
|
||||||
@@ -217,9 +215,6 @@ class MinioAdapter(ObjectRepositoryInterface, ConnectionAwareAdapter):
|
|||||||
obj.object_name for obj in objects if obj.object_name is not None
|
obj.object_name for obj in objects if obj.object_name is not None
|
||||||
]
|
]
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
"Listed objects",
|
f"Listed {len(object_names)} object(s) in bucket '{self._bucket_name}' with prefix '{prefix}'"
|
||||||
count=len(object_names),
|
|
||||||
bucket=self._bucket_name,
|
|
||||||
prefix=prefix,
|
|
||||||
)
|
)
|
||||||
return object_names
|
return object_names
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class RedisAdapter(JsonRepositoryInterface, ConnectionAwareAdapter):
|
|||||||
assert self._client is not None
|
assert self._client is not None
|
||||||
# Set data
|
# Set data
|
||||||
self._client.json().set(key, self.path, data)
|
self._client.json().set(key, self.path, data)
|
||||||
self.logger.debug("Set key", key=key, data_keys=list(data.keys()))
|
self.logger.debug(f"Set {key} to {data}")
|
||||||
|
|
||||||
def get(self, key: str) -> dict[str, Any] | None:
|
def get(self, key: str) -> dict[str, Any] | None:
|
||||||
"""Get a JSON object from Redis."""
|
"""Get a JSON object from Redis."""
|
||||||
@@ -116,7 +116,7 @@ class RedisAdapter(JsonRepositoryInterface, ConnectionAwareAdapter):
|
|||||||
dict[str, Any] | None,
|
dict[str, Any] | None,
|
||||||
self._client.json().get(key),
|
self._client.json().get(key),
|
||||||
)
|
)
|
||||||
self.logger.debug("Got value", key=key, found=data is not None)
|
self.logger.debug(f"Got {data} from {key}")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def delete(self, key: str) -> None:
|
def delete(self, key: str) -> None:
|
||||||
@@ -129,7 +129,7 @@ class RedisAdapter(JsonRepositoryInterface, ConnectionAwareAdapter):
|
|||||||
assert self._client is not None
|
assert self._client is not None
|
||||||
# Delete data
|
# Delete data
|
||||||
self._client.json().delete(key)
|
self._client.json().delete(key)
|
||||||
self.logger.debug("Deleted key", key=key)
|
self.logger.debug(f"Deleted {key}")
|
||||||
|
|
||||||
def _validate_pattern(self, pattern: str) -> None:
|
def _validate_pattern(self, pattern: str) -> None:
|
||||||
if not isinstance(pattern, str) or len(pattern) == 0:
|
if not isinstance(pattern, str) or len(pattern) == 0:
|
||||||
@@ -147,7 +147,7 @@ class RedisAdapter(JsonRepositoryInterface, ConnectionAwareAdapter):
|
|||||||
self._client.keys(pattern),
|
self._client.keys(pattern),
|
||||||
)
|
)
|
||||||
keys: list[str] = [key.decode(self.encoding) for key in keys_raw]
|
keys: list[str] = [key.decode(self.encoding) for key in keys_raw]
|
||||||
self.logger.debug("Listed keys", pattern=pattern, count=len(keys))
|
self.logger.debug(f"Got {keys} matching {pattern}")
|
||||||
return keys
|
return keys
|
||||||
|
|
||||||
def scan_keys(
|
def scan_keys(
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
# Optional overrides:
|
# Optional overrides:
|
||||||
# REGISTRY=gitea.lille-vemmelund.dk
|
# REGISTRY=gitea.lille-vemmelund.dk
|
||||||
# REGISTRY_USER=ci-bot
|
# REGISTRY_USER=ci-bot
|
||||||
# IMAGE_OWNER=lillevemmelund
|
# IMAGE_OWNER=brian
|
||||||
# IMAGE_NAME=python-repositories-ci
|
# IMAGE_NAME=python-repositories-ci
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
|||||||
|
|
||||||
REGISTRY="${REGISTRY:-gitea.lille-vemmelund.dk}"
|
REGISTRY="${REGISTRY:-gitea.lille-vemmelund.dk}"
|
||||||
REGISTRY_USER="${REGISTRY_USER:-ci-bot}"
|
REGISTRY_USER="${REGISTRY_USER:-ci-bot}"
|
||||||
IMAGE_OWNER="${IMAGE_OWNER:-lillevemmelund}"
|
IMAGE_OWNER="${IMAGE_OWNER:-brian}"
|
||||||
IMAGE_NAME="${IMAGE_NAME:-python-repositories-ci}"
|
IMAGE_NAME="${IMAGE_NAME:-python-repositories-ci}"
|
||||||
IMAGE="${IMAGE:-${REGISTRY}/${IMAGE_OWNER}/${IMAGE_NAME}}"
|
IMAGE="${IMAGE:-${REGISTRY}/${IMAGE_OWNER}/${IMAGE_NAME}}"
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ cd "$REPO_ROOT"
|
|||||||
|
|
||||||
if [[ "$MODE" == "local" ]]; then
|
if [[ "$MODE" == "local" ]]; then
|
||||||
echo "=== Building local CI image (no push): python-repositories-ci:local ==="
|
echo "=== Building local CI image (no push): python-repositories-ci:local ==="
|
||||||
docker build --network=host -f docker/ci/Dockerfile \
|
docker build -f docker/ci/Dockerfile \
|
||||||
--secret "id=uv_token,src=${TOKEN_FILE}" \
|
--secret "id=uv_token,src=${TOKEN_FILE}" \
|
||||||
-t python-repositories-ci:local \
|
-t python-repositories-ci:local \
|
||||||
.
|
.
|
||||||
@@ -64,7 +64,7 @@ echo "=== Logging in to ${REGISTRY} as ${REGISTRY_USER} ==="
|
|||||||
echo "$CI_RUNNER_TOKEN" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
|
echo "$CI_RUNNER_TOKEN" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
|
||||||
|
|
||||||
echo "=== Building ${IMAGE}:latest ==="
|
echo "=== Building ${IMAGE}:latest ==="
|
||||||
docker build --network=host -f docker/ci/Dockerfile \
|
docker build -f docker/ci/Dockerfile \
|
||||||
--secret "id=uv_token,src=${TOKEN_FILE}" \
|
--secret "id=uv_token,src=${TOKEN_FILE}" \
|
||||||
-t "${IMAGE}:latest" \
|
-t "${IMAGE}:latest" \
|
||||||
.
|
.
|
||||||
|
|||||||
@@ -102,8 +102,7 @@ def test_should_log_info_when_already_connected(
|
|||||||
"""Test that the MinioAdapter logs info when connect is called while already connected."""
|
"""Test that the MinioAdapter logs info when connect is called while already connected."""
|
||||||
with caplog.at_level(logging.INFO):
|
with caplog.at_level(logging.INFO):
|
||||||
minio_adapter.connect()
|
minio_adapter.connect()
|
||||||
assert "Already connected" in caplog.text
|
assert "Already connected to Minio" in caplog.text
|
||||||
assert "Minio" in caplog.text
|
|
||||||
|
|
||||||
|
|
||||||
def test_should_raise_connection_error_when_unable_to_connect() -> None:
|
def test_should_raise_connection_error_when_unable_to_connect() -> None:
|
||||||
@@ -151,8 +150,7 @@ def test_connect_creates_bucket_when_create_bucket_if_missing_enabled(
|
|||||||
with caplog.at_level(logging.INFO):
|
with caplog.at_level(logging.INFO):
|
||||||
adapter.connect()
|
adapter.connect()
|
||||||
|
|
||||||
assert "Creating bucket" in caplog.text
|
assert f"Creating bucket '{bucket_name}'" in caplog.text
|
||||||
assert bucket_name in caplog.text
|
|
||||||
|
|
||||||
|
|
||||||
def test_should_log_error_on_exception_during_exit(
|
def test_should_log_error_on_exception_during_exit(
|
||||||
@@ -235,11 +233,10 @@ def test_should_log_warning_when_getting_nonexistent_object(
|
|||||||
with caplog.at_level("WARNING"):
|
with caplog.at_level("WARNING"):
|
||||||
result = adapter.get(object_name)
|
result = adapter.get(object_name)
|
||||||
assert result is None
|
assert result is None
|
||||||
assert "Object not found" in caplog.text
|
assert (
|
||||||
assert object_name in caplog.text
|
f"Object '{object_name}' not found in bucket '{adapter._bucket_name}'"
|
||||||
bucket_name = adapter._bucket_name
|
in caplog.text
|
||||||
assert bucket_name is not None
|
)
|
||||||
assert bucket_name in caplog.text
|
|
||||||
|
|
||||||
|
|
||||||
def test_should_reraise_s3error_other_than_no_such_key() -> None:
|
def test_should_reraise_s3error_other_than_no_such_key() -> None:
|
||||||
|
|||||||
@@ -56,8 +56,7 @@ def test_should_log_info_when_already_connected(
|
|||||||
"""Test that the RedisAdapter logs info when connect is called while already connected."""
|
"""Test that the RedisAdapter logs info when connect is called while already connected."""
|
||||||
with caplog.at_level(logging.INFO):
|
with caplog.at_level(logging.INFO):
|
||||||
redis_adapter.connect()
|
redis_adapter.connect()
|
||||||
assert "Already connected" in caplog.text
|
assert "Already connected to Redis" in caplog.text
|
||||||
assert "Redis" in caplog.text
|
|
||||||
|
|
||||||
|
|
||||||
def test_should_raise_connection_error_when_unable_to_connect() -> None:
|
def test_should_raise_connection_error_when_unable_to_connect() -> None:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 2
|
revision = 3
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
resolution-markers = [
|
resolution-markers = [
|
||||||
"python_full_version >= '3.15'",
|
"python_full_version >= '3.15'",
|
||||||
@@ -1056,7 +1056,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-repositories"
|
name = "python-repositories"
|
||||||
version = "2.0.3"
|
version = "2.0.2"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
|
|||||||
Reference in New Issue
Block a user