From 7269794e67745815fcbee9b3e509768010c41879 Mon Sep 17 00:00:00 2001 From: atmorling Date: Wed, 18 Dec 2024 11:49:33 +0200 Subject: [PATCH] ER IO test pass (#365) --- ecoscope/io/earthranger.py | 81 +++-- ecoscope/io/earthranger_utils.py | 4 + .../io/get_events_bad_geojson.feather | Bin 0 -> 21586 bytes .../io/get_patrol_events_bad_geojson.json | 312 ++++++++++++++++++ tests/test_earthranger_io.py | 71 ++++ 5 files changed, 433 insertions(+), 35 deletions(-) create mode 100644 tests/sample_data/io/get_events_bad_geojson.feather create mode 100644 tests/sample_data/io/get_patrol_events_bad_geojson.json diff --git a/ecoscope/io/earthranger.py b/ecoscope/io/earthranger.py index f15fafce..e282ccdb 100644 --- a/ecoscope/io/earthranger.py +++ b/ecoscope/io/earthranger.py @@ -17,6 +17,7 @@ clean_kwargs, clean_time_cols, dataframe_to_dict, + filter_bad_geojson, format_iso_time, to_gdf, to_hex, @@ -512,6 +513,9 @@ def get_subjectgroup_observations( else: subjects = self.get_subjects(subject_group_name=subject_group_name, include_inactive=include_inactive) + if subjects.empty: + return subjects + return self.get_subject_observations(subjects, **kwargs) def get_event_types(self, include_inactive=False, **addl_kwargs): @@ -629,9 +633,8 @@ def get_events( if not gdf.empty: gdf = clean_time_cols(gdf) if gdf.loc[0, "location"] is not None: - gdf.loc[~gdf["geojson"].isna(), "geometry"] = gpd.GeoDataFrame.from_features( - gdf.loc[~gdf["geojson"].isna(), "geojson"] - )["geometry"] + gdf = filter_bad_geojson(gdf) + gdf["geometry"] = gpd.GeoDataFrame.from_features(gdf["geojson"])["geometry"] gdf.set_geometry("geometry", inplace=True) gdf.set_crs(4326, inplace=True) gdf.sort_values("time", inplace=True) @@ -641,7 +644,9 @@ def get_events( def get_patrol_types(self): df = pd.DataFrame(self._get("activity/patrols/types")) - return df.set_index("id") + if not df.empty: + df = df.set_index("id") + return df def get_patrols(self, since=None, until=None, patrol_type=None, patrol_type_value=None, status=None, **addl_kwargs): """ @@ -738,19 +743,20 @@ def get_patrol_events( events = [] for _, row in patrol_df.iterrows(): - if row["patrol_segments"]: - for segment in row["patrol_segments"]: - for event in segment.get("events", []): - event["patrol_id"] = row.get("id") - event["patrol_segment_id"] = segment.get("id") - event["patrol_start_time"] = (segment.get("time_range") or {}).get("start_time") - events.append(event) + for segment in row.get("patrol_segments", []): + for event in segment.get("events", []): + event["patrol_id"] = row.get("id") + event["patrol_segment_id"] = segment.get("id") + event["patrol_start_time"] = (segment.get("time_range") or {}).get("start_time") + events.append(event) events_df = pd.DataFrame(events) if events_df.empty: return events_df + events_df = filter_bad_geojson(events_df) events_df["geometry"] = events_df["geojson"].apply(lambda x: shape(x.get("geometry"))) - events_df["time"] = events_df["geojson"].apply(lambda x: x.get("properties").get("datetime")) + events_df["time"] = events_df["geojson"].apply(lambda x: x.get("properties", {}).get("datetime")) + events_df = events_df.loc[events_df["time"].notnull()] events_df = clean_time_cols(events_df) return gpd.GeoDataFrame(events_df, geometry="geometry", crs=4326) @@ -871,31 +877,33 @@ def get_patrol_observations(self, patrols_df, include_patrol_details=False, **kw until=patrol_end_time, **kwargs, ) - if include_patrol_details: - observation["patrol_id"] = patrol["id"] - observation["patrol_title"] = patrol["title"] - observation["patrol_serial_number"] = patrol["serial_number"] - observation["patrol_start_time"] = patrol_start_time - observation["patrol_end_time"] = patrol_end_time - observation["patrol_type"] = patrol_type - observation = ( - observation.reset_index() - .merge( - pd.DataFrame(df_pt).add_prefix("patrol_type__"), - left_on="patrol_type", - right_on="id", - ) - .drop( - columns=[ - "patrol_type__ordernum", - "patrol_type__icon_id", - "patrol_type__default_priority", - "patrol_type__is_active", - ] - ) - ) if len(observation) > 0: observation["groupby_col"] = patrol["id"] + + if include_patrol_details: + observation["patrol_id"] = patrol["id"] + observation["patrol_title"] = patrol["title"] + observation["patrol_serial_number"] = patrol["serial_number"] + observation["patrol_start_time"] = patrol_start_time + observation["patrol_end_time"] = patrol_end_time + observation["patrol_type"] = patrol_type + observation = ( + observation.reset_index() + .merge( + pd.DataFrame(df_pt).add_prefix("patrol_type__"), + left_on="patrol_type", + right_on="id", + ) + .drop( + columns=[ + "patrol_type__ordernum", + "patrol_type__icon_id", + "patrol_type__default_priority", + "patrol_type__is_active", + ] + ) + ) + observations.append(observation) except Exception as e: print( @@ -903,6 +911,9 @@ def get_patrol_observations(self, patrols_df, include_patrol_details=False, **kw f"end_time={patrol_end_time} failed for: {e}" ) + if not observations: + return pd.DataFrame() + df = pd.concat(observations) df = clean_time_cols(df) df = ecoscope.base.Relocations(df) diff --git a/ecoscope/io/earthranger_utils.py b/ecoscope/io/earthranger_utils.py index f622a316..cf04190d 100644 --- a/ecoscope/io/earthranger_utils.py +++ b/ecoscope/io/earthranger_utils.py @@ -70,3 +70,7 @@ def pack_columns(dataframe: pd.DataFrame, columns: typing.List): dataframe.drop(metadata_cols, inplace=True, axis=1) dataframe.rename(columns={"metadata": "additional"}, inplace=True) return dataframe + + +def filter_bad_geojson(dataframe: pd.DataFrame): + return dataframe[dataframe["geojson"].apply(lambda x: True if isinstance(x, dict) and x.get("geometry") else False)] diff --git a/tests/sample_data/io/get_events_bad_geojson.feather b/tests/sample_data/io/get_events_bad_geojson.feather new file mode 100644 index 0000000000000000000000000000000000000000..073ee1babbe4482a4762aaecab6bf3604a46440e GIT binary patch literal 21586 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 literal 0 HcmV?d00001 diff --git a/tests/sample_data/io/get_patrol_events_bad_geojson.json b/tests/sample_data/io/get_patrol_events_bad_geojson.json new file mode 100644 index 00000000..5060a778 --- /dev/null +++ b/tests/sample_data/io/get_patrol_events_bad_geojson.json @@ -0,0 +1,312 @@ +{ + "id": { + "0": "c9f6018b-b51d-4093-8516-d82141d93982" + }, + "priority": { + "0": 0 + }, + "state": { + "0": "done" + }, + "objective": { + "0": "Demo Patrol" + }, + "serial_number": { + "0": 23474 + }, + "title": { + "0": "Ecoscope Demo Patrol" + }, + "files": { + "0": [] + }, + "notes": { + "0": [] + }, + "patrol_segments": { + "0": [ + { + "id": "0e91ec92-bf8b-4e86-b579-47b814419ceb", + "patrol_type": "ecoscope_patrol", + "leader": { + "content_type": "observations.subject", + "id": "ef343e5d-f6c7-4867-ab33-2b2933b3bce5", + "name": "eco_1", + "subject_type": "person", + "subject_subtype": "ranger", + "common_name": null, + "additional": { + "rgb": "", + "sex": "", + "region": "", + "country": "", + "external_id": "", + "tm_animal_id": "", + "external_name": "" + }, + "created_at": "2024-09-09T13:29:59.189245+03:00", + "updated_at": "2024-10-23T21:07:01.505307+03:00", + "is_active": true, + "user": null, + "region": "", + "country": "", + "sex": "", + "tracks_available": false, + "image_url": "\/static\/ranger-black.svg" + }, + "scheduled_start": null, + "scheduled_end": null, + "time_range": { + "start_time": "2017-02-04T13:30:00+03:00", + "end_time": "2017-02-05T07:30:00+03:00" + }, + "start_location": { + "latitude": -2.8935350263, + "longitude": 39.2917871115 + }, + "end_location": { + "latitude": -2.9053535333, + "longitude": 39.334974731 + }, + "events": [ + { + "id": "a8c1ff24-83fc-48ff-a09c-c66d57518c34", + "serial_number": 443915, + "event_type": "poacher_camp_rep", + "priority": 0, + "title": "poacher_camp_rep", + "state": "new", + "contains": [], + "updated_at": "2024-09-09T21:01:10.460147+03:00", + "created_at": "2024-09-09T21:01:10.460077+03:00", + "geojson": { + "type": "Feature", + "properties": { + "message": "", + "datetime": "2017-02-05T00:10:00+00:00", + "image": "https://test-data.pamdas.org/static/poacher_camp-gray.svg", + "icon": { + "iconUrl": "https://test-data.pamdas.org/static/poacher_camp-gray.svg", + "iconSize": [ + 25, + 25 + ], + "iconAncor": [ + 12, + 12 + ], + "popupAncor": [ + 0, + -13 + ], + "className": "dot" + } + } + }, + "is_collection": false + }, + { + "id": "c62e681d-d955-4f29-a855-1fe95669387a", + "serial_number": 443916, + "event_type": "injured_animal_rep", + "priority": 0, + "title": "injured_animal_rep", + "state": "new", + "contains": [], + "updated_at": "2024-09-09T21:01:10.537946+03:00", + "created_at": "2024-09-09T21:01:10.537915+03:00", + "geojson": { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 39.335383647060006, + -2.894132395575768 + ] + } + }, + "is_collection": false + }, + { + "id": "da236714-fed2-4c72-8f4f-d4b1ec8dffe5", + "serial_number": 443917, + "event_type": "hwc_rep", + "priority": 0, + "title": "hwc_rep", + "state": "new", + "contains": [], + "updated_at": "2024-09-09T21:01:10.603993+03:00", + "created_at": "2024-09-09T21:01:10.603959+03:00", + "geojson": { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 39.33850583956563, + -2.890615124552055 + ] + }, + "properties": { + "message": "", + "image": "https://test-data.pamdas.org/static/hwc_rep-gray.svg", + "icon": { + "iconUrl": "https://test-data.pamdas.org/static/hwc_rep-gray.svg", + "iconSize": [ + 25, + 25 + ], + "iconAncor": [ + 12, + 12 + ], + "popupAncor": [ + 0, + -13 + ], + "className": "dot" + } + } + }, + "is_collection": false + }, + { + "id": "ebf812f5-e616-40e4-8fcf-ebb3ef6a6364", + "serial_number": 443918, + "event_type": "hwc_rep", + "priority": 0, + "title": "hwc_rep", + "state": "new", + "contains": [], + "updated_at": "2024-09-09T21:01:10.669634+03:00", + "created_at": "2024-09-09T21:01:10.669604+03:00", + "geojson": { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 39.337466773044824, + -2.888976612685578 + ] + }, + "properties": { + "message": "", + "datetime": "2017-02-05T02:30:00+00:00", + "image": "https://test-data.pamdas.org/static/hwc_rep-gray.svg", + "icon": { + "iconUrl": "https://test-data.pamdas.org/static/hwc_rep-gray.svg", + "iconSize": [ + 25, + 25 + ], + "iconAncor": [ + 12, + 12 + ], + "popupAncor": [ + 0, + -13 + ], + "className": "dot" + } + } + }, + "is_collection": false + }, + { + "id": "42a76ef1-02f8-43de-8304-753c7749e213", + "serial_number": 444080, + "event_type": "injured_animal_rep", + "priority": 0, + "title": "injured_animal_rep", + "state": "new", + "contains": [], + "updated_at": "2024-09-10T00:53:42.143250+03:00", + "created_at": "2024-09-10T00:53:42.143217+03:00", + "geojson": {}, + "is_collection": false + }, + { + "id": "42a76ef1-02f8-43de-8304-753c7749e214", + "serial_number": 444081, + "event_type": "hwc_rep", + "priority": 0, + "title": "hwc_rep", + "state": "new", + "contains": [], + "updated_at": "2024-09-10T00:53:42.143250+03:00", + "created_at": "2024-09-10T00:53:42.143217+03:00", + "is_collection": false + } + ], + "image_url": "https:\/\/test-data.pamdas.org\/static\/sprite-src\/plane-patrol-icon.svg", + "icon_id": "plane-patrol-icon", + "updates": [ + { + "message": "Report Added", + "time": "2024-09-09T18:01:10.677469+00:00", + "user": { + "username": "user1", + "first_name": "Elebot", + "last_name": "Elebot", + "id": "abc123", + "content_type": "accounts.user" + }, + "type": "add_event" + }, + { + "message": "Report Added", + "time": "2024-09-09T18:01:10.613940+00:00", + "user": { + "username": "user1", + "first_name": "Elebot", + "last_name": "Elebot", + "id": "abc123", + "content_type": "accounts.user" + }, + "type": "add_event" + }, + { + "message": "Report Added", + "time": "2024-09-09T18:01:10.544995+00:00", + "user": { + "username": "user1", + "first_name": "Elebot", + "last_name": "Elebot", + "id": "abc123", + "content_type": "accounts.user" + }, + "type": "add_event" + }, + { + "message": "Report Added", + "time": "2024-09-09T18:01:10.470386+00:00", + "user": { + "username": "user1", + "first_name": "Elebot", + "last_name": "Elebot", + "id": "abc123", + "content_type": "accounts.user" + }, + "type": "add_event" + } + ] + } + ] + }, + "updates": { + "0": [ + { + "message": "Patrol Added", + "time": "2024-09-09T18:01:10.177573+00:00", + "user": { + "username": "user1", + "first_name": "Elebot", + "last_name": "Elebot", + "id": "abc123", + "content_type": "accounts.user" + }, + "type": "add_patrol" + } + ] + } +} \ No newline at end of file diff --git a/tests/test_earthranger_io.py b/tests/test_earthranger_io.py index 6f6d7dfc..cc519d4c 100644 --- a/tests/test_earthranger_io.py +++ b/tests/test_earthranger_io.py @@ -11,11 +11,37 @@ import ecoscope from erclient import ERClientException +from ecoscope.io.earthranger import EarthRangerIO from ecoscope.io.earthranger_utils import TIME_COLS pytestmark = pytest.mark.io +@pytest.fixture +def sample_bad_events_geojson(): + """ + A mock get_events response with intentionally bad geojson: + There are 6 events in this mock + event 0: 'geometry' is None + event 5: 'geomtery' and 'properties' are None + """ + return pd.read_feather("tests/sample_data/io/get_events_bad_geojson.feather") + + +@pytest.fixture +def sample_bad_patrol_events_geojson(): + """ + A mock get_patrol_events response with intentionally bad geojson: + There's a single patrol in this mock with events that have the following problems in their json + event 0: 'geometry' key is not present + event 1: 'properties' key is not present + event 2: 'datetime' key is not present within 'properties + event 3: is untouched + event 4: 'geojson' is an empty dict + """ + return pd.read_json("tests/sample_data/io/get_patrol_events_bad_geojson.json") + + def check_time_is_parsed(df): for col in TIME_COLS: if col in df.columns: @@ -374,3 +400,48 @@ def test_get_patrol_observations_with_patrol_filter(er_io): assert "patrol_title" in observations.columns assert "patrol_start_time" in observations.columns pd.testing.assert_series_equal(observations["patrol_id"], observations["groupby_col"], check_names=False) + + +@patch("erclient.client.ERClient.get_objects_multithreaded") +def test_get_events_bad_geojson(get_objects_mock, sample_bad_events_geojson, er_io): + get_objects_mock.return_value = sample_bad_events_geojson + + events = er_io.get_events(event_type=["e00ce1f6-f9f1-48af-93c9-fb89ec493b8a"]) + assert not events.empty + # of the 6 id's in the mock we expect these 4 to be returned + assert events.index.to_list() == [ + "bcda9c6a-628c-4825-947d-72f66115fc09", + "d464672a-3cc2-4d9a-bb3f-a69c34efb09c", + "4a599a57-7a89-4eb3-bb11-d2a36d1627e2", + "bcb01505-c635-48eb-b176-2b1390a0a5bf", + ] + + +@patch("erclient.client.ERClient.get_objects_multithreaded") +def test_get_patrol_events_bad_geojson(get_objects_mock, sample_bad_patrol_events_geojson, er_io): + get_objects_mock.return_value = sample_bad_patrol_events_geojson + + patrol_events = er_io.get_patrol_events( + since=pd.Timestamp("2017-01-01").isoformat(), + until=pd.Timestamp("2017-04-01").isoformat(), + ) + assert not patrol_events.empty + # We're rejecting any geojson that's missing geometry or a timestamp + assert patrol_events.id.to_list() == ["ebf812f5-e616-40e4-8fcf-ebb3ef6a6364"] + + +@pytest.mark.parametrize( + "er_callable, er_kwargs", + [ + (EarthRangerIO.get_patrols, {}), + (EarthRangerIO.get_subjectgroup_observations, {"subject_group_id": "12345"}), + (EarthRangerIO.get_patrol_observations_with_patrol_filter, {}), + (EarthRangerIO.get_patrol_events, {}), + (EarthRangerIO.get_events, {}), + ], +) +@patch("erclient.client.ERClient._get") +def test_empty_responses(_get_mock, er_io, er_callable, er_kwargs): + _get_mock.return_value = {} + df = er_callable(er_io, **er_kwargs) + assert df.empty