diff --git a/src/ahriman/core/database/operations/event_operations.py b/src/ahriman/core/database/operations/event_operations.py index cee03b73..023d6dc9 100644 --- a/src/ahriman/core/database/operations/event_operations.py +++ b/src/ahriman/core/database/operations/event_operations.py @@ -30,6 +30,7 @@ class EventOperations(Operations): """ def event_get(self, event: str | EventType | None = None, object_id: str | None = None, + from_date: int | None = None, to_date: int | None = None, limit: int = -1, offset: int = 0, repository_id: RepositoryId | None = None) -> list[Event]: """ get list of events with filters applied @@ -37,6 +38,8 @@ class EventOperations(Operations): Args: event(str | EventType | None, optional): filter by event type (Default value = None) object_id(str | None, optional): filter by event object (Default value = None) + from_date(int | None, optional): minimal creation date, inclusive (Default value = None) + to_date(int | None, optional): maximal creation date, exclusive (Default value = None) limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1) offset(int, optional): records offset (Default value = 0) repository_id(RepositoryId, optional): repository unique identifier override (Default value = None) @@ -55,6 +58,8 @@ class EventOperations(Operations): select * from auditlog where (:event is null or event = :event) and (:object_id is null or object_id = :object_id) + and (:from_date is null or created >= :from_date) + and (:to_date is null or created < :to_date) and repository = :repository order by created desc limit :limit offset :offset ) order by created asc @@ -63,6 +68,8 @@ class EventOperations(Operations): "event": event, "object_id": object_id, "repository": repository_id.id, + "from_date": from_date, + "to_date": to_date, "limit": limit, "offset": offset, } diff --git a/src/ahriman/core/status/client.py b/src/ahriman/core/status/client.py index b8e8322f..08abc5cd 100644 --- a/src/ahriman/core/status/client.py +++ b/src/ahriman/core/status/client.py @@ -93,6 +93,7 @@ class Client: raise NotImplementedError def event_get(self, event: str | EventType | None, object_id: str | None, + from_date: int | None = None, to_date: int | None = None, limit: int = -1, offset: int = 0) -> list[Event]: """ retrieve list of events @@ -100,6 +101,8 @@ class Client: Args: event(str | EventType | None): filter by event type object_id(str | None): filter by event object + from_date(int | None, optional): minimal creation date, inclusive (Default value = None) + to_date(int | None, optional): maximal creation date, exclusive (Default value = None) limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1) offset(int, optional): records offset (Default value = 0) diff --git a/src/ahriman/core/status/local_client.py b/src/ahriman/core/status/local_client.py index 2cf3e90a..bd0d2a3e 100644 --- a/src/ahriman/core/status/local_client.py +++ b/src/ahriman/core/status/local_client.py @@ -59,6 +59,7 @@ class LocalClient(Client): self.database.event_insert(event, self.repository_id) def event_get(self, event: str | EventType | None, object_id: str | None, + from_date: int | None = None, to_date: int | None = None, limit: int = -1, offset: int = 0) -> list[Event]: """ retrieve list of events @@ -66,13 +67,15 @@ class LocalClient(Client): Args: event(str | EventType | None): filter by event type object_id(str | None): filter by event object + from_date(int | None, optional): minimal creation date, inclusive (Default value = None) + to_date(int | None, optional): maximal creation date, exclusive (Default value = None) limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1) offset(int, optional): records offset (Default value = 0) Returns: list[Event]: list of audit log events """ - return self.database.event_get(event, object_id, limit, offset, self.repository_id) + return self.database.event_get(event, object_id, from_date, to_date, limit, offset, self.repository_id) def package_changes_get(self, package_base: str) -> Changes: """ diff --git a/src/ahriman/core/status/watcher.py b/src/ahriman/core/status/watcher.py index a8b2c634..fa838710 100644 --- a/src/ahriman/core/status/watcher.py +++ b/src/ahriman/core/status/watcher.py @@ -71,7 +71,7 @@ class Watcher(LazyLogging): event_add: Callable[[Event], None] - event_get: Callable[[str | EventType | None, str | None, int, int], list[Event]] + event_get: Callable[[str | EventType | None, str | None, int | None, int | None, int, int], list[Event]] def load(self) -> None: """ diff --git a/src/ahriman/core/status/web_client.py b/src/ahriman/core/status/web_client.py index ce13050a..6b890278 100644 --- a/src/ahriman/core/status/web_client.py +++ b/src/ahriman/core/status/web_client.py @@ -178,6 +178,7 @@ class WebClient(Client, SyncAhrimanClient): self.make_request("POST", self._events_url(), params=self.repository_id.query(), json=event.view()) def event_get(self, event: str | EventType | None, object_id: str | None, + from_date: int | None = None, to_date: int | None = None, limit: int = -1, offset: int = 0) -> list[Event]: """ retrieve list of events @@ -185,6 +186,8 @@ class WebClient(Client, SyncAhrimanClient): Args: event(str | EventType | None): filter by event type object_id(str | None): filter by event object + from_date(int | None, optional): minimal creation date, inclusive (Default value = None) + to_date(int | None, optional): maximal creation date, exclusive (Default value = None) limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1) offset(int, optional): records offset (Default value = 0) @@ -196,6 +199,10 @@ class WebClient(Client, SyncAhrimanClient): query.append(("event", str(event))) if object_id is not None: query.append(("object_id", object_id)) + if from_date is not None: + query.append(("from_date", str(from_date))) + if to_date is not None: + query.append(("to_date", str(to_date))) with contextlib.suppress(Exception): response = self.make_request("GET", self._events_url(), params=query) diff --git a/src/ahriman/web/schemas/event_search_schema.py b/src/ahriman/web/schemas/event_search_schema.py index 3fd65c30..f35364b9 100644 --- a/src/ahriman/web/schemas/event_search_schema.py +++ b/src/ahriman/web/schemas/event_search_schema.py @@ -36,3 +36,11 @@ class EventSearchSchema(PaginationSchema): "description": "Event object identifier", "example": "ahriman", }) + from_date = fields.Integer(metadata={ + "description": "Minimal creation timestamp, inclusive", + "example": 1680537091, + }) + to_date = fields.Integer(metadata={ + "description": "Maximal creation timestamp, exclusive", + "example": 1680537091, + }) diff --git a/src/ahriman/web/views/base.py b/src/ahriman/web/views/base.py index 942177c5..fe7f7b4c 100644 --- a/src/ahriman/web/views/base.py +++ b/src/ahriman/web/views/base.py @@ -158,7 +158,7 @@ class BaseView(View, CorsViewMixin): value = extractor(key) if not value: raise KeyError(key) - except Exception: + except (KeyError, ValueError): raise KeyError(f"Key {key} is missing or empty") from None return value @@ -194,7 +194,7 @@ class BaseView(View, CorsViewMixin): try: limit = int(self.request.query.get("limit", default=-1)) offset = int(self.request.query.get("offset", default=0)) - except Exception as ex: + except ValueError as ex: raise HTTPBadRequest(reason=str(ex)) # some checks diff --git a/src/ahriman/web/views/v1/auditlog/events.py b/src/ahriman/web/views/v1/auditlog/events.py index 21847aa5..f696f643 100644 --- a/src/ahriman/web/views/v1/auditlog/events.py +++ b/src/ahriman/web/views/v1/auditlog/events.py @@ -64,8 +64,16 @@ class EventsView(BaseView): limit, offset = self.page() event = self.request.query.get("event") or None object_id = self.request.query.get("object_id") or None + try: + from_date = to_date = None + if (value := self.request.query.get("from_date")) is not None: + from_date = int(value) + if (value := self.request.query.get("to_date")) is not None: + to_date = int(value) + except ValueError as ex: + raise HTTPBadRequest(reason=str(ex)) - events = self.service().event_get(event, object_id, limit, offset) + events = self.service().event_get(event, object_id, from_date, to_date, limit, offset) response = [event.view() for event in events] return json_response(response) diff --git a/tests/ahriman/core/status/test_local_client.py b/tests/ahriman/core/status/test_local_client.py index b09953d1..255983a7 100644 --- a/tests/ahriman/core/status/test_local_client.py +++ b/tests/ahriman/core/status/test_local_client.py @@ -29,8 +29,9 @@ def test_event_get(local_client: LocalClient, package_ahriman: Package, mocker: must retrieve events """ event_mock = mocker.patch("ahriman.core.database.SQLite.event_get") - local_client.event_get(EventType.PackageUpdated, package_ahriman.base, 1, 2) - event_mock.assert_called_once_with(EventType.PackageUpdated, package_ahriman.base, 1, 2, local_client.repository_id) + local_client.event_get(EventType.PackageUpdated, package_ahriman.base, from_date=10, to_date=20, limit=1, offset=2) + event_mock.assert_called_once_with(EventType.PackageUpdated, package_ahriman.base, 10, 20, 1, 2, + local_client.repository_id) def test_package_changes_get(local_client: LocalClient, package_ahriman: Package, mocker: MockerFixture) -> None: diff --git a/tests/ahriman/core/status/test_web_client.py b/tests/ahriman/core/status/test_web_client.py index 5bf9843e..b092cda3 100644 --- a/tests/ahriman/core/status/test_web_client.py +++ b/tests/ahriman/core/status/test_web_client.py @@ -185,7 +185,7 @@ def test_event_get_filter(web_client: WebClient, mocker: MockerFixture) -> None: requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request", return_value=response_obj) - web_client.event_get("event", "object", 1, 2) + web_client.event_get("event", "object", limit=1, offset=2) requests_mock.assert_called_once_with("GET", pytest.helpers.anyvar(str, True), params=web_client.repository_id.query() + [ ("limit", "1"), @@ -195,6 +195,28 @@ def test_event_get_filter(web_client: WebClient, mocker: MockerFixture) -> None: ]) +def test_event_get_filter_from_to(web_client: WebClient, mocker: MockerFixture) -> None: + """ + must get events with filter by creation date + """ + response_obj = requests.Response() + response_obj._content = json.dumps(Event("", "").view()).encode("utf8") + response_obj.status_code = 200 + + requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request", return_value=response_obj) + + web_client.event_get("event", "object", from_date=1, to_date=2) + requests_mock.assert_called_once_with("GET", pytest.helpers.anyvar(str, True), + params=web_client.repository_id.query() + [ + ("limit", "-1"), + ("offset", "0"), + ("event", "event"), + ("object_id", "object"), + ("from_date", "1"), + ("to_date", "2"), + ]) + + def test_event_get_failed(web_client: WebClient, mocker: MockerFixture) -> None: """ must suppress any exception happened during events fetch diff --git a/tests/ahriman/web/views/v1/auditlog/test_view_v1_auditlog_events.py b/tests/ahriman/web/views/v1/auditlog/test_view_v1_auditlog_events.py index 47bd1213..ee6b0f3a 100644 --- a/tests/ahriman/web/views/v1/auditlog/test_view_v1_auditlog_events.py +++ b/tests/ahriman/web/views/v1/auditlog/test_view_v1_auditlog_events.py @@ -51,7 +51,6 @@ async def test_get_with_pagination(client: TestClient) -> None: await client.post("/api/v1/events", json=event1.view()) await client.post("/api/v1/events", json=event2.view()) request_schema = pytest.helpers.schema_request(EventsView.get, location="querystring") - response_schema = pytest.helpers.schema_response(EventsView.get) payload = {"limit": 1, "offset": 1} assert not request_schema.validate(payload) @@ -59,8 +58,25 @@ async def test_get_with_pagination(client: TestClient) -> None: assert response.status == 200 json = await response.json() - assert not response_schema.validate(json, many=True) + assert [Event.from_json(event) for event in json] == [event1] + +async def test_get_with_filter(client: TestClient) -> None: + """ + must get events with filter by creation date + """ + event1 = Event("event1", "object1", "message", key="value", created=1) + event2 = Event("event2", "object2", created=2) + await client.post("/api/v1/events", json=event1.view()) + await client.post("/api/v1/events", json=event2.view()) + request_schema = pytest.helpers.schema_request(EventsView.get, location="querystring") + + payload = {"from_date": 1, "to_date": 2} + assert not request_schema.validate(payload) + response = await client.get("/api/v1/events", params=payload) + assert response.status == 200 + + json = await response.json() assert [Event.from_json(event) for event in json] == [event1] @@ -78,6 +94,14 @@ async def test_get_bad_request(client: TestClient) -> None: assert response.status == 400 assert not response_schema.validate(await response.json()) + response = await client.get("/api/v1/events", params={"from_date": "from_date"}) + assert response.status == 400 + assert not response_schema.validate(await response.json()) + + response = await client.get("/api/v1/events", params={"to_date": "to_date"}) + assert response.status == 400 + assert not response_schema.validate(await response.json()) + async def test_post(client: TestClient) -> None: """