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
21 changes: 7 additions & 14 deletions .github/workflows/test-suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,26 +40,19 @@ jobs:
code_functionality:
name: "Code Functionality"
runs-on: ubuntu-latest
environment: "Test Suite"
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Pre Docker Setup
run: |
mkdir volumes/coverage
- name: Run Test Environment
run: |
docker compose up --build --exit-code-from tests
env:
HOMEASSISTANTAPI_TOKEN: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkMDE4YjQ4YzMyZTE0ODNhYjY2ZWQzOTZmYzg3ZDAyNiIsImlhdCI6MTY3ODU3NDUwMSwiZXhwIjoxOTkzOTM0NTAxfQ.fyhnfwpont4uE0gn46_Ut_pPmyn4QWv0MDaVAei2PPk" # This is non-sensitive data
- name: Post Docker Setup
run: |
sudo chown -R $USER volumes
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Install Dependencies
run: uv sync --group dev
- name: Run Tests
run: uv run pytest --cov --cov-report xml:coverage.xml
- name: Upload Coverage Report
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./volumes/coverage/coverage.xml
verbose: true
files: coverage.xml
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,4 @@ volumes/coverage/
volumes/config/.HA_VERSION
volumes/config/home-assistant*
volumes/config/deps
.code-graph/
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ WS clients accept a `max_size` parameter (default 16 MB) to handle large respons

