mirror of
				https://github.com/arcan1s/ahriman.git
				synced 2025-10-30 21:33:43 +00:00 
			
		
		
		
	feat: allow filter events by timestamp
This commit is contained in:
		| @ -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, | ||||
|                     } | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
|  | ||||
| @ -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: | ||||
|         """ | ||||
|  | ||||
| @ -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: | ||||
|         """ | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
| @ -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, | ||||
|     }) | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
| @ -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: | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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: | ||||
|     """ | ||||
|  | ||||
		Reference in New Issue
	
	Block a user