Skip to content

Commit

Permalink
Refactor code and improve documentation in EzTaskManager
Browse files Browse the repository at this point in the history
Reduced the complexity of command execution in the TaskQueueService for better code maintainability. Streamlined logging and error handling across various files for a more consistent user experience. Additionally, removed unnecessary lines and refined documentations, including docstrings, for better mass readability. Updated the GitHub workflows to boost the efficiency of the code development process.
  • Loading branch information
guglielmo committed Apr 8, 2024
1 parent 2010177 commit 153bd8f
Show file tree
Hide file tree
Showing 18 changed files with 242 additions and 62 deletions.
118 changes: 118 additions & 0 deletions .github/workflows/released.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
name: on release creation test and upload to PyPI

on:
release:
types: [created]


jobs:

lint-check:
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9, 3.10]

steps:
- uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Lint with flake8
run: |
pip install flake8
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --ignore=C901 --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --ignore=C901 --exit-zero --max-complexity=10 --max-line-length=127 --statistics
test-dj4:
needs: lint-check
runs-on: ubuntu-18.04
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9, 3.10]

steps:
- uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install "django" "file-read-backwards>=2.0.0" "uwsgidecorators-fallback>=0.0.3" "uwsgidecorators>=1.1.0" "pytz"
- name: Test with pytest
run: |
export PYTHONPATH='.':$PYTHONPATH
make test
test-dj3:
needs: lint-check
runs-on: ubuntu-18.04
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9, 3.10]

steps:
- uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install "django<4" "file-read-backwards>=2.0.0" "uwsgidecorators-fallback>=0.0.3" "uwsgidecorators>=1.1.0" "pytz"
- name: Test with pytest
run: |
export PYTHONPATH='.':$PYTHONPATH
make test
test-dj2:
needs: lint-check
runs-on: ubuntu-18.04
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9, 3.10]

steps:
- uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install "django<3" "file-read-backwards>=2.0.0" "uwsgidecorators-fallback>=0.0.3" "uwsgidecorators>=1.1.0"
- name: Test with pytest
run: |
export PYTHONPATH='.':$PYTHONPATH
make test
deploy:
needs: [test-dj2, test-dj3]
name: Build and publish distributions to PyPI
runs-on: ubuntu-18.04

