Skip to content

Commit

Permalink
Plugin optimization and cleanup (#1126)
Browse files Browse the repository at this point in the history
* Update logging

* Adding tests to backend dynamo models

* Adding data workflows alarm

* Adding batching_window

* Renaming exclusion to reason

* Update backend/api/models/plugin_blocked.py

Co-authored-by: Ashley Anderson <aganders3@gmail.com>

* Cleaning up fixture fetching

* Cleaning up shadowed variable name

* Making metadata fetch safe

---------

Co-authored-by: Ashley Anderson <aganders3@gmail.com>
  • Loading branch information
manasaV3 and aganders3 committed Jul 24, 2023
1 parent 02b0f5f commit 8071818
Show file tree
Hide file tree
Showing 11 changed files with 440 additions and 22 deletions.
35 changes: 35 additions & 0 deletions .happy/terraform/modules/cloudwatch-alarm/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,26 @@ resource aws_cloudwatch_log_metric_filter data_workflows_metrics_update_successf
}
}

resource aws_cloudwatch_log_metric_filter data_workflows_plugin_update_successful {
name = "${var.stack_name}-data-workflows-plugin-update-successful"
log_group_name = var.data_workflows_lambda_log_group_name
pattern = "Update successful for type=plugin"
count = var.metrics_enabled ? 1 : 0

metric_transformation {
name = "${var.stack_name}-data-workflows-plugin-update-successful"
namespace = local.metrics_namespace
value = "1"
unit = "Count"
}
}

locals {
backend_api_500_log_metric_name = var.metrics_enabled ? aws_cloudwatch_log_metric_filter.backend_api_500_log_metric[0].name : "backend_api_500_log_metric"
backend_plugin_update_successful_name = var.metrics_enabled ? aws_cloudwatch_log_metric_filter.backend_plugin_update_successful[0].name : "backend_plugin_update_successful"
backend_metrics_update_successful_name = var.metrics_enabled ? aws_cloudwatch_log_metric_filter.backend_metrics_update_successful[0].name : "backend_metrics_update_successful"
data_workflows_metrics_update_successful_name = var.metrics_enabled ? aws_cloudwatch_log_metric_filter.data_workflows_metrics_update_successful[0].name : "data_workflows_metrics_update_successful"
data_workflows_plugin_update_successful_name = var.metrics_enabled ? aws_cloudwatch_log_metric_filter.data_workflows_plugin_update_successful[0].name : "data_workflows_plugin_update_successful"
}

