add async subject and source read methods to AsyncERClient#27
Open
JoshuaVulcan wants to merge 4 commits into
Open
add async subject and source read methods to AsyncERClient#27JoshuaVulcan wants to merge 4 commits into
JoshuaVulcan wants to merge 4 commits into
Conversation
Add get_subjects(), get_subject(), get_subject_tracks(), get_sources() (paginated async generator), and get_source_by_id() to AsyncERClient, achieving parity with the sync ERClient's existing read methods. Includes 31 new tests covering success, pagination, error handling, parameter filtering, and network failure scenarios. Resolves ERA-12652 Co-authored-by: Cursor <cursoragent@cursor.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds async subject/source read APIs to AsyncERClient to match the sync client’s subject/source read capabilities, including paginated async iteration for sources.
Changes:
- Added
get_subjects(),get_subject(),get_subject_tracks(),get_sources(), andget_source_by_id()toAsyncERClient. - Added async tests covering success, empty results, query params, pagination, and error handling for the new endpoints.
- Added an agent-focused documentation note capturing async test mocking patterns and merge guidance.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
erclient/client.py |
Adds the new async subject/source read methods and wires get_sources() into existing _get_data() pagination/batching. |
tests/async_client/test_get_subjects.py |
New tests for get_subjects() and get_subject() (params + error cases). |
tests/async_client/test_get_subject_tracks.py |
New tests for get_subject_tracks() including date-range param behavior and errors. |
tests/async_client/test_get_sources.py |
New tests for paginated get_sources() and get_source_by_id() (success + error cases). |
docs/AGENT_LEARNINGS.md |
Adds internal notes about merge/test patterns and respx base URL usage. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| """ | ||
| return await self._get(f"spatialfeaturegroup/{feature_group_id}", params={}) | ||
|
|
||
| async def get_subjects(self, **kwargs): |
|
|
||
| ## 5. Strategy and merge order | ||
|
|
||
| - Full merge strategy and wave order: see **ER_CLIENT_MERGE_STRATEGY_RECOMMENDATION.md** in the DAS repo (`/Users/joshuak/Projects/das/`) if available, or the same filename in notes. |
Comment on lines
+1662
to
+1677
| async def get_sources(self, **kwargs): | ||
| """ | ||
| Returns an async generator to iterate over sources (paginated). | ||
|
|
||
| Optional kwargs: | ||
| page_size: Change the page size. Default 100. | ||
| batch_size: The generator returns sources in batches (list) | ||
| instead of one by one. Default 0 (means no batching). | ||
| """ | ||
| page_size = kwargs.get('page_size', 100) | ||
| batch_size = kwargs.get('batch_size', 0) | ||
| params = {'page_size': page_size} | ||
| if batch_size and page_size: | ||
| params['page_size'] = batch_size | ||
| async for source in self._get_data(endpoint='sources', params=params, batch_size=batch_size): | ||
| yield source |
Comment on lines
+1667
to
+1675
| page_size: Change the page size. Default 100. | ||
| batch_size: The generator returns sources in batches (list) | ||
| instead of one by one. Default 0 (means no batching). | ||
| """ | ||
| page_size = kwargs.get('page_size', 100) | ||
| batch_size = kwargs.get('batch_size', 0) | ||
| params = {'page_size': page_size} | ||
| if batch_size and page_size: | ||
| params['page_size'] = batch_size |
Comment on lines
+1674
to
+1675
| if batch_size and page_size: | ||
| params['page_size'] = batch_size |
Comment on lines
+3
to
+6
| **Purpose:** Domain-specific context for agents and humans continuing the JoshuaVulcan PR merge work (main → open PR branches). Append to this file as new learnings arise. | ||
|
|
||
| **Repo:** PADAS/er-client. **Base branch:** `main`. Use `gh` CLI (not GitHub MCP; org has SAML). | ||
|
|
Comment on lines
+58
to
+71
| - Full merge strategy and wave order: see **ER_CLIENT_MERGE_STRATEGY_RECOMMENDATION.md** in the DAS repo (`/Users/joshuak/Projects/das/`) if available, or the same filename in notes. | ||
| - Merges are done by hand in the GitHub UI (merge commits). This doc and the learnings here support the person/agent doing the merge-main and conflict resolution on each branch. | ||
|
|
||
| --- | ||
|
|
||
| ## 6. Progress (merge main into PR branches) | ||
|
|
||
| | PR | Branch | Status | | ||
| |----|--------|--------| | ||
| | 24 | ERA-12672/async-post-observation | Already up to date with main | | ||
| | 25 | ERA-12658/v2-event-type-schema-updates | Merged main, resolved client.py, pushed | | ||
| | 26 | ERA-12654/sync-get-event | Merged main, kept #26 get_event + main conftest, pushed | | ||
| | 27 | ERA-12652/async-subject-source-read | Merged main; respx base_url fixed; **commit/push + next PR pending** | | ||
| | 28–44 | (see strategy doc) | Pending | |
Comment on lines
+205
to
+236
| @pytest.mark.asyncio | ||
| async def test_get_sources_unauthorized(er_client): | ||
| """Test get_sources raises ERClientBadCredentials on 401""" | ||
| async with respx.mock( | ||
| base_url=er_client._api_root("v1.0"), assert_all_called=False | ||
| ) as respx_mock: | ||
| route = respx_mock.get("sources") | ||
| route.return_value = httpx.Response(httpx.codes.UNAUTHORIZED, json={}) | ||
|
|
||
| with pytest.raises(ERClientBadCredentials): | ||
| async for _ in er_client.get_sources(): | ||
| pass | ||
|
|
||
| assert route.called | ||
| await er_client.close() | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_get_sources_network_error(er_client): | ||
| """Test get_sources raises ERClientException on network error""" | ||
| async with respx.mock( | ||
| base_url=er_client._api_root("v1.0"), assert_all_called=False | ||
| ) as respx_mock: | ||
| route = respx_mock.get("sources") | ||
| route.side_effect = httpx.ConnectTimeout | ||
|
|
||
| with pytest.raises(ERClientException): | ||
| async for _ in er_client.get_sources(): | ||
| pass | ||
|
|
||
| assert route.called | ||
| await er_client.close() |
Comment on lines
+1671
to
+1676
| page_size = kwargs.get('page_size', 100) | ||
| batch_size = kwargs.get('batch_size', 0) | ||
| params = {'page_size': page_size} | ||
| if batch_size and page_size: | ||
| params['page_size'] = batch_size | ||
| async for source in self._get_data(endpoint='sources', params=params, batch_size=batch_size): |
| Optional kwargs: | ||
| page_size: Change the page size. Default 100. | ||
| batch_size: The generator returns sources in batches (list) | ||
| instead of one by one. Default 0 (means no batching). |
Comment on lines
+1
to
+75
| # Agent learnings: er-client merge and test patterns | ||
|
|
||
| **Purpose:** Domain-specific context for agents and humans continuing the JoshuaVulcan PR merge work (main → open PR branches). Append to this file as new learnings arise. | ||
|
|
||
| **Repo:** PADAS/er-client. **Base branch:** `main`. Use `gh` CLI (not GitHub MCP; org has SAML). | ||
|
|
||
| --- | ||
|
|
||
| ## 1. Project state (as of 2026-02-11) | ||
|
|
||
| - **#23** (API versioning): merged. `_api_root(version)`, `base_url` params, `api_paths.py`, service_root normalization are on `main`. | ||
| - **#45** (canonical async _delete + 204 in _call): merged. One-liner `_delete` and 204 handling are on `main`. | ||
| - **#46** (shared sync_client conftest): merged. Canonical `tests/sync_client/conftest.py` and `__init__.py` are on `main`. | ||
| - **#38**: closed. Use **#41** for analyzers/choices/gear/reports. | ||
| - **#30**: merge **almost last**; coordinate with #23’s developer. | ||
|
|
||
| --- | ||
|
|
||
| ## 2. Respx / async test URL pattern (PR #24) | ||
|
|
||
| After #23, the client builds request URLs with `_api_root(version)` (e.g. `https://example.org/api/v1.0/...`). Conftest’s `er_server_info["service_root"]` may still be the full URL including `/api/v1.0`. | ||
|
|
||
| **Rule:** In async tests that use `respx.mock()`, set the mock **base_url** to the same base the client uses: | ||
|
|
||
| - **Use:** `base_url=er_client._api_root("v1.0")` | ||
| - **Do not use:** `base_url=er_client.service_root` | ||
|
|
||
| Otherwise you get `RESPX: <Request('GET', '.../api/v1.0/subjects')> not mocked!` because the mock was registered against a different base. | ||
|
|
||
| **Reference:** Latest commit on PR #24 (`ERA-12672/async-post-observation`), file `tests/async_client/test_post_observation.py`. | ||
|
|
||
| **Files updated so far with this fix:** | ||
| `test_get_subjects.py`, `test_get_subject_tracks.py`, `test_get_sources.py` (PR 27). | ||
|
|
||
| --- | ||
|
|
||
| ## 3. Merging main into a PR branch | ||
|
|
||
| 1. `git fetch origin && git checkout <PR-branch> && git merge origin/main -m "Merge main to resolve conftest and infra conflicts"`. | ||
| 2. **Conflicts on conftest:** | ||
| For `tests/sync_client/conftest.py` and `tests/sync_client/__init__.py`, keep **main’s** version: | ||
| `git checkout --theirs -- tests/sync_client/conftest.py tests/sync_client/__init__.py` then `git add` those files. | ||
| 3. Resolve any other conflicts in `erclient/client.py` (or elsewhere) by combining main’s infra with the PR’s new methods; remove duplicate method definitions (e.g. keep one `get_event`). | ||
| 4. Run tests: `uv run pytest tests/sync_client/ tests/async_client/ -q --tb=line`. Fix any new failures (often respx `base_url` as above). | ||
| 5. Commit and push. If push is rejected (branch behind remote), `git pull origin <branch> --no-rebase`, resolve any new conftest conflict again with main’s version, commit, push. | ||
|
|
||
| --- | ||
|
|
||
| ## 4. Sync client `get_event` and tests | ||
|
|
||
| - **#26** keeps a single `get_event(event_id, ...)` (positional) for both sync and async; main’s keyword-only `get_event(*, event_id=None, ...)` was removed to avoid duplicate. | ||
| - Sync tests that assert exact URL equality with `er_client.service_root` can fail after #23. Prefer asserting that the path and `event_id` appear in the URL (e.g. `assert f"activity/event/{event_id}" in url`). | ||
|
|
||
| --- | ||
|
|
||
| ## 5. Strategy and merge order | ||
|
|
||
| - Full merge strategy and wave order: see **ER_CLIENT_MERGE_STRATEGY_RECOMMENDATION.md** in the DAS repo (`/Users/joshuak/Projects/das/`) if available, or the same filename in notes. | ||
| - Merges are done by hand in the GitHub UI (merge commits). This doc and the learnings here support the person/agent doing the merge-main and conflict resolution on each branch. | ||
|
|
||
| --- | ||
|
|
||
| ## 6. Progress (merge main into PR branches) | ||
|
|
||
| | PR | Branch | Status | | ||
| |----|--------|--------| | ||
| | 24 | ERA-12672/async-post-observation | Already up to date with main | | ||
| | 25 | ERA-12658/v2-event-type-schema-updates | Merged main, resolved client.py, pushed | | ||
| | 26 | ERA-12654/sync-get-event | Merged main, kept #26 get_event + main conftest, pushed | | ||
| | 27 | ERA-12652/async-subject-source-read | Merged main; respx base_url fixed; **commit/push + next PR pending** | | ||
| | 28–44 | (see strategy doc) | Pending | | ||
|
|
||
| --- | ||
|
|
||
| *Append new learnings below as you go.* |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
get_subjects(),get_subject(),get_subject_tracks(),get_sources(), andget_source_by_id()toAsyncERClient, achieving feature parity with the syncERClientfor subject and source read operations.get_sources()is implemented as a paginated async generator using the existing_get_data()infrastructure, consistent withget_events()andget_observations().subject_group,include_inactive, date range for tracks).Test Plan
subject_group,include_inactive,since/until)get_sources)ERClient*exceptionsConnectTimeout,ReadTimeout) raisingERClientExceptionJira
Resolves ERA-12652