steps:
- uses: actions/checkout@v1
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build distribution
run: |
python setup.py sdist bdist_wheel
- name: Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@master
with:
password: ${{ secrets.pypi_password }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
/docs/_build/
/demoproject/htmlcov/
/demoproject/staticfiles/
__pycache__/
*.pyc
2 changes: 0 additions & 2 deletions demoproject/demoproject/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,6 @@
# https://docs.djangoproject.com/en/dev/ref/settings/#email-port
EMAIL_PORT = 1025



# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field

Expand Down
1 change: 0 additions & 1 deletion demoproject/demoproject/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,3 @@
# Do not connect labels signals during tests
LABELS_CONNECT_SIGNALS = False
TOPICS_CONNECT_SIGNALS = False

2 changes: 1 addition & 1 deletion demoproject/demoproject/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.urls import include, path

urlpatterns = [
path('admin/', admin.site.urls),
Expand Down
2 changes: 1 addition & 1 deletion eztaskmanager/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Django application to manage async tasks via admin interface"""
"""Django application to manage async tasks via admin interface."""

# PEP 440 - version number format
VERSION = (0, 1, 0)
Expand Down
19 changes: 13 additions & 6 deletions eztaskmanager/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
from django.utils.translation import gettext_lazy as _
from pytz import timezone

from eztaskmanager.models import AppCommand, TaskCategory, Task, LaunchReport
from eztaskmanager.models import AppCommand, LaunchReport, Task, TaskCategory
from eztaskmanager.services.queues import TaskQueueException
from eztaskmanager.settings import EZTASKMANAGER_N_LINES_IN_REPORT_LOG, EZTASKMANAGER_SHOW_LOGVIEWER_LINK
from eztaskmanager.settings import (EZTASKMANAGER_N_LINES_IN_REPORT_LOG,
EZTASKMANAGER_SHOW_LOGVIEWER_LINK)


def convert_to_local_dt(dt):
Expand Down Expand Up @@ -82,11 +83,11 @@ def __init__(self, wrapped_queryset):
self.wrapped_queryset = wrapped_queryset

def __getattr__(self, attr):
"""Getattr method"""
"""Getattr method."""
return self._safe_delete

def __iter__(self):
"""Yeld obj from wrapperd queryset."""
"""Yield obj from wrapped queryset."""
for obj in self.wrapped_queryset:
yield obj

Expand Down Expand Up @@ -184,7 +185,9 @@ class LaunchReportInline(LaunchReportMixin, admin.TabularInline):

max_num = 5
extra = 0
fields = readonly_fields = ("invocation_result", "invocation_datetime", "log_tail_html", "n_log_errors", "n_log_warnings", )
fields = readonly_fields = (
"invocation_result", "invocation_datetime", "log_tail_html", "n_log_errors", "n_log_warnings",
)
ordering = [
"-invocation_datetime",
]
Expand Down Expand Up @@ -300,6 +303,7 @@ class TaskAdmin(BulkDeleteMixin, admin.ModelAdmin):
search_fields = ("name", "command__app_name", "command__name")

def launch_tasks(self, request, queryset):
"""Put many tasks in the queue."""
from eztaskmanager.services.queues import get_task_service

service = get_task_service()
Expand All @@ -310,6 +314,7 @@ def launch_tasks(self, request, queryset):
launch_tasks.short_description = 'Launch selected tasks'

def stop_tasks(self, request, queryset):
"""Remove many tasks from the queue."""
from eztaskmanager.services.queues import get_task_service

service = get_task_service()
Expand All @@ -329,13 +334,15 @@ def repetition(self, obj):
repetition.short_description = _("Repetition rate")

def name_desc(self, obj):
"""Show the note on mouse over."""
return format_html(
f"<span title=\"{obj.note}\">{obj.name}</span>"
)

name_desc.short_description = _("Name")

def invocation(self, obj):
"""Show the command name, with arguments."""
return format_html(
f"<span style=\"font-weight: normal; font-family: Courier\"><b>{obj.command.name}</b> <br/>"
f"{' '.join(obj.arguments.split(','))}</span>"
Expand All @@ -344,7 +351,7 @@ def invocation(self, obj):
invocation.short_description = _("Invocation")

def last_result_with_logviewer_link(self, obj):

"""Show the last result, with a log to the logviewer."""
s = "-"
link_text = _("Show log messages")
last_report = obj.launchreport_set.order_by('invocation_datetime').last()
Expand Down
2 changes: 1 addition & 1 deletion eztaskmanager/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class EZTaskmanagerConfig(AppConfig):
name = "eztaskmanager"
verbose_name = _("eztaskmanager")

notification_handlers: Dict[str, "NotificationHandler"] = {}
notification_handlers: Dict[str, "NotificationHandler"] = {} # noqa: F821

def _register_notification_handlers(self) -> None:
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@


class Command(LoggerEnabledCommand):
"""Command for testing live logger. Perform a simple iteration, up to a maximum limit,
"""Command for testing live logger.
Performs a simple iteration, up to a maximum limit,
sleeping 0.1 seconds between each number generation.
Generates 10 numbers per second, logging them at debug level.
Expand Down
1 change: 1 addition & 0 deletions eztaskmanager/management/commands/test_logging_command.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Test logging command."""

from django.core import management

from eztaskmanager.services.logger import LoggerEnabledCommand


Expand Down
30 changes: 19 additions & 11 deletions eztaskmanager/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,36 +64,36 @@ class LaunchReport(models.Model):

@classmethod
def get_notification_handlers(cls):
"""Get the list of notification handlers to send the report to."""
return apps.get_app_config('eztaskmanager').notification_handlers

def get_log_lines(self):
# Format the log entries as you like, here is an example
"""Format the log entries, here is an example."""
log_lines = [
f"{log.timestamp} - {log.level} - {log.message}"
for log in self.logs.order_by('timestamp')
]
return log_lines

def read_log_lines(self, offset: int):
"""Uses an offset to read lines of the llog related to the report (self) starting from the offset
"""
Use an offset to read lines of the llog related to the report (self) starting from the offset.
:param: offset lines to start from
:return: 2-tuple (list, int)
- list of lines of log records from offset
- the number of total lines
"""
"""
log_lines = [
f"{log.timestamp} - {log.level} - {log.message}"
for log in self.logs.all()
]
return log_lines[offset:], len(log_lines)

def log_tail(self, n_lines=10):
"""
Return the last lines of the logs of a launch_report
"""
"""Return the last lines of the logs of a launch_report."""
# Get the related logs
logs = self.logs.order_by('-timestamp')[:n_lines]
total_logs = self.logs.count()
Expand All @@ -113,17 +113,21 @@ def log_tail(self, n_lines=10):

@property
def n_log_lines(self):
"""Return the number of log lines for this report."""
return self.logs.count()

@property
def n_log_errors(self):
"""Return the number of errors in this report."""
return self.logs.filter(level="ERROR").count()

@property
def n_log_warnings(self):
"""Return the number of warnings in this report."""
return self.logs.filter(level="WARNING").count()

def delete(self, *args, **kwargs):
"""Refresh the task cache after deleting this report."""
task = self.task
# call the parent delete method
super().delete(*args, **kwargs)
Expand All @@ -139,6 +143,8 @@ def __str__(self):


class Log(models.Model):
"""The log generated by a report."""

launch_report = models.ForeignKey(
'LaunchReport',
on_delete=models.CASCADE,
Expand Down Expand Up @@ -227,8 +233,10 @@ class Task(models.Model):
blank=True, null=True,
verbose_name=_("Initial scheduling")
)

@property
def scheduling_utc(self):
"""Sho the scheduling time, in UTC."""
if self.scheduling:
return self.scheduling.astimezone(timezone.timezone.utc)
else:
Expand All @@ -241,7 +249,7 @@ def scheduling_utc(self):

@property
def is_periodic(self):
"""A periodic task is such only if both repetition period and rate are set"""
"""A periodic task is such only if both repetition period and rate are set."""
return self.repetition_period is not None and self.repetition_rate is not None

note = models.TextField(
Expand Down Expand Up @@ -306,10 +314,8 @@ def interval_in_seconds(self):
@property
def _args_dict(self):
"""
Method: _args_dict
Description:
This method returns a dictionary containing arguments and their corresponding parameters.
It parses the 'arguments' attribute of the instance and splits it into individual arguments
* using a comma as a delimiter. Each argument is then further split into chunks using whitespace or
an equals sign as a separator. The first chunk is considered the argument name, while
Expand Down Expand Up @@ -364,6 +370,7 @@ def options(self):
def complete_args(self):
"""
Returns a list containing all the non-null values from the dictionary of arguments.
Get all task args in order to avoid problems with required options.
:return: A list containing non-null argument values.
Expand All @@ -383,6 +390,7 @@ def complete_args(self):
)

def compute_cache(self):
"""Compute cached values for this task."""
reports = self.launchreport_set.order_by('-invocation_datetime')
if reports.exists():
latest_report = reports[0] # get the latest execution report
Expand All @@ -403,7 +411,7 @@ def compute_cache(self):
self.save()

def prune_reports(self, n: int = EZTASKMANAGER_N_REPORTS_INLINE):
"""Delete all Task's LaunchReports except latest `n`"""
"""Delete all Task's LaunchReports except latest `n`."""
if n:
last_n_reports_ids = (
LaunchReport.objects.filter(task=self)
Expand Down
Loading

0 comments on commit 153bd8f

Please sign in to comment.