Skip to content

Commit

Permalink
feat: add remove_events cli command, add remove_events method to repo…
Browse files Browse the repository at this point in the history
…sitories
  • Loading branch information
mutantsan committed Nov 19, 2024
1 parent 7caf585 commit 9df26cc
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 17 deletions.
60 changes: 57 additions & 3 deletions ckanext/event_audit/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import click

from ckanext.event_audit import types, utils
from ckanext.event_audit.repositories.base import RemoveFiltered, RemoveAll

__all__ = [
"event_audit",
Expand Down Expand Up @@ -61,11 +62,64 @@ def export_data(exporter_name: str, start: dt, end: dt | None, config: str | Non
exporter = utils.get_exporter(exporter_name)(**config_dict)
except TypeError as e:
return click.secho(f"Invalid exporter config: {config}. Error: {e}", fg="red")

if not exporter:
return click.secho(f"Unknown exporter: {exporter_name}", fg="red")
except ValueError as e:
return click.secho(e, fg="red")

if start and end and start > end:
return click.secho("Start date must be before the end date.", fg="red")

click.echo(exporter.from_filters(types.Filters(time_from=start, time_to=end)))


@event_audit.command()
@click.option("--repository", required=False, help="The repository name")
@click.option(
"--start",
required=False,
type=click.DateTime(formats=["%Y-%m-%d"]),
help="ISO format start date",
)
@click.option(
"--end",
required=False,
type=click.DateTime(formats=["%Y-%m-%d"]),
help="ISO format end date",
)
def remove_events(repository: str | None, start: dt | None, end: dt | None):
"""Remove events from the repository by time range.
Args:
repository (str | None): The repository name. If not provided, the
active repository will be used.
start (str): The start date string in %Y-%m-%d format.
end (str | None): The end date string in %Y-%m-%d format.
Example:
$ ckan event-audit remove-events --start=2024-11-11 --end=2024-11-12
"""
if start and end and start > end:
return click.secho("Start date must be before the end date.", fg="red")

repo = utils.get_repo(repository) if repository else utils.get_active_repo()

if not repo:
return click.secho(f"Unknown repository: {repository}", fg="red")

if not isinstance(repo, (RemoveFiltered, RemoveAll)):
return click.secho(
f"Repository {repository} does not support removing events.", fg="red"
)

if not isinstance(repo, RemoveFiltered):
if start or end:
return click.secho(
(
f"Repository {repository} does not support removing events by time range. "
"Please remove the --start and --end flags to delete all events."
),
fg="red",
)
else:
return repo.remove_all_events()

return repo.remove_events(types.Filters(time_from=start, time_to=end))
19 changes: 19 additions & 0 deletions ckanext/event_audit/repositories/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ def remove_event(self, event_id: Any) -> types.Result:
"""
raise NotImplementedError

def remove_events(self, filters: types.Filters) -> types.Result:
"""Removes a filtered set of events from the repository.
Args:
filters (types.Filters): filters to apply.
Returns:
types.Result: result of the operation.
"""
raise NotImplementedError

def remove_all_events(self) -> types.Result:
"""Removes all events from the repository.
Expand Down Expand Up @@ -135,3 +146,11 @@ class RemoveAll:
If the repository supports remove all events, it should inherit from
this class.
"""


class RemoveFiltered:
"""Mark the repository as supporting remove a filtered set of events.
If the repository supports remove a filtered set of events, it should inherit from
this class.
"""
4 changes: 4 additions & 0 deletions ckanext/event_audit/repositories/cloudwatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ def remove_event(self, event_id: str) -> types.Result:
"""
raise NotImplementedError

def remove_events(self, filters: types.Filters) -> types.Result:
"""See `remove_event` method docstring."""
raise NotImplementedError

def remove_all_events(self) -> types.Result:
"""Removes all events from the repository.
Expand Down
41 changes: 39 additions & 2 deletions ckanext/event_audit/repositories/postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,24 @@ def filter_events(self, filters: types.Filters) -> List[types.Event]:
Args:
filters (types.Filters): filters to apply.
Returns:
List[types.Event]: list of events.
"""
return [
types.Event.model_validate(event) for event in self._filter_events(filters)
]

def _filter_events(self, filters: types.Filters) -> list[model.EventModel]:
"""Filters events based on provided filter criteria.
Args:
filters (types.Filters): filters to apply.
Returns:
list[model.EventModel]: list of event models.
"""

query = select(model.EventModel)

filterable_fields = [
Expand All @@ -108,8 +125,7 @@ def filter_events(self, filters: types.Filters) -> List[types.Event]:

query.order_by(model.EventModel.timestamp)

result = self.session.execute(query).scalars().all()
return [types.Event.model_validate(event) for event in result]
return self.session.execute(query).scalars().all()

def remove_event(
self,
Expand All @@ -136,6 +152,27 @@ def remove_event(

return types.Result(status=False, message="Event not found")

def remove_events(self, filters: types.Filters) -> types.Result:
"""Removes a filtered set of events from the repository.
Args:
filters (types.Filters): filters to apply.
Returns:
types.Result: result of the operation.
"""

events = self._filter_events(filters)

for event in events:
event.delete(defer_commit=True)

self.session.commit()

return types.Result(
status=True, message=f"{len(events)} event(s) removed successfully"
)

def remove_all_events(self) -> types.Result:
"""Removes all events from the repository.
Expand Down
22 changes: 21 additions & 1 deletion ckanext/event_audit/repositories/redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
AbstractRepository,
RemoveAll,
RemoveSingle,
RemoveFiltered,
)

REDIS_SET_KEY = "event-audit"


class RedisRepository(AbstractRepository, RemoveAll, RemoveSingle):
class RedisRepository(AbstractRepository, RemoveAll, RemoveSingle, RemoveFiltered):
@classmethod
def get_name(cls) -> str:
return "redis"
Expand Down Expand Up @@ -166,6 +167,25 @@ def remove_event(self, event_id: float) -> types.Result:

return types.Result(status=True, message="Event removed successfully")

def remove_events(self, filters: types.Filters) -> types.Result:
"""Removes a filtered set of events from the repository.
Args:
filters (types.Filters): filters to apply.
Returns:
types.Result: result of the operation.
"""
events = self.filter_events(filters)

for event in events:
key = self._build_event_key(event)
self.conn.hdel(REDIS_SET_KEY, key)

return types.Result(
status=True, message=f"{len(events)} event(s) removed successfully"
)

def remove_all_events(self) -> types.Result:
"""Removes all events from the repository.
Expand Down
9 changes: 9 additions & 0 deletions ckanext/event_audit/tests/repositories/test_cloudwatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from datetime import timezone as tz
from typing import Any

import pytest
from botocore.stub import Stubber

from ckanext.event_audit import const, types
Expand Down Expand Up @@ -162,3 +163,11 @@ def test_remove_all_events(
result = repo.remove_all_events()

assert result.status

def test_remove_filtered_events(
self, cloudwatch_repo: tuple[CloudWatchRepository, Stubber]
):
repo, _ = cloudwatch_repo

with pytest.raises(NotImplementedError):
repo.remove_events(types.Filters())
17 changes: 17 additions & 0 deletions ckanext/event_audit/tests/repositories/test_postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,20 @@ def test_remove_all_events(

events = repo.filter_events(types.Filters())
assert len(events) == 0

def test_remove_filtered_events(
self, event_factory: Callable[..., types.Event], repo: PostgresRepository
):
event_factory(category="test")

for _ in range(5):
repo.write_event(event_factory(category="test2"))

assert len(repo.filter_events(types.Filters())) == 5

status = repo.remove_events(types.Filters(category="test2"))
assert status.message == "5 event(s) removed successfully"
assert status.status

events = repo.filter_events(types.Filters())
assert len(events) == 0
17 changes: 17 additions & 0 deletions ckanext/event_audit/tests/repositories/test_redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,20 @@ def test_redis_remove_all_events(

events = repo.filter_events(types.Filters())
assert len(events) == 0

def test_redis_remove_filtered_events(
self, event_factory: Callable[..., types.Event], repo: RedisRepository
):
event_factory(category="test")

for _ in range(5):
repo.write_event(event_factory(category="test2"))

assert len(repo.filter_events(types.Filters())) == 5

status = repo.remove_events(types.Filters(category="test2"))
assert status.message == "5 event(s) removed successfully"
assert status.status

events = repo.filter_events(types.Filters())
assert len(events) == 0
40 changes: 40 additions & 0 deletions ckanext/event_audit/tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import pytest

from ckanext.event_audit.cli import export_data, remove_events


class TestExportDataCLI:
"""Test that we are able to export data to a file using the CLI."""

def test_exporter_doesnt_exist(self, cli):
result = cli.invoke(export_data, ["xxx", "--start", "2024-1-1"])

assert "Exporter xxx not found" in result.output

def test_csv_no_data(self, cli):
result = cli.invoke(export_data, ["csv", "--start", "2024-1-1"])

assert result.output == "\n"

def test_invalid_config_type(self, cli):
result = cli.invoke(
export_data, ["csv", "--start", "2024-1-1", "--config", "xxx"]
)

assert "Invalid JSON format for config" in result.output

def test_invalid_exporter_config(self, cli):
result = cli.invoke(
export_data, ["csv", "--start", "2024-1-1", "--config", '{"xxx": "yyy"}']
)

assert "got an unexpected keyword argument 'xxx'" in result.output
assert "Invalid exporter config" in result.output

def test_end_date_goes_before_start_date(self, cli):
result = cli.invoke(
export_data,
["csv", "--start", "2024-1-2", "--end", "2024-1-1"],
)

assert "Start date must be before the end date" in result.output
11 changes: 0 additions & 11 deletions docs/repositories/test.md

This file was deleted.

0 comments on commit 9df26cc

Please sign in to comment.