Skip to content

ERA-12680: Add v2 subject tracks and async track methods#40

Open
JoshuaVulcan wants to merge 4 commits intomainfrom
ERA-12680
Open

ERA-12680: Add v2 subject tracks and async track methods#40
JoshuaVulcan wants to merge 4 commits intomainfrom
ERA-12680

Conversation

@JoshuaVulcan
Copy link
Contributor

@JoshuaVulcan JoshuaVulcan commented Feb 11, 2026

Summary

  • Adds a version parameter to ERClient.get_subject_tracks() to support both the v1 (flat coordinates, default) and v2 (segmented GeoJSON FeatureCollection at /api/v2.0/subject/{id}/tracks/) endpoints.
  • Adds _er_url_versioned() helper to both sync and async clients for building version-specific API URLs.
  • Adds async get_subject_tracks() with identical v1/v2 support to AsyncERClient.
  • Adds async get_subject_source_tracks() for per-source track retrieval.
  • Fixes async _call() to properly handle full URLs (skips _er_url() prepend when path already starts with http).
  • v2 endpoint supports additional filter query params: show_excluded, group_by_flags, max_speed_kmh, max_gap_ms, max_gap_seconds, max_gap_minutes.
  • Adds 22 new tests covering sync and async clients, v1/v2 versions, date filtering, extra params, error handling, and URL construction.

Jira

ERA-12680

Test plan

  • All 109 tests pass (22 new + 87 existing)
  • Sync get_subject_tracks() v1 default behavior preserved
  • Sync get_subject_tracks(version='2.0') hits /api/v2.0/ endpoint
  • v2 extra params (show_excluded, max_speed_kmh, etc.) forwarded correctly
  • Unknown kwargs are not leaked as query params
  • Async get_subject_tracks() v1 and v2 both work correctly
  • Async get_subject_source_tracks() hits correct endpoint
  • 404 errors raise ERClientNotFound for both versions
  • _er_url_versioned() correctly swaps version in URL

- Add `version` parameter to `ERClient.get_subject_tracks()` supporting
  both v1 (flat coordinates, default) and v2 (segmented GeoJSON
  FeatureCollection at /api/v2.0/subject/{id}/tracks/).
- Add `_er_url_versioned()` helper to both clients for building
  versioned API URLs.
- Add async `get_subject_tracks()` with identical v1/v2 support.
- Add async `get_subject_source_tracks()` for per-source track retrieval.
- Fix async `_call()` to handle full URLs (skip `_er_url()` prepend).
- v2 forwards extra query params: show_excluded, group_by_flags,
  max_speed_kmh, max_gap_ms, max_gap_seconds, max_gap_minutes.
- Add comprehensive sync and async test suites (22 new tests).

Co-authored-by: Cursor <cursoragent@cursor.com>
@JoshuaVulcan JoshuaVulcan added autoreviewing PR is currently being auto-reviewed and removed autoreviewing PR is currently being auto-reviewed labels Feb 11, 2026
…rl pattern

Replace the brittle `_er_url_versioned()` method (which uses regex
substitution on service_root) with the cleaner `_api_root(version)` +
`base_url` parameter pattern from PR #23. This adds `_api_root()`,
updates `_er_url()` to accept a `base_url` param, and threads
`base_url` through `_get`, `_post`, `_patch`, and `_call` on both
sync and async clients.

Updates get_subject_tracks(version='2.0') to use
`base_url=self._api_root('v2.0')` instead of building versioned URLs
via regex. Updates tests to use `_api_root()` instead of manual
string replacement.

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

V2 URL Convention Alignment

Replaced the _er_url_versioned() regex-based method with the _api_root(version) + base_url parameter pattern established in PR #23.

Changes:

  • Removed _er_url_versioned() from both ERClient and AsyncERClient
  • Added _api_root(version) method to both clients
  • Updated _er_url() to accept an optional base_url parameter
  • Threaded base_url through _get, _post, _patch, and _call on both sync and async clients
  • Updated get_subject_tracks(version='2.0') to use base_url=self._api_root('v2.0') instead of building versioned URLs via regex
  • Updated tests to use er_client._api_root('v2.0') instead of manual string replacement, and replaced _er_url_versioned helper tests with _api_root tests
  • Reverted the path.startswith('http') guard in async _call() in favor of the base_url approach

