-
-
Notifications
You must be signed in to change notification settings - Fork 188
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into devel-2.0
- Loading branch information
Showing
6 changed files
with
199 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
from datetime import datetime | ||
from logging import Logger | ||
from threading import Timer | ||
from typing import Generic, TypeVar, Optional, Callable, Type, Any | ||
import contextlib | ||
|
||
import humanize | ||
|
||
T = TypeVar('T') | ||
|
||
|
||
@contextlib.contextmanager | ||
def progress_logger(items: int, | ||
interval_seconds: int, | ||
progress_template: str, | ||
finish_template: Optional[str], | ||
logger: Logger, | ||
progress_item_type: Type[T] = Any) -> Callable[[T], T]: | ||
progress = Progress[progress_item_type](items) | ||
plogger = ProgressLogger(progress, interval_seconds, progress_template, logger).start() | ||
try: | ||
yield progress.observe | ||
finally: | ||
plogger.finish(finish_template) | ||
|
||
|
||
class Progress(Generic[T]): | ||
def __init__(self, items: int): | ||
self.items = items | ||
self.observations = 0 | ||
|
||
def observe(self, observation: T) -> T: | ||
self.observations = self.observations + 1 | ||
return observation | ||
|
||
def get_progress(self) -> str: | ||
return f'{self.observations} of {self.items}' | ||
|
||
|
||
class ProgressLogger: | ||
def __init__(self, progress: Progress, interval_seconds: int, template: str, logger: Logger): | ||
self._progress = progress | ||
self._interval_seconds = interval_seconds | ||
self._template = template | ||
self._logger = logger | ||
|
||
self._start = None | ||
self._duration = None | ||
self._timer = self._get_progress_timer() | ||
|
||
def start(self) -> 'ProgressLogger': | ||
self._start = datetime.utcnow() | ||
self._timer.start() | ||
return self | ||
|
||
def finish(self, template: Optional[str] = None): | ||
self._duration = datetime.utcnow() - self._start | ||
self._start = None | ||
self._timer.cancel() | ||
|
||
if template: | ||
self._logger.info(template.format(items=self._progress.items, | ||
observations=self._progress.observations, | ||
duration=self.duration)) | ||
|
||
@property | ||
def duration(self) -> str: | ||
return humanize.precisedelta(self._duration) | ||
|
||
def _get_progress_timer(self): | ||
timer = Timer(self._interval_seconds, self._log_progress) | ||
timer.setDaemon(daemonic=True) | ||
return timer | ||
|
||
def _log_progress(self): | ||
if self._start is None: | ||
return | ||
|
||
delta = datetime.utcnow() - self._start | ||
self._logger.info(self._template.format(progress=self._progress.get_progress(), time=humanize.precisedelta(delta))) | ||
self._timer = self._get_progress_timer() | ||
self._timer.start() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,9 @@ | ||
# dataclasses does not exist before Python3.7, needed as dependency | ||
dataclasses;python_version<"3.7" | ||
humanize==3.14.0 | ||
junitparser==2.5.0 | ||
lxml==4.8.0 | ||
psutil==5.9.1 | ||
PyGithub==1.55 | ||
urllib3==1.26.9 | ||
requests==2.27.1 | ||
lxml==4.8.0 | ||
urllib3==1.26.9 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import unittest | ||
from datetime import datetime, timezone | ||
|
||
import mock | ||
|
||
from publish.progress import Progress, ProgressLogger | ||
|
||
|
||
class TestProgress(unittest.TestCase): | ||
def test_get_progress(self): | ||
progress = Progress(10) | ||
self.assertEqual('0 of 10', progress.get_progress()) | ||
self.assertEqual('0 of 10', progress.get_progress()) | ||
self.assertEqual('item', progress.observe('item')) | ||
self.assertEqual('1 of 10', progress.get_progress()) | ||
self.assertEqual('1 of 10', progress.get_progress()) | ||
self.assertEqual(1, progress.observe(1)) | ||
self.assertEqual('2 of 10', progress.get_progress()) | ||
self.assertEqual('2 of 10', progress.get_progress()) | ||
self.assertEqual(1.2, progress.observe(1.2)) | ||
self.assertEqual('3 of 10', progress.get_progress()) | ||
self.assertEqual('3 of 10', progress.get_progress()) | ||
obj = object() | ||
self.assertEqual(obj, progress.observe(obj)) | ||
self.assertEqual('4 of 10', progress.get_progress()) | ||
self.assertEqual('4 of 10', progress.get_progress()) | ||
|
||
|
||
class TestProgressLogger(unittest.TestCase): | ||
def test(self): | ||
progress = Progress(10) | ||
logger = mock.MagicMock(info=mock.Mock()) | ||
plogger = ProgressLogger(progress, 60, 'progress: {progress} in {time}', logger) | ||
try: | ||
ts = datetime(2022, 6, 1, 12, 34, 56, tzinfo=timezone.utc) | ||
with mock.patch('publish.progress.datetime', utcnow=mock.Mock(return_value=ts)): | ||
plogger.start() | ||
logger.info.assert_not_called() | ||
|
||
progress.observe('item') | ||
logger.info.assert_not_called() | ||
|
||
ts = datetime(2022, 6, 1, 12, 35, 00, tzinfo=timezone.utc) | ||
with mock.patch('publish.progress.datetime', utcnow=mock.Mock(return_value=ts)): | ||
plogger._log_progress() | ||
self.assertEqual([mock.call('progress: 1 of 10 in 4 seconds')], logger.info.call_args_list) | ||
logger.info.reset_mock() | ||
|
||
progress.observe('item') | ||
progress.observe('item') | ||
logger.info.assert_not_called() | ||
|
||
ts = datetime(2022, 6, 1, 12, 40, 00, tzinfo=timezone.utc) | ||
with mock.patch('publish.progress.datetime', utcnow=mock.Mock(return_value=ts)): | ||
plogger._log_progress() | ||
self.assertEqual([mock.call('progress: 3 of 10 in 5 minutes and 4 seconds')], logger.info.call_args_list) | ||
logger.info.reset_mock() | ||
finally: | ||
ts = datetime(2022, 6, 1, 12, 41, 23, tzinfo=timezone.utc) | ||
with mock.patch('publish.progress.datetime', utcnow=mock.Mock(return_value=ts)): | ||
plogger.finish('finished: {observations} of {items} in {duration}') | ||
self.assertEqual([mock.call('finished: 3 of 10 in 6 minutes and 27 seconds')], logger.info.call_args_list) | ||
logger.info.reset_mock() | ||
|
||
self.assertEqual('6 minutes and 27 seconds', plogger.duration) |