Compare commits

..
15 Commits
Author SHA1 Message Date
brian 9e4b1e2c5e Merge pull request 'Avoid testcontainers Redis deprecation warning in integration tests.' (#18) from fix/redis-testcontainer-deprecation into main
Code Quality Pipeline / code-quality (push) Successful in 31s
Release on merge to main / release (push) Successful in 6s
Test Python Package / test (push) Successful in 43s
Reviewed-on: https://gitea.lille-vemmelund.dk/brian/python-repositories/pulls/18
2026-06-29 15:04:39 +02:00
Brian Bjarke JensenandCursor 0096df78a5 Avoid testcontainers Redis deprecation warning in integration tests.
Code Quality Pipeline / code-quality (pull_request) Successful in 47s
PR Title Check / check-title (pull_request) Successful in 5s
Test Python Package / test (pull_request) Successful in 52s
Use a local RedisTestContainer with wait strategies instead of the deprecated RedisContainer, and keep conftest focused on fixtures.

Co-authored-by: Cursor <[email protected]>
2026-06-29 15:00:45 +02:00
brian 3073bfbf50 Merge pull request 'Fix PR coverage comment posting in CI.' (#17) from fix/coverage-pr-comment into main
Code Quality Pipeline / code-quality (push) Successful in 36s
Release on merge to main / release (push) Successful in 5s
Test Python Package / test (push) Successful in 47s
Reviewed-on: https://gitea.lille-vemmelund.dk/brian/python-repositories/pulls/17
2026-06-29 11:37:34 +02:00
Brian Bjarke JensenandCursor 30a1de32e8 Fix PR coverage comment posting in CI.
Test Python Package / test (pull_request) Successful in 48s
Code Quality Pipeline / code-quality (pull_request) Successful in 31s
PR Title Check / check-title (pull_request) Successful in 5s
Build the Gitea API payload with json.dumps so multiline coverage output is valid JSON, and fail the step when the API rejects the request.

Co-authored-by: Cursor <[email protected]>
2026-06-28 23:46:49 +02:00
brian 8465957976 Merge pull request '[patch] update dependencies' (#16) from renovate/auto-deps-update into main
Test Python Package / test (push) Successful in 47s
Code Quality Pipeline / code-quality (push) Successful in 30s
Release on merge to main / release (push) Failing after 7s
Reviewed-on: https://gitea.lille-vemmelund.dk/brian/python-repositories/pulls/16
2026-06-28 23:46:13 +02:00
Brian Bjarke Jensen 3ecb9c3136 chore: retrigger CI
Code Quality Pipeline / code-quality (pull_request) Successful in 46s
PR Title Check / check-title (pull_request) Successful in 6s
Test Python Package / test (pull_request) Successful in 47s
2026-06-28 23:41:02 +02:00
Brian Bjarke JensenandCursor 0f31170209 Fix mypy errors from minio 7.2.20 type stubs.
Code Quality Pipeline / code-quality (pull_request) Successful in 36s
PR Title Check / check-title (pull_request) Failing after 6s
Test Python Package / test (pull_request) Successful in 43s
Guard bucket_name before Minio API calls, update S3Error test mocks for the new constructor signature, and add types-redis to the pre-commit mypy hook.

Co-authored-by: Cursor <[email protected]>
2026-06-28 23:32:31 +02:00
CI Bot e3cab5e0b6 chore(deps): update dependencies [automated]
Code Quality Pipeline / code-quality (pull_request) Failing after 22s
PR Title Check / check-title (pull_request) Successful in 6s
Test Python Package / test (pull_request) Successful in 1m3s
2026-06-28 21:22:26 +00:00
brian f428a974f6 Merge pull request 'Fix dependency bot PR creation for Gitea.' (#15) from cursor/fix-dependency-bot-gitea-pr into main
Code Quality Pipeline / code-quality (push) Successful in 31s
Release on merge to main / release (push) Successful in 6s
Test Python Package / test (push) Successful in 57s
Reviewed-on: https://gitea.lille-vemmelund.dk/brian/python-repositories/pulls/15
2026-06-28 22:56:14 +02:00
Brian Bjarke JensenandCursor a3b9912107 Fix dependency bot PR creation for Gitea.
Code Quality Pipeline / code-quality (pull_request) Successful in 27s
PR Title Check / check-title (pull_request) Successful in 6s
Test Python Package / test (pull_request) Successful in 1m3s
Replace peter-evans/create-pull-request with a Gitea API script that commits, pushes, and opens the deps update PR.

Co-authored-by: Cursor <[email protected]>
2026-06-28 22:41:59 +02:00
brian abaa078471 Merge pull request 'Fix dependency update bot to use uv lock --upgrade.' (#14) from cursor/fix-dependency-update-bot-uv-lock into main
Code Quality Pipeline / code-quality (push) Successful in 47s
Release on merge to main / release (push) Successful in 6s
Test Python Package / test (push) Successful in 1m2s
Reviewed-on: https://gitea.lille-vemmelund.dk/brian/python-repositories/pulls/14
2026-06-28 22:34:48 +02:00
Brian Bjarke JensenandCursor 9864f0cc9d Fix dependency update bot to use uv lock --upgrade.
Code Quality Pipeline / code-quality (pull_request) Successful in 45s
PR Title Check / check-title (pull_request) Successful in 6s
Test Python Package / test (pull_request) Successful in 53s
Replace invalid uv pip update/compile commands with the native lockfile upgrade workflow.

Co-authored-by: Cursor <[email protected]>
2026-06-28 22:30:28 +02:00
brian e39b467b6b Merge pull request 'Automate releases from PR titles and clean up CI workflows.' (#13) from cursor/pr-title-release-automation into main
Code Quality Pipeline / code-quality (push) Successful in 30s
Release on merge to main / release (push) Successful in 6s
Test Python Package / test (push) Successful in 1m4s
Reviewed-on: https://gitea.lille-vemmelund.dk/brian/python-repositories/pulls/13
2026-06-28 22:25:01 +02:00
Brian Bjarke Jensen 85c1a64e14 Sync uv.lock with pyproject.toml version 0.3.1.
Code Quality Pipeline / code-quality (pull_request) Successful in 30s
PR Title Check / check-title (pull_request) Successful in 6s
Test Python Package / test (pull_request) Successful in 1m7s
2026-06-28 22:21:59 +02:00
Brian Bjarke JensenandCursor a8491c005c Automate releases from PR titles and clean up CI workflows.
Code Quality Pipeline / code-quality (pull_request) Failing after 39s
PR Title Check / check-title (pull_request) Successful in 5s
Test Python Package / test (pull_request) Successful in 1m3s
Add release and PR title checks, shared CI scripts, Gitea PR template, and align pyproject.toml to v0.3.1.

Co-authored-by: Cursor <[email protected]>
2026-06-28 22:18:40 +02:00
24 changed files with 1413 additions and 664 deletions
+19
View File
@@ -0,0 +1,19 @@
## Version bump
If this PR changes files under `python_repositories/`, the **title must** start with one of:
- `[patch]` or `[fix]` — bug fix (0.3.1 → 0.3.2)
- `[minor]` or `[feat]` — new feature (0.3.1 → 0.4.0)
- `[major]` or `[breaking]` — breaking change (0.3.1 → 1.0.0)
Docs-, CI-, or test-only PRs do not need a prefix.
**Example title:** `[minor] Add streaming support to RedisAdapter`
## Summary
<!-- What changed and why -->
## Test plan
- [ ] Integration tests pass locally
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
python-version-file: .python-version
- name: Install uv
run: pip install uv
@@ -36,7 +36,7 @@ jobs:
run: uv run ruff format --check .
- name: Pyupgrade check
run: uv run pyupgrade --py313-plus $(git ls-files '*.py') && git diff --exit-code
run: uv run pyupgrade --py312-plus $(git ls-files '*.py') && git diff --exit-code
- name: Prettier format
run: uv run pre-commit run prettier --all-files
+13 -15
View File
@@ -11,26 +11,24 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.CI_RUNNER_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
python-version-file: .python-version
- name: Install uv and pip-review
run: |
pip install uv pip-review
- name: Install uv
run: pip install uv
- name: Update dependencies in pyproject.toml
run: |
uv pip update --all --group dev
uv pip compile --all
- name: Upgrade dependencies
run: uv lock --upgrade
- name: Commit and push changes
uses: peter-evans/create-pull-request@v6
with:
commit-message: "chore(deps): update dependencies [automated]"
branch: "renovate/auto-deps-update"
title: "chore(deps): update dependencies"
body: "This PR was created automatically to update dependencies."
token: ${{ secrets.CI_RUNNER_TOKEN }}
env:
API_URL: ${{ vars.API_URL }}
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
CI_RUNNER_TOKEN: ${{ secrets.CI_RUNNER_TOKEN }}
run: scripts/ci/create-deps-update-pr.sh
+23
View File
@@ -0,0 +1,23 @@
name: PR Title Check
on:
pull_request:
branches:
- main
jobs:
check-title:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check PR title when source files change
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
git fetch origin "${{ github.base_ref }}"
git diff --name-only "origin/${{ github.base_ref }}...HEAD" \
| scripts/ci/check-pr-title.sh "$PR_TITLE"
+2 -6
View File
@@ -17,17 +17,13 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
python-version-file: .python-version
- name: Install uv
run: pip install uv
- name: Update version in pyproject.toml to match tag
run: |
TAG_VERSION="${GITHUB_REF##*/}"
VERSION="${TAG_VERSION#v}"
sed -i.bak -E "s/^version = \".*\"/version = \"${VERSION}\"/" pyproject.toml
rm pyproject.toml.bak
run: scripts/ci/bump-version.sh --from-tag "${GITHUB_REF##*/}"
- name: Build package
run: |
+73
View File
@@ -0,0 +1,73 @@
name: Release on merge to main
on:
push:
branches:
- main
jobs:
release:
if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.CI_RUNNER_TOKEN }}
- name: Parse merge commit
id: meta
env:
COMMIT_MSG: ${{ github.event.head_commit.message }}
run: scripts/ci/parse-merge-commit.sh "$COMMIT_MSG"
- name: Bump version
if: steps.meta.outputs.bump != 'skip'
id: bump
run: scripts/ci/bump-version.sh "${{ steps.meta.outputs.bump }}"
- name: Generate release notes
if: steps.meta.outputs.bump != 'skip'
id: notes
env:
PR_TITLE: ${{ steps.meta.outputs.pr_title }}
run: |
PREV_TAG=$(git describe --tags --abbrev=0)
NEW_TAG="v${{ steps.bump.outputs.version }}"
scripts/ci/generate-release-notes.sh "$NEW_TAG" "$PR_TITLE" "$PREV_TAG"
- name: Commit, tag, and push
if: steps.meta.outputs.bump != 'skip'
env:
VERSION: ${{ steps.bump.outputs.version }}
CI_RUNNER_TOKEN: ${{ secrets.CI_RUNNER_TOKEN }}
run: |
git config user.name "CI Bot"
git config user.email "[email protected]"
git add pyproject.toml
git commit -m "chore: release v${VERSION} [skip ci]"
git tag "v${VERSION}"
git remote set-url origin "https://x-access-token:${CI_RUNNER_TOKEN}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git"
git push origin main
git push origin "v${VERSION}"
- name: Create Gitea release
if: steps.meta.outputs.bump != 'skip'
env:
API_URL: ${{ vars.API_URL }}
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
CI_RUNNER_TOKEN: ${{ secrets.CI_RUNNER_TOKEN }}
VERSION: ${{ steps.bump.outputs.version }}
PR_TITLE: ${{ steps.meta.outputs.pr_title }}
RELEASE_BODY: ${{ steps.notes.outputs.body }}
run: |
SUMMARY=$(echo "$PR_TITLE" | sed -E 's/^\[(patch|fix|minor|feat|major|breaking)\][[:space:]]*//i')
export RELEASE_NAME="v${VERSION} — ${SUMMARY}"
PAYLOAD=$(python3 -c 'import json, os; print(json.dumps({"tag_name": "v" + os.environ["VERSION"], "name": os.environ["RELEASE_NAME"], "body": os.environ["RELEASE_BODY"]}))')
curl -s -X POST \
"${API_URL}/repos/${REPO_OWNER}/${REPO_NAME}/releases" \
-H "Authorization: token ${CI_RUNNER_TOKEN}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"
-29
View File
@@ -1,29 +0,0 @@
name: Security Audit
on:
schedule:
- cron: "0 0 * * 0" # Weekly
workflow_dispatch:
jobs:
safety:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
- name: Install uv
run: pip install uv
- name: Install dependencies
env:
UV_LINK_MODE: copy
run: uv sync
- name: Run safety check
run: uv run safety check
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
python-version-file: .python-version
- name: Install uv
run: pip install uv
@@ -1,38 +0,0 @@
name: Sync pyproject.toml Version
on:
workflow_run:
workflows: ["Publish Python Package"]
types:
- completed
jobs:
commit-version-update:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
- name: Configure git
run: |
git config user.name "CI Bot"
git config user.email "[email protected]"
- name: Update version in pyproject.toml to match tag
run: |
TAG_VERSION="${{ github.event.workflow_run.head_branch }}"
VERSION="${TAG_VERSION#v}"
sed -i.bak -E "s/^version = \".*\"/version = \"${VERSION}\"/" pyproject.toml
rm pyproject.toml.bak
- name: Commit and push changes
run: |
git add pyproject.toml
git commit -m "chore: update version in pyproject.toml to ${VERSION} [skip ci]" || echo "No changes to commit"
git push
+17 -5
View File
@@ -16,7 +16,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
python-version-file: .python-version
- name: Install uv
run: pip install uv
@@ -32,6 +32,7 @@ jobs:
run: uv run pytest --cov=python_repositories --cov-report=term-missing > coverage.txt
- name: Post coverage summary to PR
if: github.event_name == 'pull_request'
env:
API_URL: ${{ vars.API_URL }}
REPO_OWNER: ${{ github.repository_owner }}
@@ -39,9 +40,20 @@ jobs:
PR_NUMBER: ${{ github.event.pull_request.number }}
CI_RUNNER_TOKEN: ${{ secrets.CI_RUNNER_TOKEN }}
run: |
COVERAGE=$(cat coverage.txt)
COMMENT_BODY="**Test Coverage Report:**\n\`\`\`\n$COVERAGE\n\`\`\`"
curl -s -X POST "$API_URL/repos/$REPO_OWNER/$REPO_NAME/issues/$PR_NUMBER/comments" \
PAYLOAD=$(python3 -c '
import json
import pathlib
coverage = pathlib.Path("coverage.txt").read_text()
print(
json.dumps(
{
"body": f"**Test Coverage Report:**\n```\n{coverage}\n```",
}
)
)
')
curl -sf -X POST "$API_URL/repos/$REPO_OWNER/$REPO_NAME/issues/$PR_NUMBER/comments" \
-H "Authorization: token $CI_RUNNER_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"body\": \"$COMMENT_BODY\"}"
-d "$PAYLOAD"
+3 -1
View File
@@ -24,13 +24,15 @@ repos:
rev: v1.8.0
hooks:
- id: mypy
additional_dependencies:
- types-redis
# Python syntax modernization with pyupgrade
- repo: https://github.com/asottile/pyupgrade
rev: v3.20.0
hooks:
- id: pyupgrade
args: ["--py311-plus"]
args: ["--py312-plus"]
# Formatting for Markdown, JSON, and YAML with Prettier
- repo: https://github.com/pre-commit/mirrors-prettier
+35
View File
@@ -112,3 +112,38 @@ uv run pre-commit run --all-files
```
Integration tests require Docker (testcontainers).
## Releases
Releases are automated when a pull request is merged to `main`. CI reads the **merged PR title** to decide whether and how to bump the version.
### How it works
1. Open a PR targeting `main` (see [`.gitea/PULL_REQUEST_TEMPLATE.md`](.gitea/PULL_REQUEST_TEMPLATE.md)).
2. If the PR changes files under `python_repositories/`, the title **must** start with a version bump prefix (enforced by CI).
3. On merge, [`release.yml`](.gitea/workflows/release.yml) bumps [`pyproject.toml`](pyproject.toml), commits, tags `vX.Y.Z`, creates a Gitea release with auto-generated notes, and pushes the tag.
4. [`publish.yml`](.gitea/workflows/publish.yml) builds and publishes the package to the Gitea Package Registry.
Docs-, CI-, and test-only PRs do not need a prefix and will not trigger a release.
### PR title prefixes
| Prefix | Bump | Example |
| ------------------------- | ---------- | --------------------- |
| `[patch]` or `[fix]` | patch | `0.3.1``0.3.2` |
| `[minor]` or `[feat]` | minor | `0.3.1``0.4.0` |
| `[major]` or `[breaking]` | major | `0.3.1``1.0.0` |
| _(none)_ | no release | docs / CI / deps only |
Example titles:
- `[minor] Add public repository interfaces and subclassable adapter CRUD API`
- `[patch] Fix mypy context manager typing for adapter subclasses`
### Release notes
Release notes are generated from commits since the previous tag (see [`scripts/ci/generate-release-notes.sh`](scripts/ci/generate-release-notes.sh)).
### Manual release
You can still push a `v*.*.*` tag manually; `publish.yml` will build and publish. The current baseline version is **0.3.1**.
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "python-repositories"
version = "0.1.0"
version = "0.3.1"
description = "Various python repository interfaces exposed as a python package."
authors = [
{ name = "Brian Bjarke Jensen", email = "[email protected]" }
@@ -131,7 +131,7 @@ class MinioAdapter(
if not isinstance(content_type, str) or len(content_type) == 0:
raise ValueError("content_type must be a non-empty string")
# Check connection
if self._client is None or not self.is_connected:
if self._client is None or self._bucket_name is None or not self.is_connected:
raise ConnectionError("Not connected to Minio")
# Prepare buffer for reading
num_bytes = data.getbuffer().nbytes
@@ -156,7 +156,7 @@ class MinioAdapter(
if not isinstance(object_name, str) or len(object_name) == 0:
raise ValueError("object_name must be a non-empty string")
# Check connection
if self._client is None or not self.is_connected:
if self._client is None or self._bucket_name is None or not self.is_connected:
raise ConnectionError("Not connected to Minio")
# Get data from bucket
# N.B. bucket name is set when connecting
@@ -191,7 +191,7 @@ class MinioAdapter(
if not isinstance(object_name, str) or len(object_name) == 0:
raise ValueError("object_name must be a non-empty string")
# Check connection
if self._client is None or not self.is_connected:
if self._client is None or self._bucket_name is None or not self.is_connected:
raise ConnectionError("Not connected to Minio")
# Delete object from bucket
# N.B. bucket name is set when connecting
@@ -210,7 +210,7 @@ class MinioAdapter(
raise ValueError("prefix must be a string")
# Check connection
# N.B. bucket name is set when connecting
if self._client is None or not self.is_connected:
if self._client is None or self._bucket_name is None or not self.is_connected:
raise ConnectionError("Not connected to Minio")
# List objects in bucket
objects = self._client.list_objects(
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=common.sh
source "${SCRIPT_DIR}/common.sh"
usage() {
echo "Usage: bump-version.sh <major|minor|patch>" >&2
echo " bump-version.sh --from-tag <vX.Y.Z>" >&2
exit 1
}
if [[ $# -lt 1 ]]; then
usage
fi
if [[ "$1" == "--from-tag" ]]; then
if [[ $# -ne 2 ]]; then
usage
fi
VERSION="${2#v}"
else
BUMP_TYPE="$1"
CURRENT=$(latest_tag_version)
VERSION=$(bump_semver "$CURRENT" "$BUMP_TYPE")
fi
set_pyproject_version "$VERSION"
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
else
echo "version=${VERSION}"
fi
echo "Updated pyproject.toml to version ${VERSION}" >&2
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=common.sh
source "${SCRIPT_DIR}/common.sh"
PR_TITLE="${1:-${PR_TITLE:-}}"
if [[ -z "$PR_TITLE" ]]; then
echo "Usage: check-pr-title.sh <pr-title> (changed files on stdin)" >&2
exit 1
fi
CHANGED_FILES=$(cat)
if ! has_source_changes "$CHANGED_FILES"; then
echo "No changes under ${SOURCE_DIR}/ — PR title prefix not required."
exit 0
fi
if title_has_bump_prefix "$PR_TITLE"; then
echo "PR title has a valid version bump prefix."
exit 0
fi
cat >&2 <<EOF
PR changes files under ${SOURCE_DIR}/ but the title lacks a version bump prefix.
Title: ${PR_TITLE}
When changing package source, start the PR title with one of:
[patch] or [fix] — bug fix
[minor] or [feat] — new feature
[major] or [breaking] — breaking change
Example: [patch] Fix connection retry in RedisAdapter
See .gitea/PULL_REQUEST_TEMPLATE.md for details.
EOF
exit 1
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# Shared helpers for release and PR title CI scripts.
SOURCE_DIR="python_repositories"
# Returns bump type (major|minor|patch) or empty if no valid prefix.
bump_type_from_title() {
local title="$1"
local lower
lower=$(echo "$title" | tr '[:upper:]' '[:lower:]')
if echo "$lower" | grep -qE '^\[(major|breaking)\]'; then
echo "major"
elif echo "$lower" | grep -qE '^\[(minor|feat)\]'; then
echo "minor"
elif echo "$lower" | grep -qE '^\[(patch|fix)\]'; then
echo "patch"
fi
}
title_has_bump_prefix() {
[[ -n "$(bump_type_from_title "$1")" ]]
}
strip_bump_prefix() {
local title="$1"
echo "$title" | sed -E 's/^\[(patch|fix|minor|feat|major|breaking)\][[:space:]]*//i'
}
extract_pr_title_from_merge_commit() {
local msg="$1"
echo "$msg" | sed -n "s/^Merge pull request '\(.*\)' (#.*/\1/p"
}
changed_source_files() {
grep -E "^${SOURCE_DIR}/" || true
}
has_source_changes() {
local files="$1"
echo "$files" | changed_source_files | grep -q .
}
read_pyproject_version() {
sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml | head -1
}
latest_tag_version() {
local tag
tag=$(git describe --tags --abbrev=0 2>/dev/null || true)
if [[ -n "$tag" ]]; then
echo "${tag#v}"
else
read_pyproject_version
fi
}
set_pyproject_version() {
local version="$1"
sed -i.bak -E "s/^version = \".*\"/version = \"${version}\"/" pyproject.toml
rm -f pyproject.toml.bak
}
bump_semver() {
local current="$1"
local bump_type="$2"
local major minor patch
IFS=. read -r major minor patch <<< "$current"
case "$bump_type" in
major)
major=$((major + 1))
minor=0
patch=0
;;
minor)
minor=$((minor + 1))
patch=0
;;
patch)
patch=$((patch + 1))
;;
*)
echo "Unknown bump type: $bump_type" >&2
return 1
;;
esac
echo "${major}.${minor}.${patch}"
}
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
set -euo pipefail
: "${API_URL:?API_URL is required}"
: "${REPO_OWNER:?REPO_OWNER is required}"
: "${REPO_NAME:?REPO_NAME is required}"
: "${CI_RUNNER_TOKEN:?CI_RUNNER_TOKEN is required}"
: "${GITHUB_SERVER_URL:?GITHUB_SERVER_URL is required}"
: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
BRANCH="${BRANCH:-renovate/auto-deps-update}"
BASE_BRANCH="${BASE_BRANCH:-main}"
LOCKFILE="${LOCKFILE:-uv.lock}"
if git diff --quiet "$LOCKFILE"; then
echo "No dependency updates available."
exit 0
fi
git config user.name "CI Bot"
git config user.email "[email protected]"
git add "$LOCKFILE"
git commit -m "chore(deps): update dependencies [automated]"
git remote set-url origin "https://x-access-token:${CI_RUNNER_TOKEN}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git"
git push origin "HEAD:${BRANCH}"
if curl -s "${API_URL}/repos/${REPO_OWNER}/${REPO_NAME}/pulls?state=open" \
-H "Authorization: token ${CI_RUNNER_TOKEN}" \
| BRANCH="$BRANCH" python3 -c '
import json
import os
import sys
branch = os.environ["BRANCH"]
prs = json.load(sys.stdin)
sys.exit(0 if any(pr.get("head", {}).get("ref") == branch for pr in prs) else 1)
'; then
echo "Pull request already exists; branch push updates it."
exit 0
fi
PAYLOAD=$(BRANCH="$BRANCH" BASE_BRANCH="$BASE_BRANCH" python3 -c '
import json
import os
print(
json.dumps(
{
"title": "chore(deps): update dependencies",
"body": "This PR was created automatically to update dependencies.",
"head": os.environ["BRANCH"],
"base": os.environ["BASE_BRANCH"],
}
)
)
')
curl -sf -X POST "${API_URL}/repos/${REPO_OWNER}/${REPO_NAME}/pulls" \
-H "Authorization: token ${CI_RUNNER_TOKEN}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"
echo "Created pull request."
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=common.sh
source "${SCRIPT_DIR}/common.sh"
NEW_TAG="${1:-}"
SUMMARY="${2:-}"
PREV_TAG="${3:-}"
if [[ -z "$NEW_TAG" ]]; then
echo "Usage: generate-release-notes.sh <new-tag> [summary] [prev-tag]" >&2
exit 1
fi
if [[ -z "$PREV_TAG" ]]; then
PREV_TAG=$(git describe --tags --abbrev=0 "${NEW_TAG}^" 2>/dev/null || git describe --tags --abbrev=0 2>/dev/null || true)
fi
NOTES="## Summary"
if [[ -n "$SUMMARY" ]]; then
NOTES="${NOTES}
$(strip_bump_prefix "$SUMMARY")"
else
NOTES="${NOTES}
Automated release ${NEW_TAG}."
fi
if [[ -n "$PREV_TAG" ]]; then
NOTES="${NOTES}
## Changes since ${PREV_TAG}
$(git log "${PREV_TAG}..HEAD" --pretty=format:'- %h %s' || true)"
else
NOTES="${NOTES}
## Changes
$(git log --pretty=format:'- %h %s' -20 || true)"
fi
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
{
echo 'body<<EOF'
echo "$NOTES"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
else
echo "$NOTES"
fi
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=common.sh
source "${SCRIPT_DIR}/common.sh"
COMMIT_MSG="${1:-${COMMIT_MSG:-}}"
if [[ -z "$COMMIT_MSG" ]]; then
echo "Usage: parse-merge-commit.sh <commit-message>" >&2
exit 1
fi
PR_TITLE=$(extract_pr_title_from_merge_commit "$COMMIT_MSG")
if [[ -z "$PR_TITLE" ]]; then
bump="skip"
else
bump_type=$(bump_type_from_title "$PR_TITLE")
if [[ -n "$bump_type" ]]; then
bump="$bump_type"
else
bump="skip"
fi
fi
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
{
echo "pr_title=${PR_TITLE}"
echo "bump=${bump}"
} >> "$GITHUB_OUTPUT"
else
echo "pr_title=${PR_TITLE}"
echo "bump=${bump}"
fi
+13 -10
View File
@@ -1,16 +1,19 @@
"""Integration tests configuration."""
import logging
import os
from collections.abc import Generator
import pytest
import redis
import structlog
import logging
from minio import Minio
from testcontainers.redis import RedisContainer
from testcontainers.minio import MinioContainer
from tests.integration.redis_container_test import REDIS_PORT, RedisTestContainer
collect_ignore = ["redis_container_test.py"]
MINIO_ACCESS_KEY = "minioadmin"
MINIO_SECRET_KEY = "minioadmin"
MINIO_BUCKET = "test-bucket"
@@ -37,16 +40,16 @@ def configure_logging() -> None:
@pytest.fixture(scope="session")
def redis_container() -> Generator[str]:
def redis_container() -> Generator[str, None, None]:
"""Set up a Redis container for testing and yield the Redis URI."""
# Start container
container = RedisContainer(
container = RedisTestContainer(
image="redis/redis-stack:7.2.0-v0",
)
container.start()
# Set environment variable for Redis URI
redis_host = container.get_container_host_ip()
redis_port = container.get_exposed_port(6379)
redis_port = container.get_exposed_port(REDIS_PORT)
redis_uri = f"redis://{redis_host}:{redis_port}"
yield redis_uri
@@ -56,7 +59,7 @@ def redis_container() -> Generator[str]:
@pytest.fixture(scope="session")
def minio_container() -> Generator[dict[str, str]]:
def minio_container() -> Generator[dict[str, str], None, None]:
"""Set up a Minio container for testing and yield the Minio URI."""
# Start container
container = MinioContainer(
@@ -86,7 +89,7 @@ def minio_container() -> Generator[dict[str, str]]:
def set_environment_variables(
redis_container: str,
minio_container: dict[str, str],
) -> Generator[dict[str, str]]:
) -> Generator[dict[str, str], None, None]:
"""Set environment variables needed for tests."""
# Build environment variables dictionary
env_vars = {"REDIS_URI": redis_container}
@@ -103,7 +106,7 @@ def set_environment_variables(
@pytest.fixture(scope="session")
def raw_redis_client(redis_container: str) -> Generator[redis.Redis]:
def raw_redis_client(redis_container: str) -> Generator[redis.Redis, None, None]:
"""Provide a raw Redis client connected to the test Redis container."""
# Connect client
client = redis.Redis.from_url(
@@ -119,7 +122,7 @@ def raw_redis_client(redis_container: str) -> Generator[redis.Redis]:
@pytest.fixture(scope="session")
def raw_minio_client(minio_container: dict[str, str]) -> Generator[Minio]:
def raw_minio_client(minio_container: dict[str, str]) -> Generator[Minio, None, None]:
"""Provide a raw Minio client connected to the test Minio container."""
# Connect client
client = Minio(
+16 -15
View File
@@ -9,6 +9,7 @@ import random
import os
import logging
from minio import S3Error
from urllib3.response import BaseHTTPResponse
from python_repositories.adapters.minio_adapter import MinioAdapter
from python_repositories.interfaces import ObjectRepositoryInterface
@@ -42,7 +43,7 @@ def same_data(
@pytest.fixture(scope="module")
def data() -> Generator[BytesIO]:
def data() -> Generator[BytesIO, None, None]:
"""Provide a sample data bytes for tests."""
# Generate random bytes
random_bytes = random.randbytes(2**21) # 2 MiB
@@ -53,7 +54,7 @@ def data() -> Generator[BytesIO]:
def data_in_minio(
raw_minio_client: Minio,
data: BytesIO,
) -> Generator[tuple[str, BytesIO]]:
) -> Generator[tuple[str, BytesIO], None, None]:
"""Fixture to set up a known value in Minio before each test."""
object_name = "test_object"
bucket_name = str(os.getenv("MINIO_BUCKET"))
@@ -77,7 +78,7 @@ def data_in_minio(
@pytest.fixture(scope="module")
def minio_adapter() -> Generator[MinioAdapter]:
def minio_adapter() -> Generator[MinioAdapter, None, None]:
"""Fixture to provide a connected MinioAdapter instance."""
adapter = MinioAdapter()
adapter.connect()
@@ -234,12 +235,12 @@ def test_should_log_warning_when_getting_nonexistent_object(
adapter = MinioAdapter()
adapter._client = MagicMock(spec=Minio)
adapter._client.get_object.side_effect = S3Error(
code="NoSuchKey",
message="",
resource="",
request_id="",
host_id="",
response="",
MagicMock(spec=BaseHTTPResponse),
"NoSuchKey",
"",
"",
"",
"",
bucket_name="test-bucket",
object_name="missing-object",
)
@@ -264,12 +265,12 @@ def test_should_log_error_when_getting_with_s3error_other_than_no_such_key(
adapter = MinioAdapter()
adapter._client = MagicMock(spec=Minio)
other_s3error = S3Error(
code="UnhandledError",
message="",
resource="",
request_id="",
host_id="",
response="",
MagicMock(spec=BaseHTTPResponse),
"UnhandledError",
"",
"",
"",
"",
bucket_name="test-bucket",
object_name="missing-object",
)
+36
View File
@@ -0,0 +1,36 @@
"""Redis test container without testcontainers' deprecated wait decorator."""
from typing import Any, cast
import redis
from testcontainers.core.container import DockerContainer
from testcontainers.core.waiting_utils import WaitStrategy, WaitStrategyTarget
REDIS_PORT = 6379
class _RedisPingWaitStrategy(WaitStrategy):
def __init__(self) -> None:
super().__init__()
self.with_transient_exceptions(redis.exceptions.ConnectionError)
def wait_until_ready(self, container: WaitStrategyTarget) -> None:
redis_container = cast("RedisTestContainer", container)
if not self._poll(lambda: redis_container.get_client().ping()):
raise redis.exceptions.ConnectionError("Could not connect to Redis")
class RedisTestContainer(DockerContainer):
"""Redis container using wait strategies instead of the deprecated decorator."""
def __init__(self, image: str, port: int = REDIS_PORT) -> None:
super().__init__(image, _wait_strategy=_RedisPingWaitStrategy())
self.port = port
self.with_exposed_ports(self.port)
def get_client(self, **kwargs: Any) -> redis.Redis:
return redis.Redis(
host=self.get_container_host_ip(),
port=self.get_exposed_port(self.port),
**kwargs,
)
Generated
+834 -537
View File
File diff suppressed because it is too large Load Diff