This PR is now compatible with PR #23's infrastructure and will merge cleanly once #23 lands.

… test URL base

Co-authored-by: Cursor <cursoragent@cursor.com>
@JoshuaVulcan JoshuaVulcan requested a review from a team as a code owner February 12, 2026 02:27
Copy link
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

Adds v2 (segmented) subject track retrieval support alongside the existing v1 endpoint, and expands async client parity, with new sync/async test coverage for URL construction and filtering behavior.

Changes:

  • Extend sync ERClient.get_subject_tracks() to support a v2 tracks endpoint and forward v2-specific filter params.
  • Add async get_subject_tracks() (v1/v2) and async get_subject_source_tracks() to AsyncERClient.
  • Add new sync + async test modules covering v1/v2 behavior, date filtering, extra params, and 404 handling.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 11 comments.

File Description
erclient/client.py Adds sync+async subject track methods with v2 endpoint support and new async per-source track retrieval.
tests/sync_client/test_get_subject_tracks.py New sync tests validating v1 default behavior, v2 routing, params forwarding, and _api_root.
tests/async_client/test_get_subject_tracks.py New async tests for v1/v2 tracks, per-source tracks, query params, and 404 error mapping.

💡 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.

Comment on lines +1 to +4
import httpx
import pytest
import respx
from datetime import datetime, timezone
Comment on lines +173 to +179
result = await er_client.get_subject_tracks(
subject_id=SUBJECT_ID,
version="2.0",
show_excluded="true",
max_speed_kmh=120.0,
max_gap_minutes=30,
)
Comment on lines +133 to +135
result = await er_client.get_subject_tracks(
subject_id=SUBJECT_ID, version="2.0"
)
Comment on lines +889 to +892
def get_subject_tracks(self, subject_id='', start=None, end=None, version='1.0', **kwargs):
"""
Get the latest tracks for the Subject having the given subject_id.

Comment on lines +889 to 921
def get_subject_tracks(self, subject_id='', start=None, end=None, version='1.0', **kwargs):
"""
Get the latest tracks for the Subject having the given subject_id.

:param subject_id: The UUID of the subject.
:param start: datetime lower-bound filter (sent as ``since``).
:param end: datetime upper-bound filter (sent as ``until``).
:param version: API version string, either '1.0' (default, legacy flat
coordinates) or '2.0' (segmented GeoJSON FeatureCollection).
:param kwargs: Extra query params forwarded to the v2 endpoint such as
``show_excluded``, ``group_by_flags``, ``max_speed_kmh``,
``max_gap_ms``, ``max_gap_seconds``, ``max_gap_minutes``.
:return: Track data (format varies by version).
"""
p = {}
if start is not None and isinstance(start, datetime):
p['since'] = start.isoformat()
if end is not None and isinstance(end, datetime):
p['until'] = end.isoformat()

if version == '2.0':
# v2 supports additional filter params
for key in ('show_excluded', 'group_by_flags', 'max_speed_kmh',
'max_gap_ms', 'max_gap_seconds', 'max_gap_minutes'):
if key in kwargs:
p[key] = kwargs[key]
return self._get(
path=f'subject/{subject_id}/tracks/',
base_url=self._api_root('v2.0'),
params=p,
)

return self._get(path='subject/{0}/tracks'.format(subject_id), params=p)
Comment on lines +153 to +156
result = await er_client.get_subject_tracks(
subject_id=SUBJECT_ID, start=start, end=end, version="2.0"
)

)

start = datetime(2025, 1, 1, tzinfo=timezone.utc)
result = await er_client.get_subject_source_tracks(

import pytest

from erclient.client import ERClient
Comment on lines +126 to +128
result = er_client.get_subject_tracks(
subject_id=SUBJECT_ID, version="2.0"
)
Comment on lines +1646 to +1658
if version == '2.0':
for key in ('show_excluded', 'group_by_flags', 'max_speed_kmh',
'max_gap_ms', 'max_gap_seconds', 'max_gap_minutes'):
if key in kwargs:
p[key] = kwargs[key]
return await self._get(
path=f'subject/{subject_id}/tracks/',
base_url=self._api_root('v2.0'),
params=p,
)

return await self._get(path=f'subject/{subject_id}/tracks', params=p)

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.

2 participants