- Unified method signatures across all four client classes for consistency
- Expanded ruff lint rules to `ALL` (from just E, F, W)
- Test coverage improved to ~99%
- Test coverage at 100%
- Modernized type annotations throughout
- Response content is now read lazily, eliminating internal `_buffer` access hacks
- Response content is now read lazily, eliminating internal `_buffer` access hacks
- Migrated HTTP/async/WebSocket transport from `requests`/`aiohttp`/`websockets` to `niquests`
- Cassette-based testing via [`nimax`](https://pypi.org/project/nimax/) — tests replay pre-recorded HTTP cassettes and no longer require a running Home Assistant instance
- CI simplified: dropped Docker Compose, pytest runs directly against cassettes
39 changes: 34 additions & 5 deletions docs/CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,39 @@ After that you are now ready to make your changes to the codebase!

Testing
********
In order to test your changes you need to have an API URL, and a Long Lived Access Token.
Follow the :ref:`Quickstart Section <access_token_setup>` for getting those.
If you setup the Development Environment then your API URL will most likely be something along the lines of :code:`https://localhost:8123/api`.
Then you can test your changes by passing the API URL, and Long Lived Access Token to the :class:`homeassistant_api.Client` object.

Tests use pre-recorded cassettes so you do **not** need a running Home Assistant instance to run the test suite.
Each test has its own cassette stored under :code:`tests/cassettes/<module>/<test_name>.json`.

Running the test suite
=======================

.. code-block:: bash

$ uv run pytest

Recording cassettes for a new test
=====================================

If you add a new test that makes real HTTP or WebSocket requests, you need to record its cassette against a live Home Assistant instance.

1. Get an API URL and a Long-Lived Access Token by following the :ref:`Quickstart Section <access_token_setup>`.
2. Export the environment variables:

.. code-block:: bash

$ export HOMEASSISTANTAPI_URL="http://<your-ha-host>:8123/api"
$ export HOMEASSISTANTAPI_WS_URL="ws://<your-ha-host>:8123/api/websocket"
$ export HOMEASSISTANTAPI_TOKEN="<your-token>"

3. Run pytest with the :code:`--record` flag to record cassettes:

.. code-block:: bash

$ uv run pytest --record

This records a fresh :code:`.json` cassette for every test.
Commit the cassette files alongside your test so CI can replay them without a live server.

.. _styling:

Expand All @@ -69,7 +98,7 @@ Code Styling Guidelines
In order to make sure that our code is easy to read, and navigate.
As well as to stop stupid mistakes like typos, undefined variables, etc.
We enforce code standards.
Using the tools, :code:`ruff`, :code:`zuban`, :code:`pytest`, and :code:`docker`, we make make sure that our code quality is top notch, and that are changes work everywhere.
Using the tools, :code:`ruff`, :code:`zuban`, and :code:`pytest`, we make sure that our code quality is top notch and that changes work everywhere.
You can those tools manually yourself, but they also run automatically when you open a PR.

Merging Your Contributions
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,13 @@ dev = [
"aiosqlite>=0.22",
"prek>=0.3.8",
"pre-commit>=4.5.1",
"nimax",
]

[tool.nimax]
cassette_library_dir = "tests/cassettes"
match_on = ["method", "uri"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "session"
Expand Down
55 changes: 55 additions & 0 deletions tests/cassettes/test_client/test_async_websocket_client_ping.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"nimax_version": "1.0.0",
"http_interactions": [],
"websocket_sessions": [
{
"uri": "ws://localhost:8123/api/websocket",
"handshake_recorded_at": "2026-04-21T02:58:30.337336+00:00",
"protocol": null,
"frames": [
{
"direction": "recv",
"type": "text",
"payload": "{\"type\":\"auth_required\",\"ha_version\":\"2026.4.3\"}",
"offset_ms": 0
},
{
"direction": "send",
"type": "text",
"payload": "{\"access_token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkMDE4YjQ4YzMyZTE0ODNhYjY2ZWQzOTZmYzg3ZDAyNiIsImlhdCI6MTY3ODU3NDUwMSwiZXhwIjoxOTkzOTM0NTAxfQ.fyhnfwpont4uE0gn46_Ut_pPmyn4QWv0MDaVAei2PPk\", \"type\": \"auth\"}",
"offset_ms": 0
},
{
"direction": "recv",
"type": "text",
"payload": "{\"type\":\"auth_ok\",\"ha_version\":\"2026.4.3\"}",
"offset_ms": 0
},
{
"direction": "send",
"type": "text",
"payload": "{\"features\": {}, \"id\": 1, \"type\": \"supported_features\"}",
"offset_ms": 1
},
{
"direction": "recv",
"type": "text",
"payload": "{\"id\":1,\"type\":\"result\",\"success\":true,\"result\":null}",
"offset_ms": 1
},
{
"direction": "send",
"type": "text",
"payload": "{\"id\": 2, \"type\": \"ping\"}",
"offset_ms": 1
},
{
"direction": "recv",
"type": "text",
"payload": "{\"id\":2,\"type\":\"pong\"}",
"offset_ms": 1
}
]
}
]
}
52 changes: 52 additions & 0 deletions tests/cassettes/test_client/test_custom_async_session.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"nimax_version": "1.0.0",
"http_interactions": [
{
"request": {
"method": "GET",
"uri": "http://localhost:8123/api/",
"headers": {},
"body": null
},
"response": {
"status": {
"code": 200,
"message": "OK"
},
"headers": {
"Content-Type": [
"application/json"
],
"Referrer-Policy": [
"no-referrer"
],
"X-Content-Type-Options": [
"nosniff"
],
"Server": [
""
],
"X-Frame-Options": [
"SAMEORIGIN"
],
"Content-Length": [
"34"
],
"Content-Encoding": [
"deflate"
],
"Date": [
"Tue, 21 Apr 2026 02:58:30 GMT"
]
},
"body": {
"string": "{\"message\":\"API running.\"}"
},
"protocol": null,
"url": "http://localhost:8123/api/"
},
"recorded_at": "2026-04-21T02:58:30.320437+00:00"
}
],
"websocket_sessions": []
}
52 changes: 52 additions & 0 deletions tests/cassettes/test_client/test_custom_session.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"nimax_version": "1.0.0",
"http_interactions": [
{
"request": {
"method": "GET",
"uri": "http://localhost:8123/api/",
"headers": {},
"body": null
},
"response": {
"status": {
"code": 200,
"message": "OK"
},
"headers": {
"Content-Type": [
"application/json"
],
"Referrer-Policy": [
"no-referrer"
],
"X-Content-Type-Options": [
"nosniff"
],
"Server": [
""
],
"X-Frame-Options": [
"SAMEORIGIN"
],
"Content-Length": [
"34"
],
"Content-Encoding": [
"deflate"
],
"Date": [
"Tue, 21 Apr 2026 02:58:30 GMT"
]
},
"body": {
"string": "{\"message\":\"API running.\"}"
},
"protocol": null,
"url": "http://localhost:8123/api/"
},
"recorded_at": "2026-04-21T02:58:30.308844+00:00"
}
],
"websocket_sessions": []
}
52 changes: 52 additions & 0 deletions tests/cassettes/test_client/test_default_async_session.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"nimax_version": "1.0.0",
"http_interactions": [
{
"request": {
"method": "GET",
"uri": "http://localhost:8123/api/",
"headers": {},
"body": null
},
"response": {
"status": {
"code": 200,
"message": "OK"
},
"headers": {
"Content-Type": [
"application/json"
],
"Referrer-Policy": [
"no-referrer"
],
"X-Content-Type-Options": [
"nosniff"
],
"Server": [
""
],
"X-Frame-Options": [
"SAMEORIGIN"
],
"Content-Length": [
"34"
],
"Content-Encoding": [
"deflate"
],
"Date": [
"Tue, 21 Apr 2026 02:58:30 GMT"
]
},
"body": {
"string": "{\"message\":\"API running.\"}"
},
"protocol": null,
"url": "http://localhost:8123/api/"
},
"recorded_at": "2026-04-21T02:58:30.324739+00:00"
}
],
"websocket_sessions": []
}
52 changes: 52 additions & 0 deletions tests/cassettes/test_client/test_default_session.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"nimax_version": "1.0.0",
"http_interactions": [
{
"request": {
"method": "GET",
"uri": "http://localhost:8123/api/",
"headers": {},
"body": null
},
"response": {
"status": {
"code": 200,
"message": "OK"
},
"headers": {
"Content-Type": [
"application/json"
],
"Referrer-Policy": [
"no-referrer"
],
"X-Content-Type-Options": [
"nosniff"
],
"Server": [
""
],
"X-Frame-Options": [
"SAMEORIGIN"
],
"Content-Length": [
"34"
],
"Content-Encoding": [
"deflate"
],
"Date": [
"Tue, 21 Apr 2026 02:58:30 GMT"
]
},
"body": {
"string": "{\"message\":\"API running.\"}"
},
"protocol": null,
"url": "http://localhost:8123/api/"
},
"recorded_at": "2026-04-21T02:58:30.313718+00:00"
}
],
"websocket_sessions": []
}
Loading
Loading