diff --git a/404.html b/404.html index f4e979c..103f5d1 100644 --- a/404.html +++ b/404.html @@ -12,7 +12,7 @@ - + @@ -401,6 +401,8 @@ + + @@ -439,11 +441,11 @@
  • - + - Active repository + Repository @@ -480,6 +482,27 @@ +
  • + + + + + Admin panel + + + + +
  • + + + + + + + + + +
  • diff --git a/assets/_mkdocstrings.css b/assets/_mkdocstrings.css index b500381..85449ec 100644 --- a/assets/_mkdocstrings.css +++ b/assets/_mkdocstrings.css @@ -26,33 +26,20 @@ float: right; } -/* Parameter headings must be inline, not blocks. */ -.doc-heading-parameter { - display: inline; -} - -/* Prefer space on the right, not the left of parameter permalinks. */ -.doc-heading-parameter .headerlink { - margin-left: 0 !important; - margin-right: 0.2rem; -} - /* Backward-compatibility: docstring section titles in bold. */ .doc-section-title { font-weight: bold; } /* Symbols in Navigation and ToC. */ -:root, :host, +:root, [data-md-color-scheme="default"] { - --doc-symbol-parameter-fg-color: #df50af; --doc-symbol-attribute-fg-color: #953800; --doc-symbol-function-fg-color: #8250df; --doc-symbol-method-fg-color: #8250df; --doc-symbol-class-fg-color: #0550ae; --doc-symbol-module-fg-color: #5cad0f; - --doc-symbol-parameter-bg-color: #df50af1a; --doc-symbol-attribute-bg-color: #9538001a; --doc-symbol-function-bg-color: #8250df1a; --doc-symbol-method-bg-color: #8250df1a; @@ -61,14 +48,12 @@ } [data-md-color-scheme="slate"] { - --doc-symbol-parameter-fg-color: #ffa8cc; --doc-symbol-attribute-fg-color: #ffa657; --doc-symbol-function-fg-color: #d2a8ff; --doc-symbol-method-fg-color: #d2a8ff; --doc-symbol-class-fg-color: #79c0ff; --doc-symbol-module-fg-color: #baff79; - --doc-symbol-parameter-bg-color: #ffa8cc1a; --doc-symbol-attribute-bg-color: #ffa6571a; --doc-symbol-function-bg-color: #d2a8ff1a; --doc-symbol-method-bg-color: #d2a8ff1a; @@ -83,15 +68,6 @@ code.doc-symbol { font-weight: bold; } -code.doc-symbol-parameter { - color: var(--doc-symbol-parameter-fg-color); - background-color: var(--doc-symbol-parameter-bg-color); -} - -code.doc-symbol-parameter::after { - content: "param"; -} - code.doc-symbol-attribute { color: var(--doc-symbol-attribute-fg-color); background-color: var(--doc-symbol-attribute-bg-color); diff --git a/cli/index.html b/cli/index.html index 9ff36e8..e9e0fba 100644 --- a/cli/index.html +++ b/cli/index.html @@ -14,11 +14,11 @@ - + - + @@ -470,6 +470,8 @@ + + @@ -508,11 +510,11 @@
  • - + - Active repository + Repository @@ -549,6 +551,27 @@ +
  • + + + + + Admin panel + + + + +
  • + + + + + + + + + +
  • @@ -1028,11 +1051,6 @@

    CLI

    - - - - -
    @@ -1066,9 +1084,7 @@

    - - exporter_name - + exporter_name

    The name of the exporter.

    @@ -1082,9 +1098,7 @@

    - - start - + start

    The start date string in %Y-%m-%d format.

    @@ -1098,9 +1112,7 @@

    - - end - + end

    The end date string in %Y-%m-%d format.

    @@ -1114,9 +1126,7 @@

    - - config - + config

    The exporter config in JSON format. See the exporter's @@ -1306,9 +1316,7 @@

    - - repository - + repository

    The repository name. If not provided, the @@ -1323,9 +1331,7 @@

    - - start - + start

    The start date string in %Y-%m-%d format.

    @@ -1339,9 +1345,7 @@

    - - end - + end

    The end date string in %Y-%m-%d format.

    @@ -1538,13 +1542,13 @@

    - + @@ -944,18 +1035,34 @@ -

    Active repository

    -

    The event audit logs are stored in a configurable storages, we call them repositories.

    -

    The default repository is redis, but it can be changed to a different one. To do this, we have to set the following configuration options in the CKAN configuration file:

    -
    ckanext.event_audit.active_repo = postgres
    -
    +

    Repository

    + +

    The event audit logs are stored in a configurable storages, we call them repositories. To use an extension, you have to choose one of the available repositories.

    The following repositories are available:

    1. redis - the default repository, stores logs in Redis.
    2. postgres - stores logs in a PostgreSQL database.
    3. cloudwatch - stores logs in AWS CloudWatch.
    +
    +Note

    If the cloudwatch repository is used, the extension will automatically create a log group in CloudWatch. Also, check the CloudWatch repository documentation for additional configuration options.

    +
    +

    Active repository

    +

    The default repository is redis, but it can be changed to a different one. To do this, we have to set the following configuration options in the CKAN configuration file:

    +
    ckanext.event_audit.active_repo = postgres
    +
    +

    List of available repositories

    +

    You can restrict a list of available repositories by setting the following configuration option in the CKAN configuration file:

    +
    ckanext.event_audit.active_repo = cloudwatch
    +ckanext.event_audit.restrict_available_repos = cloudwatch
    +
    +
    +Note +

    By default, we're not restricting the list of available repositories. It means that all registered repositories are available for use.

    +
    +

    This could be useful if you want to limit the available repositories to a specific set of options due to some security concerns. +This config option won't be available in the admin interface and can't be changed in real time.

    diff --git a/configure/tracking/index.html b/configure/tracking/index.html index 5ff5c4d..fa9c38a 100644 --- a/configure/tracking/index.html +++ b/configure/tracking/index.html @@ -18,7 +18,7 @@ - + @@ -414,6 +414,8 @@ + + @@ -452,11 +454,11 @@
  • - + - Active repository + Repository @@ -493,6 +495,27 @@ +
  • + + + + + Admin panel + + + + +
  • + + + + + + + + + +
  • diff --git a/exporters/basic/index.html b/exporters/basic/index.html index b6fc540..be1e475 100644 --- a/exporters/basic/index.html +++ b/exporters/basic/index.html @@ -18,7 +18,7 @@ - + @@ -412,6 +412,8 @@ + + @@ -450,11 +452,11 @@
  • - + - Active repository + Repository @@ -491,6 +493,27 @@ +
  • + + + + + Admin panel + + + + +
  • + + + + + + + + + +
  • @@ -1058,11 +1081,6 @@

    Base exporter class

    - - - - -
    @@ -1102,9 +1120,7 @@

    - - events - + events

    events to export

    @@ -1176,9 +1192,7 @@

    - - filters - + filters

    search filters.

    @@ -1192,9 +1206,7 @@

    - - repo_name - + repo_name

    name of the repo to use. Defaults to None.

    diff --git a/exporters/csv/index.html b/exporters/csv/index.html index 8331edc..0a0b2ac 100644 --- a/exporters/csv/index.html +++ b/exporters/csv/index.html @@ -18,7 +18,7 @@ - + @@ -412,6 +412,8 @@ + + @@ -450,11 +452,11 @@
  • - + - Active repository + Repository @@ -491,6 +493,27 @@ +
  • + + + + + Admin panel + + + + +
  • + + + + + + + + + +
  • @@ -1032,11 +1055,6 @@

    CSV exporter

    - - - - -
    @@ -1070,9 +1088,7 @@

    - - delimiter - + delimiter

    delimiter. Defaults to ",".

    @@ -1090,9 +1106,7 @@

    - - quotechar - + quotechar

    quote character. Defaults to '"'.

    @@ -1110,9 +1124,7 @@

    - - quoting - + quoting

    quoting. Defaults to QUOTE_ALL.

    @@ -1130,9 +1142,7 @@

    - - ignore_fields - + ignore_fields

    fields to ignore. By @@ -1180,9 +1190,7 @@

    - - events - + events

    events to export.

    diff --git a/exporters/json/index.html b/exporters/json/index.html index f6ba974..ae4de50 100644 --- a/exporters/json/index.html +++ b/exporters/json/index.html @@ -18,7 +18,7 @@ - + @@ -412,6 +412,8 @@ + + @@ -450,11 +452,11 @@
  • - + - Active repository + Repository @@ -491,6 +493,27 @@ +
  • + + + + + Admin panel + + + + +
  • + + + + + + + + + +
  • @@ -1034,11 +1057,6 @@

    JSON exporter

    - - - - -
    @@ -1072,9 +1090,7 @@

    - - stringify - + stringify

    whether to return a string or a dict. By @@ -1122,9 +1138,7 @@

    - - events - + events

    events to export.

    diff --git a/exporters/tsv/index.html b/exporters/tsv/index.html index 36a4929..a91703b 100644 --- a/exporters/tsv/index.html +++ b/exporters/tsv/index.html @@ -18,7 +18,7 @@ - + @@ -412,6 +412,8 @@ + + @@ -450,11 +452,11 @@
  • - + - Active repository + Repository @@ -491,6 +493,27 @@ +
  • + + + + + Admin panel + + + + +
  • + + + + + + + + + +
  • @@ -1014,11 +1037,6 @@

    TSV exporter

    - - - - -
    @@ -1052,9 +1070,7 @@

    - - ignore_fields - + ignore_fields

    fields to ignore. By diff --git a/exporters/xlsx/index.html b/exporters/xlsx/index.html index 7363a4f..e857a34 100644 --- a/exporters/xlsx/index.html +++ b/exporters/xlsx/index.html @@ -18,7 +18,7 @@ - + @@ -412,6 +412,8 @@ + + @@ -450,11 +452,11 @@

  • - + - Active repository + Repository @@ -491,6 +493,27 @@ +
  • + + + + + Admin panel + + + + +
  • + + + + + + + + + +
  • @@ -1032,11 +1055,6 @@

    XLSX exporter

    - - - - -
    @@ -1070,9 +1088,7 @@

    - - file_path - + file_path

    path to the file or BytesIO object to @@ -1087,9 +1103,7 @@

    - - ignore_fields - + ignore_fields

    fields to ignore. By @@ -1137,9 +1151,7 @@

    - - events - + events

    events to export.

    diff --git a/img/ap_config.png b/img/ap_config.png new file mode 100644 index 0000000..faad130 Binary files /dev/null and b/img/ap_config.png differ diff --git a/img/ap_toolbar.png b/img/ap_toolbar.png new file mode 100644 index 0000000..e31dd83 Binary files /dev/null and b/img/ap_toolbar.png differ diff --git a/index.html b/index.html index 6cad978..d5c06ea 100644 --- a/index.html +++ b/index.html @@ -16,7 +16,7 @@ - + @@ -477,6 +477,8 @@ + + @@ -515,11 +517,11 @@
  • - + - Active repository + Repository @@ -556,6 +558,27 @@ +
  • + + + + + Admin panel + + + + +
  • + + + + + + + + + +
  • diff --git a/install/index.html b/install/index.html index b98b588..468fa3d 100644 --- a/install/index.html +++ b/install/index.html @@ -18,7 +18,7 @@ - + @@ -470,6 +470,8 @@ + + @@ -508,11 +510,11 @@
  • - + - Active repository + Repository @@ -549,6 +551,27 @@ +
  • + + + + + Admin panel + + + + +
  • + + + + + + + + + +
  • diff --git a/interfaces/index.html b/interfaces/index.html index cab4aa2..6b5e999 100644 --- a/interfaces/index.html +++ b/interfaces/index.html @@ -18,7 +18,7 @@ - + @@ -488,6 +488,8 @@ + + @@ -526,11 +528,11 @@
  • - + - Active repository + Repository @@ -567,6 +569,27 @@ +
  • + + + + + Admin panel + + + + +
  • + + + + + + + + + +
  • @@ -1098,11 +1121,6 @@

    IEventAudit

    - - - - -
    diff --git a/repositories/abstract/index.html b/repositories/abstract/index.html index 1522a0f..f18caa8 100644 --- a/repositories/abstract/index.html +++ b/repositories/abstract/index.html @@ -18,7 +18,7 @@ - + @@ -412,6 +412,8 @@ + + @@ -450,11 +452,11 @@
  • - + - Active repository + Repository @@ -491,6 +493,27 @@ +
  • + + + + + Admin panel + + + + +
  • + + + + + + + + + +
  • @@ -1205,11 +1228,6 @@

    Abstract

    - - - - -
    @@ -1260,9 +1278,7 @@

    - - event_data - + event_data

    event data.

    @@ -1329,9 +1345,7 @@

    - - event - + event

    event to write.

    @@ -1402,9 +1416,7 @@

    - - filters - + filters

    filters to apply.

    @@ -1451,9 +1463,7 @@

    - - event_id - + event_id

    event ID.

    @@ -1611,9 +1621,7 @@

    - - event_id - + event_id

    event ID.

    @@ -1680,9 +1688,7 @@

    - - filters - + filters

    filters to apply.

    @@ -1802,9 +1808,7 @@

    - - event - + event

    event to write.

    @@ -1871,9 +1875,7 @@

    - - events - + events

    events to write.

    diff --git a/repositories/basic/index.html b/repositories/basic/index.html index 8094c99..448d975 100644 --- a/repositories/basic/index.html +++ b/repositories/basic/index.html @@ -18,7 +18,7 @@ - + @@ -407,6 +407,8 @@ + + @@ -445,11 +447,11 @@
  • - + - Active repository + Repository @@ -486,6 +488,27 @@ +
  • + + + + + Admin panel + + + + +
  • + + + + + + + + + +
  • diff --git a/repositories/cloudwatch/index.html b/repositories/cloudwatch/index.html index 2b0880a..ae9d36f 100644 --- a/repositories/cloudwatch/index.html +++ b/repositories/cloudwatch/index.html @@ -18,7 +18,7 @@ - + @@ -412,6 +412,8 @@ + + @@ -450,11 +452,11 @@
  • - + - Active repository + Repository @@ -491,6 +493,27 @@ +
  • + + + + + Admin panel + + + + +
  • + + + + + + + + + +
  • @@ -1140,11 +1163,6 @@

    Cloudwatch repository

    - - - - -
    @@ -1178,9 +1196,7 @@

    - - credentials - + credentials

    AWS credentials. @@ -1199,9 +1215,7 @@

    - - log_group - + log_group

    CloudWatch log group name.

    @@ -1219,9 +1233,7 @@

    - - log_stream - + log_stream

    CloudWatch log stream name.

    @@ -1268,9 +1280,7 @@

    - - filters - + filters

    filters to apply.

    @@ -1313,9 +1323,7 @@

    - - event_id - + event_id

    event ID.

    @@ -1513,9 +1521,7 @@

    - - event - + event

    event to write.

    diff --git a/repositories/custom/index.html b/repositories/custom/index.html index 044e1ed..4012c40 100644 --- a/repositories/custom/index.html +++ b/repositories/custom/index.html @@ -16,7 +16,7 @@ - + @@ -405,6 +405,8 @@ + + @@ -443,11 +445,11 @@
  • - + - Active repository + Repository @@ -484,6 +486,27 @@ +
  • + + + + + Admin panel + + + + +
  • + + + + + + + + + +
  • diff --git a/repositories/postgres/index.html b/repositories/postgres/index.html index b176f5d..1ce3330 100644 --- a/repositories/postgres/index.html +++ b/repositories/postgres/index.html @@ -18,7 +18,7 @@ - + @@ -412,6 +412,8 @@ + + @@ -450,11 +452,11 @@
  • - + - Active repository + Repository @@ -491,6 +493,27 @@ +
  • + + + + + Admin panel + + + + +
  • + + + + + + + + + +
  • @@ -1140,11 +1163,6 @@

    Postgres repository

    - - - - -
    @@ -1178,9 +1196,7 @@

    - - filters - + filters

    filters to apply.

    @@ -1247,9 +1263,7 @@

    - - event_id - + event_id

    event ID.

    @@ -1357,9 +1371,7 @@

    - - event_id - + event_id

    event ID.

    @@ -1373,9 +1385,7 @@

    - - session - + session

    session to use.

    @@ -1393,9 +1403,7 @@

    - - defer_commit - + defer_commit

    whether to defer the commit.

    @@ -1466,9 +1474,7 @@

    - - filters - + filters

    filters to apply.

    @@ -1580,9 +1586,7 @@

    - - event - + event

    event to write.

    @@ -1596,9 +1600,7 @@

    - - session - + session

    session to use.

    @@ -1616,9 +1618,7 @@

    - - defer_commit - + defer_commit

    whether to defer the commit.

    @@ -1689,9 +1689,7 @@

    - - events - + events

    events to write.

    diff --git a/repositories/redis/index.html b/repositories/redis/index.html index 9f59eb0..f8865da 100644 --- a/repositories/redis/index.html +++ b/repositories/redis/index.html @@ -18,7 +18,7 @@ - + @@ -412,6 +412,8 @@ + + @@ -450,11 +452,11 @@
  • - + - Active repository + Repository @@ -491,6 +493,27 @@ +
  • + + + + + Admin panel + + + + +
  • + + + + + + + + + +
  • @@ -1122,11 +1145,6 @@

    Redis repository

    - - - - -
    @@ -1160,9 +1178,7 @@

    - - filters - + filters

    filters to apply.

    @@ -1205,9 +1221,7 @@

    - - event_id - + event_id

    event ID.

    @@ -1291,9 +1305,7 @@

    - - event_id - + event_id

    event ID.

    @@ -1360,9 +1372,7 @@

    - - filters - + filters

    filters to apply.

    @@ -1474,9 +1484,7 @@

    - - event - + event

    event to write.

    diff --git a/search/search_index.json b/search/search_index.json index 325e010..626d236 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-\\.\\_]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Home","text":""},{"location":"#ckanext-event-audit","title":"ckanext-event-audit","text":"

    This extension will capture and retain a comprehensive record of all changes within a CKAN app.

    "},{"location":"#developer-installation","title":"Developer installation","text":"

    To install ckanext-event-audit for development, activate your CKAN virtualenv and do:

    git clone https://github.com/DataShades/ckanext-event-audit.git\ncd ckanext-event-audit\npip install -e .\npip install -r dev-requirements.txt\n
    "},{"location":"#tests","title":"Tests","text":"

    To run the tests, do:

    pytest --ckan-ini=test.ini\n
    "},{"location":"#license","title":"License","text":"

    AGPL

    "},{"location":"cli/","title":"CLI","text":""},{"location":"cli/#event_audit.cli.export_data","title":"export_data(exporter_name, start, end, config)","text":"

    Export data using the specified exporter.

    PARAMETER DESCRIPTION exporter_name

    The name of the exporter.

    TYPE: str

    start

    The start date string in %Y-%m-%d format.

    TYPE: str

    end

    The end date string in %Y-%m-%d format.

    TYPE: str | None

    config

    The exporter config in JSON format. See the exporter's documentation for args details.

    TYPE: str | None

    RETURNS DESCRIPTION str

    The exported data

    Example

    $ ckan event-audit export-data csv --start=2024-11-11 > report.csv

    $ ckan event-audit export-data json --start=2024-11-11 | jq '[.[] | {id, category, action}]'

    $ ckan event-audit export-data xlsx --start=2024-11-11 --config='{\"file_path\": \"/tmp/test.xlsx\"}'

    Source code in ckanext/event_audit/cli.py
    @event_audit.command()\n@click.argument(\"exporter_name\", type=str)\n@click.option(\n    \"--start\",\n    required=True,\n    type=click.DateTime(formats=[\"%Y-%m-%d\"]),\n    help=\"ISO format start date\",\n)\n@click.option(\n    \"--end\",\n    required=False,\n    type=click.DateTime(formats=[\"%Y-%m-%d\"]),\n    help=\"ISO format end date\",\n)\n@click.option(\"--config\", required=False, type=str, help=\"Custom config in JSON format\")\ndef export_data(exporter_name: str, start: dt, end: dt | None, config: str | None):\n    \"\"\"Export data using the specified exporter.\n\n    Args:\n        exporter_name (str): The name of the exporter.\n        start (str): The start date string in %Y-%m-%d format.\n        end (str | None): The end date string in %Y-%m-%d format.\n        config (str | None): The exporter config in JSON format. See the exporter's\n            documentation for args details.\n\n    Returns:\n        str : The exported data\n\n    Example:\n        $ ckan event-audit export-data csv --start=2024-11-11 > report.csv\n\n        $ ckan event-audit export-data json --start=2024-11-11 | jq '[.[] |\n            {id, category, action}]'\n\n        $ ckan event-audit export-data xlsx --start=2024-11-11\n            --config='{\"file_path\": \"/tmp/test.xlsx\"}'\n    \"\"\"\n    start = UTC.localize(start) if start else None\n    end = UTC.localize(end) if end else None\n\n    try:\n        config_dict = json.loads(config or \"{}\")\n    except json.JSONDecodeError:\n        return click.secho(\"Invalid JSON format for config.\")\n\n    try:\n        exporter = utils.get_exporter(exporter_name)(**config_dict)\n    except TypeError as e:\n        return click.secho(f\"Invalid exporter config: {config}. Error: {e}\", fg=\"red\")\n    except ValueError as e:\n        return click.secho(e, fg=\"red\")\n\n    if start and end and start > end:\n        return click.secho(\"Start date must be before the end date.\", fg=\"red\")\n\n    click.echo(exporter.from_filters(types.Filters(time_from=start, time_to=end)))\n
    "},{"location":"cli/#event_audit.cli.remove_events","title":"remove_events(repository, start, end)","text":"

    Remove events from the repository by time range.

    PARAMETER DESCRIPTION repository

    The repository name. If not provided, the active repository will be used.

    TYPE: str | None

    start

    The start date string in %Y-%m-%d format.

    TYPE: str

    end

    The end date string in %Y-%m-%d format.

    TYPE: str | None

    Example

    $ ckan event-audit remove-events --start=2024-11-11 --end=2024-11-12

    Source code in ckanext/event_audit/cli.py
    @event_audit.command()\n@click.option(\"--repository\", required=False, help=\"The repository name\")\n@click.option(\n    \"--start\",\n    required=False,\n    type=click.DateTime(formats=[\"%Y-%m-%d\"]),\n    help=\"ISO format start date\",\n)\n@click.option(\n    \"--end\",\n    required=False,\n    type=click.DateTime(formats=[\"%Y-%m-%d\"]),\n    help=\"ISO format end date\",\n)\ndef remove_events(repository: str | None, start: dt | None, end: dt | None):\n    \"\"\"Remove events from the repository by time range.\n\n    Args:\n        repository (str | None): The repository name. If not provided, the\n            active repository will be used.\n        start (str): The start date string in %Y-%m-%d format.\n        end (str | None): The end date string in %Y-%m-%d format.\n\n    Example:\n        $ ckan event-audit remove-events --start=2024-11-11 --end=2024-11-12\n    \"\"\"\n    start = UTC.localize(start) if start else None\n    end = UTC.localize(end) if end else None\n\n    if start and end and start > end:\n        return click.secho(\"Start date must be before the end date.\", fg=\"red\")\n\n    try:\n        repo = utils.get_repo(repository) if repository else utils.get_active_repo()\n    except ValueError:\n        return click.secho(f\"Unknown repository: {repository}\", fg=\"red\")\n\n    if not (start or end) and not isinstance(repo, repositories.RemoveAll):\n        return click.secho(\n            f\"Repository {repository} does not support removing events.\", fg=\"red\"\n        )\n\n    if not isinstance(repo, repositories.RemoveFiltered):\n        if start or end:\n            return click.secho(\n                (\n                    f\"Repository {repository} does not support removing events \"\n                    \"by time range. \"\n                    \"Please remove the --start and --end flags to delete all events.\"\n                ),\n                fg=\"red\",\n            )\n        return repo.remove_all_events()\n\n    return repo.remove_events(types.Filters(time_from=start, time_to=end))\n
    "},{"location":"install/","title":"Installation","text":""},{"location":"install/#requirements","title":"Requirements","text":"

    Compatibility with core CKAN versions:

    CKAN version Compatible? 2.9 no 2.10 yes 2.11 yes master yes"},{"location":"install/#installation_1","title":"Installation","text":"
    1. Install the extension from PyPI:

      pip install ckanext-event-audit\n

    2. Enable the plugin in your CKAN configuration file (e.g. ckan.ini or production.ini):

      ckan.plugins = ... event_audit ...\n

    3. Run DB migrations:

      ckan db upgrade -p event_audit\n

    4. Configure the extension up to your needs and you're ready to go. See the documentation for more details about the configuration options.

    "},{"location":"interfaces/","title":"Interfaces","text":""},{"location":"interfaces/#ieventaudit","title":"IEventAudit","text":"

    Extend functionality of ckanext-event-audit.

    Example:

    import ckan.plugins as p\n\nfrom ckanext.event_audit.interfaces import IEventAudit\nfrom ckanext.event_audit.repositories import AbstractRepository\nfrom ckanext.event_audit.exporters import AbstractExporter\n\nclass MyPlugin(p.SingletonPlugin):\n    p.implements(IEventAudit, inherit=True)\n\n    def register_repository(self) -> dict[str, type[AbstractRepository]]:\n        return {\n            \"my_repo\": MyRepository,\n        }\n\n    def register_exporter(self) -> dict[str, type[AbstractExporter]]:\n        return {\n            \"my_exporter\": MyExporter,\n        }\n\n    def skip_event(self, event: types.Event) -> bool:\n        if event.category == \"api\" and event.action == \"status_show\":\n            return True\n\n        if event.category == \"model\" and event.action_object == \"Dashboard\":\n            return True\n\n        return False\n

    "},{"location":"interfaces/#event_audit.interfaces.IEventAudit.register_exporter","title":"register_exporter()","text":"

    Return the exporters provided by this plugin.

    Example
    def register_exporter(self):\n    return {\n        \"csv\": CSVExporter,\n    }\n
    RETURNS DESCRIPTION dict[str, type[AbstractExporter]]

    mapping of exporter names to exporter classes

    "},{"location":"interfaces/#event_audit.interfaces.IEventAudit.register_repository","title":"register_repository()","text":"

    Return the repositories provided by this plugin.

    Example
    def register_repository(self):\n    return {\n        \"my_repo\": MyRepository,\n    }\n
    RETURNS DESCRIPTION dict[str, type[AbstractRepository]]

    mapping of repository names to repository classes

    "},{"location":"interfaces/#event_audit.interfaces.IEventAudit.skip_event","title":"skip_event(event)","text":"

    Skip an event.

    This method is called before writing the event to the repository.

    Example
    def skip_event(self, event: types.Event) -> bool:\n    if event.category == \"api\" and event.action == \"status_show\":\n        return True\n\n    if  event.category == \"model\"  and event.action_object == \"Dashboard\":\n        return True\n\n    return False\n
    RETURNS DESCRIPTION bool

    True if the event should be skipped, False otherwise

    "},{"location":"usage/","title":"Usage","text":"

    To use an event audit in your extension, you should get an instance of the repository class. There are two ways to do this:

    1. Using the get_repo function:

      import ckanext.event_audit.utils as utils\n\nrepo = utils.get_active_repo()\n\nevent = repo.build_event({\"category\": \"xxx\", \"action\": \"xxx\"})\n\nrepo.write_event(event)\n

      The get_active_repo function will return an instance of the active repository class that is configured in the CKAN configuration file.

    2. Using the get_repo function with a specific repository:

      import ckanext.event_audit.utils as utils\n\nrepo = utils.get_repo(\"file\")\n\nevent = repo.build_event({\"category\": \"xxx\", \"action\": \"xxx\"})\n\nrepo.write_event(event)\n

      The get_repo function will return an instance of the repository class that is specified in the argument. You can use this method to get a specific repository instance.

    "},{"location":"utils/","title":"Utility Functions","text":""},{"location":"utils/#event_audit.utils.get_active_repo","title":"get_active_repo()","text":"

    Get the active repository.

    The active repository is the one that is currently configured in the extension configuration.

    RETURNS DESCRIPTION AbstractRepository

    The active repository.

    "},{"location":"utils/#event_audit.utils.get_available_exporters","title":"get_available_exporters()","text":"

    Retrieve a dictionary of available exporters.

    This function collects and returns a dictionary where the keys are exporter names (as strings) and the values are the corresponding exporter classes.

    RETURNS DESCRIPTION dict[str, type[AbstractExporter]]

    A dictionary mapping exporter names to their respective exporter classes.

    "},{"location":"utils/#event_audit.utils.get_available_repos","title":"get_available_repos()","text":"

    Retrieve a dictionary of available repositories.

    This function collects and returns a dictionary where the keys are repository names (as strings) and the values are the corresponding repository classes.

    RETURNS DESCRIPTION dict[str, type[AbstractRepository]]

    A dictionary mapping repository names to their respective repository classes.

    "},{"location":"utils/#event_audit.utils.get_exporter","title":"get_exporter(exporter_name)","text":"

    Retrieve an exporter class by name.

    This function retrieves an exporter class by name. If the exporter is not found, a ValueError is raised.

    PARAMETER DESCRIPTION exporter_name

    The name of the exporter to retrieve.

    TYPE: str

    RETURNS DESCRIPTION type[AbstractExporter]

    The exporter class.

    "},{"location":"utils/#event_audit.utils.get_repo","title":"get_repo(repo_name)","text":"

    Retrieve a repository class by name.

    This function retrieves a repository class by name. If the repository is not found, a ValueError is raised.

    PARAMETER DESCRIPTION repo_name

    The name of the repository to retrieve.

    TYPE: str

    RETURNS DESCRIPTION AbstractRepository

    The repository class.

    "},{"location":"utils/#event_audit.utils.test_active_connection","title":"test_active_connection()","text":"

    Test the connection to the active repository.

    When we test the connection, we store the result in the repository object, so we can reuse it later.

    RETURNS DESCRIPTION bool

    whether the connection is active

    "},{"location":"validators/","title":"Validators","text":""},{"location":"validators/#event_audit.logic.validators.audit_repo_exists","title":"audit_repo_exists(value, context)","text":"

    Check if the repository with the given name is registered.

    PARAMETER DESCRIPTION value

    The repository name.

    TYPE: Any

    context

    The CKAN context.

    TYPE: Context

    RETURNS DESCRIPTION Any

    The repository name if it exists.

    TYPE: Any

    RAISES DESCRIPTION Invalid

    If the repository does not exist.

    Source code in ckanext/event_audit/logic/validators.py
    def audit_repo_exists(value: Any, context: Context) -> Any:\n    \"\"\"Check if the repository with the given name is registered.\n\n    Args:\n        value (Any): The repository name.\n        context (Context): The CKAN context.\n\n    Returns:\n        Any: The repository name if it exists.\n\n    Raises:\n        tk.Invalid: If the repository does not exist.\n    \"\"\"\n    if value not in utils.get_available_repos():\n        raise tk.Invalid(f\"Repository `{value}` is not registered\")\n\n    return value\n
    "},{"location":"configure/active_repo/","title":"Active repository","text":"

    The event audit logs are stored in a configurable storages, we call them repositories.

    The default repository is redis, but it can be changed to a different one. To do this, we have to set the following configuration options in the CKAN configuration file:

    ckanext.event_audit.active_repo = postgres\n

    The following repositories are available:

    1. redis - the default repository, stores logs in Redis.
    2. postgres - stores logs in a PostgreSQL database.
    3. cloudwatch - stores logs in AWS CloudWatch.

    If the cloudwatch repository is used, the extension will automatically create a log group in CloudWatch. Also, check the CloudWatch repository documentation for additional configuration options.

    "},{"location":"configure/async/","title":"Asynchronous processing","text":"

    To avoid blocking the main thread, the extension uses a separate thread to write the audit logs. A separate thread will be started automatically along with the CKAN application.

    The thread is responsible for storing the logs in the configured repository.

    By default, we're using the threaded mode. However, if you want to disable the threaded mode, you can do this by setting the following configuration option in the CKAN configuration file:

    ckanext.event_audit.threaded_mode = false\n

    Disabling the threaded mode will cause the extension to write the logs in the main thread. This can be useful for debugging purposes.

    Note, that pairing it with the Cloudwatch repository is not recommended, as it can block the main thread for a long time. Network operations can be slow and can cause the application to hang for a while.

    If your custom repository involves a network operations, it's recommended to keep the threaded mode enabled.

    "},{"location":"configure/async/#batch-size","title":"Batch size","text":"

    The extension writes the logs in batches. The batch size can be adjusted by setting the following configuration option in the CKAN configuration file:

    ckanext.event_audit.batch.size = 50\n

    By default, we're accumulating 50 events before writing them to the repository.

    "},{"location":"configure/async/#batch-timeout","title":"Batch timeout","text":"

    Force push the events to the repository after this time in seconds since the last push:

    ckanext.event_audit.batch.timeout = 3600\n

    The default value is 3600 seconds (1 hour). This options is required to ensure that the logs are written to the repository in case of low activity.

    "},{"location":"configure/cloudwatch/","title":"Cloudwatch","text":"

    Using Cloudwatch repository requires you to configure the following options in the CKAN configuration file:

    ckanext.event_audit.cloudwatch.access_key = YOUR_ACCESS_KEY\nckanext.event_audit.cloudwatch.secret_key = YOUR_SECRET_KEY\nckanext.event_audit.cloudwatch.region = YOUR_REGION\n

    See the AWS documentation for more information on how to obtain these values and configure the AWS Cloudwatch service.

    "},{"location":"configure/ignore/","title":"Ignore events","text":"

    The extension provides a various set of configuration options to adjust the behavior of the audit logs.

    Warning

    These config options are applicable only for built-in tracking methods (API, Database) and not related to client's usage of the extension.

    "},{"location":"configure/ignore/#ignoring-categories","title":"Ignoring categories","text":"

    The extension allows to ignore specific categories of events. To do this, we have to set the following configuration option in the CKAN configuration file:

    ckanext.event_audit.ignore.categories = test\n

    By default, we're not ignoring any categories. The categories option is a comma-separated list of categories that should be ignored.

    Categories are arbitrary strings that can be used to group events.

    "},{"location":"configure/ignore/#ignoring-actions","title":"Ignoring actions","text":"

    The extension allows to ignore specific actions. To do this, we have to set the following configuration option in the CKAN configuration file:

    ckanext.event_audit.ignore.actions = test\n

    Some actions might be called more frequently than others, and we might not be interested in storing them. The actions option is a comma-separated list of actions that should be ignored.

    By default, we're excluding next actions from being stored:

    • editable_config_list
    • editable_config_change
    • get_site_user
    • ckanext_pages_list
    • user_show
    "},{"location":"configure/ignore/#ignoring-models","title":"Ignoring models","text":"

    The extension allows to ignore specific models. To do this, we have to set the following configuration option in the CKAN configuration file:

    ckanext.event_audit.ignore.models = User Package Resource\n

    By default, we're excluding next models from being stored:

    • Option
    "},{"location":"configure/tracking/","title":"In-built tracking","text":"

    There are two built-in trackers in the extension that are enabled by default and work out of the box - API and Database trackers.

    "},{"location":"configure/tracking/#api-tracker","title":"API tracker","text":"

    Captures all events that are triggered by the CKAN API. Everything that is called via tk.get_action will be tracked by this tracker, unless it's explicitly ignored by the configuration. See the ignore section for more details.

    To disable the API tracker, specify this in the configuration file:

    ckanext.event_audit.track.api = false\n

    We can ignore specific actions from being tracked by setting the ckanext.event_audit.ignore.actions configuration option. See the ignore section for more details.

    "},{"location":"configure/tracking/#database-tracker","title":"Database tracker","text":"

    We're utilising the SQLAlchemy\u2019s event system for tracking database interactions. The audit event creation will be triggered when the model is created, updated, or deleted.

    To disable the Database tracker, specify this in the configuration file:

    ckanext.event_audit.track.model = false\n

    We can ignore specific models from being tracked by setting the ckanext.event_audit.ignore.models configuration option. See the ignore section for more details.

    "},{"location":"configure/tracking/#custom-trackers","title":"Custom trackers","text":"

    You can create and write an event anywhere in your codebase.

    TODO: add link to usage docs

    "},{"location":"exporters/basic/","title":"Basic","text":"

    The exporters allow you to export the event audit logs to a different file format.

    The following exporters are available:

    1. CSV Exporter: Exports the event audit logs to a CSV file.
    2. JSON Exporter: Exports the event audit logs to a JSON file.
    3. TSV Exporter: Exports the event audit logs to a TSV file.
    4. XLSX Exporter: Exports the event audit logs to an XLSX file.

    Each exporter has its own configuration options. The configuration options are described in the respective exporter's documentation section.

    "},{"location":"exporters/basic/#base-exporter-class","title":"Base exporter class","text":"

    Base class for all exporters.

    Exporters are used to export a lsit of events to a specific file format.

    "},{"location":"exporters/basic/#event_audit.exporters.base.AbstractExporter.export","title":"export(events) abstractmethod","text":"

    Export events to a specific format.

    We are not providing a specific return type, because it will depend on the specific exporter implementation.

    PARAMETER DESCRIPTION events

    events to export

    TYPE: Iterable[Event]

    RETURNS DESCRIPTION Any

    exported data.

    TYPE: Any

    "},{"location":"exporters/basic/#event_audit.exporters.base.AbstractExporter.from_filters","title":"from_filters(filters, repo_name=None)","text":"

    Export events from a repo using the given filters.

    If repo_name is not provided, the active repo is used.

    PARAMETER DESCRIPTION filters

    search filters.

    TYPE: Filters

    repo_name

    name of the repo to use. Defaults to None.

    TYPE: str | None DEFAULT: None

    RETURNS DESCRIPTION Any

    exported data.

    TYPE: Any

    "},{"location":"exporters/csv/","title":"CSV exporter","text":""},{"location":"exporters/csv/#event_audit.exporters.csv.CSVExporter.__init__","title":"__init__(delimiter=',', quotechar='\"', quoting=QUOTE_ALL, ignore_fields=None)","text":"

    CSV exporter.

    PARAMETER DESCRIPTION delimiter

    delimiter. Defaults to \",\".

    TYPE: str DEFAULT: ','

    quotechar

    quote character. Defaults to '\"'.

    TYPE: str DEFAULT: '\"'

    quoting

    quoting. Defaults to QUOTE_ALL.

    TYPE: int DEFAULT: QUOTE_ALL

    ignore_fields

    fields to ignore. By default we ignore the \"result\" and \"payload\" fields.

    TYPE: list[str] | None DEFAULT: None

    "},{"location":"exporters/csv/#event_audit.exporters.csv.CSVExporter.export","title":"export(events)","text":"

    Export events to CSV format.

    PARAMETER DESCRIPTION events

    events to export.

    TYPE: Iterable[Event]

    RETURNS DESCRIPTION str | None

    str | None: CSV data.

    "},{"location":"exporters/json/","title":"JSON exporter","text":"

    Bases: AbstractExporter

    "},{"location":"exporters/json/#event_audit.exporters.json.JSONExporter.__init__","title":"__init__(stringify=True)","text":"

    JSON exporter.

    PARAMETER DESCRIPTION stringify

    whether to return a string or a dict. By default we return a string.

    TYPE: bool DEFAULT: True

    "},{"location":"exporters/json/#event_audit.exporters.json.JSONExporter.export","title":"export(events)","text":"

    Export events to JSON format.

    PARAMETER DESCRIPTION events

    events to export.

    TYPE: Iterable[Event]

    RETURNS DESCRIPTION list[dict[str, Any]] | str | None

    str | None: JSON data.

    "},{"location":"exporters/tsv/","title":"TSV exporter","text":""},{"location":"exporters/tsv/#event_audit.exporters.tsv.TSVExporter.__init__","title":"__init__(ignore_fields=None)","text":"

    TSV exporter.

    PARAMETER DESCRIPTION ignore_fields

    fields to ignore. By default we ignore the \"result\" and \"payload\" fields.

    TYPE: list[str] | None DEFAULT: None

    "},{"location":"exporters/xlsx/","title":"XLSX exporter","text":""},{"location":"exporters/xlsx/#event_audit.exporters.xlsx.XLSXExporter.__init__","title":"__init__(file_path, ignore_fields=None)","text":"

    XLSX exporter.

    PARAMETER DESCRIPTION file_path

    path to the file or BytesIO object to write the XLSX data.

    TYPE: str | BytesIO

    ignore_fields

    fields to ignore. By default we ignore the \"result\" and \"payload\" fields.

    TYPE: list[str] | None DEFAULT: None

    "},{"location":"exporters/xlsx/#event_audit.exporters.xlsx.XLSXExporter.export","title":"export(events)","text":"

    Export events to a XLSX file.

    PARAMETER DESCRIPTION events

    events to export.

    TYPE: Iterable[Event]

    RETURNS DESCRIPTION str | BytesIO | None

    str | BytesIO | None: path to the file or BytesIO object if the

    str | BytesIO | None

    export was successful, None otherwise.

    "},{"location":"repositories/abstract/","title":"Abstract","text":""},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.__new__","title":"__new__(*args, **kwargs)","text":"

    Singleton pattern implementation.

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.build_event","title":"build_event(event_data)","text":"

    Build an event object from the provided data.

    PARAMETER DESCRIPTION event_data

    event data.

    TYPE: EventData

    RETURNS DESCRIPTION Event

    types.Event: event object.

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.enqueue_event","title":"enqueue_event(event)","text":"

    Enqueue an event to be written to the repository.

    PARAMETER DESCRIPTION event

    event to write.

    TYPE: Event

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.filter_events","title":"filter_events(filters) abstractmethod","text":"

    Filters events based on provided filter criteria.

    PARAMETER DESCRIPTION filters

    filters to apply.

    TYPE: Filters

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.get_event","title":"get_event(event_id) abstractmethod","text":"

    Retrieves a single event from the repository.

    PARAMETER DESCRIPTION event_id

    event ID.

    TYPE: str

    RETURNS DESCRIPTION Event | None

    types.Event | None: event object or None if not found.

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.get_name","title":"get_name() abstractmethod classmethod","text":"

    Return the name of the repository.

    RETURNS DESCRIPTION str

    name of the repository.

    TYPE: str

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.remove_all_events","title":"remove_all_events()","text":"

    Removes all events from the repository.

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.remove_event","title":"remove_event(event_id)","text":"

    Removes a single event from the repository.

    PARAMETER DESCRIPTION event_id

    event ID.

    TYPE: Any

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.remove_events","title":"remove_events(filters)","text":"

    Removes a filtered set of events from the repository.

    PARAMETER DESCRIPTION filters

    filters to apply.

    TYPE: Filters

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.test_connection","title":"test_connection() abstractmethod","text":"

    Test the connection to the repository.

    RETURNS DESCRIPTION bool

    whether the connection was successful.

    TYPE: bool

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.write_event","title":"write_event(event) abstractmethod","text":"

    Writes a single event to the repository.

    PARAMETER DESCRIPTION event

    event to write.

    TYPE: Event

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.write_events","title":"write_events(events)","text":"

    Write multiple events to the repository.

    PARAMETER DESCRIPTION events

    events to write.

    TYPE: Iterable[Event]

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/basic/","title":"Basic","text":"

    Repositories are the storages where the event audit logs are stored. There are a few basic repositories, that you can use out of the box:

    1. redis - the default repository, stores logs in Redis.
    2. postgres - stores logs in a PostgreSQL database.
    3. cloudwatch - stores logs in AWS CloudWatch.

    You can also implement your own repository. To do this, you need to create a new class that inherits from the AbstractRepository class and implement all the required methods.

    See the abstract repository documentation and custom repository documentation for more information.

    "},{"location":"repositories/cloudwatch/","title":"Cloudwatch repository","text":""},{"location":"repositories/cloudwatch/#event_audit.repositories.cloudwatch.CloudWatchRepository.__init__","title":"__init__(credentials=None, log_group='/ckan/event-audit', log_stream='event-audit-stream')","text":"

    CloudWatch repository.

    PARAMETER DESCRIPTION credentials

    AWS credentials. If not provided, the extension configuration will be used.

    TYPE: AWSCredentials | None DEFAULT: None

    log_group

    CloudWatch log group name.

    TYPE: str DEFAULT: '/ckan/event-audit'

    log_stream

    CloudWatch log stream name.

    TYPE: str DEFAULT: 'event-audit-stream'

    "},{"location":"repositories/cloudwatch/#event_audit.repositories.cloudwatch.CloudWatchRepository.filter_events","title":"filter_events(filters)","text":"

    Filters events based on provided filter criteria.

    PARAMETER DESCRIPTION filters

    filters to apply.

    TYPE: Filters

    "},{"location":"repositories/cloudwatch/#event_audit.repositories.cloudwatch.CloudWatchRepository.get_event","title":"get_event(event_id)","text":"

    Retrieves a single event from the repository.

    PARAMETER DESCRIPTION event_id

    event ID.

    TYPE: str

    RETURNS DESCRIPTION Optional[Event]

    types.Event | None: event object or None if not found.

    "},{"location":"repositories/cloudwatch/#event_audit.repositories.cloudwatch.CloudWatchRepository.remove_all_events","title":"remove_all_events()","text":"

    Removes all events from the repository.

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/cloudwatch/#event_audit.repositories.cloudwatch.CloudWatchRepository.remove_event","title":"remove_event(event_id)","text":"

    Remove operation is not supported for CloudWatch logs.

    As of today, you cannot delete a single log event from CloudWatch log stream, the alternative will be using Lambda functions: set a Lambda function trigger, filter all logs, then write the remaining logs to a new log group/stream, then delete the original log stream.

    It's potentially too expensive to do this operation, so it's not implemented.

    Note

    The remove single event operation is not supported

    "},{"location":"repositories/cloudwatch/#event_audit.repositories.cloudwatch.CloudWatchRepository.remove_events","title":"remove_events(filters)","text":"

    See remove_event method docstring.

    "},{"location":"repositories/cloudwatch/#event_audit.repositories.cloudwatch.CloudWatchRepository.test_connection","title":"test_connection()","text":"

    Tests the connection to the repository.

    RETURNS DESCRIPTION bool

    whether the connection was successful.

    TYPE: bool

    "},{"location":"repositories/cloudwatch/#event_audit.repositories.cloudwatch.CloudWatchRepository.write_event","title":"write_event(event)","text":"

    Writes a single event to the repository.

    PARAMETER DESCRIPTION event

    event to write.

    TYPE: Event

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/custom/","title":"Custom","text":"

    Here you can see a naive example of how to implement a custom repository, that stores logs in a json file.

    from ckanext.event_audit.repositories import AbstractRepository\n\nclass FileRepository(AbstractRepository):\n    def __init__(self, file_path: str | None = None):\n        self.file_path = file_path or '/tmp/event_audit.json'\n\n    def log(self) -> str:\n        return \"file\"\n\n    def write_event(self, event: types.Event) -> types.Result:\n        with open(self.file_path, 'a') as f:\n            data = json.load(f)\n            data[event.id] = event.model_dump()\n\n            f.write(json.dumps(data))\n\n        return types.Result(success=True)\n\n    def get_event(self, event_id: Any) -> types.Event | None:\n        with open(self.file_path, 'r') as f:\n            data = json.load(f)\n\n            if event_id in data:\n                return types.Event.model_validate(data[event_id])\n\n        return None\n\n    def filter_events(self, filters: types.Filters) -> list[types.Event]:\n        with open(self.file_path, 'r') as f:\n            data = json.load(f)\n\n            result = []\n\n            for event in data.values():\n                if _match_filters(event, filters):\n                    result.append(types.Event.model_validate(event))\n\n            return result\n\n    def _match_filters(self, event: types.EventData, filters: types.Filters) -> bool:\n        ...\n\n    def test_connection(self) -> types.Result:\n        return types.Result(success=True)\n

    In this version, it doesn't implement the remove_event, remove_events and remove_all_events methods, but you can implement them in the same way as the other methods. If the repository able to remove one or multiple events, it must inherits from the respective class - RemoveSingle or RemoveAll. For example:

    import os\n\nfrom ckanext.event_audit.repositories import (\n    AbstractRepository,\n    RemoveSingle,\n    RemoveAll,\n    RemoveFiltered,\n)\n\n\nclass FileRepository(AbstractRepository, RemoveSingle, RemoveAll, RemoveFiltered):\n    ...\n\n    def remove_event(self, event_id: Any) -> types.Result:\n        with open(self.file_path, \"w\") as f:\n            data = json.load(f)\n\n            if event_id in data:\n                del data[event_id]\n                f.write(json.dumps(data))\n\n                return types.Result(success=True)\n\n        return types.Result(success=False, message=\"Event not found\")\n\n    def remove_events(self, filters: types.Filters) -> types.Result:\n        with open(self.file_path, \"w\") as f:\n            data = json.load(f)\n\n            for event_id, event in data.items():\n                if _match_filters(event, filters):\n                    del data[event_id]\n\n            f.write(json.dumps(data))\n\n        return types.Result(success=True)\n\n    def _match_filters(\n        self, event: types.EventData, filters: types.Filters\n    ) -> bool: ...\n\n    def remove_all_events(self, filters: types.Filters) -> types.Result:\n        \"\"\"Removes the file if exists.\"\"\"\n\n        if os.path.exists(self.file_path):\n            os.remove(self.file_path)\n\n        return types.Result(success=True)\n
    "},{"location":"repositories/postgres/","title":"Postgres repository","text":""},{"location":"repositories/postgres/#event_audit.repositories.postgres.PostgresRepository.filter_events","title":"filter_events(filters)","text":"

    Filters events based on provided filter criteria.

    PARAMETER DESCRIPTION filters

    filters to apply.

    TYPE: Filters

    RETURNS DESCRIPTION List[Event]

    List[types.Event]: list of events.

    "},{"location":"repositories/postgres/#event_audit.repositories.postgres.PostgresRepository.get_event","title":"get_event(event_id)","text":"

    Retrieves a single event from the repository.

    PARAMETER DESCRIPTION event_id

    event ID.

    TYPE: str

    RETURNS DESCRIPTION Event | None

    types.Event | None: event object or None if not found.

    "},{"location":"repositories/postgres/#event_audit.repositories.postgres.PostgresRepository.remove_all_events","title":"remove_all_events()","text":"

    Removes all events from the repository.

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/postgres/#event_audit.repositories.postgres.PostgresRepository.remove_event","title":"remove_event(event_id, session=None, defer_commit=False)","text":"

    Removes a single event from the repository.

    PARAMETER DESCRIPTION event_id

    event ID.

    TYPE: str

    session

    session to use.

    TYPE: Session | None DEFAULT: None

    defer_commit

    whether to defer the commit.

    TYPE: bool DEFAULT: False

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/postgres/#event_audit.repositories.postgres.PostgresRepository.remove_events","title":"remove_events(filters)","text":"

    Removes a filtered set of events from the repository.

    PARAMETER DESCRIPTION filters

    filters to apply.

    TYPE: Filters

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/postgres/#event_audit.repositories.postgres.PostgresRepository.test_connection","title":"test_connection()","text":"

    Tests the connection to the repository.

    RETURNS DESCRIPTION bool

    whether the connection was successful.

    TYPE: bool

    "},{"location":"repositories/postgres/#event_audit.repositories.postgres.PostgresRepository.write_event","title":"write_event(event, session=None, defer_commit=False)","text":"

    Writes a single event to the repository.

    PARAMETER DESCRIPTION event

    event to write.

    TYPE: Event

    session

    session to use.

    TYPE: Session | None DEFAULT: None

    defer_commit

    whether to defer the commit.

    TYPE: bool DEFAULT: False

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/postgres/#event_audit.repositories.postgres.PostgresRepository.write_events","title":"write_events(events)","text":"

    Write multiple events to the repository.

    PARAMETER DESCRIPTION events

    events to write.

    TYPE: Iterable[Event]

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/redis/","title":"Redis repository","text":""},{"location":"repositories/redis/#event_audit.repositories.redis.RedisRepository.filter_events","title":"filter_events(filters)","text":"

    Filters events based on patterns generated from the provided filters.

    PARAMETER DESCRIPTION filters

    filters to apply.

    TYPE: Filters

    "},{"location":"repositories/redis/#event_audit.repositories.redis.RedisRepository.get_event","title":"get_event(event_id)","text":"

    Get an event by its ID.

    PARAMETER DESCRIPTION event_id

    event ID.

    TYPE: float

    "},{"location":"repositories/redis/#event_audit.repositories.redis.RedisRepository.remove_all_events","title":"remove_all_events()","text":"

    Removes all events from the repository.

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/redis/#event_audit.repositories.redis.RedisRepository.remove_event","title":"remove_event(event_id)","text":"

    Removes an event by its ID.

    PARAMETER DESCRIPTION event_id

    event ID.

    TYPE: float

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/redis/#event_audit.repositories.redis.RedisRepository.remove_events","title":"remove_events(filters)","text":"

    Removes a filtered set of events from the repository.

    PARAMETER DESCRIPTION filters

    filters to apply.

    TYPE: Filters

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/redis/#event_audit.repositories.redis.RedisRepository.test_connection","title":"test_connection()","text":"

    Tests the connection to the repository.

    RETURNS DESCRIPTION bool

    whether the connection was successful.

    TYPE: bool

    "},{"location":"repositories/redis/#event_audit.repositories.redis.RedisRepository.write_event","title":"write_event(event)","text":"

    Writes an event to Redis.

    PARAMETER DESCRIPTION event

    event to write.

    TYPE: Event

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-\\.\\_]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Home","text":""},{"location":"#ckanext-event-audit","title":"ckanext-event-audit","text":"

    This extension will capture and retain a comprehensive record of all changes within a CKAN app.

    "},{"location":"#developer-installation","title":"Developer installation","text":"

    To install ckanext-event-audit for development, activate your CKAN virtualenv and do:

    git clone https://github.com/DataShades/ckanext-event-audit.git\ncd ckanext-event-audit\npip install -e .\npip install -r dev-requirements.txt\n
    "},{"location":"#tests","title":"Tests","text":"

    To run the tests, do:

    pytest --ckan-ini=test.ini\n
    "},{"location":"#license","title":"License","text":"

    AGPL

    "},{"location":"cli/","title":"CLI","text":""},{"location":"cli/#event_audit.cli.export_data","title":"export_data(exporter_name, start, end, config)","text":"

    Export data using the specified exporter.

    PARAMETER DESCRIPTION exporter_name

    The name of the exporter.

    TYPE: str

    start

    The start date string in %Y-%m-%d format.

    TYPE: str

    end

    The end date string in %Y-%m-%d format.

    TYPE: str | None

    config

    The exporter config in JSON format. See the exporter's documentation for args details.

    TYPE: str | None

    RETURNS DESCRIPTION str

    The exported data

    Example

    $ ckan event-audit export-data csv --start=2024-11-11 > report.csv

    $ ckan event-audit export-data json --start=2024-11-11 | jq '[.[] | {id, category, action}]'

    $ ckan event-audit export-data xlsx --start=2024-11-11 --config='{\"file_path\": \"/tmp/test.xlsx\"}'

    Source code in ckanext/event_audit/cli.py
    @event_audit.command()\n@click.argument(\"exporter_name\", type=str)\n@click.option(\n    \"--start\",\n    required=True,\n    type=click.DateTime(formats=[\"%Y-%m-%d\"]),\n    help=\"ISO format start date\",\n)\n@click.option(\n    \"--end\",\n    required=False,\n    type=click.DateTime(formats=[\"%Y-%m-%d\"]),\n    help=\"ISO format end date\",\n)\n@click.option(\"--config\", required=False, type=str, help=\"Custom config in JSON format\")\ndef export_data(exporter_name: str, start: dt, end: dt | None, config: str | None):\n    \"\"\"Export data using the specified exporter.\n\n    Args:\n        exporter_name (str): The name of the exporter.\n        start (str): The start date string in %Y-%m-%d format.\n        end (str | None): The end date string in %Y-%m-%d format.\n        config (str | None): The exporter config in JSON format. See the exporter's\n            documentation for args details.\n\n    Returns:\n        str : The exported data\n\n    Example:\n        $ ckan event-audit export-data csv --start=2024-11-11 > report.csv\n\n        $ ckan event-audit export-data json --start=2024-11-11 | jq '[.[] |\n            {id, category, action}]'\n\n        $ ckan event-audit export-data xlsx --start=2024-11-11\n            --config='{\"file_path\": \"/tmp/test.xlsx\"}'\n    \"\"\"\n    start = UTC.localize(start) if start else None\n    end = UTC.localize(end) if end else None\n\n    try:\n        config_dict = json.loads(config or \"{}\")\n    except json.JSONDecodeError:\n        return click.secho(\"Invalid JSON format for config.\")\n\n    try:\n        exporter = utils.get_exporter(exporter_name)(**config_dict)\n    except TypeError as e:\n        return click.secho(f\"Invalid exporter config: {config}. Error: {e}\", fg=\"red\")\n    except ValueError as e:\n        return click.secho(e, fg=\"red\")\n\n    if start and end and start > end:\n        return click.secho(\"Start date must be before the end date.\", fg=\"red\")\n\n    click.echo(exporter.from_filters(types.Filters(time_from=start, time_to=end)))\n
    "},{"location":"cli/#event_audit.cli.remove_events","title":"remove_events(repository, start, end)","text":"

    Remove events from the repository by time range.

    PARAMETER DESCRIPTION repository

    The repository name. If not provided, the active repository will be used.

    TYPE: str | None

    start

    The start date string in %Y-%m-%d format.

    TYPE: str

    end

    The end date string in %Y-%m-%d format.

    TYPE: str | None

    Example

    $ ckan event-audit remove-events --start=2024-11-11 --end=2024-11-12

    Source code in ckanext/event_audit/cli.py
    @event_audit.command()\n@click.option(\"--repository\", required=False, help=\"The repository name\")\n@click.option(\n    \"--start\",\n    required=False,\n    type=click.DateTime(formats=[\"%Y-%m-%d\"]),\n    help=\"ISO format start date\",\n)\n@click.option(\n    \"--end\",\n    required=False,\n    type=click.DateTime(formats=[\"%Y-%m-%d\"]),\n    help=\"ISO format end date\",\n)\ndef remove_events(repository: str | None, start: dt | None, end: dt | None):\n    \"\"\"Remove events from the repository by time range.\n\n    Args:\n        repository (str | None): The repository name. If not provided, the\n            active repository will be used.\n        start (str): The start date string in %Y-%m-%d format.\n        end (str | None): The end date string in %Y-%m-%d format.\n\n    Example:\n        $ ckan event-audit remove-events --start=2024-11-11 --end=2024-11-12\n    \"\"\"\n    start = UTC.localize(start) if start else None\n    end = UTC.localize(end) if end else None\n\n    if start and end and start > end:\n        return click.secho(\"Start date must be before the end date.\", fg=\"red\")\n\n    try:\n        repo = utils.get_repo(repository) if repository else utils.get_active_repo()\n    except ValueError:\n        return click.secho(f\"Unknown repository: {repository}\", fg=\"red\")\n\n    if not (start or end) and not isinstance(repo, repositories.RemoveAll):\n        return click.secho(\n            f\"Repository {repository} does not support removing events.\", fg=\"red\"\n        )\n\n    if not isinstance(repo, repositories.RemoveFiltered):\n        if start or end:\n            return click.secho(\n                (\n                    f\"Repository {repository} does not support removing events \"\n                    \"by time range. \"\n                    \"Please remove the --start and --end flags to delete all events.\"\n                ),\n                fg=\"red\",\n            )\n        return repo.remove_all_events()\n\n    return repo.remove_events(types.Filters(time_from=start, time_to=end))\n
    "},{"location":"install/","title":"Installation","text":""},{"location":"install/#requirements","title":"Requirements","text":"

    Compatibility with core CKAN versions:

    CKAN version Compatible? 2.9 no 2.10 yes 2.11 yes master yes"},{"location":"install/#installation_1","title":"Installation","text":"
    1. Install the extension from PyPI:

      pip install ckanext-event-audit\n

    2. Enable the plugin in your CKAN configuration file (e.g. ckan.ini or production.ini):

      ckan.plugins = ... event_audit ...\n

    3. Run DB migrations:

      ckan db upgrade -p event_audit\n

    4. Configure the extension up to your needs and you're ready to go. See the documentation for more details about the configuration options.

    "},{"location":"interfaces/","title":"Interfaces","text":""},{"location":"interfaces/#ieventaudit","title":"IEventAudit","text":"

    Extend functionality of ckanext-event-audit.

    Example:

    import ckan.plugins as p\n\nfrom ckanext.event_audit.interfaces import IEventAudit\nfrom ckanext.event_audit.repositories import AbstractRepository\nfrom ckanext.event_audit.exporters import AbstractExporter\n\nclass MyPlugin(p.SingletonPlugin):\n    p.implements(IEventAudit, inherit=True)\n\n    def register_repository(self) -> dict[str, type[AbstractRepository]]:\n        return {\n            \"my_repo\": MyRepository,\n        }\n\n    def register_exporter(self) -> dict[str, type[AbstractExporter]]:\n        return {\n            \"my_exporter\": MyExporter,\n        }\n\n    def skip_event(self, event: types.Event) -> bool:\n        if event.category == \"api\" and event.action == \"status_show\":\n            return True\n\n        if event.category == \"model\" and event.action_object == \"Dashboard\":\n            return True\n\n        return False\n

    "},{"location":"interfaces/#event_audit.interfaces.IEventAudit.register_exporter","title":"register_exporter()","text":"

    Return the exporters provided by this plugin.

    Example
    def register_exporter(self):\n    return {\n        \"csv\": CSVExporter,\n    }\n
    RETURNS DESCRIPTION dict[str, type[AbstractExporter]]

    mapping of exporter names to exporter classes

    "},{"location":"interfaces/#event_audit.interfaces.IEventAudit.register_repository","title":"register_repository()","text":"

    Return the repositories provided by this plugin.

    Example
    def register_repository(self):\n    return {\n        \"my_repo\": MyRepository,\n    }\n
    RETURNS DESCRIPTION dict[str, type[AbstractRepository]]

    mapping of repository names to repository classes

    "},{"location":"interfaces/#event_audit.interfaces.IEventAudit.skip_event","title":"skip_event(event)","text":"

    Skip an event.

    This method is called before writing the event to the repository.

    Example
    def skip_event(self, event: types.Event) -> bool:\n    if event.category == \"api\" and event.action == \"status_show\":\n        return True\n\n    if  event.category == \"model\"  and event.action_object == \"Dashboard\":\n        return True\n\n    return False\n
    RETURNS DESCRIPTION bool

    True if the event should be skipped, False otherwise

    "},{"location":"usage/","title":"Usage","text":"

    To use an event audit in your extension, you should get an instance of the repository class. There are two ways to do this:

    1. Using the get_repo function:

      import ckanext.event_audit.utils as utils\n\nrepo = utils.get_active_repo()\n\nevent = repo.build_event({\"category\": \"xxx\", \"action\": \"xxx\"})\n\nrepo.write_event(event)\n

      The get_active_repo function will return an instance of the active repository class that is configured in the CKAN configuration file.

    2. Using the get_repo function with a specific repository:

      import ckanext.event_audit.utils as utils\n\nrepo = utils.get_repo(\"file\")\n\nevent = repo.build_event({\"category\": \"xxx\", \"action\": \"xxx\"})\n\nrepo.write_event(event)\n

      The get_repo function will return an instance of the repository class that is specified in the argument. You can use this method to get a specific repository instance.

    "},{"location":"utils/","title":"Utility Functions","text":""},{"location":"utils/#event_audit.utils.get_active_repo","title":"get_active_repo()","text":"

    Get the active repository.

    The active repository is the one that is currently configured in the extension configuration.

    RETURNS DESCRIPTION AbstractRepository

    The active repository.

    "},{"location":"utils/#event_audit.utils.get_available_exporters","title":"get_available_exporters()","text":"

    Retrieve a dictionary of available exporters.

    This function collects and returns a dictionary where the keys are exporter names (as strings) and the values are the corresponding exporter classes.

    RETURNS DESCRIPTION dict[str, type[AbstractExporter]]

    A dictionary mapping exporter names to their respective exporter classes.

    "},{"location":"utils/#event_audit.utils.get_available_repos","title":"get_available_repos()","text":"

    Retrieve a dictionary of available repositories.

    This function collects and returns a dictionary where the keys are repository names (as strings) and the values are the corresponding repository classes.

    RETURNS DESCRIPTION dict[str, type[AbstractRepository]]

    A dictionary mapping repository names to their respective repository classes.

    "},{"location":"utils/#event_audit.utils.get_exporter","title":"get_exporter(exporter_name)","text":"

    Retrieve an exporter class by name.

    This function retrieves an exporter class by name. If the exporter is not found, a ValueError is raised.

    PARAMETER DESCRIPTION exporter_name

    The name of the exporter to retrieve.

    TYPE: str

    RETURNS DESCRIPTION type[AbstractExporter]

    The exporter class.

    "},{"location":"utils/#event_audit.utils.get_repo","title":"get_repo(repo_name)","text":"

    Retrieve a repository class by name.

    This function retrieves a repository class by name. If the repository is not found, a ValueError is raised.

    PARAMETER DESCRIPTION repo_name

    The name of the repository to retrieve.

    TYPE: str

    RETURNS DESCRIPTION AbstractRepository

    The repository class.

    "},{"location":"utils/#event_audit.utils.test_active_connection","title":"test_active_connection()","text":"

    Test the connection to the active repository.

    When we test the connection, we store the result in the repository object, so we can reuse it later.

    RETURNS DESCRIPTION bool

    whether the connection is active

    "},{"location":"validators/","title":"Validators","text":""},{"location":"validators/#event_audit.logic.validators.audit_repo_exists","title":"audit_repo_exists(value, context)","text":"

    Check if the repository with the given name is registered.

    PARAMETER DESCRIPTION value

    The repository name.

    TYPE: Any

    context

    The CKAN context.

    TYPE: Context

    RETURNS DESCRIPTION Any

    The repository name if it exists.

    TYPE: Any

    RAISES DESCRIPTION Invalid

    If the repository does not exist.

    Source code in ckanext/event_audit/logic/validators.py
    def audit_repo_exists(value: Any, context: Context) -> Any:\n    \"\"\"Check if the repository with the given name is registered.\n\n    Args:\n        value (Any): The repository name.\n        context (Context): The CKAN context.\n\n    Returns:\n        Any: The repository name if it exists.\n\n    Raises:\n        tk.Invalid: If the repository does not exist.\n    \"\"\"\n    if value not in utils.get_available_repos():\n        raise tk.Invalid(f\"Repository `{value}` is not registered\")\n\n    return value\n
    "},{"location":"configure/admin_panel/","title":"Admin panel","text":"

    We have an integration with the ckanext-admin-panel extension, which allows you to manage the CKAN configuration from the web interface. To enable this integration, you need to install the ckanext-admin-panel extension and configure it as described in the ckanext-admin-panel documentation.

    Note

    The admin panel is available only for sysadmin users.

    By default, the admin pages are not being registered. But if you want to enable it, you can set the respective option to true in your CKAN configuration file.

    ckanext.event_audit.enable_admin_panel = true\n
    "},{"location":"configure/admin_panel/#configuration-with-ckanext-admin-panel","title":"Configuration with ckanext-admin-panel","text":"

    The ckanext-admin-panel allows you to configure the extension in real time from the web interface.

    "},{"location":"configure/async/","title":"Asynchronous processing","text":"

    To avoid blocking the main thread, the extension uses a separate thread to write the audit logs. A separate thread will be started automatically along with the CKAN application.

    The thread is responsible for storing the logs in the configured repository.

    By default, we're using the threaded mode. However, if you want to disable the threaded mode, you can do this by setting the following configuration option in the CKAN configuration file:

    ckanext.event_audit.threaded_mode = false\n

    Disabling the threaded mode will cause the extension to write the logs in the main thread. This can be useful for debugging purposes.

    Note, that pairing it with the Cloudwatch repository is not recommended, as it can block the main thread for a long time. Network operations can be slow and can cause the application to hang for a while.

    If your custom repository involves a network operations, it's recommended to keep the threaded mode enabled.

    "},{"location":"configure/async/#batch-size","title":"Batch size","text":"

    The extension writes the logs in batches. The batch size can be adjusted by setting the following configuration option in the CKAN configuration file:

    ckanext.event_audit.batch.size = 50\n

    By default, we're accumulating 50 events before writing them to the repository.

    "},{"location":"configure/async/#batch-timeout","title":"Batch timeout","text":"

    Force push the events to the repository after this time in seconds since the last push:

    ckanext.event_audit.batch.timeout = 3600\n

    The default value is 3600 seconds (1 hour). This options is required to ensure that the logs are written to the repository in case of low activity.

    "},{"location":"configure/cloudwatch/","title":"Cloudwatch","text":"

    Using Cloudwatch repository requires you to configure the following options in the CKAN configuration file:

    ckanext.event_audit.cloudwatch.access_key = YOUR_ACCESS_KEY\nckanext.event_audit.cloudwatch.secret_key = YOUR_SECRET_KEY\nckanext.event_audit.cloudwatch.region = YOUR_REGION\n

    See the AWS documentation for more information on how to obtain these values and configure the AWS Cloudwatch service.

    "},{"location":"configure/ignore/","title":"Ignore events","text":"

    The extension provides a various set of configuration options to adjust the behavior of the audit logs.

    Warning

    These config options are applicable only for built-in tracking methods (API, Database) and not related to client's usage of the extension.

    "},{"location":"configure/ignore/#ignoring-categories","title":"Ignoring categories","text":"

    The extension allows to ignore specific categories of events. To do this, we have to set the following configuration option in the CKAN configuration file:

    ckanext.event_audit.ignore.categories = test\n

    By default, we're not ignoring any categories. The categories option is a comma-separated list of categories that should be ignored.

    Categories are arbitrary strings that can be used to group events.

    "},{"location":"configure/ignore/#ignoring-actions","title":"Ignoring actions","text":"

    The extension allows to ignore specific actions. To do this, we have to set the following configuration option in the CKAN configuration file:

    ckanext.event_audit.ignore.actions = test\n

    Some actions might be called more frequently than others, and we might not be interested in storing them. The actions option is a comma-separated list of actions that should be ignored.

    By default, we're excluding next actions from being stored:

    • editable_config_list
    • editable_config_change
    • get_site_user
    • ckanext_pages_list
    • user_show
    "},{"location":"configure/ignore/#ignoring-models","title":"Ignoring models","text":"

    The extension allows to ignore specific models. To do this, we have to set the following configuration option in the CKAN configuration file:

    ckanext.event_audit.ignore.models = User Package Resource\n

    By default, we're excluding next models from being stored:

    • Option
    "},{"location":"configure/repository/","title":"Repository","text":"

    The event audit logs are stored in a configurable storages, we call them repositories. To use an extension, you have to choose one of the available repositories.

    The following repositories are available:

    1. redis - the default repository, stores logs in Redis.
    2. postgres - stores logs in a PostgreSQL database.
    3. cloudwatch - stores logs in AWS CloudWatch.
    Note

    If the cloudwatch repository is used, the extension will automatically create a log group in CloudWatch. Also, check the CloudWatch repository documentation for additional configuration options.

    "},{"location":"configure/repository/#active-repository","title":"Active repository","text":"

    The default repository is redis, but it can be changed to a different one. To do this, we have to set the following configuration options in the CKAN configuration file:

    ckanext.event_audit.active_repo = postgres\n
    "},{"location":"configure/repository/#list-of-available-repositories","title":"List of available repositories","text":"

    You can restrict a list of available repositories by setting the following configuration option in the CKAN configuration file:

    ckanext.event_audit.active_repo = cloudwatch\nckanext.event_audit.restrict_available_repos = cloudwatch\n
    Note

    By default, we're not restricting the list of available repositories. It means that all registered repositories are available for use.

    This could be useful if you want to limit the available repositories to a specific set of options due to some security concerns. This config option won't be available in the admin interface and can't be changed in real time.

    "},{"location":"configure/tracking/","title":"In-built tracking","text":"

    There are two built-in trackers in the extension that are enabled by default and work out of the box - API and Database trackers.

    "},{"location":"configure/tracking/#api-tracker","title":"API tracker","text":"

    Captures all events that are triggered by the CKAN API. Everything that is called via tk.get_action will be tracked by this tracker, unless it's explicitly ignored by the configuration. See the ignore section for more details.

    To disable the API tracker, specify this in the configuration file:

    ckanext.event_audit.track.api = false\n

    We can ignore specific actions from being tracked by setting the ckanext.event_audit.ignore.actions configuration option. See the ignore section for more details.

    "},{"location":"configure/tracking/#database-tracker","title":"Database tracker","text":"

    We're utilising the SQLAlchemy\u2019s event system for tracking database interactions. The audit event creation will be triggered when the model is created, updated, or deleted.

    To disable the Database tracker, specify this in the configuration file:

    ckanext.event_audit.track.model = false\n

    We can ignore specific models from being tracked by setting the ckanext.event_audit.ignore.models configuration option. See the ignore section for more details.

    "},{"location":"configure/tracking/#custom-trackers","title":"Custom trackers","text":"

    You can create and write an event anywhere in your codebase.

    TODO: add link to usage docs

    "},{"location":"exporters/basic/","title":"Basic","text":"

    The exporters allow you to export the event audit logs to a different file format.

    The following exporters are available:

    1. CSV Exporter: Exports the event audit logs to a CSV file.
    2. JSON Exporter: Exports the event audit logs to a JSON file.
    3. TSV Exporter: Exports the event audit logs to a TSV file.
    4. XLSX Exporter: Exports the event audit logs to an XLSX file.

    Each exporter has its own configuration options. The configuration options are described in the respective exporter's documentation section.

    "},{"location":"exporters/basic/#base-exporter-class","title":"Base exporter class","text":"

    Base class for all exporters.

    Exporters are used to export a lsit of events to a specific file format.

    "},{"location":"exporters/basic/#event_audit.exporters.base.AbstractExporter.export","title":"export(events) abstractmethod","text":"

    Export events to a specific format.

    We are not providing a specific return type, because it will depend on the specific exporter implementation.

    PARAMETER DESCRIPTION events

    events to export

    TYPE: Iterable[Event]

    RETURNS DESCRIPTION Any

    exported data.

    TYPE: Any

    "},{"location":"exporters/basic/#event_audit.exporters.base.AbstractExporter.from_filters","title":"from_filters(filters, repo_name=None)","text":"

    Export events from a repo using the given filters.

    If repo_name is not provided, the active repo is used.

    PARAMETER DESCRIPTION filters

    search filters.

    TYPE: Filters

    repo_name

    name of the repo to use. Defaults to None.

    TYPE: str | None DEFAULT: None

    RETURNS DESCRIPTION Any

    exported data.

    TYPE: Any

    "},{"location":"exporters/csv/","title":"CSV exporter","text":""},{"location":"exporters/csv/#event_audit.exporters.csv.CSVExporter.__init__","title":"__init__(delimiter=',', quotechar='\"', quoting=QUOTE_ALL, ignore_fields=None)","text":"

    CSV exporter.

    PARAMETER DESCRIPTION delimiter

    delimiter. Defaults to \",\".

    TYPE: str DEFAULT: ','

    quotechar

    quote character. Defaults to '\"'.

    TYPE: str DEFAULT: '\"'

    quoting

    quoting. Defaults to QUOTE_ALL.

    TYPE: int DEFAULT: QUOTE_ALL

    ignore_fields

    fields to ignore. By default we ignore the \"result\" and \"payload\" fields.

    TYPE: list[str] | None DEFAULT: None

    "},{"location":"exporters/csv/#event_audit.exporters.csv.CSVExporter.export","title":"export(events)","text":"

    Export events to CSV format.

    PARAMETER DESCRIPTION events

    events to export.

    TYPE: Iterable[Event]

    RETURNS DESCRIPTION str | None

    str | None: CSV data.

    "},{"location":"exporters/json/","title":"JSON exporter","text":"

    Bases: AbstractExporter

    "},{"location":"exporters/json/#event_audit.exporters.json.JSONExporter.__init__","title":"__init__(stringify=True)","text":"

    JSON exporter.

    PARAMETER DESCRIPTION stringify

    whether to return a string or a dict. By default we return a string.

    TYPE: bool DEFAULT: True

    "},{"location":"exporters/json/#event_audit.exporters.json.JSONExporter.export","title":"export(events)","text":"

    Export events to JSON format.

    PARAMETER DESCRIPTION events

    events to export.

    TYPE: Iterable[Event]

    RETURNS DESCRIPTION list[dict[str, Any]] | str | None

    str | None: JSON data.

    "},{"location":"exporters/tsv/","title":"TSV exporter","text":""},{"location":"exporters/tsv/#event_audit.exporters.tsv.TSVExporter.__init__","title":"__init__(ignore_fields=None)","text":"

    TSV exporter.

    PARAMETER DESCRIPTION ignore_fields

    fields to ignore. By default we ignore the \"result\" and \"payload\" fields.

    TYPE: list[str] | None DEFAULT: None

    "},{"location":"exporters/xlsx/","title":"XLSX exporter","text":""},{"location":"exporters/xlsx/#event_audit.exporters.xlsx.XLSXExporter.__init__","title":"__init__(file_path, ignore_fields=None)","text":"

    XLSX exporter.

    PARAMETER DESCRIPTION file_path

    path to the file or BytesIO object to write the XLSX data.

    TYPE: str | BytesIO

    ignore_fields

    fields to ignore. By default we ignore the \"result\" and \"payload\" fields.

    TYPE: list[str] | None DEFAULT: None

    "},{"location":"exporters/xlsx/#event_audit.exporters.xlsx.XLSXExporter.export","title":"export(events)","text":"

    Export events to a XLSX file.

    PARAMETER DESCRIPTION events

    events to export.

    TYPE: Iterable[Event]

    RETURNS DESCRIPTION str | BytesIO | None

    str | BytesIO | None: path to the file or BytesIO object if the

    str | BytesIO | None

    export was successful, None otherwise.

    "},{"location":"repositories/abstract/","title":"Abstract","text":""},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.__new__","title":"__new__(*args, **kwargs)","text":"

    Singleton pattern implementation.

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.build_event","title":"build_event(event_data)","text":"

    Build an event object from the provided data.

    PARAMETER DESCRIPTION event_data

    event data.

    TYPE: EventData

    RETURNS DESCRIPTION Event

    types.Event: event object.

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.enqueue_event","title":"enqueue_event(event)","text":"

    Enqueue an event to be written to the repository.

    PARAMETER DESCRIPTION event

    event to write.

    TYPE: Event

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.filter_events","title":"filter_events(filters) abstractmethod","text":"

    Filters events based on provided filter criteria.

    PARAMETER DESCRIPTION filters

    filters to apply.

    TYPE: Filters

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.get_event","title":"get_event(event_id) abstractmethod","text":"

    Retrieves a single event from the repository.

    PARAMETER DESCRIPTION event_id

    event ID.

    TYPE: str

    RETURNS DESCRIPTION Event | None

    types.Event | None: event object or None if not found.

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.get_name","title":"get_name() abstractmethod classmethod","text":"

    Return the name of the repository.

    RETURNS DESCRIPTION str

    name of the repository.

    TYPE: str

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.remove_all_events","title":"remove_all_events()","text":"

    Removes all events from the repository.

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.remove_event","title":"remove_event(event_id)","text":"

    Removes a single event from the repository.

    PARAMETER DESCRIPTION event_id

    event ID.

    TYPE: Any

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.remove_events","title":"remove_events(filters)","text":"

    Removes a filtered set of events from the repository.

    PARAMETER DESCRIPTION filters

    filters to apply.

    TYPE: Filters

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.test_connection","title":"test_connection() abstractmethod","text":"

    Test the connection to the repository.

    RETURNS DESCRIPTION bool

    whether the connection was successful.

    TYPE: bool

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.write_event","title":"write_event(event) abstractmethod","text":"

    Writes a single event to the repository.

    PARAMETER DESCRIPTION event

    event to write.

    TYPE: Event

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/abstract/#event_audit.repositories.base.AbstractRepository.write_events","title":"write_events(events)","text":"

    Write multiple events to the repository.

    PARAMETER DESCRIPTION events

    events to write.

    TYPE: Iterable[Event]

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/basic/","title":"Basic","text":"

    Repositories are the storages where the event audit logs are stored. There are a few basic repositories, that you can use out of the box:

    1. redis - the default repository, stores logs in Redis.
    2. postgres - stores logs in a PostgreSQL database.
    3. cloudwatch - stores logs in AWS CloudWatch.

    You can also implement your own repository. To do this, you need to create a new class that inherits from the AbstractRepository class and implement all the required methods.

    See the abstract repository documentation and custom repository documentation for more information.

    "},{"location":"repositories/cloudwatch/","title":"Cloudwatch repository","text":""},{"location":"repositories/cloudwatch/#event_audit.repositories.cloudwatch.CloudWatchRepository.__init__","title":"__init__(credentials=None, log_group='/ckan/event-audit', log_stream='event-audit-stream')","text":"

    CloudWatch repository.

    PARAMETER DESCRIPTION credentials

    AWS credentials. If not provided, the extension configuration will be used.

    TYPE: AWSCredentials | None DEFAULT: None

    log_group

    CloudWatch log group name.

    TYPE: str DEFAULT: '/ckan/event-audit'

    log_stream

    CloudWatch log stream name.

    TYPE: str DEFAULT: 'event-audit-stream'

    "},{"location":"repositories/cloudwatch/#event_audit.repositories.cloudwatch.CloudWatchRepository.filter_events","title":"filter_events(filters)","text":"

    Filters events based on provided filter criteria.

    PARAMETER DESCRIPTION filters

    filters to apply.

    TYPE: Filters

    "},{"location":"repositories/cloudwatch/#event_audit.repositories.cloudwatch.CloudWatchRepository.get_event","title":"get_event(event_id)","text":"

    Retrieves a single event from the repository.

    PARAMETER DESCRIPTION event_id

    event ID.

    TYPE: str

    RETURNS DESCRIPTION Optional[Event]

    types.Event | None: event object or None if not found.

    "},{"location":"repositories/cloudwatch/#event_audit.repositories.cloudwatch.CloudWatchRepository.remove_all_events","title":"remove_all_events()","text":"

    Removes all events from the repository.

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/cloudwatch/#event_audit.repositories.cloudwatch.CloudWatchRepository.remove_event","title":"remove_event(event_id)","text":"

    Remove operation is not supported for CloudWatch logs.

    As of today, you cannot delete a single log event from CloudWatch log stream, the alternative will be using Lambda functions: set a Lambda function trigger, filter all logs, then write the remaining logs to a new log group/stream, then delete the original log stream.

    It's potentially too expensive to do this operation, so it's not implemented.

    Note

    The remove single event operation is not supported

    "},{"location":"repositories/cloudwatch/#event_audit.repositories.cloudwatch.CloudWatchRepository.remove_events","title":"remove_events(filters)","text":"

    See remove_event method docstring.

    "},{"location":"repositories/cloudwatch/#event_audit.repositories.cloudwatch.CloudWatchRepository.test_connection","title":"test_connection()","text":"

    Tests the connection to the repository.

    RETURNS DESCRIPTION bool

    whether the connection was successful.

    TYPE: bool

    "},{"location":"repositories/cloudwatch/#event_audit.repositories.cloudwatch.CloudWatchRepository.write_event","title":"write_event(event)","text":"

    Writes a single event to the repository.

    PARAMETER DESCRIPTION event

    event to write.

    TYPE: Event

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/custom/","title":"Custom","text":"

    Here you can see a naive example of how to implement a custom repository, that stores logs in a json file.

    from ckanext.event_audit.repositories import AbstractRepository\n\nclass FileRepository(AbstractRepository):\n    def __init__(self, file_path: str | None = None):\n        self.file_path = file_path or '/tmp/event_audit.json'\n\n    def log(self) -> str:\n        return \"file\"\n\n    def write_event(self, event: types.Event) -> types.Result:\n        with open(self.file_path, 'a') as f:\n            data = json.load(f)\n            data[event.id] = event.model_dump()\n\n            f.write(json.dumps(data))\n\n        return types.Result(success=True)\n\n    def get_event(self, event_id: Any) -> types.Event | None:\n        with open(self.file_path, 'r') as f:\n            data = json.load(f)\n\n            if event_id in data:\n                return types.Event.model_validate(data[event_id])\n\n        return None\n\n    def filter_events(self, filters: types.Filters) -> list[types.Event]:\n        with open(self.file_path, 'r') as f:\n            data = json.load(f)\n\n            result = []\n\n            for event in data.values():\n                if _match_filters(event, filters):\n                    result.append(types.Event.model_validate(event))\n\n            return result\n\n    def _match_filters(self, event: types.EventData, filters: types.Filters) -> bool:\n        ...\n\n    def test_connection(self) -> types.Result:\n        return types.Result(success=True)\n

    In this version, it doesn't implement the remove_event, remove_events and remove_all_events methods, but you can implement them in the same way as the other methods. If the repository able to remove one or multiple events, it must inherits from the respective class - RemoveSingle or RemoveAll. For example:

    import os\n\nfrom ckanext.event_audit.repositories import (\n    AbstractRepository,\n    RemoveSingle,\n    RemoveAll,\n    RemoveFiltered,\n)\n\n\nclass FileRepository(AbstractRepository, RemoveSingle, RemoveAll, RemoveFiltered):\n    ...\n\n    def remove_event(self, event_id: Any) -> types.Result:\n        with open(self.file_path, \"w\") as f:\n            data = json.load(f)\n\n            if event_id in data:\n                del data[event_id]\n                f.write(json.dumps(data))\n\n                return types.Result(success=True)\n\n        return types.Result(success=False, message=\"Event not found\")\n\n    def remove_events(self, filters: types.Filters) -> types.Result:\n        with open(self.file_path, \"w\") as f:\n            data = json.load(f)\n\n            for event_id, event in data.items():\n                if _match_filters(event, filters):\n                    del data[event_id]\n\n            f.write(json.dumps(data))\n\n        return types.Result(success=True)\n\n    def _match_filters(\n        self, event: types.EventData, filters: types.Filters\n    ) -> bool: ...\n\n    def remove_all_events(self, filters: types.Filters) -> types.Result:\n        \"\"\"Removes the file if exists.\"\"\"\n\n        if os.path.exists(self.file_path):\n            os.remove(self.file_path)\n\n        return types.Result(success=True)\n
    "},{"location":"repositories/postgres/","title":"Postgres repository","text":""},{"location":"repositories/postgres/#event_audit.repositories.postgres.PostgresRepository.filter_events","title":"filter_events(filters)","text":"

    Filters events based on provided filter criteria.

    PARAMETER DESCRIPTION filters

    filters to apply.

    TYPE: Filters

    RETURNS DESCRIPTION List[Event]

    List[types.Event]: list of events.

    "},{"location":"repositories/postgres/#event_audit.repositories.postgres.PostgresRepository.get_event","title":"get_event(event_id)","text":"

    Retrieves a single event from the repository.

    PARAMETER DESCRIPTION event_id

    event ID.

    TYPE: str

    RETURNS DESCRIPTION Event | None

    types.Event | None: event object or None if not found.

    "},{"location":"repositories/postgres/#event_audit.repositories.postgres.PostgresRepository.remove_all_events","title":"remove_all_events()","text":"

    Removes all events from the repository.

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/postgres/#event_audit.repositories.postgres.PostgresRepository.remove_event","title":"remove_event(event_id, session=None, defer_commit=False)","text":"

    Removes a single event from the repository.

    PARAMETER DESCRIPTION event_id

    event ID.

    TYPE: str

    session

    session to use.

    TYPE: Session | None DEFAULT: None

    defer_commit

    whether to defer the commit.

    TYPE: bool DEFAULT: False

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/postgres/#event_audit.repositories.postgres.PostgresRepository.remove_events","title":"remove_events(filters)","text":"

    Removes a filtered set of events from the repository.

    PARAMETER DESCRIPTION filters

    filters to apply.

    TYPE: Filters

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/postgres/#event_audit.repositories.postgres.PostgresRepository.test_connection","title":"test_connection()","text":"

    Tests the connection to the repository.

    RETURNS DESCRIPTION bool

    whether the connection was successful.

    TYPE: bool

    "},{"location":"repositories/postgres/#event_audit.repositories.postgres.PostgresRepository.write_event","title":"write_event(event, session=None, defer_commit=False)","text":"

    Writes a single event to the repository.

    PARAMETER DESCRIPTION event

    event to write.

    TYPE: Event

    session

    session to use.

    TYPE: Session | None DEFAULT: None

    defer_commit

    whether to defer the commit.

    TYPE: bool DEFAULT: False

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/postgres/#event_audit.repositories.postgres.PostgresRepository.write_events","title":"write_events(events)","text":"

    Write multiple events to the repository.

    PARAMETER DESCRIPTION events

    events to write.

    TYPE: Iterable[Event]

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/redis/","title":"Redis repository","text":""},{"location":"repositories/redis/#event_audit.repositories.redis.RedisRepository.filter_events","title":"filter_events(filters)","text":"

    Filters events based on patterns generated from the provided filters.

    PARAMETER DESCRIPTION filters

    filters to apply.

    TYPE: Filters

    "},{"location":"repositories/redis/#event_audit.repositories.redis.RedisRepository.get_event","title":"get_event(event_id)","text":"

    Get an event by its ID.

    PARAMETER DESCRIPTION event_id

    event ID.

    TYPE: float

    "},{"location":"repositories/redis/#event_audit.repositories.redis.RedisRepository.remove_all_events","title":"remove_all_events()","text":"

    Removes all events from the repository.

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/redis/#event_audit.repositories.redis.RedisRepository.remove_event","title":"remove_event(event_id)","text":"

    Removes an event by its ID.

    PARAMETER DESCRIPTION event_id

    event ID.

    TYPE: float

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/redis/#event_audit.repositories.redis.RedisRepository.remove_events","title":"remove_events(filters)","text":"

    Removes a filtered set of events from the repository.

    PARAMETER DESCRIPTION filters

    filters to apply.

    TYPE: Filters

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "},{"location":"repositories/redis/#event_audit.repositories.redis.RedisRepository.test_connection","title":"test_connection()","text":"

    Tests the connection to the repository.

    RETURNS DESCRIPTION bool

    whether the connection was successful.

    TYPE: bool

    "},{"location":"repositories/redis/#event_audit.repositories.redis.RedisRepository.write_event","title":"write_event(event)","text":"

    Writes an event to Redis.

    PARAMETER DESCRIPTION event

    event to write.

    TYPE: Event

    RETURNS DESCRIPTION Result

    types.Result: result of the operation.

    "}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index 0dc3a42..8990eee 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,94 +2,98 @@ https://.github.io/ckanext-event-audit/ - 2024-11-20 + 2024-11-25 https://.github.io/ckanext-event-audit/cli/ - 2024-11-20 + 2024-11-25 https://.github.io/ckanext-event-audit/install/ - 2024-11-20 + 2024-11-25 https://.github.io/ckanext-event-audit/interfaces/ - 2024-11-20 + 2024-11-25 https://.github.io/ckanext-event-audit/usage/ - 2024-11-20 + 2024-11-25 https://.github.io/ckanext-event-audit/utils/ - 2024-11-20 + 2024-11-25 https://.github.io/ckanext-event-audit/validators/ - 2024-11-20 + 2024-11-25 - https://.github.io/ckanext-event-audit/configure/active_repo/ - 2024-11-20 + https://.github.io/ckanext-event-audit/configure/admin_panel/ + 2024-11-25 https://.github.io/ckanext-event-audit/configure/async/ - 2024-11-20 + 2024-11-25 https://.github.io/ckanext-event-audit/configure/cloudwatch/ - 2024-11-20 + 2024-11-25 https://.github.io/ckanext-event-audit/configure/ignore/ - 2024-11-20 + 2024-11-25 + + + https://.github.io/ckanext-event-audit/configure/repository/ + 2024-11-25 https://.github.io/ckanext-event-audit/configure/tracking/ - 2024-11-20 + 2024-11-25 https://.github.io/ckanext-event-audit/exporters/basic/ - 2024-11-20 + 2024-11-25 https://.github.io/ckanext-event-audit/exporters/csv/ - 2024-11-20 + 2024-11-25 https://.github.io/ckanext-event-audit/exporters/json/ - 2024-11-20 + 2024-11-25 https://.github.io/ckanext-event-audit/exporters/tsv/ - 2024-11-20 + 2024-11-25 https://.github.io/ckanext-event-audit/exporters/xlsx/ - 2024-11-20 + 2024-11-25 https://.github.io/ckanext-event-audit/repositories/abstract/ - 2024-11-20 + 2024-11-25 https://.github.io/ckanext-event-audit/repositories/basic/ - 2024-11-20 + 2024-11-25 https://.github.io/ckanext-event-audit/repositories/cloudwatch/ - 2024-11-20 + 2024-11-25 https://.github.io/ckanext-event-audit/repositories/custom/ - 2024-11-20 + 2024-11-25 https://.github.io/ckanext-event-audit/repositories/postgres/ - 2024-11-20 + 2024-11-25 https://.github.io/ckanext-event-audit/repositories/redis/ - 2024-11-20 + 2024-11-25 \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz index 438b76e..8e4c467 100644 Binary files a/sitemap.xml.gz and b/sitemap.xml.gz differ diff --git a/usage/index.html b/usage/index.html index f55089f..b39390b 100644 --- a/usage/index.html +++ b/usage/index.html @@ -18,7 +18,7 @@ - + @@ -415,6 +415,8 @@ + + @@ -453,11 +455,11 @@
  • - + - Active repository + Repository @@ -494,6 +496,27 @@ +
  • + + + + + Admin panel + + + + +
  • + + + + + + + + + +
  • diff --git a/utils/index.html b/utils/index.html index 3876c3e..40e9b0c 100644 --- a/utils/index.html +++ b/utils/index.html @@ -18,7 +18,7 @@ - + @@ -506,6 +506,8 @@ + + @@ -544,11 +546,11 @@
  • - + - Active repository + Repository @@ -585,6 +587,27 @@ +
  • + + + + + Admin panel + + + + +
  • + + + + + + + + + +
  • @@ -1100,11 +1123,6 @@

    Utility Functions

    - - - - -