Skip to content

Commit

Permalink
feature: allow registering new repositories
Browse files Browse the repository at this point in the history
  • Loading branch information
mutantsan committed Oct 22, 2024
1 parent 23cdfc7 commit 1da9a37
Show file tree
Hide file tree
Showing 14 changed files with 184 additions and 16 deletions.
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,65 @@ do:
pip install -r dev-requirements.txt


## Register new repositories

There are few repositories available by default, but you can register new repositories to store the events. Think of it as a way to store the events in different databases or services. We don't want to limit the extension to a specific storage. The main idea is to provide a way to store, retrieve, and filter the events.

To register a new repository, you need to define a repository class and register it.

### Defining the repository class

To register a new repository, you need to define a repository class that inherits from `AbstractRepository` and implements the following methods: `write_event`, `get_event`, and `filter_events`.

For example:

```python
from ckanext.event_audit.repositories import AbstractRepository
from ckanext.event_audit import types


class MyRepository(AbstractRepository):
name = "my_repository"

@classmethod
def get_name(cls) -> str:
return "my_repository"

def write_event(self, event: types.Event) -> types.WriteStatus:
pass

def get_event(self, event_id: str) -> types.Event | None:
pass

def filter_events(self, filters: types.Filters) -> list[types.Event]:
pass
```

See the existing repositories as examples (`ckanext/event_audit/repositories/`).

### Registering the repository

To register the new repository, you need to use a IEventAudit interface and the `register_repository` method.

For example:

```python
from ckanext.event_audit.interfaces import IEventAudit
from ckanext.your_extension.repositories import MyRepository

class MyRepositoryPlugin(plugins.SingletonPlugin):
...
plugins.implements(IEventAudit, inherit=True)

# IEventAudit

def register_repository(self) -> dict[str, type[AbstractRepository]]:
return {
MyRepository.get_name(): MyRepository,
}
```


## Tests

To run the tests, do:
Expand Down
2 changes: 1 addition & 1 deletion ckanext/event_audit/config_declaration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ groups:
- key: ckanext.event_audit.active_repo
description: The active repository to store the audit logs
default: redis
validators: one_of(["redis",])
validators: audit_active_repo_validator
editable: true
17 changes: 17 additions & 0 deletions ckanext/event_audit/interfaces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from __future__ import annotations

from ckan.plugins.interfaces import Interface

import ckanext.event_audit.repositories as repos


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:
{RedisRepository.get_name(): RedisRepository}
"""
return {}
Empty file.
15 changes: 15 additions & 0 deletions ckanext/event_audit/logic/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from __future__ import annotations

from typing import Any

import ckan.plugins.toolkit as tk
from ckan.types import Context

from ckanext.event_audit import utils


def audit_active_repo_validator(value: Any, context: Context) -> Any:
if value not in utils.get_available_repos():
raise tk.Invalid(f"Repository `{value}` is not registered")

return value
20 changes: 19 additions & 1 deletion ckanext/event_audit/plugin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
from __future__ import annotations

import os
import yaml

import ckan.plugins as plugins
import ckan.plugins.toolkit as tk
from ckan.config.declaration import Declaration, Key
from ckan.logic import clear_validators_cache


@tk.blanket.config_declarations
@tk.blanket.validators
class EventAuditPlugin(plugins.SingletonPlugin):
pass
plugins.implements(plugins.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))
7 changes: 6 additions & 1 deletion ckanext/event_audit/repositories/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
class AbstractRepository(ABC):
name = "abstract"

@classmethod
@abstractmethod
def get_name(cls) -> str:
"""Return the name of the repository"""

@abstractmethod
def write_event(self, event: types.Event) -> types.WriteStatus:
"""Write an event to the repository. This method accepts an Event object
Expand All @@ -17,7 +22,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:
def get_event(self, event_id: str) -> types.Event | None:
"""Get a single event by its ID"""

@abstractmethod
Expand Down
4 changes: 4 additions & 0 deletions ckanext/event_audit/repositories/redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
class RedisRepository(AbstractRepository):
name = "redis"

@classmethod
def get_name(cls) -> str:
return "redis"

def __init__(self) -> None:
self.conn = connect_to_redis()

Expand Down
41 changes: 41 additions & 0 deletions ckanext/event_audit/tests/test_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from __future__ import annotations

import pytest

import ckan.plugins as p

from ckanext.event_audit.interfaces import IEventAudit
from ckanext.event_audit.repositories import AbstractRepository
from ckanext.event_audit import types, utils


class MyRepository(AbstractRepository):
@classmethod
def get_name(cls) -> str:
return "my_repository"

def write_event(self, event: types.Event) -> types.WriteStatus:
return types.WriteStatus(status=True)

def get_event(self, event_id: str) -> types.Event | None:
return None

def filter_events(self, filters: types.Filters) -> list[types.Event]:
return []


class TestEventAuditPlugin(p.SingletonPlugin):
p.implements(IEventAudit, inherit=True)

def register_repository(self) -> dict[str, type[AbstractRepository]]:
return {
MyRepository.get_name(): MyRepository,
}


@pytest.mark.ckan_config("ckan.plugins", "event_audit test_event_audit")
@pytest.mark.usefixtures("non_clean_db", "with_plugins")
class TestOTLInterace(object):
def test_get_available_repos(self, app, user, sysadmin):
repos = utils.get_available_repos()
assert MyRepository.get_name() in repos
2 changes: 1 addition & 1 deletion ckanext/event_audit/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ def test_get_available_repos(self):
def test_get_active_repo(self):
result = utils.get_active_repo()

assert result.name == "redis"
assert result.get_name() == "redis"
assert result is repositories.RedisRepository
20 changes: 14 additions & 6 deletions ckanext/event_audit/utils.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
from __future__ import annotations

from typing import Type

import ckan.plugins as p

import ckanext.event_audit.repositories as repos
import ckanext.event_audit.config as audit_config
from ckanext.event_audit.interfaces import IEventAudit


def get_available_repos() -> dict[str, Type[repos.AbstractRepository]]:
def get_available_repos() -> dict[str, type[repos.AbstractRepository]]:
"""Get the available repositories
Returns:
dict[str, Type[repos.AbstractRepository]]: The available repositories
dict[str, type[repos.AbstractRepository]]: The available repositories
"""
return {
repos.RedisRepository.name: repos.RedisRepository,
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

return plugin_repos


def get_active_repo() -> Type[repos.AbstractRepository]:
def get_active_repo() -> type[repos.AbstractRepository]:
"""Get the active repository.
Returns:
Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
pydantic>=2.3.0,<2.4.0
10 changes: 6 additions & 4 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[metadata]
name = ckanext-event-audit
version = 0.0.1
description =
version = 0.1.0
description =
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com//ckanext-event-audit
author = LD
author_email =
author_email =
license = AGPL
classifiers =
Development Status :: 4 - Beta
Expand All @@ -20,14 +20,16 @@ keywords = CKAN
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
ckan = ckan.lib.extract:extract_ckan

[options.extras_require]

Expand Down
2 changes: 1 addition & 1 deletion test.ini
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use = config:../ckan/test-core.ini

# Insert any custom config settings to be used when running your extension's
# tests here. These will override the one defined in CKAN core's test-core.ini
ckan.plugins = event_audit
ckan.plugins = event_audit test_event_audit


# Logging configuration
Expand Down

0 comments on commit 1da9a37

Please sign in to comment.