From 4fa44b053263a823502579f48c689b1509053d21 Mon Sep 17 00:00:00 2001 From: Evgenii Alekseev Date: Tue, 27 Aug 2024 15:12:26 +0300 Subject: [PATCH] refactor: allow event to receive keyword arguments This change also replaces the dataclass implementation of the class to custom one --- docs/ahriman.core.database.migrations.rst | 8 ++ docs/ahriman.core.database.operations.rst | 8 ++ docs/ahriman.models.rst | 16 ++++ docs/ahriman.web.schemas.rst | 16 ++++ docs/ahriman.web.views.v1.auditlog.rst | 21 +++++ docs/ahriman.web.views.v1.rst | 1 + .../database/operations/event_operations.py | 9 +- src/ahriman/models/event.py | 82 +++++++++++++++---- .../operations/test_event_operations.py | 2 +- tests/ahriman/models/test_event.py | 44 +++++++++- .../auditlog/test_view_v1_auditlog_events.py | 6 +- 11 files changed, 183 insertions(+), 30 deletions(-) create mode 100644 docs/ahriman.web.views.v1.auditlog.rst diff --git a/docs/ahriman.core.database.migrations.rst b/docs/ahriman.core.database.migrations.rst index 16fdf617..b2c80257 100644 --- a/docs/ahriman.core.database.migrations.rst +++ b/docs/ahriman.core.database.migrations.rst @@ -116,6 +116,14 @@ ahriman.core.database.migrations.m013\_dependencies module :no-undoc-members: :show-inheritance: +ahriman.core.database.migrations.m014\_auditlog module +------------------------------------------------------ + +.. automodule:: ahriman.core.database.migrations.m014_auditlog + :members: + :no-undoc-members: + :show-inheritance: + Module contents --------------- diff --git a/docs/ahriman.core.database.operations.rst b/docs/ahriman.core.database.operations.rst index 326979c1..68ca92f3 100644 --- a/docs/ahriman.core.database.operations.rst +++ b/docs/ahriman.core.database.operations.rst @@ -36,6 +36,14 @@ ahriman.core.database.operations.dependencies\_operations module :no-undoc-members: :show-inheritance: +ahriman.core.database.operations.event\_operations module +--------------------------------------------------------- + +.. automodule:: ahriman.core.database.operations.event_operations + :members: + :no-undoc-members: + :show-inheritance: + ahriman.core.database.operations.logs\_operations module -------------------------------------------------------- diff --git a/docs/ahriman.models.rst b/docs/ahriman.models.rst index c2b53818..a1af1af1 100644 --- a/docs/ahriman.models.rst +++ b/docs/ahriman.models.rst @@ -68,6 +68,14 @@ ahriman.models.dependencies module :no-undoc-members: :show-inheritance: +ahriman.models.event module +--------------------------- + +.. automodule:: ahriman.models.event + :members: + :no-undoc-members: + :show-inheritance: + ahriman.models.filesystem\_package module ----------------------------------------- @@ -100,6 +108,14 @@ ahriman.models.log\_record\_id module :no-undoc-members: :show-inheritance: +ahriman.models.metrics\_timer module +------------------------------------ + +.. automodule:: ahriman.models.metrics_timer + :members: + :no-undoc-members: + :show-inheritance: + ahriman.models.migration module ------------------------------- diff --git a/docs/ahriman.web.schemas.rst b/docs/ahriman.web.schemas.rst index bd4b5000..92c59694 100644 --- a/docs/ahriman.web.schemas.rst +++ b/docs/ahriman.web.schemas.rst @@ -60,6 +60,22 @@ ahriman.web.schemas.error\_schema module :no-undoc-members: :show-inheritance: +ahriman.web.schemas.event\_schema module +---------------------------------------- + +.. automodule:: ahriman.web.schemas.event_schema + :members: + :no-undoc-members: + :show-inheritance: + +ahriman.web.schemas.event\_search\_schema module +------------------------------------------------ + +.. automodule:: ahriman.web.schemas.event_search_schema + :members: + :no-undoc-members: + :show-inheritance: + ahriman.web.schemas.file\_schema module --------------------------------------- diff --git a/docs/ahriman.web.views.v1.auditlog.rst b/docs/ahriman.web.views.v1.auditlog.rst new file mode 100644 index 00000000..05cc602f --- /dev/null +++ b/docs/ahriman.web.views.v1.auditlog.rst @@ -0,0 +1,21 @@ +ahriman.web.views.v1.auditlog package +===================================== + +Submodules +---------- + +ahriman.web.views.v1.auditlog.events module +------------------------------------------- + +.. automodule:: ahriman.web.views.v1.auditlog.events + :members: + :no-undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: ahriman.web.views.v1.auditlog + :members: + :no-undoc-members: + :show-inheritance: diff --git a/docs/ahriman.web.views.v1.rst b/docs/ahriman.web.views.v1.rst index d484c6c8..747aa355 100644 --- a/docs/ahriman.web.views.v1.rst +++ b/docs/ahriman.web.views.v1.rst @@ -7,6 +7,7 @@ Subpackages .. toctree:: :maxdepth: 4 + ahriman.web.views.v1.auditlog ahriman.web.views.v1.distributed ahriman.web.views.v1.packages ahriman.web.views.v1.service diff --git a/src/ahriman/core/database/operations/event_operations.py b/src/ahriman/core/database/operations/event_operations.py index 8d51899d..c2616278 100644 --- a/src/ahriman/core/database/operations/event_operations.py +++ b/src/ahriman/core/database/operations/event_operations.py @@ -48,13 +48,8 @@ class EventOperations(Operations): def run(connection: Connection) -> list[Event]: return [ - Event( - event=row["event"], - object_id=row["object_id"], - message=row["message"], - data=row["data"], - created=row["created"], - ) for row in connection.execute( + Event.from_json(row) + for row in connection.execute( """ select created, event, object_id, message, data from auditlog where (:event is null or event = :event) diff --git a/src/ahriman/models/event.py b/src/ahriman/models/event.py index 201e166c..a159bb87 100644 --- a/src/ahriman/models/event.py +++ b/src/ahriman/models/event.py @@ -17,11 +17,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from dataclasses import dataclass, field, fields from enum import StrEnum from typing import Any, Self -from ahriman.core.utils import dataclass_view, filter_json, utcnow +from ahriman.core.utils import utcnow class EventType(StrEnum): @@ -41,7 +40,6 @@ class EventType(StrEnum): PackageUpdated = "package-updated" -@dataclass(frozen=True) class Event: """ audit log event @@ -54,18 +52,24 @@ class Event: object_id(str): object identifier """ - event: str | EventType - object_id: str - message: str | None = None - data: dict[str, Any] = field(default_factory=dict) - created: int = field(default_factory=lambda: int(utcnow().timestamp())) + def __init__(self, event: str | EventType, object_id: str, message: str | None = None, created: int | None = None, + **kwargs: Any): + """ + default constructor - def __post_init__(self) -> None: + Args: + event(str | EventType): event type + object_id(str): object identifier + message(str | None): event message if available + created(int | None, optional): event timestamp (Default value = None) + **kwargs(Any): event metadata """ - convert event type to enum if it is a well-known event type - """ - if self.event in EventType: - object.__setattr__(self, "event", EventType(self.event)) + self.event = EventType(event) if event in EventType else event + self.object_id = object_id + self.created = created or int(utcnow().timestamp()) + + self.message = message + self.data = kwargs @classmethod def from_json(cls, dump: dict[str, Any]) -> Self: @@ -78,9 +82,25 @@ class Event: Returns: Self: dependencies object """ - # filter to only known fields - known_fields = [pair.name for pair in fields(cls)] - return cls(**filter_json(dump, known_fields)) + return cls( + event=dump["event"], + object_id=dump["object_id"], + message=dump.get("message"), + created=dump.get("created"), + **dump.get("data", {}), + ) + + def get(self, key: str) -> Any: + """ + get a property + + Args: + key(str): key to lookup in data + + Returns: + Any: metadata property if available or ``None`` otherwise + """ + return self.data.get(key) def view(self) -> dict[str, Any]: """ @@ -89,4 +109,32 @@ class Event: Returns: dict[str, Any]: json-friendly dictionary """ - return dataclass_view(self) + dump = { + "event": self.event, + "object_id": self.object_id, + "created": self.created, + } + if self.message is not None: + dump["message"] = self.message + if self.data: + dump["data"] = self.data + + return dump + + def __eq__(self, other: Any) -> bool: + """ + check if other is the same object + + Args: + other(Any): other object instance + + Returns: + bool: ``True`` if the other object is the same and ``False`` otherwise + """ + if not isinstance(other, Event): + return False + return self.event == other.event \ + and self.object_id == other.object_id \ + and self.message == other.message \ + and self.created == other.created \ + and self.data == other.data diff --git a/tests/ahriman/core/database/operations/test_event_operations.py b/tests/ahriman/core/database/operations/test_event_operations.py index 4a66d392..58336f54 100644 --- a/tests/ahriman/core/database/operations/test_event_operations.py +++ b/tests/ahriman/core/database/operations/test_event_operations.py @@ -8,7 +8,7 @@ def test_event_insert_get(database: SQLite, package_ahriman: Package) -> None: """ must insert and get event """ - event = Event(EventType.PackageUpdated, package_ahriman.base, "Updated", {"key": "value"}) + event = Event(EventType.PackageUpdated, package_ahriman.base, "Updated", key="value") database.event_insert(event) assert database.event_get() == [event] diff --git a/tests/ahriman/models/test_event.py b/tests/ahriman/models/test_event.py index 0de5377d..91fb6276 100644 --- a/tests/ahriman/models/test_event.py +++ b/tests/ahriman/models/test_event.py @@ -1,17 +1,57 @@ from ahriman.models.event import Event, EventType -def test_post_init() -> None: +def test_init() -> None: """ must replace event type for known types """ assert Event("random", "") assert isinstance(Event(str(EventType.PackageUpdated), "").event, EventType) + assert Event("", "", key="value").data == {"key": "value"} + + assert Event("", "").created > 0 + def test_from_json_view() -> None: """ must construct and serialize event to json """ - event = Event("event", "object", "message", {"key": "value"}) + event = Event("event", "object", "message", key="value") assert Event.from_json(event.view()) == event + + +def test_get() -> None: + """ + must return property correctly + """ + assert Event("event", "object", "message", key="value").get("key") == "value" + assert Event("event", "object").get("key") is None + + +def test_view_empty() -> None: + """ + must skip empty fields during (de-)serialization + """ + event = Event("event", "object") + assert Event.from_json(event.view()) == event + assert "message" not in event.view() + assert "data" not in event.view() + + +def test_eq() -> None: + """ + must compare two events + """ + event1 = Event("1", "1", "1", 1, key="value") + assert event1 == event1 + + event2 = Event("2", "2", "2", 2, key="value") + assert event1 != event2 + + +def test_eq_other() -> None: + """ + must return False in case if object is not an instance of event + """ + assert Event("1", "1") != 42 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 cf534ee6..45eedda0 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 @@ -27,7 +27,7 @@ async def test_get(client: TestClient) -> None: """ must return all events """ - event1 = Event("event1", "object1", "message", {"key": "value"}) + event1 = Event("event1", "object1", "message", key="value") event2 = Event("event2", "object2") await client.post("/api/v1/events", json=event1.view()) await client.post("/api/v1/events", json=event2.view()) @@ -46,7 +46,7 @@ async def test_get_with_pagination(client: TestClient) -> None: """ must get events with pagination """ - event1 = Event("event1", "object1", "message", {"key": "value"}) + event1 = Event("event1", "object1", "message", key="value") event2 = Event("event2", "object2") await client.post("/api/v1/events", json=event1.view()) await client.post("/api/v1/events", json=event2.view()) @@ -83,7 +83,7 @@ async def test_post(client: TestClient) -> None: """ must create event """ - event = Event("event1", "object1", "message", {"key": "value"}) + event = Event("event1", "object1", "message", key="value") request_schema = pytest.helpers.schema_request(EventsView.post) payload = event.view()