diff --git a/ckanext/event_audit/config.py b/ckanext/event_audit/config.py index 1e48a77..0cbc522 100644 --- a/ckanext/event_audit/config.py +++ b/ckanext/event_audit/config.py @@ -3,5 +3,6 @@ CONF_ACTIVE_REPO = "ckanext.event_audit.active_repo" -def get_active_repo() -> str: +def active_repo() -> str: + """The active repository to store the audit logs.""" return tk.config[CONF_ACTIVE_REPO] diff --git a/ckanext/event_audit/interfaces.py b/ckanext/event_audit/interfaces.py index 1af8048..7891d4b 100644 --- a/ckanext/event_audit/interfaces.py +++ b/ckanext/event_audit/interfaces.py @@ -9,9 +9,13 @@ class IEventAudit(Interface): def register_repository(self) -> dict[str, type[repos.AbstractRepository]]: """Return the repositories provided by this plugin. - Return a dictionary mapping repository names (strings) to - repository classes. For example: + Example: + ``` + def register_repository(self): + return {RedisRepository.get_name(): RedisRepository} + ``` - {RedisRepository.get_name(): RedisRepository} + Returns: + mapping of repository names to repository classes """ return {} diff --git a/ckanext/event_audit/migration/event_audit/versions/9256fa265b84_create_event_table.py b/ckanext/event_audit/migration/event_audit/versions/001_9256fa265b84_create_event_table.py similarity index 90% rename from ckanext/event_audit/migration/event_audit/versions/9256fa265b84_create_event_table.py rename to ckanext/event_audit/migration/event_audit/versions/001_9256fa265b84_create_event_table.py index e32f5cd..a1e8865 100644 --- a/ckanext/event_audit/migration/event_audit/versions/9256fa265b84_create_event_table.py +++ b/ckanext/event_audit/migration/event_audit/versions/001_9256fa265b84_create_event_table.py @@ -1,10 +1,11 @@ -"""Create Event table +"""Create Event table. Revision ID: 9256fa265b84 Revises: Create Date: 2024-10-23 12:03:33.876737 """ + import sqlalchemy as sa from alembic import op @@ -27,8 +28,8 @@ def upgrade(): sa.Column("target_type", sa.String()), sa.Column("target_id", sa.String()), sa.Column("timestamp", sa.TIMESTAMP(timezone=True), nullable=False), - sa.Column("result", sa.JSON(), default={}), - sa.Column("payload", sa.JSON(), default={}), + sa.Column("result", sa.JSON(), server_default="{}"), + sa.Column("payload", sa.JSON(), server_default="{}"), ) op.create_index("ix_event_category", "event_audit_event", ["category"]) diff --git a/ckanext/event_audit/model.py b/ckanext/event_audit/model.py index 38120e0..b5b86fb 100644 --- a/ckanext/event_audit/model.py +++ b/ckanext/event_audit/model.py @@ -1,29 +1,46 @@ from __future__ import annotations -from sqlalchemy import TIMESTAMP, Column, Index, String +from datetime import datetime +from typing import Any + +from sqlalchemy import TIMESTAMP, Column, Index, String, Table from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.ext.mutable import MutableDict +from sqlalchemy.orm import Mapped import ckan.plugins.toolkit as tk from ckan import model class EventModel(tk.BaseModel): - __tablename__ = "event_audit_event" - - id = Column(String, primary_key=True) - category = Column(String, nullable=False, index=True) - action = Column(String, nullable=False, index=True) - actor = Column(String, index=True) - action_object = Column(String, index=True) - action_object_id = Column(String, index=True) - target_type = Column(String, index=True) - target_id = Column(String, index=True) - timestamp = Column(TIMESTAMP(timezone=True), nullable=False, index=True) - result = Column(MutableDict.as_mutable(JSONB), default="{}") - payload = Column(MutableDict.as_mutable(JSONB), default="{}") + __table__ = Table( + "event_audit_event", + tk.BaseModel.metadata, + Column("id", String, primary_key=True), + Column("category", String, nullable=False, index=True), + Column("action", String, nullable=False, index=True), + Column("actor", String, index=True), + Column("action_object", String, index=True), + Column("action_object_id", String, index=True), + Column("target_type", String, index=True), + Column("target_id", String, index=True), + Column("timestamp", TIMESTAMP(timezone=True), nullable=False, index=True), + Column("result", MutableDict.as_mutable(JSONB), default="{}"), # type: ignore + Column("payload", MutableDict.as_mutable(JSONB), default="{}"), # type: ignore + Index("ix_event_actor_action", "actor", "action"), + ) - __table_args__ = (Index("ix_event_actor_action", "actor", "action"),) + id: Mapped[str] + category: Mapped[str] + action: Mapped[str] + actor: Mapped[str | None] + action_object: Mapped[str | None] + action_object_id: Mapped[str | None] + target_type: Mapped[str | None] + target_id: Mapped[str | None] + timestamp: Mapped[datetime] + result: Mapped[dict[str, Any]] + payload: Mapped[dict[str, Any]] def save(self) -> None: model.Session.add(self) diff --git a/ckanext/event_audit/plugin.py b/ckanext/event_audit/plugin.py index 13efde5..452c50a 100644 --- a/ckanext/event_audit/plugin.py +++ b/ckanext/event_audit/plugin.py @@ -1,26 +1,10 @@ from __future__ import annotations -import os - -import yaml - import ckan.plugins.toolkit as tk from ckan import plugins as p -from ckan.config.declaration import Declaration, Key -from ckan.logic import clear_validators_cache @tk.blanket.config_declarations @tk.blanket.validators class EventAuditPlugin(p.SingletonPlugin): - p.implements(p.IConfigDeclaration) - - # IConfigDeclaration - - def declare_config_options(self, declaration: Declaration, key: Key): - # this call allows using custom validators in config declarations - clear_validators_cache() - - here = os.path.dirname(__file__) - with open(os.path.join(here, "config_declaration.yaml"), "rb") as src: - declaration.load_dict(yaml.safe_load(src)) + pass diff --git a/ckanext/event_audit/repositories/base.py b/ckanext/event_audit/repositories/base.py index 80c0b98..d5dff3f 100644 --- a/ckanext/event_audit/repositories/base.py +++ b/ckanext/event_audit/repositories/base.py @@ -1,6 +1,7 @@ from __future__ import annotations from abc import ABC, abstractmethod +from typing import Any from ckanext.event_audit import types @@ -25,7 +26,7 @@ def build_event(self, event_data: types.EventData) -> types.Event: return types.Event(**event_data) @abstractmethod - def get_event(self, event_id: str) -> types.Event | None: + def get_event(self, event_id: Any) -> types.Event | None: """Get a single event by its ID.""" @abstractmethod diff --git a/ckanext/event_audit/repositories/redis.py b/ckanext/event_audit/repositories/redis.py index 556b219..1865dcc 100644 --- a/ckanext/event_audit/repositories/redis.py +++ b/ckanext/event_audit/repositories/redis.py @@ -2,6 +2,7 @@ from datetime import datetime as dt from datetime import timezone as tz +from typing import Any from ckan.lib.redis import connect_to_redis @@ -51,7 +52,7 @@ def get_event(self, event_id: float) -> types.Event | None: for event_data in result.values(): return types.Event.model_validate_json(event_data) - def filter_events(self, filters: types.Filters) -> list[types.Event]: + def filter_events(self, filters: types.Filters | Any) -> list[types.Event]: """Filters events based on patterns generated from the provided filters.""" if not isinstance(filters, types.Filters): raise TypeError( @@ -67,7 +68,7 @@ def filter_events(self, filters: types.Filters) -> list[types.Event]: break cursor, result = self.conn.hscan( - REDIS_SET_KEY, cursor=cursor, match=pattern # type: ignore + REDIS_SET_KEY, cursor=cursor, match=pattern, # type: ignore ) matching_events.extend( diff --git a/ckanext/event_audit/tests/test_config.py b/ckanext/event_audit/tests/test_config.py index d8dcdae..5ecc5e4 100644 --- a/ckanext/event_audit/tests/test_config.py +++ b/ckanext/event_audit/tests/test_config.py @@ -8,4 +8,4 @@ @pytest.mark.usefixtures("with_plugins") class TestEventAuditConfig: def test_get_active_repo_default(self): - assert config.get_active_repo() == "redis" + assert config.active_repo() == "redis" diff --git a/ckanext/event_audit/types.py b/ckanext/event_audit/types.py index 7ec657f..89575b1 100644 --- a/ckanext/event_audit/types.py +++ b/ckanext/event_audit/types.py @@ -3,7 +3,7 @@ import uuid from dataclasses import dataclass from datetime import datetime, timezone -from typing import Any, Dict, Literal, Optional, TypedDict, Union +from typing import Any, Dict, Optional, TypedDict, Union from pydantic import BaseModel, ConfigDict, Field, validator @@ -83,10 +83,10 @@ def validate_actor(cls, v: str) -> str: @validator("timestamp") @classmethod def validate_timestamp(cls, v: Union[str, datetime]) -> str: - if v and isinstance(v, datetime): + if isinstance(v, datetime): return v.isoformat() - if not v or not isinstance(v, str): + if not v: raise ValueError("The `timestamp` field must be a non-empty string.") try: @@ -100,13 +100,13 @@ def validate_timestamp(cls, v: Union[str, datetime]) -> str: class ModelEvent(Event): """TODO: do we need it?""" - category: Literal["model"] = "model" + category: str = "model" class ApiEvent(Event): """TODO: do we need it?""" - category: Literal["api"] = "api" + category: str = "api" class Filters(BaseModel): diff --git a/ckanext/event_audit/utils.py b/ckanext/event_audit/utils.py index c680fd8..81c5a76 100644 --- a/ckanext/event_audit/utils.py +++ b/ckanext/event_audit/utils.py @@ -2,8 +2,8 @@ import ckan.plugins as p -import ckanext.event_audit.config as audit_config -import ckanext.event_audit.repositories as repos +from ckanext.event_audit import config +from ckanext.event_audit import repositories as repos from ckanext.event_audit.interfaces import IEventAudit @@ -11,15 +11,14 @@ def get_available_repos() -> dict[str, type[repos.AbstractRepository]]: """Get the available repositories. Returns: - dict[str, type[repos.AbstractRepository]]: The available repositories + available repositories """ plugin_repos: dict[str, type[repos.AbstractRepository]] = { repos.RedisRepository.get_name(): repos.RedisRepository, } for plugin in reversed(list(p.PluginImplementations(IEventAudit))): - for name, repo in plugin.register_repository().items(): - plugin_repos[name] = repo + plugin_repos.update(plugin.register_repository()) return plugin_repos @@ -28,9 +27,9 @@ def get_active_repo() -> type[repos.AbstractRepository]: """Get the active repository. Returns: - Type[repos.AbstractRepository]: The active repository + the active repository """ repos = get_available_repos() - active_repo_name = audit_config.get_active_repo() + active_repo_name = config.active_repo() return repos[active_repo_name] diff --git a/pyproject.toml b/pyproject.toml index 64ddeb4..2780878 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,50 @@ +[build-system] +requires = [ "setuptools>=61.2"] +build-backend = "setuptools.build_meta" + +[project] +name = "ckanext-event-audit" +version = "0.1.0" +description = "" +classifiers = [ + "Development Status :: 4 - Beta", + "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", +] +keywords = [ "CKAN" ] +dependencies = [ + "pydantic>=2.3.0,<2.4.0", +] +authors = [ + {name = "DataShades", email = "datashades@linkdigital.com.au"}, + {name = "LD"}, +] + +[project.readme] +file = "README.md" +content-type = "text/markdown" + +[project.license] +text = "AGPL" + +[project.urls] +Homepage = "https://github.com/DataShades/ckanext-event-audit" +Documentation = "https://datashades.github.io/ckanext-event-audit/" + +[project.optional-dependencies] + +[project.entry-points."ckan.plugins"] +event_audit = "ckanext.event_audit.plugin:EventAuditPlugin" +test_event_audit = "ckanext.event_audit.tests.test_interface:TestEventAuditPlugin" + +[project.entry-points."babel.extractors"] +ckan = "ckan.lib.extract:extract_ckan" + +[tool.setuptools.packages] +find = {} + [tool.ruff] target-version = "py38" exclude = [ diff --git a/setup.cfg b/setup.cfg index 7f7fd21..b7bb127 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,38 +1,3 @@ -[metadata] -name = ckanext-event-audit -version = 0.1.0 -description = -long_description = file: README.md -long_description_content_type = text/markdown -url = https://github.com/DataShades/ckanext-event-audit -author = LD -author_email = -license = AGPL -classifiers = - Development Status :: 4 - Beta - License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+) - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 -keywords = CKAN - -[options] -packages = find: -namespace_packages = ckanext -install_requires = - pydantic>=2.3.0,<2.4.0 -include_package_data = True - -[options.entry_points] -ckan.plugins = - event_audit = ckanext.event_audit.plugin:EventAuditPlugin - test_event_audit = ckanext.event_audit.tests.test_interface:TestEventAuditPlugin - -babel.extractors = - ckan = ckan.lib.extract:extract_ckan - -[options.extras_require] - [extract_messages] keywords = translate isPlural add_comments = TRANSLATORS: @@ -54,10 +19,3 @@ previous = true domain = ckanext-event_audit directory = ckanext/event_audit/i18n statistics = true - -[tool:pytest] -filterwarnings = - ignore::sqlalchemy.exc.SADeprecationWarning - ignore::sqlalchemy.exc.SAWarning - ignore::DeprecationWarning -addopts = --ckan-ini test.ini