From 73d4cf49a320a7dbebbf8a28dbdc419c3bd96095 Mon Sep 17 00:00:00 2001 From: Justin Jeffery <34625666+jjeff07@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:53:56 -0400 Subject: [PATCH 1/4] fix: Response statuses were not raised. --- .github/workflows/release.yml | 128 ++++++++++++++++++++++++++++++++ src/libreclient/_base_client.py | 2 + src/libreclient/_route_types.py | 0 tests/unit/test_client.py | 55 ++++++++++++++ 4 files changed, 185 insertions(+) create mode 100644 .github/workflows/release.yml delete mode 100644 src/libreclient/_route_types.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ef20995 --- /dev/null +++ b/.github/workflows/release.yml @@ -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 diff --git a/src/libreclient/_base_client.py b/src/libreclient/_base_client.py index 20ee746..19a1437 100644 --- a/src/libreclient/_base_client.py +++ b/src/libreclient/_base_client.py @@ -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( @@ -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 diff --git a/src/libreclient/_route_types.py b/src/libreclient/_route_types.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 7df702a..bb3043c 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -1,5 +1,8 @@ import asyncio +import pytest +from niquests import HTTPError + from libreclient.client import LibreClientAsync, LibreClientSync from libreclient.config import LibreConfig @@ -59,3 +62,55 @@ 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()) + From 1c191672955e13efc10c60bf979f7171daf5830c Mon Sep 17 00:00:00 2001 From: Justin Jeffery <34625666+jjeff07@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:55:34 -0400 Subject: [PATCH 2/4] style: Fix format. --- tests/unit/test_client.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index bb3043c..8a389d9 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -72,9 +72,7 @@ class FakeResponse: status_code = 404 def raise_for_status(self): - raise HTTPError( - "404 Not Found", response=self - ) + raise HTTPError("404 Not Found", response=self) def json(self): return {"status": "error", "message": "Not Found"} @@ -97,9 +95,7 @@ class FakeResponse: status_code = 500 def raise_for_status(self): - raise HTTPError( - "500 Internal Server Error", response=self - ) + raise HTTPError("500 Internal Server Error", response=self) def json(self): return {"status": "error", "message": "Server Error"} @@ -113,4 +109,3 @@ async def fake_request(*a, **kw): asyncio.run(client.system.ping()) asyncio.run(client.close()) - From 7468a8a064a424976abf1ac7c0147cfea6433a7e Mon Sep 17 00:00:00 2001 From: Justin Jeffery <34625666+jjeff07@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:57:05 -0400 Subject: [PATCH 3/4] fix: Response statuses were not raised. (#2) * fix: Response statuses were not raised. * style: Fix format. --- .github/workflows/release.yml | 128 ++++++++++++++++++++++++++++++++ src/libreclient/_base_client.py | 2 + src/libreclient/_route_types.py | 0 tests/unit/test_client.py | 50 +++++++++++++ 4 files changed, 180 insertions(+) create mode 100644 .github/workflows/release.yml delete mode 100644 src/libreclient/_route_types.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ef20995 --- /dev/null +++ b/.github/workflows/release.yml @@ -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 diff --git a/src/libreclient/_base_client.py b/src/libreclient/_base_client.py index 20ee746..19a1437 100644 --- a/src/libreclient/_base_client.py +++ b/src/libreclient/_base_client.py @@ -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( @@ -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 diff --git a/src/libreclient/_route_types.py b/src/libreclient/_route_types.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 7df702a..8a389d9 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -1,5 +1,8 @@ import asyncio +import pytest +from niquests import HTTPError + from libreclient.client import LibreClientAsync, LibreClientSync from libreclient.config import LibreConfig @@ -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()) From bdfc84380bd751571d004471a3f4a7347b77a777 Mon Sep 17 00:00:00 2001 From: Justin Jeffery <34625666+jjeff07@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:18:22 -0400 Subject: [PATCH 4/4] ci: Fix CI/CD. --- .github/workflows/ci.yml | 19 +++++---- .github/workflows/release.yml | 73 ++++++++++++++++++++++++++--------- 2 files changed, 64 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0fd8698..e2f9340 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,11 +11,11 @@ jobs: name: Lint & Format runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: astral-sh/setup-uv@v4 + - uses: astral-sh/setup-uv@v8.2.0 with: - enable-cache: true + enable-cache: "true" - name: Install dependencies run: uv sync @@ -30,11 +30,11 @@ jobs: name: Complexity Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: astral-sh/setup-uv@v4 + - uses: astral-sh/setup-uv@v8.2.0 with: - enable-cache: true + enable-cache: "true" - name: Install dependencies run: uv sync @@ -49,15 +49,14 @@ jobs: matrix: python-version: ["3.12", "3.13"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: astral-sh/setup-uv@v4 + - uses: astral-sh/setup-uv@v8.2.0 with: - enable-cache: true + enable-cache: "true" - name: Install dependencies run: uv sync --python ${{ matrix.python-version }} - name: Run tests run: uv run pytest --tb=short - diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef20995..1ae610f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,21 +18,20 @@ permissions: jobs: release: - name: Bump, Changelog, Build & Publish + name: Bump, Changelog, Build & Tag runs-on: ubuntu-latest - environment: - name: pypi - url: https://pypi.org/project/libreclient/ + outputs: + version: ${{ steps.bump.outputs.version }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.RELEASE_TOKEN }} - - uses: astral-sh/setup-uv@v4 + - uses: astral-sh/setup-uv@v8.2.0 with: - enable-cache: true + enable-cache: "true" - name: Install dependencies run: uv sync @@ -91,25 +90,24 @@ jobs: - name: Build package run: uv build + # --- Upload dist for publish jobs --- + - name: Upload dist artifacts + uses: actions/upload-artifact@v7 + with: + name: dist + path: dist/ + # --- Commit & Tag --- - name: Commit version bump and changelog run: | - git add pyproject.toml uv.lock CHANGELOG.md + git add pyproject.toml uv.lock CHANGELOG.md src/ 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 + uses: softprops/action-gh-release@v3 with: tag_name: v${{ steps.bump.outputs.version }} name: v${{ steps.bump.outputs.version }} @@ -126,3 +124,42 @@ jobs: git checkout dev git merge --ff-only origin/main git push origin dev + + publish-testpypi: + name: Publish to TestPyPI + needs: release + runs-on: ubuntu-latest + continue-on-error: true + environment: + name: testpypi + url: https://test.pypi.org/project/libreclient/ + + steps: + - uses: actions/download-artifact@v8 + with: + name: dist + path: dist/ + + - uses: astral-sh/setup-uv@v8.2.0 + + - name: Publish to TestPyPI + run: uv publish --index-url https://test.pypi.org/legacy/ + + publish-pypi: + name: Publish to PyPI + needs: release + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/libreclient/ + + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - uses: astral-sh/setup-uv@v8.2.0 + + - name: Publish to PyPI + run: uv publish