From 39dc55761a7c28a92808ed942b58377877f374a6 Mon Sep 17 00:00:00 2001 From: JoshuaVulcan <38018017+JoshuaVulcan@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:05:19 -0800 Subject: [PATCH 1/3] Add mapping and spatial feature methods to both sync and async clients ERA-12666: Adds complete feature parity for mapping/spatial endpoints: ERClient (sync) and AsyncERClient (async): - get_features, get_feature: GeoJSON feature read - get_featuresets, get_featureset: Feature set read - get_maps: Map listing - get_layers, get_layer: Map layer read - get_featureclasses: Spatial feature type listing - get_spatialfeaturegroups, get_spatialfeaturegroup: Spatial feature group list/detail - post_spatialfeaturegroup, patch_spatialfeaturegroup, delete_spatialfeaturegroup: Group CRUD - get_spatialfeatures, get_spatialfeature: Spatial feature list/detail - post_spatialfeature, patch_spatialfeature, delete_spatialfeature: Feature CRUD - AsyncERClient._delete helper for DELETE operations Includes 54 tests covering success, not-found, forbidden, and error scenarios for both sync and async clients. Co-authored-by: Cursor --- erclient/client.py | 156 ++++++++ tests/async_client/test_mapping_spatial.py | 439 +++++++++++++++++++++ tests/sync_client/__init__.py | 0 tests/sync_client/conftest.py | 21 + tests/sync_client/test_mapping_spatial.py | 278 +++++++++++++ 5 files changed, 894 insertions(+) create mode 100644 tests/async_client/test_mapping_spatial.py create mode 100644 tests/sync_client/__init__.py create mode 100644 tests/sync_client/conftest.py create mode 100644 tests/sync_client/test_mapping_spatial.py diff --git a/erclient/client.py b/erclient/client.py index 06de6e3..c15ff14 100644 --- a/erclient/client.py +++ b/erclient/client.py @@ -987,6 +987,84 @@ def get_sources(self, page_size=100): def get_users(self): return self._get('users') + # -- Mapping / Spatial Feature Methods -- + + def get_features(self): + """Get a list of features (GeoJSON).""" + return self._get('features') + + def get_feature(self, feature_id): + """Get a single feature by ID (GeoJSON).""" + return self._get(f'feature/{feature_id}') + + def get_featuresets(self): + """Get a list of feature sets.""" + return self._get('featureset') + + def get_featureset(self, featureset_id): + """Get a single feature set by ID (GeoJSON).""" + return self._get(f'featureset/{featureset_id}') + + def get_maps(self): + """Get a list of maps.""" + return self._get('maps') + + def get_layers(self): + """Get a list of map layers.""" + return self._get('layers') + + def get_layer(self, layer_id): + """Get a single map layer by ID.""" + return self._get(f'layer/{layer_id}') + + def get_featureclasses(self): + """Get a list of spatial feature types (feature classes).""" + return self._get('featureclass') + + def get_spatialfeaturegroups(self): + """Get a list of spatial feature groups.""" + return self._get('spatialfeaturegroup') + + def get_spatialfeaturegroup(self, group_id): + """Get a single spatial feature group by ID.""" + return self._get(f'spatialfeaturegroup/{group_id}') + + def post_spatialfeaturegroup(self, data): + """Create a new spatial feature group.""" + self.logger.debug('Posting spatial feature group: %s', data) + return self._post('spatialfeaturegroup', payload=data) + + def patch_spatialfeaturegroup(self, group_id, data): + """Update a spatial feature group.""" + self.logger.debug('Patching spatial feature group %s: %s', group_id, data) + return self._patch(f'spatialfeaturegroup/{group_id}', payload=data) + + def delete_spatialfeaturegroup(self, group_id): + """Delete a spatial feature group.""" + return self._delete(f'spatialfeaturegroup/{group_id}/') + + def get_spatialfeatures(self): + """Get a list of spatial features.""" + return self._get('spatialfeature') + + def get_spatialfeature(self, feature_id): + """Get a single spatial feature by ID.""" + return self._get(f'spatialfeature/{feature_id}') + + def post_spatialfeature(self, data): + """Create a new spatial feature.""" + self.logger.debug('Posting spatial feature: %s', data) + return self._post('spatialfeature', payload=data) + + def patch_spatialfeature(self, feature_id, data): + """Update a spatial feature.""" + self.logger.debug('Patching spatial feature %s: %s', feature_id, data) + return self._patch(f'spatialfeature/{feature_id}', payload=data) + + def delete_spatialfeature(self, feature_id): + """Delete a spatial feature.""" + return self._delete(f'spatialfeature/{feature_id}/') + class AsyncERClient(object): """ @@ -1393,6 +1471,84 @@ async def get_feature_group(self, feature_group_id: str): """ return await self._get(f"spatialfeaturegroup/{feature_group_id}", params={}) + # -- Mapping / Spatial Feature Methods -- + + async def get_features(self): + """Get a list of features (GeoJSON).""" + return await self._get('features') + + async def get_feature(self, feature_id): + """Get a single feature by ID (GeoJSON).""" + return await self._get(f'feature/{feature_id}') + + async def get_featuresets(self): + """Get a list of feature sets.""" + return await self._get('featureset') + + async def get_featureset(self, featureset_id): + """Get a single feature set by ID (GeoJSON).""" + return await self._get(f'featureset/{featureset_id}') + + async def get_maps(self): + """Get a list of maps.""" + return await self._get('maps') + + async def get_layers(self): + """Get a list of map layers.""" + return await self._get('layers') + + async def get_layer(self, layer_id): + """Get a single map layer by ID.""" + return await self._get(f'layer/{layer_id}') + + async def get_featureclasses(self): + """Get a list of spatial feature types (feature classes).""" + return await self._get('featureclass') + + async def get_spatialfeaturegroups(self): + """Get a list of spatial feature groups.""" + return await self._get('spatialfeaturegroup') + + async def get_spatialfeaturegroup(self, group_id): + """Get a single spatial feature group by ID.""" + return await self._get(f'spatialfeaturegroup/{group_id}') + + async def post_spatialfeaturegroup(self, data): + """Create a new spatial feature group.""" + self.logger.debug(f'Posting spatial feature group: {data}') + return await self._post('spatialfeaturegroup', payload=data) + + async def patch_spatialfeaturegroup(self, group_id, data): + """Update a spatial feature group.""" + self.logger.debug(f'Patching spatial feature group {group_id}: {data}') + return await self._patch(f'spatialfeaturegroup/{group_id}', payload=data) + + async def delete_spatialfeaturegroup(self, group_id): + """Delete a spatial feature group.""" + return await self._delete(f'spatialfeaturegroup/{group_id}/') + + async def get_spatialfeatures(self): + """Get a list of spatial features.""" + return await self._get('spatialfeature') + + async def get_spatialfeature(self, feature_id): + """Get a single spatial feature by ID.""" + return await self._get(f'spatialfeature/{feature_id}') + + async def post_spatialfeature(self, data): + """Create a new spatial feature.""" + self.logger.debug(f'Posting spatial feature: {data}') + return await self._post('spatialfeature', payload=data) + + async def patch_spatialfeature(self, feature_id, data): + """Update a spatial feature.""" + self.logger.debug(f'Patching spatial feature {feature_id}: {data}') + return await self._patch(f'spatialfeature/{feature_id}', payload=data) + + async def delete_spatialfeature(self, feature_id): + """Delete a spatial feature.""" + return await self._delete(f'spatialfeature/{feature_id}/') + async def _get_data(self, endpoint, params, batch_size=0): if "page" not in params: # Use cursor paginator unless the user has specified a page params["use_cursor"] = "true" diff --git a/tests/async_client/test_mapping_spatial.py b/tests/async_client/test_mapping_spatial.py new file mode 100644 index 0000000..0634030 --- /dev/null +++ b/tests/async_client/test_mapping_spatial.py @@ -0,0 +1,439 @@ +import httpx +import pytest +import respx + +from erclient import ERClientNotFound, ERClientPermissionDenied + + +# -- Fixtures -- + +@pytest.fixture +def feature_response(): + return { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [36.79, -1.29]}, + "properties": {"name": "Ranger Post Alpha", "id": "fp-001"}, + } + + +@pytest.fixture +def features_list_response(feature_response): + return [feature_response] + + +@pytest.fixture +def featureset_response(): + return { + "id": "fs-001", + "name": "Ranger Posts", + "type": "FeatureCollection", + "features": [], + } + + +@pytest.fixture +def featuresets_list_response(featureset_response): + return [featureset_response] + + +@pytest.fixture +def map_response(): + return { + "id": "map-001", + "title": "Park Overview", + "layers": [], + } + + +@pytest.fixture +def maps_list_response(map_response): + return [map_response] + + +@pytest.fixture +def layer_response(): + return { + "id": "layer-001", + "title": "Roads", + "type": "geojson", + } + + +@pytest.fixture +def layers_list_response(layer_response): + return [layer_response] + + +@pytest.fixture +def featureclass_response(): + return { + "id": "fc-001", + "name": "boundary", + "display": "Boundary", + } + + +@pytest.fixture +def featureclasses_list_response(featureclass_response): + return [featureclass_response] + + +@pytest.fixture +def spatialfeaturegroup_payload(): + return {"name": "Protected Areas", "description": "All protected areas"} + + +@pytest.fixture +def spatialfeaturegroup_response(): + return { + "id": "sfg-001", + "name": "Protected Areas", + "description": "All protected areas", + "is_visible": True, + } + + +@pytest.fixture +def spatialfeaturegroups_list_response(spatialfeaturegroup_response): + return [spatialfeaturegroup_response] + + +@pytest.fixture +def spatialfeature_payload(): + return { + "name": "Nairobi National Park", + "type": "Polygon", + "geojson": { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [[[36.8, -1.3], [36.9, -1.3], [36.9, -1.4], [36.8, -1.4], [36.8, -1.3]]], + }, + }, + } + + +@pytest.fixture +def spatialfeature_response(): + return { + "id": "sf-001", + "name": "Nairobi National Park", + "type": "Polygon", + "geojson": { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [[[36.8, -1.3], [36.9, -1.3], [36.9, -1.4], [36.8, -1.4], [36.8, -1.3]]], + }, + }, + } + + +@pytest.fixture +def spatialfeatures_list_response(spatialfeature_response): + return [spatialfeature_response] + + +# -- Read-only endpoint tests -- + +@pytest.mark.asyncio +async def test_get_features(er_client, features_list_response): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.get("features") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": features_list_response}) + result = await er_client.get_features() + assert route.called + assert result == features_list_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_feature(er_client, feature_response): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.get("feature/fp-001") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": feature_response}) + result = await er_client.get_feature("fp-001") + assert route.called + assert result == feature_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_feature_not_found(er_client): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.get("feature/nonexistent") + route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json={"status": {"code": 404}}) + with pytest.raises(ERClientNotFound): + await er_client.get_feature("nonexistent") + assert route.called + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_featuresets(er_client, featuresets_list_response): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.get("featureset") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": featuresets_list_response}) + result = await er_client.get_featuresets() + assert route.called + assert result == featuresets_list_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_featureset(er_client, featureset_response): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.get("featureset/fs-001") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": featureset_response}) + result = await er_client.get_featureset("fs-001") + assert route.called + assert result == featureset_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_maps(er_client, maps_list_response): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.get("maps") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": maps_list_response}) + result = await er_client.get_maps() + assert route.called + assert result == maps_list_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_layers(er_client, layers_list_response): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.get("layers") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": layers_list_response}) + result = await er_client.get_layers() + assert route.called + assert result == layers_list_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_layer(er_client, layer_response): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.get("layer/layer-001") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": layer_response}) + result = await er_client.get_layer("layer-001") + assert route.called + assert result == layer_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_featureclasses(er_client, featureclasses_list_response): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.get("featureclass") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": featureclasses_list_response}) + result = await er_client.get_featureclasses() + assert route.called + assert result == featureclasses_list_response + await er_client.close() + + +# -- Spatial Feature Group CRUD tests -- + +@pytest.mark.asyncio +async def test_get_spatialfeaturegroups(er_client, spatialfeaturegroups_list_response): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.get("spatialfeaturegroup") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": spatialfeaturegroups_list_response}) + result = await er_client.get_spatialfeaturegroups() + assert route.called + assert result == spatialfeaturegroups_list_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_spatialfeaturegroup(er_client, spatialfeaturegroup_response): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.get("spatialfeaturegroup/sfg-001") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": spatialfeaturegroup_response}) + result = await er_client.get_spatialfeaturegroup("sfg-001") + assert route.called + assert result == spatialfeaturegroup_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_spatialfeaturegroup_not_found(er_client): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.get("spatialfeaturegroup/nonexistent") + route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json={"status": {"code": 404}}) + with pytest.raises(ERClientNotFound): + await er_client.get_spatialfeaturegroup("nonexistent") + assert route.called + await er_client.close() + + +@pytest.mark.asyncio +async def test_post_spatialfeaturegroup(er_client, spatialfeaturegroup_payload, spatialfeaturegroup_response): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.post("spatialfeaturegroup") + route.return_value = httpx.Response(httpx.codes.CREATED, json={"data": spatialfeaturegroup_response}) + result = await er_client.post_spatialfeaturegroup(spatialfeaturegroup_payload) + assert route.called + assert result == spatialfeaturegroup_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_post_spatialfeaturegroup_forbidden(er_client, spatialfeaturegroup_payload): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.post("spatialfeaturegroup") + route.return_value = httpx.Response(httpx.codes.FORBIDDEN, json={"status": {"code": 403}}) + with pytest.raises(ERClientPermissionDenied): + await er_client.post_spatialfeaturegroup(spatialfeaturegroup_payload) + assert route.called + await er_client.close() + + +@pytest.mark.asyncio +async def test_patch_spatialfeaturegroup(er_client, spatialfeaturegroup_response): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.patch("spatialfeaturegroup/sfg-001") + updated = {**spatialfeaturegroup_response, "name": "Updated Name"} + route.return_value = httpx.Response(httpx.codes.OK, json={"data": updated}) + result = await er_client.patch_spatialfeaturegroup("sfg-001", {"name": "Updated Name"}) + assert route.called + assert result["name"] == "Updated Name" + await er_client.close() + + +@pytest.mark.asyncio +async def test_patch_spatialfeaturegroup_not_found(er_client): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.patch("spatialfeaturegroup/nonexistent") + route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json={"status": {"code": 404}}) + with pytest.raises(ERClientNotFound): + await er_client.patch_spatialfeaturegroup("nonexistent", {"name": "x"}) + assert route.called + await er_client.close() + + +@pytest.mark.asyncio +async def test_delete_spatialfeaturegroup(er_client): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.delete("spatialfeaturegroup/sfg-001/") + route.return_value = httpx.Response(httpx.codes.NO_CONTENT) + result = await er_client.delete_spatialfeaturegroup("sfg-001") + assert route.called + assert result is None + await er_client.close() + + +@pytest.mark.asyncio +async def test_delete_spatialfeaturegroup_not_found(er_client): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.delete("spatialfeaturegroup/nonexistent/") + route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json={"status": {"code": 404}}) + with pytest.raises(ERClientNotFound): + await er_client.delete_spatialfeaturegroup("nonexistent") + assert route.called + await er_client.close() + + +# -- Spatial Feature CRUD tests -- + +@pytest.mark.asyncio +async def test_get_spatialfeatures(er_client, spatialfeatures_list_response): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.get("spatialfeature") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": spatialfeatures_list_response}) + result = await er_client.get_spatialfeatures() + assert route.called + assert result == spatialfeatures_list_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_spatialfeature(er_client, spatialfeature_response): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.get("spatialfeature/sf-001") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": spatialfeature_response}) + result = await er_client.get_spatialfeature("sf-001") + assert route.called + assert result == spatialfeature_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_get_spatialfeature_not_found(er_client): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.get("spatialfeature/nonexistent") + route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json={"status": {"code": 404}}) + with pytest.raises(ERClientNotFound): + await er_client.get_spatialfeature("nonexistent") + assert route.called + await er_client.close() + + +@pytest.mark.asyncio +async def test_post_spatialfeature(er_client, spatialfeature_payload, spatialfeature_response): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.post("spatialfeature") + route.return_value = httpx.Response(httpx.codes.CREATED, json={"data": spatialfeature_response}) + result = await er_client.post_spatialfeature(spatialfeature_payload) + assert route.called + assert result == spatialfeature_response + await er_client.close() + + +@pytest.mark.asyncio +async def test_post_spatialfeature_forbidden(er_client, spatialfeature_payload): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.post("spatialfeature") + route.return_value = httpx.Response(httpx.codes.FORBIDDEN, json={"status": {"code": 403}}) + with pytest.raises(ERClientPermissionDenied): + await er_client.post_spatialfeature(spatialfeature_payload) + assert route.called + await er_client.close() + + +@pytest.mark.asyncio +async def test_patch_spatialfeature(er_client, spatialfeature_response): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.patch("spatialfeature/sf-001") + updated = {**spatialfeature_response, "name": "Updated Park"} + route.return_value = httpx.Response(httpx.codes.OK, json={"data": updated}) + result = await er_client.patch_spatialfeature("sf-001", {"name": "Updated Park"}) + assert route.called + assert result["name"] == "Updated Park" + await er_client.close() + + +@pytest.mark.asyncio +async def test_patch_spatialfeature_not_found(er_client): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.patch("spatialfeature/nonexistent") + route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json={"status": {"code": 404}}) + with pytest.raises(ERClientNotFound): + await er_client.patch_spatialfeature("nonexistent", {"name": "x"}) + assert route.called + await er_client.close() + + +@pytest.mark.asyncio +async def test_delete_spatialfeature(er_client): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.delete("spatialfeature/sf-001/") + route.return_value = httpx.Response(httpx.codes.NO_CONTENT) + result = await er_client.delete_spatialfeature("sf-001") + assert route.called + assert result is None + await er_client.close() + + +@pytest.mark.asyncio +async def test_delete_spatialfeature_not_found(er_client): + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.delete("spatialfeature/nonexistent/") + route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json={"status": {"code": 404}}) + with pytest.raises(ERClientNotFound): + await er_client.delete_spatialfeature("nonexistent") + assert route.called + await er_client.close() diff --git a/tests/sync_client/__init__.py b/tests/sync_client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/sync_client/conftest.py b/tests/sync_client/conftest.py new file mode 100644 index 0000000..6d9d6da --- /dev/null +++ b/tests/sync_client/conftest.py @@ -0,0 +1,21 @@ +import pytest + +from erclient.client import ERClient + + +@pytest.fixture +def er_server_info(): + return { + "service_root": "https://fake-site.erdomain.org/api/v1.0", + "username": "test", + "password": "test", + "token": "1110c87681cd1d12ad07c2d0f57d15d6079ae5d8", + "token_url": "https://fake-auth.erdomain.org/oauth2/token", + "client_id": "das_web_client", + "provider_key": "testintegration", + } + + +@pytest.fixture +def er_client(er_server_info): + return ERClient(**er_server_info) diff --git a/tests/sync_client/test_mapping_spatial.py b/tests/sync_client/test_mapping_spatial.py new file mode 100644 index 0000000..6afc199 --- /dev/null +++ b/tests/sync_client/test_mapping_spatial.py @@ -0,0 +1,278 @@ +import json +from unittest.mock import MagicMock + +import pytest + +from erclient import ERClientNotFound, ERClientPermissionDenied + + +def _mock_response(status_code=200, json_data=None): + """Helper to create a mock response object.""" + response = MagicMock() + response.ok = 200 <= status_code < 400 + response.status_code = status_code + response.text = json.dumps(json_data) if json_data else "" + response.json.return_value = json_data + response.url = "https://fake-site.erdomain.org/api/v1.0/test" + return response + + +# -- Read-only endpoint tests -- + + +def test_get_features(er_client): + expected = [{"type": "Feature", "properties": {"name": "Post A"}}] + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_features() + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_feature(er_client): + expected = {"type": "Feature", "properties": {"name": "Post A", "id": "fp-001"}} + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_feature("fp-001") + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_feature_not_found(er_client): + er_client._http_session.get = MagicMock( + return_value=_mock_response(404, {"status": {"code": 404}}) + ) + with pytest.raises(ERClientNotFound): + er_client.get_feature("nonexistent") + + +def test_get_featuresets(er_client): + expected = [{"id": "fs-001", "name": "Ranger Posts"}] + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_featuresets() + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_featureset(er_client): + expected = {"id": "fs-001", "name": "Ranger Posts"} + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_featureset("fs-001") + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_maps(er_client): + expected = [{"id": "map-001", "title": "Park Overview"}] + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_maps() + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_layers(er_client): + expected = [{"id": "layer-001", "title": "Roads"}] + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_layers() + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_layer(er_client): + expected = {"id": "layer-001", "title": "Roads"} + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_layer("layer-001") + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_featureclasses(er_client): + expected = [{"id": "fc-001", "name": "boundary"}] + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_featureclasses() + er_client._http_session.get.assert_called_once() + assert result == expected + + +# -- Spatial Feature Group CRUD tests -- + + +def test_get_spatialfeaturegroups(er_client): + expected = [{"id": "sfg-001", "name": "Protected Areas"}] + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_spatialfeaturegroups() + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_spatialfeaturegroup(er_client): + expected = {"id": "sfg-001", "name": "Protected Areas"} + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_spatialfeaturegroup("sfg-001") + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_spatialfeaturegroup_not_found(er_client): + er_client._http_session.get = MagicMock( + return_value=_mock_response(404, {"status": {"code": 404}}) + ) + with pytest.raises(ERClientNotFound): + er_client.get_spatialfeaturegroup("nonexistent") + + +def test_post_spatialfeaturegroup(er_client): + payload = {"name": "Protected Areas"} + created = {"data": {"id": "sfg-001", "name": "Protected Areas"}, "status": {"code": 201}} + er_client._http_session.post = MagicMock( + return_value=_mock_response(201, created) + ) + result = er_client.post_spatialfeaturegroup(payload) + er_client._http_session.post.assert_called_once() + assert result == created["data"] + + +def test_post_spatialfeaturegroup_forbidden(er_client): + er_client._http_session.post = MagicMock( + return_value=_mock_response(403, {"status": {"detail": "Forbidden"}}) + ) + with pytest.raises(ERClientPermissionDenied): + er_client.post_spatialfeaturegroup({"name": "Test"}) + + +def test_patch_spatialfeaturegroup(er_client): + updated = {"data": {"id": "sfg-001", "name": "Renamed"}, "status": {"code": 200}} + er_client._http_session.patch = MagicMock( + return_value=_mock_response(200, updated) + ) + result = er_client.patch_spatialfeaturegroup("sfg-001", {"name": "Renamed"}) + er_client._http_session.patch.assert_called_once() + assert result["name"] == "Renamed" + + +def test_patch_spatialfeaturegroup_not_found(er_client): + er_client._http_session.patch = MagicMock( + return_value=_mock_response(404, {"status": {"code": 404}}) + ) + with pytest.raises(ERClientNotFound): + er_client.patch_spatialfeaturegroup("nonexistent", {"name": "X"}) + + +def test_delete_spatialfeaturegroup(er_client): + er_client._http_session.delete = MagicMock( + return_value=_mock_response(204) + ) + result = er_client.delete_spatialfeaturegroup("sfg-001") + er_client._http_session.delete.assert_called_once() + assert result is True + + +def test_delete_spatialfeaturegroup_not_found(er_client): + er_client._http_session.delete = MagicMock( + return_value=_mock_response(404, {"status": {"code": 404}}) + ) + with pytest.raises(ERClientNotFound): + er_client.delete_spatialfeaturegroup("nonexistent") + + +# -- Spatial Feature CRUD tests -- + + +def test_get_spatialfeatures(er_client): + expected = [{"id": "sf-001", "name": "Nairobi NP"}] + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_spatialfeatures() + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_spatialfeature(er_client): + expected = {"id": "sf-001", "name": "Nairobi NP"} + er_client._http_session.get = MagicMock( + return_value=_mock_response(200, {"data": expected}) + ) + result = er_client.get_spatialfeature("sf-001") + er_client._http_session.get.assert_called_once() + assert result == expected + + +def test_get_spatialfeature_not_found(er_client): + er_client._http_session.get = MagicMock( + return_value=_mock_response(404, {"status": {"code": 404}}) + ) + with pytest.raises(ERClientNotFound): + er_client.get_spatialfeature("nonexistent") + + +def test_post_spatialfeature(er_client): + payload = {"name": "Nairobi NP"} + created = {"data": {"id": "sf-001", "name": "Nairobi NP"}, "status": {"code": 201}} + er_client._http_session.post = MagicMock( + return_value=_mock_response(201, created) + ) + result = er_client.post_spatialfeature(payload) + er_client._http_session.post.assert_called_once() + assert result == created["data"] + + +def test_post_spatialfeature_forbidden(er_client): + er_client._http_session.post = MagicMock( + return_value=_mock_response(403, {"status": {"detail": "Forbidden"}}) + ) + with pytest.raises(ERClientPermissionDenied): + er_client.post_spatialfeature({"name": "Test"}) + + +def test_patch_spatialfeature(er_client): + updated = {"data": {"id": "sf-001", "name": "Updated Park"}, "status": {"code": 200}} + er_client._http_session.patch = MagicMock( + return_value=_mock_response(200, updated) + ) + result = er_client.patch_spatialfeature("sf-001", {"name": "Updated Park"}) + er_client._http_session.patch.assert_called_once() + assert result["name"] == "Updated Park" + + +def test_patch_spatialfeature_not_found(er_client): + er_client._http_session.patch = MagicMock( + return_value=_mock_response(404, {"status": {"code": 404}}) + ) + with pytest.raises(ERClientNotFound): + er_client.patch_spatialfeature("nonexistent", {"name": "X"}) + + +def test_delete_spatialfeature(er_client): + er_client._http_session.delete = MagicMock( + return_value=_mock_response(204) + ) + result = er_client.delete_spatialfeature("sf-001") + er_client._http_session.delete.assert_called_once() + assert result is True + + +def test_delete_spatialfeature_not_found(er_client): + er_client._http_session.delete = MagicMock( + return_value=_mock_response(404, {"status": {"code": 404}}) + ) + with pytest.raises(ERClientNotFound): + er_client.delete_spatialfeature("nonexistent") From 265069df5931753cf145144c3400912c9fb0b5e5 Mon Sep 17 00:00:00 2001 From: JoshuaVulcan <38018017+JoshuaVulcan@users.noreply.github.com> Date: Wed, 11 Feb 2026 09:45:28 -0800 Subject: [PATCH 2/3] Deprecate get_feature_group() in favor of get_spatialfeaturegroup() - Add warnings.warn(DeprecationWarning) in async get_feature_group() - Delegate to get_spatialfeaturegroup() for implementation - Add test that deprecation is emitted and delegation works Addresses ER_CLIENT_PR_REVIEWS.md prescriptive action for PR #34. Co-authored-by: Cursor --- erclient/client.py | 18 +++++++++++------- tests/async_client/test_mapping_spatial.py | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/erclient/client.py b/erclient/client.py index c15ff14..d3621db 100644 --- a/erclient/client.py +++ b/erclient/client.py @@ -5,6 +5,7 @@ import math import re import time +import warnings from datetime import datetime, timedelta, timezone from http import HTTPStatus from typing import List @@ -1461,15 +1462,18 @@ async def get_source_assignments(self, subject_ids: List[str] = None, source_ids async def get_feature_group(self, feature_group_id: str): """ - Get a feature group by id + Get a feature group by id. - Args: - feature_group_id (int): id of the feature group - - Returns: - dict: feature group data + .. deprecated:: 1.x + Use :meth:`get_spatialfeaturegroup` instead; the name matches the + DAS API path ``spatialfeaturegroup``. """ - return await self._get(f"spatialfeaturegroup/{feature_group_id}", params={}) + warnings.warn( + "get_feature_group() is deprecated; use get_spatialfeaturegroup() instead.", + DeprecationWarning, + stacklevel=2, + ) + return await self.get_spatialfeaturegroup(feature_group_id) # -- Mapping / Spatial Feature Methods -- diff --git a/tests/async_client/test_mapping_spatial.py b/tests/async_client/test_mapping_spatial.py index 0634030..d87faf3 100644 --- a/tests/async_client/test_mapping_spatial.py +++ b/tests/async_client/test_mapping_spatial.py @@ -270,6 +270,21 @@ async def test_get_spatialfeaturegroup_not_found(er_client): await er_client.close() +@pytest.mark.asyncio +async def test_get_feature_group_deprecated_delegates_to_get_spatialfeaturegroup( + er_client, spatialfeaturegroup_response +): + """get_feature_group is deprecated and delegates to get_spatialfeaturegroup.""" + async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + route = respx_mock.get("spatialfeaturegroup/sfg-001") + route.return_value = httpx.Response(httpx.codes.OK, json={"data": spatialfeaturegroup_response}) + with pytest.warns(DeprecationWarning, match="get_feature_group.*get_spatialfeaturegroup"): + result = await er_client.get_feature_group("sfg-001") + assert route.called + assert result == spatialfeaturegroup_response + await er_client.close() + + @pytest.mark.asyncio async def test_post_spatialfeaturegroup(er_client, spatialfeaturegroup_payload, spatialfeaturegroup_response): async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: From 62c85885bef2b43eddad07978a2055d0b301bf3a Mon Sep 17 00:00:00 2001 From: JoshuaVulcan <38018017+JoshuaVulcan@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:49:28 -0800 Subject: [PATCH 3/3] Fix mapping/spatial async test URL base and 204 delete assert Co-authored-by: Cursor --- tests/async_client/test_mapping_spatial.py | 60 +++++++++++----------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/tests/async_client/test_mapping_spatial.py b/tests/async_client/test_mapping_spatial.py index d87faf3..7feaef9 100644 --- a/tests/async_client/test_mapping_spatial.py +++ b/tests/async_client/test_mapping_spatial.py @@ -138,7 +138,7 @@ def spatialfeatures_list_response(spatialfeature_response): @pytest.mark.asyncio async def test_get_features(er_client, features_list_response): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.get("features") route.return_value = httpx.Response(httpx.codes.OK, json={"data": features_list_response}) result = await er_client.get_features() @@ -149,7 +149,7 @@ async def test_get_features(er_client, features_list_response): @pytest.mark.asyncio async def test_get_feature(er_client, feature_response): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.get("feature/fp-001") route.return_value = httpx.Response(httpx.codes.OK, json={"data": feature_response}) result = await er_client.get_feature("fp-001") @@ -160,7 +160,7 @@ async def test_get_feature(er_client, feature_response): @pytest.mark.asyncio async def test_get_feature_not_found(er_client): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.get("feature/nonexistent") route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json={"status": {"code": 404}}) with pytest.raises(ERClientNotFound): @@ -171,7 +171,7 @@ async def test_get_feature_not_found(er_client): @pytest.mark.asyncio async def test_get_featuresets(er_client, featuresets_list_response): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.get("featureset") route.return_value = httpx.Response(httpx.codes.OK, json={"data": featuresets_list_response}) result = await er_client.get_featuresets() @@ -182,7 +182,7 @@ async def test_get_featuresets(er_client, featuresets_list_response): @pytest.mark.asyncio async def test_get_featureset(er_client, featureset_response): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.get("featureset/fs-001") route.return_value = httpx.Response(httpx.codes.OK, json={"data": featureset_response}) result = await er_client.get_featureset("fs-001") @@ -193,7 +193,7 @@ async def test_get_featureset(er_client, featureset_response): @pytest.mark.asyncio async def test_get_maps(er_client, maps_list_response): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.get("maps") route.return_value = httpx.Response(httpx.codes.OK, json={"data": maps_list_response}) result = await er_client.get_maps() @@ -204,7 +204,7 @@ async def test_get_maps(er_client, maps_list_response): @pytest.mark.asyncio async def test_get_layers(er_client, layers_list_response): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.get("layers") route.return_value = httpx.Response(httpx.codes.OK, json={"data": layers_list_response}) result = await er_client.get_layers() @@ -215,7 +215,7 @@ async def test_get_layers(er_client, layers_list_response): @pytest.mark.asyncio async def test_get_layer(er_client, layer_response): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.get("layer/layer-001") route.return_value = httpx.Response(httpx.codes.OK, json={"data": layer_response}) result = await er_client.get_layer("layer-001") @@ -226,7 +226,7 @@ async def test_get_layer(er_client, layer_response): @pytest.mark.asyncio async def test_get_featureclasses(er_client, featureclasses_list_response): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.get("featureclass") route.return_value = httpx.Response(httpx.codes.OK, json={"data": featureclasses_list_response}) result = await er_client.get_featureclasses() @@ -239,7 +239,7 @@ async def test_get_featureclasses(er_client, featureclasses_list_response): @pytest.mark.asyncio async def test_get_spatialfeaturegroups(er_client, spatialfeaturegroups_list_response): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.get("spatialfeaturegroup") route.return_value = httpx.Response(httpx.codes.OK, json={"data": spatialfeaturegroups_list_response}) result = await er_client.get_spatialfeaturegroups() @@ -250,7 +250,7 @@ async def test_get_spatialfeaturegroups(er_client, spatialfeaturegroups_list_res @pytest.mark.asyncio async def test_get_spatialfeaturegroup(er_client, spatialfeaturegroup_response): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.get("spatialfeaturegroup/sfg-001") route.return_value = httpx.Response(httpx.codes.OK, json={"data": spatialfeaturegroup_response}) result = await er_client.get_spatialfeaturegroup("sfg-001") @@ -261,7 +261,7 @@ async def test_get_spatialfeaturegroup(er_client, spatialfeaturegroup_response): @pytest.mark.asyncio async def test_get_spatialfeaturegroup_not_found(er_client): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.get("spatialfeaturegroup/nonexistent") route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json={"status": {"code": 404}}) with pytest.raises(ERClientNotFound): @@ -275,7 +275,7 @@ async def test_get_feature_group_deprecated_delegates_to_get_spatialfeaturegroup er_client, spatialfeaturegroup_response ): """get_feature_group is deprecated and delegates to get_spatialfeaturegroup.""" - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.get("spatialfeaturegroup/sfg-001") route.return_value = httpx.Response(httpx.codes.OK, json={"data": spatialfeaturegroup_response}) with pytest.warns(DeprecationWarning, match="get_feature_group.*get_spatialfeaturegroup"): @@ -287,7 +287,7 @@ async def test_get_feature_group_deprecated_delegates_to_get_spatialfeaturegroup @pytest.mark.asyncio async def test_post_spatialfeaturegroup(er_client, spatialfeaturegroup_payload, spatialfeaturegroup_response): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.post("spatialfeaturegroup") route.return_value = httpx.Response(httpx.codes.CREATED, json={"data": spatialfeaturegroup_response}) result = await er_client.post_spatialfeaturegroup(spatialfeaturegroup_payload) @@ -298,7 +298,7 @@ async def test_post_spatialfeaturegroup(er_client, spatialfeaturegroup_payload, @pytest.mark.asyncio async def test_post_spatialfeaturegroup_forbidden(er_client, spatialfeaturegroup_payload): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.post("spatialfeaturegroup") route.return_value = httpx.Response(httpx.codes.FORBIDDEN, json={"status": {"code": 403}}) with pytest.raises(ERClientPermissionDenied): @@ -309,7 +309,7 @@ async def test_post_spatialfeaturegroup_forbidden(er_client, spatialfeaturegroup @pytest.mark.asyncio async def test_patch_spatialfeaturegroup(er_client, spatialfeaturegroup_response): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.patch("spatialfeaturegroup/sfg-001") updated = {**spatialfeaturegroup_response, "name": "Updated Name"} route.return_value = httpx.Response(httpx.codes.OK, json={"data": updated}) @@ -321,7 +321,7 @@ async def test_patch_spatialfeaturegroup(er_client, spatialfeaturegroup_response @pytest.mark.asyncio async def test_patch_spatialfeaturegroup_not_found(er_client): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.patch("spatialfeaturegroup/nonexistent") route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json={"status": {"code": 404}}) with pytest.raises(ERClientNotFound): @@ -332,18 +332,18 @@ async def test_patch_spatialfeaturegroup_not_found(er_client): @pytest.mark.asyncio async def test_delete_spatialfeaturegroup(er_client): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.delete("spatialfeaturegroup/sfg-001/") route.return_value = httpx.Response(httpx.codes.NO_CONTENT) result = await er_client.delete_spatialfeaturegroup("sfg-001") assert route.called - assert result is None + assert result is True # 204 No Content returns True (canonical _delete behavior) await er_client.close() @pytest.mark.asyncio async def test_delete_spatialfeaturegroup_not_found(er_client): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.delete("spatialfeaturegroup/nonexistent/") route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json={"status": {"code": 404}}) with pytest.raises(ERClientNotFound): @@ -356,7 +356,7 @@ async def test_delete_spatialfeaturegroup_not_found(er_client): @pytest.mark.asyncio async def test_get_spatialfeatures(er_client, spatialfeatures_list_response): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.get("spatialfeature") route.return_value = httpx.Response(httpx.codes.OK, json={"data": spatialfeatures_list_response}) result = await er_client.get_spatialfeatures() @@ -367,7 +367,7 @@ async def test_get_spatialfeatures(er_client, spatialfeatures_list_response): @pytest.mark.asyncio async def test_get_spatialfeature(er_client, spatialfeature_response): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.get("spatialfeature/sf-001") route.return_value = httpx.Response(httpx.codes.OK, json={"data": spatialfeature_response}) result = await er_client.get_spatialfeature("sf-001") @@ -378,7 +378,7 @@ async def test_get_spatialfeature(er_client, spatialfeature_response): @pytest.mark.asyncio async def test_get_spatialfeature_not_found(er_client): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.get("spatialfeature/nonexistent") route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json={"status": {"code": 404}}) with pytest.raises(ERClientNotFound): @@ -389,7 +389,7 @@ async def test_get_spatialfeature_not_found(er_client): @pytest.mark.asyncio async def test_post_spatialfeature(er_client, spatialfeature_payload, spatialfeature_response): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.post("spatialfeature") route.return_value = httpx.Response(httpx.codes.CREATED, json={"data": spatialfeature_response}) result = await er_client.post_spatialfeature(spatialfeature_payload) @@ -400,7 +400,7 @@ async def test_post_spatialfeature(er_client, spatialfeature_payload, spatialfea @pytest.mark.asyncio async def test_post_spatialfeature_forbidden(er_client, spatialfeature_payload): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.post("spatialfeature") route.return_value = httpx.Response(httpx.codes.FORBIDDEN, json={"status": {"code": 403}}) with pytest.raises(ERClientPermissionDenied): @@ -411,7 +411,7 @@ async def test_post_spatialfeature_forbidden(er_client, spatialfeature_payload): @pytest.mark.asyncio async def test_patch_spatialfeature(er_client, spatialfeature_response): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.patch("spatialfeature/sf-001") updated = {**spatialfeature_response, "name": "Updated Park"} route.return_value = httpx.Response(httpx.codes.OK, json={"data": updated}) @@ -423,7 +423,7 @@ async def test_patch_spatialfeature(er_client, spatialfeature_response): @pytest.mark.asyncio async def test_patch_spatialfeature_not_found(er_client): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.patch("spatialfeature/nonexistent") route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json={"status": {"code": 404}}) with pytest.raises(ERClientNotFound): @@ -434,18 +434,18 @@ async def test_patch_spatialfeature_not_found(er_client): @pytest.mark.asyncio async def test_delete_spatialfeature(er_client): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.delete("spatialfeature/sf-001/") route.return_value = httpx.Response(httpx.codes.NO_CONTENT) result = await er_client.delete_spatialfeature("sf-001") assert route.called - assert result is None + assert result is True # 204 No Content returns True (canonical _delete behavior) await er_client.close() @pytest.mark.asyncio async def test_delete_spatialfeature_not_found(er_client): - async with respx.mock(base_url=er_client.service_root, assert_all_called=False) as respx_mock: + async with respx.mock(base_url=er_client._api_root("v1.0"), assert_all_called=False) as respx_mock: route = respx_mock.delete("spatialfeature/nonexistent/") route.return_value = httpx.Response(httpx.codes.NOT_FOUND, json={"status": {"code": 404}}) with pytest.raises(ERClientNotFound):