ERA-12688: Add GPX file upload and status tracking to both clients#43
Open
JoshuaVulcan wants to merge 5 commits intomainfrom
Open
ERA-12688: Add GPX file upload and status tracking to both clients#43JoshuaVulcan wants to merge 5 commits intomainfrom
JoshuaVulcan wants to merge 5 commits intomainfrom
Conversation
Add upload_gpx() and get_gpx_upload_status() methods to ERClient (sync)
and AsyncERClient (async) for uploading GPX files to a source and
polling the resulting background task for completion.
- upload_gpx(source_id, filepath=None, file=None) -- POST multipart
form to source/{id}/gpxdata, accepts either a file path or an
already-opened file object
- get_gpx_upload_status(source_id, task_id) -- GET the task status at
source/{id}/gpxdata/status/{task_id}
Includes comprehensive tests for both clients covering success, error
handling (not found, forbidden, timeout, server error), and input
validation (no file provided raises ValueError).
Co-authored-by: Cursor <cursoragent@cursor.com>
# Conflicts: # tests/sync_client/conftest.py
Co-authored-by: Cursor <cursoragent@cursor.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds GPX upload support and background-task status polling to both the sync (ERClient) and async (AsyncERClient) EarthRanger clients, along with new test coverage for the new endpoints.
Changes:
- Add
upload_gpx(source_id, filepath=None, file=None)to sync + async clients (multipart upload tosource/{id}/gpxdata). - Add
get_gpx_upload_status(source_id, task_id)to sync + async clients (pollsource/{id}/gpxdata/status/{task_id}). - Add new sync + async test modules covering success and several error scenarios.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
erclient/client.py |
Adds new sync/async client methods for GPX upload and task-status polling. |
tests/sync_client/test_gpx_upload.py |
New sync tests for GPX upload + status polling (currently missing some scenarios claimed in PR description). |
tests/async_client/test_gpx_upload.py |
New async tests for GPX upload + status polling. |
💡 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
+1651
to
+1670
| async def upload_gpx(self, source_id, filepath=None, file=None): | ||
| """ | ||
| Upload a GPX file for a source. Returns an async task reference. | ||
|
|
||
| :param source_id: The source UUID | ||
| :param filepath: Path to a .gpx file on disk (mutually exclusive with file) | ||
| :param file: An already-opened file-like object (mutually exclusive with filepath) | ||
| :return: Task data dict (contains task_id for status polling) | ||
| """ | ||
| self.logger.debug(f'Uploading GPX for source {source_id}') | ||
| path = f'source/{source_id}/gpxdata' | ||
| if file: | ||
| files = {'gpx_file': file} | ||
| return await self._post_form(path, files=files) | ||
| elif filepath: | ||
| with open(filepath, 'rb') as f: | ||
| files = {'gpx_file': f} | ||
| return await self._post_form(path, files=files) | ||
| else: | ||
| raise ValueError('Either filepath or file must be provided') |
Comment on lines
+15
to
+17
| from .conftest import _mock_response | ||
|
|
||
|
|
Comment on lines
+93
to
+141
| class TestUploadGpx: | ||
| def test_upload_gpx_with_file_object_success(self, er_client, gpx_file, gpx_upload_response): | ||
| mock_resp = _mock_response(201, json_data=gpx_upload_response) | ||
| with patch.object(requests, "post", return_value=mock_resp) as mock_post: | ||
| result = er_client.upload_gpx(source_id=SOURCE_ID, file=gpx_file) | ||
| assert mock_post.called | ||
| assert result == gpx_upload_response["data"] | ||
| assert result["process_status"]["task_id"] == TASK_ID | ||
|
|
||
| def test_upload_gpx_with_filepath_success(self, er_client, gpx_file_content, gpx_upload_response, tmp_path): | ||
| gpx_path = tmp_path / "test_track.gpx" | ||
| gpx_path.write_bytes(gpx_file_content) | ||
|
|
||
| mock_resp = _mock_response(201, json_data=gpx_upload_response) | ||
| with patch.object(requests, "post", return_value=mock_resp) as mock_post: | ||
| result = er_client.upload_gpx(source_id=SOURCE_ID, filepath=str(gpx_path)) | ||
| assert mock_post.called | ||
| assert result == gpx_upload_response["data"] | ||
|
|
||
| def test_upload_gpx_no_file_raises_error(self, er_client): | ||
| with pytest.raises(ValueError, match="Either filepath or file must be provided"): | ||
| er_client.upload_gpx(source_id=SOURCE_ID) | ||
|
|
||
| def test_upload_gpx_not_found(self, er_client, gpx_file): | ||
| not_found = {"status": {"code": 404, "detail": "source not found"}} | ||
| mock_resp = _mock_response(404, json_data=not_found) | ||
| with patch.object(requests, "post", return_value=mock_resp): | ||
| with pytest.raises(ERClientNotFound): | ||
| er_client.upload_gpx(source_id=SOURCE_ID, file=gpx_file) | ||
|
|
||
| def test_upload_gpx_forbidden(self, er_client, gpx_file): | ||
| forbidden = { | ||
| "status": { | ||
| "code": 403, | ||
| "detail": "You do not have permission to perform this action.", | ||
| } | ||
| } | ||
| mock_resp = _mock_response(403, json_data=forbidden) | ||
| with patch.object(requests, "post", return_value=mock_resp): | ||
| with pytest.raises(ERClientPermissionDenied): | ||
| er_client.upload_gpx(source_id=SOURCE_ID, file=gpx_file) | ||
|
|
||
| def test_upload_gpx_server_error(self, er_client, gpx_file): | ||
| error_resp = {"status": {"code": 500, "message": "Internal Server Error"}} | ||
| mock_resp = _mock_response(500, json_data=error_resp) | ||
| with patch.object(requests, "post", return_value=mock_resp): | ||
| with pytest.raises(ERClientException): | ||
| er_client.upload_gpx(source_id=SOURCE_ID, file=gpx_file) | ||
|
|
Comment on lines
+1
to
+8
| import io | ||
| import json | ||
| import re | ||
|
|
||
| import httpx | ||
| import pytest | ||
| import respx | ||
|
|
Comment on lines
+1056
to
+1075
| def upload_gpx(self, source_id, filepath=None, file=None): | ||
| """ | ||
| Upload a GPX file for a source. Returns an async task reference. | ||
|
|
||
| :param source_id: The source UUID | ||
| :param filepath: Path to a .gpx file on disk (mutually exclusive with file) | ||
| :param file: An already-opened file-like object (mutually exclusive with filepath) | ||
| :return: Task data dict (contains task_id for status polling) | ||
| """ | ||
| self.logger.debug(f'Uploading GPX for source {source_id}') | ||
| path = f'source/{source_id}/gpxdata' | ||
| if file: | ||
| files = {'gpx_file': file} | ||
| return self._post_form(path, files=files) | ||
| elif filepath: | ||
| with open(filepath, 'rb') as f: | ||
| files = {'gpx_file': f} | ||
| return self._post_form(path, files=files) | ||
| else: | ||
| raise ValueError('Either filepath or file must be provided') |
Comment on lines
+1069
to
+1073
| return self._post_form(path, files=files) | ||
| elif filepath: | ||
| with open(filepath, 'rb') as f: | ||
| files = {'gpx_file': f} | ||
| return self._post_form(path, files=files) |
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
upload_gpx(source_id, filepath=None, file=None)to bothERClientandAsyncERClient-- POSTs a multipart form tosource/{id}/gpxdatawith thegpx_filefield, accepting either a file path on disk or a pre-opened file-like objectget_gpx_upload_status(source_id, task_id)to both clients -- GETssource/{id}/gpxdata/status/{task_id}to poll background task progressTest plan
unittest.mockpatchingEndpoints covered
source/{id}/gpxdatasource/{id}/gpxdata/status/{task_id}