module backend_api_500_alarm {
Expand Down Expand Up @@ -145,6 +160,26 @@ module plugins_missing_update_alarm {
treat_missing_data = "breaching"
}

module data_workflow_plugins_missing_update_alarm {
source = "terraform-aws-modules/cloudwatch/aws//modules/metric-alarm"
version = "3.3.0"

alarm_actions = [local.alarm_sns_arn]
alarm_name = "${var.stack_name}-dataworkflow-plugins-update-alarm"
alarm_description = "data-workflows plugin update failure"
comparison_operator = "LessThanThreshold"
create_metric_alarm = var.alarms_enabled
datapoints_to_alarm = 2
evaluation_periods = 3
metric_name = local.data_workflows_plugin_update_successful_name
namespace = local.metrics_namespace
period = local.period
statistic = "Sum"
tags = var.tags
threshold = 1
treat_missing_data = "breaching"
}

module backend_metrics_missing_update_alarm {
source = "terraform-aws-modules/cloudwatch/aws//modules/metric-alarm"
version = "3.3.0"
Expand Down
1 change: 1 addition & 0 deletions .happy/terraform/modules/ecs-stack/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ resource aws_lambda_event_source_mapping data_workflow_plugin_metadata_event_sou
function_name = module.data_workflows_lambda.function_name
batch_size = 100
starting_position = "LATEST"
maximum_batching_window_in_seconds = 60
}

module api_gateway_proxy_stage {
Expand Down
20 changes: 12 additions & 8 deletions backend/api/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
import pandas as pd

from api.models import (
github_activity, install_activity, plugin, plugin_blocked, plugin_metadata
github_activity,
install_activity,
plugin as plugin_model,
plugin_blocked,
plugin_metadata as plugin_metadata_model
)
from utils.github import get_github_metadata, get_artifact
from utils.pypi import query_pypi, get_plugin_pypi_metadata
Expand Down Expand Up @@ -47,7 +51,7 @@ def get_public_plugins(use_dynamo: bool = False) -> Dict[str, str]:
:return: dict of public plugins and their versions
"""
if use_dynamo:
return plugin.get_latest_by_visibility()
return plugin_model.get_latest_by_visibility()

public_plugins = get_cache("cache/public-plugins.json")
return public_plugins if public_plugins else {}
Expand All @@ -60,7 +64,7 @@ def get_hidden_plugins(use_dynamo: bool = False) -> Dict[str, str]:
:return: dict of hidden plugins and their versions
"""
if use_dynamo:
return plugin.get_hidden_plugins()
return plugin_model.get_hidden_plugins()

hidden_plugins = get_cache('cache/hidden-plugins.json')
return hidden_plugins if hidden_plugins else {}
Expand All @@ -87,7 +91,7 @@ def get_plugin(name: str, version: str = None, use_dynamo: bool = False) -> dict
:return: plugin metadata dictionary
"""
if use_dynamo:
return plugin.get_plugin(name, version)
return plugin_model.get_plugin(name, version)

plugins = get_valid_plugins()
if name not in plugins:
Expand Down Expand Up @@ -144,10 +148,10 @@ def get_manifest(name: str, version: str = None, use_dynamo: bool = False) -> di
"""
if use_dynamo:
if not version:
version = plugin.get_latest_version(name)
version = plugin_model.get_latest_version(name)
if not version:
return {}
manifest_metadata = plugin_metadata.get_manifest(name, version)
manifest_metadata = plugin_metadata_model.get_manifest(name, version)
else:
plugins = get_valid_plugins()
if name not in plugins:
Expand Down Expand Up @@ -179,7 +183,7 @@ def get_index(use_dynamo: bool = False) -> List[Dict[str, Any]]:
:return: dict for index page metadata
"""
if use_dynamo:
plugins = plugin.get_index()
plugins = plugin_model.get_index()
total_installs = install_activity.get_total_installs_by_plugins()
for item in plugins:
item["total_installs"] = total_installs.get(item["name"], 0)
Expand Down Expand Up @@ -207,7 +211,7 @@ def get_excluded_plugins(use_dynamo: bool = False) -> Dict[str, str]:
"""
if use_dynamo:
return {
**plugin.get_excluded_plugins(),
**plugin_model.get_excluded_plugins(),
**plugin_blocked.get_blocked_plugins()
}

Expand Down
264 changes: 264 additions & 0 deletions backend/api/models/_tests/test_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
import pytest
from moto import mock_dynamodb

from api.models._tests.conftest import create_dynamo_table
from api.models import plugin


class TestPlugin:

@pytest.fixture()
def plugin_table(self, aws_credentials, dynamo_env_variables):
with mock_dynamodb():
yield create_dynamo_table(plugin._Plugin, "plugin")

@pytest.fixture()
def plugin1_data2_2(self):
return {
"authors": [{"name": "Napari Hub"}],
"code_repository": "https://github.com/naparihub/plugin-1",
"description_content_type": "text",
"description_text": "napari plugin with a detailed description.",
"development_status": ["Development Status :: 2 - Pre-Alpha"],
"display_name": "Plugin 1",
"first_released": "2021-12-21T11:39:40.715897Z",
"license": "BSD-3-Clause",
"name": "plugin-1",
"npe2": True,
"operating_system": ["Operating System :: OS Independent"],
"plugin_types": ["reader"],
"python_version": ">=3.7",
"reader_file_extensions": ["*.pdb", "*.cif"],
"release_date": "2022-01-31T17:59:24.494345Z",
"summary": "A napari plugin that does imaging magic.",
"version": "2.2",
"writer_file_extensions": [],
"writer_save_layers": []
}

@pytest.fixture()
def plugin1_data2_3(self, plugin1_data2_2):
plugin1_data2_2["version"] = "2.3"
plugin1_data2_2["release_date"] = "2022-02-31T17:59:24.494345Z"
return plugin1_data2_2

@pytest.fixture()
def plugin4_data(self):
return {
"authors": "creator-3",
"category": ["categories for plugin-4"],
"code_repository": "https://github.com/creator3/plugin4",
"description_content_type": "text/markdown",
"description_text": "foo",
"development_status": ["Development Status :: Beta"],
"display_name": "Plugin 4",
"first_released": "2023-06-26T03:23:35",
"license": "Apache",
"name": "plugin-4",
"npe2": False,
"operating_system": ["Operating System :: Unix"],
"plugin_types": ["writer", "theme"],
"python_version": ">=3.9",
"reader_file_extensions": [],
"release_date": "2023-07-05T02:30:15",
"summary": "A summary of the plugin",
"version": "5.8",
"writer_file_extensions": [".zarr"],
"writer_save_layers": ["labels", "image"],
}

@pytest.fixture()
def plugin2_data0_5(self):
return {"foo": "bar", "release_date": "2025-04-03"}

@pytest.fixture()
def plugin2_data1_0_0(self):
return {"test": "baz", "release_date": "2025-05-03"}

@pytest.fixture()
def data(
self,
plugin1_data2_2,
plugin1_data2_3,
plugin2_data0_5,
plugin2_data1_0_0,
plugin4_data
):
return [
{
"name": "plugin-1",
"version": "2.2",
"visibility": "PUBLIC",
"data": plugin1_data2_2,
},
{
"name": "plugin-1",
"version": "2.3",
"visibility": "PUBLIC",
"is_latest": "true",
"data": plugin1_data2_3,
},
{
"name": "plugin-2",
"version": "0.5",
"visibility": "HIDDEN",
"excluded": "HIDDEN",
"is_latest": "true",
"data": plugin2_data0_5,
},
{
"name": "plugin-2",
"version": "1.0.0",
"visibility": "HIDDEN",
"excluded": "HIDDEN",
"is_latest": "true",
"data": plugin2_data1_0_0,
},
{
"name": "plugin-3",
"version": "1.5",
"visibility": "DISABLED",
"excluded": "DISABLED",
},
{
"name": "plugin-3",
"version": "1.6",
"visibility": "DISABLED",
"excluded": "DISABLED",
"is_latest": "true",
},
{
"name": "plugin-4",
"version": "5.0",
"visibility": "PUBLIC",
"is_latest": "true",
"data": plugin4_data,
},
]

@pytest.fixture()
def get_fixture(self, request):
def _get_fixture(name):
return request.getfixturevalue(name) if name else {}
return _get_fixture

@classmethod
def _put_items(cls, table, data):
for item in data:
if item.get("data", {}).get("release_date"):
item["release_date"] = item["data"]["release_date"]
table.put_item(Item=item)

def test_get_latest_by_visibility_default_visibility(
self, plugin_table, data
):
self._put_items(plugin_table, data)

actual = plugin.get_latest_by_visibility()

assert actual == {"plugin-1": "2.3", "plugin-4": "5.0"}

@pytest.mark.parametrize("visibility", [
("PUBLIC", "HIDDEN", "DISABLED"),
])
def test_get_latest_by_visibility_by_visibility_without_data(
self, plugin_table, visibility
):
actual = plugin.get_latest_by_visibility(visibility)

assert actual == {}

@pytest.mark.parametrize("visibility, expected", [
("PUBLIC", {"plugin-1": "2.3", "plugin-4": "5.0"}),
("HIDDEN", {"plugin-2": "1.0.0"}),
("DISABLED", {"plugin-3": "1.6"}),
])
def test_get_latest_by_visibility_by_visibility_with_data(
self, plugin_table, data, visibility, expected
):
self._put_items(plugin_table, data)

actual = plugin.get_latest_by_visibility(visibility)
assert actual == expected

def test_get_index_with_data(
self, plugin_table, data, plugin1_data2_3, plugin4_data
):
self._put_items(plugin_table, data)

actual = plugin.get_index()

expected = [plugin1_data2_3, plugin4_data]
sorted(actual, key=lambda item: item["name"])
sorted(expected, key=lambda item: item["name"])
assert actual == expected

def test_get_index_without_data(self, plugin_table):
actual = plugin.get_index()

assert actual == []

def test_get_hidden_plugins_with_data(self, plugin_table, data):
self._put_items(plugin_table, data)

actual = plugin.get_hidden_plugins()
assert actual == {"plugin-2": "1.0.0"}

def test_get_hidden_plugins_without_data(self, plugin_table):
actual = plugin.get_hidden_plugins()
assert actual == {}

@pytest.mark.parametrize("name, version, has_data, fixture_name", [
("plugin-1", "2.2", True, "plugin1_data2_2"),
("plugin-1", "2.4", False, None),
("plugin-2", "0.5", True, "plugin2_data0_5"),
("plugin-8", "2.2", False, None),
])
def test_get_plugin_with_version(
self,
plugin_table,
data,
get_fixture,
name,
version,
has_data,
fixture_name
):
self._put_items(plugin_table, data)

actual = plugin.get_plugin(name, version)

expected = get_fixture(fixture_name)
assert actual == expected

@pytest.mark.parametrize("name, has_data, fixture_name", [
("plugin-1", True, "plugin1_data2_2"),
("plugin-2", True, "plugin2_data1_0_0"),
("plugin-8", False, None),
])
def test_get_plugin_without_version(
self, plugin_table, data, get_fixture, name, has_data, fixture_name
):
self._put_items(plugin_table, data)

actual = plugin.get_plugin(name, None)

expected = get_fixture(fixture_name)
assert actual == expected

def test_get_excluded_plugins(self, plugin_table, data):
self._put_items(plugin_table, data)

actual = plugin.get_excluded_plugins()

assert actual == {"plugin-2": "hidden", "plugin-3": "disabled"}

@pytest.mark.parametrize("name, expected", [
("plugin-1", "2.3"),
("plugin-2", "1.0.0"),
("plugin-8", None),
])
def test_get_excluded_plugins(self, plugin_table, data, name, expected):
self._put_items(plugin_table, data)

assert plugin.get_latest_version(name) == expected
Loading

0 comments on commit 8071818

Please sign in to comment.