Skip to content

Commit

Permalink
Adds Progress Reporting models into pulp.platform.models.
Browse files Browse the repository at this point in the history
This creates three new models: ProgressBar, ProgressReportingBase,
and ProgressSpinner. These can be used to create progress reports.

https://pulp.plan.io/issues/2092
closes #2092
  • Loading branch information
Brian Bouterse committed Sep 3, 2016
1 parent 03a260b commit 6bb7932
Show file tree
Hide file tree
Showing 2 changed files with 236 additions and 0 deletions.
3 changes: 3 additions & 0 deletions platform/pulp/platform/models/__init__.py
Expand Up @@ -9,3 +9,6 @@
RepositoryContent) # NOQA

from .task import ReservedResource, Worker, Task, TaskTag, TaskLock, ScheduledCalls # NOQA

# Moved here to avoid a circular import with Task
from .progress import ProgressBar, ProgressSpinner # NOQA
233 changes: 233 additions & 0 deletions platform/pulp/platform/models/progress.py
@@ -0,0 +1,233 @@
"""
Django models related to Progress Reporting
"""
from gettext import gettext as _
import logging

from django.db import models

from pulp.platform.models import Model, Task
from pulp.server.async.tasks import get_current_task_id


__all__ = [
'ProgressBar',
'ProgressSpinner',
]


_logger = logging.getLogger(__name__)


class ProgressReport(Model):
"""
A base model for all progress reporting.
All progress reports have a message, state, and are related to a Task.
Fields:
:cvar message: A short message for the progress update, typically shown to the user. (required)
:type message: models.TextField
:cvar state: The state of the progress update. Defaults to `WAITING`. This field uses a limited
set of choices of field states. See `STATES` for possible states.
:type state: models.TextField
:cvar total: The total count of items to be handled by the ProgressBar (required)
:type total: models.IntegerField
:cvar done: The count of items already processed. Defaults to 0.
:type done: models.IntegerField
Relations:
:cvar task: The task associated with this progress report. If left unset when save() is called
it will be set to the current task_id.
:type task: models.ForeignKey
"""
WAITING = 'waiting'
SKIPPED = 'skipped'
RUNNING = 'running'
COMPLETED = 'completed'
ERRORED = 'errored'
CANCELED = 'canceled'
STATES = (
(WAITING, 'Waiting'),
(SKIPPED, 'Skipped'),
(RUNNING, 'Running'),
(COMPLETED, 'Completed'),
(ERRORED, 'Errored'),
(CANCELED, 'Canceled')
)
message = models.TextField()
state = models.TextField(choices=STATES, default=WAITING)

total = models.IntegerField(null=True)
done = models.IntegerField(default=0)

task = models.ForeignKey("Task", on_delete=models.CASCADE)

def save(self, *args, **kwargs):
"""
Auto-set the task_id if running inside a task
If the task_id is already set it will not be updated. If it is unset and this is running
inside of a task it will be auto-set prior to saving.
:param args: positional arguments to be passed on to the real save
:type args: list
:param kwargs: keyword arguments to be passed on to the real save
:type kwargs: dict
"""
if self.task_id is None:
self.task_id = Task.objects.get(id=get_current_task_id())
super(ProgressReport, self).save(*args, **kwargs)


def __enter__(self):
"""
Saves the progress report state as RUNNING
"""
if self.state != self.RUNNING:
self.state = self.RUNNING
self.state.save()

def __exit__(self, type, value, traceback):
"""
Update the progress report state to COMPLETED or ERRORED.
If an exception occurs the progress report state is saved as ERRORED and the exception is
not suppressed. If the context manager exited without exception the progress report state
is saved as COMPLETED.
See the context manager documentation for more info on __exit__ parameters
"""
if type is None:
self.state = self.COMPLETED
self.save()
else:
self.state = self.ERRORED
self.save()


class ProgressSpinner(ProgressReport):
"""
Shows progress reporting when the count of items is not known.
Plugin writers should create these objects to show progress reporting of a single step or
aspect of work which has a name and a state. For example:
>>> ProgressSpinner(message='Publishing Metadata') # default state is 'waiting'
>>> ProgressSpinner(message='Publishing Metadata', state='running') # specify 'running'
Update the state to COMPLETED and save it:
>>> metadata_progress = ProgressSpinner(message='Publishing Metadata', state='running')
>>> metadata_progress.state = 'completed'
>>> metadata_progress.save()
The ProgressSpinner() is a context manager that provides automatic state transitions for the
RUNNING COMPLETED and ERRORED states. Use it as follows:
>>> spinner = ProgressSpinner('Publishing Metadata')
>>> with spinner:
>>> # spinner is at 'running'
>>> publish_metadata()
>>> # spinner is at 'completed' if no exception or 'errored' if an exception was raised
You can also use this short form:
>>> with ProgressSpinner('Publishing Metadata'):
>>> publish_metadata()
ProgressSpinner objects are associated with a Task and auto-discover and populate the task id
when saved.
"""

class Meta:
proxy = True


class ProgressBar(ProgressReport):
"""
Shows progress reporting when the count of items is known.
Plugin writers should create these objects to show progress reporting of a single step or
aspect of work which has a name and state along with total and done counts. For example:
>>> ProgressBar(message='Publishing files', total=23) # default: state='waiting' and done=0
>>> ProgressBar(message='Publishing files', total=23, state='running') # specify the state
>>> ProgressBar(message='Publishing files', total=23, done=16) # already completed 16
Update the state to COMPLETED and save it:
>>> progress_bar = ProgressBar('Publishing files', total=23, state='running')
>>> progress_bar.state = 'completed'
>>> progress_bar.save()
The ProgressBar() is a context manager that provides automatic state transitions for the RUNNING
COMPLETED and ERRORED states. The increment() method can be called in the loop as work is
completed. Use it as follows:
>>> progress_bar = ProgressBar(message='Publishing files', total=len(files_iterator))
>>> progress_bar.save()
>>> with progress_bar:
>>> # progress_bar now at 'running'
>>> for file in files_iterator:
>>> handle(file)
>>> progress_bar.increment() # increments and saves
>>> # progress_bar is at 'completed' if no exception or 'errored' if an exception was raised
A convenience method called iter() allows you to avoid calling increment() directly:
>>> progress_bar = ProgressBar(message='Publishing files', total=len(files_iterator))
>>> progress_bar.save()
>>> with progress_bar:
>>> for file in progress_bar.iter(files_iterator):
>>> handle(file)
You can also use this short form:
>>> with ProgressBar(message='Publishing files', total=len(files_iterator)):
>>> for file in progress_bar.iter(files_iterator):
>>> handle(file)
ProgressBar objects are associated with a Task and auto-discover and populate the task id when
saved.
"""

class Meta:
proxy = True

def increment(self):
"""
Increment done count and save the progress bar.
This will increment and save the self.done attribute which is useful to put into a loop
processing items.
"""
self.done += 1
if self.done > self.total:
_logger.warning(_('Too many items processed for ProgressBar %s') % self.message)
self.save()

def iter(self, iter):
"""
Iterate and automatically call increment().
>>> progress_bar = ProgressBar(message='Publishing files', total=23)
>>> progress_bar.save()
>>> for file in progress_bar.iter(files_iterator):
>>> handle(file)
:param iter: The iterator to loop through while incrementing
:type iter: iterator
:return: generator which yields items out of the argument iter
"""
for x in iter:
yield x
self.increment()

0 comments on commit 6bb7932

Please sign in to comment.