Skip to content
Merged
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
128 changes: 128 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
name: Release

on:
workflow_dispatch:
inputs:
bump:
description: "Version bump type"
required: true
type: choice
options:
- patch
- minor
- major

permissions:
contents: write
id-token: write

jobs:
release:
name: Bump, Changelog, Build & Publish
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/libreclient/

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- uses: astral-sh/setup-uv@v4
with:
enable-cache: true

- name: Install dependencies
run: uv sync

- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

# --- Version Bump ---
- name: Get current version
id: current
run: echo "version=$(uv version --short)" >> "$GITHUB_OUTPUT"

- name: Bump version
id: bump
run: |
uv version --bump ${{ inputs.bump }}
echo "version=$(uv version --short)" >> "$GITHUB_OUTPUT"
echo "Bumped version: ${{ steps.current.outputs.version }} → $(uv version --short)"

# --- Changelog ---
- name: Generate changelog entry
run: |
NEW_VERSION="${{ steps.bump.outputs.version }}"
DATE=$(date +%Y-%m-%d)
PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")

# Get commit messages since last tag (or all if no tags)
if [ -n "$PREV_TAG" ]; then
COMMITS=$(git log "${PREV_TAG}..HEAD" --pretty=format:"- %s" --no-merges)
else
COMMITS=$(git log --pretty=format:"- %s" --no-merges)
fi

# Build the new changelog entry
ENTRY="## [${NEW_VERSION}] - ${DATE}\n\n### Changed\n\n${COMMITS}"

# Insert after [Unreleased] section
sed -i "/^## \[Unreleased\]/a\\\\n${ENTRY}" CHANGELOG.md

# Update comparison links at bottom
if [ -n "$PREV_TAG" ]; then
sed -i "s|\[Unreleased\]:.*|\[Unreleased\]: https://github.com/jjeff07/libreclient/compare/v${NEW_VERSION}...HEAD|" CHANGELOG.md
sed -i "/^\[Unreleased\]/a [${NEW_VERSION}]: https://github.com/jjeff07/libreclient/compare/${PREV_TAG}...v${NEW_VERSION}" CHANGELOG.md
else
sed -i "s|\[Unreleased\]:.*|\[Unreleased\]: https://github.com/jjeff07/libreclient/compare/v${NEW_VERSION}...HEAD|" CHANGELOG.md
echo "[${NEW_VERSION}]: https://github.com/jjeff07/libreclient/releases/tag/v${NEW_VERSION}" >> CHANGELOG.md
fi

# --- Generate Type Stubs ---
- name: Generate .pyi stubs
run: uv run python generate_stubs.py

# --- Build ---
- name: Build package
run: uv build

# --- Commit & Tag ---
- name: Commit version bump and changelog
run: |
git add pyproject.toml uv.lock CHANGELOG.md
git commit -m "release: v${{ steps.bump.outputs.version }}"
git tag "v${{ steps.bump.outputs.version }}"
git push origin HEAD --tags

# --- Publish ---
- name: Publish to TestPyPI
continue-on-error: true
run: uv publish --index-url https://test.pypi.org/legacy/

- name: Publish to PyPI
run: uv publish

# --- GitHub Release ---
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.bump.outputs.version }}
name: v${{ steps.bump.outputs.version }}
body: |
## Changes

See [CHANGELOG.md](https://github.com/jjeff07/libreclient/blob/main/CHANGELOG.md) for details.
files: dist/*

# --- Sync dev branch ---
- name: Fast-forward dev to main
run: |
git fetch origin dev
git checkout dev
git merge --ff-only origin/main
git push origin dev
2 changes: 2 additions & 0 deletions src/libreclient/_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ async def _request(
response = await self._request_raw(
method, self._api_url(path), **kwargs
)
response.raise_for_status()
return await _json_from_response(response)

async def _request_bytes(
Expand All @@ -38,6 +39,7 @@ async def _request_bytes(
response = await self._request_raw(
method, self._api_url(path), **kwargs
)
response.raise_for_status()
payload = response.content
if inspect.isawaitable(payload):
payload = await payload
Expand Down
Empty file removed src/libreclient/_route_types.py
Empty file.
50 changes: 50 additions & 0 deletions tests/unit/test_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import asyncio

import pytest
from niquests import HTTPError

from libreclient.client import LibreClientAsync, LibreClientSync
from libreclient.config import LibreConfig

Expand Down Expand Up @@ -59,3 +62,50 @@ async def close(self): # noqa: S7503

asyncio.run(client.close())
assert state["closed"] is True


def test_sync_client_raises_for_http_error(monkeypatch) -> None:
"""Client should raise HTTPError on non-2xx responses."""
client = LibreClientSync(url="https://nms.example.com", token="x")

class FakeResponse:
status_code = 404

def raise_for_status(self):
raise HTTPError("404 Not Found", response=self)

def json(self):
return {"status": "error", "message": "Not Found"}

monkeypatch.setattr(
client._session, "request", lambda *a, **kw: FakeResponse()
)

with pytest.raises(HTTPError):
client.system.ping()

client.close()


def test_async_client_raises_for_http_error(monkeypatch) -> None:
"""Async client should raise HTTPError on non-2xx responses."""
client = LibreClientAsync(url="https://nms.example.com", token="x")

class FakeResponse:
status_code = 500

def raise_for_status(self):
raise HTTPError("500 Internal Server Error", response=self)

def json(self):
return {"status": "error", "message": "Server Error"}

async def fake_request(*a, **kw):
return FakeResponse()

monkeypatch.setattr(client._session, "request", fake_request)

with pytest.raises(HTTPError):
asyncio.run(client.system.ping())

asyncio.run(client.close())
Loading