Skip to content

add async post_observation() to AsyncERClient#24

Merged
chrisdoehring merged 13 commits into
mainfrom
ERA-12672/async-post-observation
May 26, 2026
Merged

add async post_observation() to AsyncERClient#24
chrisdoehring merged 13 commits into
mainfrom
ERA-12672/async-post-observation

Conversation

@JoshuaVulcan
Copy link
Copy Markdown
Contributor

@JoshuaVulcan JoshuaVulcan commented Feb 10, 2026

Summary

  • Adds post_observation() to AsyncERClient, matching the sync ERClient's existing method
  • Supports posting a single observation or a list of observations
  • Applies _clean_observation() to normalize recorded_at datetime objects to ISO format strings
  • 7 new tests covering success (single + list), datetime cleaning, timeouts, 403, 404, and 409 responses

Test plan

  • uv run pytest -- all 97 tests pass (90 existing + 7 new)

Resolves ERA-12672

Adds post_observation() to the async client, matching the sync
ERClient's existing method. Supports single observations and lists,
applies _clean_observation() to normalize recorded_at to ISO format.

ERA-12672

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds the post_observation() async method to AsyncERClient, bringing it to feature parity with the existing sync ERClient. The implementation correctly mirrors the sync version by handling both single observations and lists, applying datetime normalization through _clean_observation(), and posting to the observations endpoint.

Changes:

  • Added async def post_observation() method to AsyncERClient class
  • Added comprehensive test suite with 7 tests covering success cases, datetime cleaning, and various error scenarios (timeout, 403, 404, 409)

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
erclient/client.py Added async post_observation() method that mirrors the sync version, handling single/list observations with datetime cleaning
tests/async_client/test_post_observation.py New test file with 7 tests covering success, list handling, datetime conversion, and error scenarios

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/async_client/test_post_observation.py Outdated
Comment thread tests/async_client/test_post_observation.py
Comment thread tests/async_client/test_post_observation.py Outdated
Comment thread tests/async_client/test_post_observation.py
@JoshuaVulcan JoshuaVulcan added autoreviewing PR is currently being auto-reviewed and removed autoreviewing PR is currently being auto-reviewed labels Feb 11, 2026
JoshuaVulcan and others added 2 commits February 11, 2026 08:53
Exercises the isinstance(observation, (list, set)) branch with a
monkeypatched _clean_observation since dicts are unhashable and
cannot be placed in a real set.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@JoshuaVulcan JoshuaVulcan requested a review from a team as a code owner February 11, 2026 22:26
JoshuaVulcan and others added 8 commits February 11, 2026 14:32
Co-authored-by: Cursor <cursoragent@cursor.com>
…ervation tests

Co-authored-by: Cursor <cursoragent@cursor.com>
Register observation route with full URL (service_root + '/observations')
so respx matches the client request. urljoin(base_url, 'observations')
yields .../api/observations, which did not match .../api/v1.0/observations.

Co-authored-by: Cursor <cursoragent@cursor.com>
Omit base_url from respx.mock() and register the observations route with
the full URL only. Avoids base_url merging that can prevent the route
from matching the actual request in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>
… async tests

Match test_post_sensor_observation pattern so respx route matches the
request URL across Python 3.10-3.12. Remove unused re import.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ng slash)

Co-authored-by: Cursor <cursoragent@cursor.com>
JoshuaVulcan added a commit that referenced this pull request Feb 12, 2026
Match PR #24 pattern so mocks match client request URL after #23.
Add docs/AGENT_LEARNINGS.md for merge and test patterns.

Co-authored-by: Cursor <cursoragent@cursor.com>
chrisdoehring
chrisdoehring previously approved these changes Mar 13, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

import respx

from erclient import (ERClientException, ERClientNotFound,
ERClientPermissionDenied, ERClientServiceUnreachable)
Comment on lines +35 to +36
response = await er_client.post_observation(observations)
assert route.called
with pytest.MonkeyPatch.context() as mp:
mp.setattr(er_client, '_clean_observation', lambda o: list(o))
response = await er_client.post_observation(observations)

Comment on lines +49 to +50
This test uses a frozenset wrapper to exercise the isinstance(obs, (list, set))
branch and confirm it produces a list payload.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

import respx

from erclient import (ERClientException, ERClientNotFound,
ERClientPermissionDenied, ERClientServiceUnreachable)
Comment on lines +46 to +55
"""Verify that a set input is iterated and each element cleaned individually.

Note: in practice, observation dicts are unhashable so callers use lists.
This test uses a frozenset wrapper to exercise the isinstance(obs, (list, set))
branch and confirm it produces a list payload.
"""
# Use simple hashable stand-ins to verify the set branch
obs_a = ("obs_a",)
obs_b = ("obs_b",)
observations = {obs_a, obs_b} # a real set
Comment on lines +66 to +74
with pytest.MonkeyPatch.context() as mp:
mp.setattr(er_client, '_clean_observation', lambda o: list(o))
response = await er_client.post_observation(observations)

assert route.called
request_body = json.loads(route.calls[0].request.content)
assert isinstance(request_body, list)
assert len(request_body) == 2
await er_client.close()
Comment on lines +144 to +154
async def test_post_observation_conflict(er_client, position, conflict_response):
async with respx.mock(
base_url=er_client._api_root("v1.0"), assert_all_called=False
) as respx_mock:
route = respx_mock.post('observations')
route.return_value = httpx.Response(
httpx.codes.CONFLICT, json=conflict_response)
with pytest.raises(ERClientException) as exc_info:
await er_client.post_observation(position)
assert exc_info.value.status_code == httpx.codes.CONFLICT
assert route.called
- Drop unused import ERClientServiceUnreachable; import
  ERClientRateLimitExceeded instead (Copilot: unused import / autoflake).
- Assert response == {} in list-success test instead of leaving the
  return value unused (Copilot: unused variable + missing assertion).
- Remove unused response assignment in set-input test (Copilot: unused
  variable).
- Fix set-input test docstring that said "frozenset wrapper" while the
  code uses a plain set (Copilot: docstring mismatch).
- Tighten the 409 test to assert ERClientRateLimitExceeded, the specific
  subclass _handle_http_status_error maps CONFLICT to, instead of the
  generic ERClientException (Copilot: assert specific exception type).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@chrisdoehring chrisdoehring merged commit f04c66a into main May 26, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants