From fbc2dee7a0deb4837b35fbb1c194f267768648fd Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 27 Feb 2026 16:44:01 +0000 Subject: [PATCH 1/2] fix(api): return list of events from insert endpoint (match aw-server-rust) The POST /api/0/buckets/{id}/events endpoint previously returned either a single event dict or null, inconsistent with aw-server-rust which always returns a list of events with server-assigned IDs. Changes: - api.py: create_events() now returns List[Event], using insert_one for single events (to get ID) and insert_many for bulk - rest.py: Always serialize response as a list of event dicts - tests: Add test_insert_event_returns_list and test_insert_events_returns_list This enables aw-client to use response.json()[0] uniformly against both server implementations (see ActivityWatch/aw-client#103). --- aw_server/api.py | 14 +++++++++++--- aw_server/rest.py | 4 ++-- tests/test_server.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/aw_server/api.py b/aw_server/api.py index bd7178ed..18c60c23 100644 --- a/aw_server/api.py +++ b/aw_server/api.py @@ -228,11 +228,19 @@ def get_events( return events @check_bucket_exists - def create_events(self, bucket_id: str, events: List[Event]) -> Optional[Event]: + def create_events(self, bucket_id: str, events: List[Event]) -> List[Event]: """Create events for a bucket. Can handle both single events and multiple ones. - Returns the inserted event when a single event was inserted, otherwise None.""" - return self.db[bucket_id].insert(events) + Always returns a list of inserted events (matching aw-server-rust behavior). + For single events, the returned event includes the server-assigned ID. + For bulk inserts, events may not have IDs due to storage limitations.""" + if len(events) == 1: + # Pass as single Event so Bucket.insert uses insert_one (returns Event with ID) + inserted = self.db[bucket_id].insert(events[0]) + return [inserted] + else: + self.db[bucket_id].insert(events) + return events @check_bucket_exists def get_eventcount( diff --git a/aw_server/rest.py b/aw_server/rest.py index 8b4b6ff9..98d7b261 100644 --- a/aw_server/rest.py +++ b/aw_server/rest.py @@ -225,8 +225,8 @@ def post(self, bucket_id): else: raise BadRequest("Invalid POST data", "") - event = current_app.api.create_events(bucket_id, events) - return event.to_json_dict() if event else None, 200 + events = current_app.api.create_events(bucket_id, events) + return [e.to_json_dict() for e in events], 200 @api.route("/0/buckets//events/count") diff --git a/tests/test_server.py b/tests/test_server.py index 1c1c2ea6..428bd36f 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -88,4 +88,48 @@ def get_events(): assert len(r.json) == n_events +def test_insert_event_returns_list(flask_client, bucket): + """Test that POST /events returns a list of events with IDs (matching aw-server-rust).""" + now = datetime.now() + event_data = {"timestamp": now.isoformat(), "duration": 0, "data": {"label": "test"}} + + # Single event as list + r = flask_client.post( + f"/api/0/buckets/{bucket}/events", + json=[event_data], + ) + assert r.status_code == 200 + assert isinstance(r.json, list), f"Expected list, got {type(r.json)}" + assert len(r.json) == 1 + assert r.json[0]["id"] is not None + assert r.json[0]["data"] == {"label": "test"} + + # Single event as dict (legacy format) + r = flask_client.post( + f"/api/0/buckets/{bucket}/events", + json=event_data, + ) + assert r.status_code == 200 + assert isinstance(r.json, list), f"Expected list, got {type(r.json)}" + assert len(r.json) == 1 + assert r.json[0]["id"] is not None + + +def test_insert_events_returns_list(flask_client, bucket): + """Test that POST /events with multiple events returns a list.""" + now = datetime.now() + events_data = [ + {"timestamp": (now - timedelta(hours=i)).isoformat(), "duration": 0, "data": {"label": f"test-{i}"}} + for i in range(3) + ] + + r = flask_client.post( + f"/api/0/buckets/{bucket}/events", + json=events_data, + ) + assert r.status_code == 200 + assert isinstance(r.json, list), f"Expected list, got {type(r.json)}" + assert len(r.json) == 3 + + # TODO: Add benchmark for basic AFK-filtering query From 5273dc3984c96cfc09003755d06a3b634a2cfc7a Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 27 Feb 2026 16:47:35 +0000 Subject: [PATCH 2/2] style: format tests with black --- tests/test_server.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index 428bd36f..6cb013f0 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -91,7 +91,11 @@ def get_events(): def test_insert_event_returns_list(flask_client, bucket): """Test that POST /events returns a list of events with IDs (matching aw-server-rust).""" now = datetime.now() - event_data = {"timestamp": now.isoformat(), "duration": 0, "data": {"label": "test"}} + event_data = { + "timestamp": now.isoformat(), + "duration": 0, + "data": {"label": "test"}, + } # Single event as list r = flask_client.post( @@ -119,7 +123,11 @@ def test_insert_events_returns_list(flask_client, bucket): """Test that POST /events with multiple events returns a list.""" now = datetime.now() events_data = [ - {"timestamp": (now - timedelta(hours=i)).isoformat(), "duration": 0, "data": {"label": f"test-{i}"}} + { + "timestamp": (now - timedelta(hours=i)).isoformat(), + "duration": 0, + "data": {"label": f"test-{i}"}, + } for i in range(3) ]