Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[flake8]
max-line-length = 88
max-line-length = 140
ignore =
W503,
E231,
Expand Down
15 changes: 15 additions & 0 deletions api_app/analyzers_manager/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,24 @@ class BaseAnalyzerMixin(Plugin, metaclass=ABCMeta):
@classmethod
@property
def config_exception(cls):
"""Returns the AnalyzerConfigurationException class."""
return AnalyzerConfigurationException

@property
def analyzer_name(self) -> str:
"""Returns the name of the analyzer."""
return self._config.name

@classmethod
@property
def report_model(cls):
"""Returns the AnalyzerReport model."""
return AnalyzerReport

@classmethod
@property
def config_model(cls):
"""Returns the AnalyzerConfig model."""
return AnalyzerConfig

def get_exceptions_to_catch(self):
Expand Down Expand Up @@ -98,6 +102,12 @@ def _validate_result(self, result, level=0, max_recursion=190):
return result

def after_run_success(self, content):
"""
Handles actions after a successful run.

Args:
content (any): The content to process after a successful run.
"""
super().after_run_success(self._validate_result(content, max_recursion=15))


Expand Down Expand Up @@ -194,6 +204,11 @@ def read_file_bytes(self) -> bytes:

@property
def filepath(self) -> str:
"""Returns the file path, retrieving the file from storage if necessary.

Returns:
str: The file path.
"""
if not self.__filepath:
self.__filepath = self._job.file.storage.retrieve(
file=self._job.file, analyzer=self.analyzer_name
Expand Down
125 changes: 118 additions & 7 deletions api_app/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@

class Plugin(metaclass=ABCMeta):
"""
Abstract Base class for plugins.
For internal use only.
Abstract Base class for plugins. Provides a framework for defining and running
plugins within a specified configuration.

Attributes:
config (PythonConfig): Configuration for the plugin.
kwargs: Additional keyword arguments.
"""

def __init__(
Expand All @@ -40,6 +44,12 @@ def __init__(

@property
def name(self):
"""
Get the name of the plugin.

Returns:
str: The name of the plugin.
"""
return self._config.name

@classmethod
Expand All @@ -50,6 +60,12 @@ def python_base_path(cls) -> PosixPath:

@classmethod
def all_subclasses(cls):
"""
Retrieve all subclasses of the plugin class.

Returns:
list: Sorted list of plugin subclasses.
"""
posix_dir = PosixPath(str(cls.python_base_path).replace(".", "/"))
for plugin in posix_dir.rglob("*.py"):
if plugin.stem == "__init__":
Expand All @@ -65,30 +81,72 @@ def all_subclasses(cls):

@cached_property
def _job(self) -> "Job":
"""
Get the job associated with the plugin.

Returns:
Job: The job instance.
"""
return Job.objects.get(pk=self.job_id)

@property
def job_id(self) -> int:
"""
Get the job ID.

Returns:
int: The job ID.
"""
return self._job_id

@job_id.setter
def job_id(self, value):
"""
Set the job ID.

Args:
value (int): The job ID.
"""
self._job_id = value

@cached_property
def _user(self):
"""
Get the user associated with the job.

Returns:
User: The user instance.
"""
return self._job.user

def __repr__(self):
"""
Get the string representation of the plugin.

Returns:
str: The string representation of the plugin.
"""
return str(self)

def __str__(self):
"""
Get the string representation of the plugin.

Returns:
str: The string representation of the plugin.
"""
try:
return f"({self.__class__.__name__}, job: #{self.job_id})"
except AttributeError:
return f"{self.__class__.__name__}"

def config(self, runtime_configuration: typing.Dict):
"""
Configure the plugin with runtime parameters.

Args:
runtime_configuration (dict): Runtime configuration parameters.
"""
self.__parameters = self._config.read_configured_params(
self._user, runtime_configuration
)
Expand All @@ -104,25 +162,33 @@ def config(self, runtime_configuration: typing.Dict):

def before_run(self):
"""
function called directly before run function.
Function called directly before the run function.
"""

@abstractmethod
def run(self) -> dict:
"""
Called from *start* fn and wrapped in a try-catch block.
Should be overwritten in child class
:returns report
Called from *start* function and wrapped in a try-catch block.
Should be overwritten in child class.

Returns:
dict: Report generated by the plugin.
"""

def after_run(self):
"""
function called after run function.
Function called after the run function.
"""
self.report.end_time = timezone.now()
self.report.save()

def after_run_success(self, content: typing.Any):
"""
Handle the successful completion of the run function.

Args:
content (Any): Content generated by the plugin.
"""
# avoiding JSON serialization errors for types: File and bytes
report_content = content
if isinstance(report_content, typing.List):
Expand All @@ -140,6 +206,12 @@ def after_run_success(self, content: typing.Any):
self.report.save(update_fields=["status", "report"])

def log_error(self, e):
"""
Log an error encountered during the run function.

Args:
e (Exception): The exception to log.
"""
if isinstance(
e, (*self.get_exceptions_to_catch(), SoftTimeLimitExceeded, HTTPError)
):
Expand All @@ -151,6 +223,12 @@ def log_error(self, e):
logger.exception(error_message)

def after_run_failed(self, e: Exception):
"""
Handle the failure of the run function.

Args:
e (Exception): The exception that caused the failure.
"""
self.report.errors.append(str(e))
self.report.status = self.report.Status.FAILED
self.report.save(update_fields=["status", "errors"])
Expand Down Expand Up @@ -246,6 +324,12 @@ def _monkeypatch(cls, patches: list = None) -> None:
@classmethod
@property
def python_module(cls) -> PythonModule:
"""
Get the Python module associated with the plugin.

Returns:
PythonModule: The Python module instance.
"""
valid_module = cls.__module__.replace(str(cls.python_base_path), "")
# remove the starting dot
valid_module = valid_module[1:]
Expand All @@ -255,9 +339,24 @@ def python_module(cls) -> PythonModule:

@classmethod
def update(cls) -> bool:
"""
Update the plugin. Must be implemented by subclasses.

Returns:
bool: Whether the update was successful.
"""
raise NotImplementedError("No update implemented")

def _get_health_check_url(self, user: User = None) -> typing.Optional[str]:
"""
Get the URL for performing a health check.

Args:
user (User): The user instance.

Returns:
typing.Optional[str]: The health check URL.
"""
params = (
self._config.parameters.annotate_configured(self._config, user)
.annotate_value_for_user(self._config, user)
Expand All @@ -274,6 +373,15 @@ def _get_health_check_url(self, user: User = None) -> typing.Optional[str]:
return None

def health_check(self, user: User = None) -> bool:
"""
Perform a health check for the plugin.

Args:
user (User): The user instance.

Returns:
bool: Whether the health check was successful.
"""
url = self._get_health_check_url(user)
if url and url.startswith("http"):
if settings.STAGE_CI or settings.MOCK_CONNECTIONS:
Expand All @@ -296,6 +404,9 @@ def health_check(self, user: User = None) -> bool:
raise NotImplementedError()

def disable_for_rate_limit(self):
"""
Disable the plugin due to rate limiting.
"""
logger.info(f"Trying to disable for rate limit {self}")
if self._user.has_membership():
org_configuration = self._config.get_or_create_org_configuration(
Expand Down
16 changes: 16 additions & 0 deletions api_app/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ def decorator_deprecated(func):

@functools.wraps(func)
def wrapper_deprecated(*args, **kwargs):
"""
Wrapper function to log a warning and amend the response with
deprecation headers.
"""
# do something before handling the request, could e.g. issue a django signal
logger.warning("Deprecated endpoint %s called", func.__name__)

Expand All @@ -39,6 +43,18 @@ def wrapper_deprecated(*args, **kwargs):


def prevent_signal_recursion(func):
"""
Decorator to prevent recursion issues when saving Django model instances.
It ensures that the decorated function does not cause a recursion loop
during model save operations.

Args:
func (function): The function to be decorated.

Returns:
function: The wrapper function that prevents recursion.
"""

@functools.wraps(func)
def no_recursion(sender, instance=None, **kwargs):
if not instance:
Expand Down
Loading