././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1625483600.9365168 celery-progress-0.1.1/0000775000175000017500000000000000000000000014577 5ustar00czueczue00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1625482152.0 celery-progress-0.1.1/LICENSE0000664000175000017500000000205100000000000015602 0ustar00czueczue00000000000000MIT License Copyright (c) 2018 Cory Zue Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1625482152.0 celery-progress-0.1.1/MANIFEST.in0000664000175000017500000000014600000000000016336 0ustar00czueczue00000000000000include LICENSE include README.md recursive-include celery_progress/static * recursive-include docs * ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1625483600.9365168 celery-progress-0.1.1/PKG-INFO0000664000175000017500000002011500000000000015673 0ustar00czueczue00000000000000Metadata-Version: 2.1 Name: celery-progress Version: 0.1.1 Summary: Drop in, configurable, dependency-free progress bars for your Django/Celery applications. Home-page: https://github.com/czue/celery-progress Author: Cory Zue Author-email: cory@coryzue.com License: MIT License Platform: UNKNOWN Classifier: Environment :: Web Environment Classifier: Framework :: Django Classifier: Framework :: Django :: 1.11 Classifier: Framework :: Django :: 2.0 Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Description-Content-Type: text/markdown Provides-Extra: websockets Provides-Extra: redis Provides-Extra: rabbitmq License-File: LICENSE # Celery Progress Bars for Django Drop in, dependency-free progress bars for your Django/Celery applications. Super simple setup. Lots of customization available. ## Demo [Celery Progress Bar demo on Build With Django](https://buildwithdjango.com/projects/celery-progress/) ### Github demo application: build a download progress bar for Django Starting with Celery can be challenging, [eeintech](https://github.com/eeintech) built a complete [Django demo application](https://github.com/eeintech/django-celery-progress-demo) along with a [step-by-step guide](https://eeinte.ch/stream/progress-bar-django-using-celery/) to get you started on building your own progress bar! ## Installation If you haven't already, make sure you have properly [set up celery in your project](https://docs.celeryproject.org/en/stable/getting-started/first-steps-with-celery.html#first-steps). Then install this library: ```bash pip install celery-progress ``` ## Usage ### Prerequisites First add `celery_progress` to your `INSTALLED_APPS` in `settings.py`. Then add the following url config to your main `urls.py`: ```python from django.urls import re_path, include re_path(r'^celery-progress/', include('celery_progress.urls')), # the endpoint is configurable ``` ### Recording Progress In your task you should add something like this: ```python from celery import shared_task from celery_progress.backend import ProgressRecorder import time @shared_task(bind=True) def my_task(self, seconds): progress_recorder = ProgressRecorder(self) result = 0 for i in range(seconds): time.sleep(1) result += i progress_recorder.set_progress(i + 1, seconds) return result ``` You can add an optional progress description like this: ```python progress_recorder.set_progress(i + 1, seconds, description='my progress description') ``` ### Displaying progress In the view where you call the task you need to get the task ID like so: **views.py** ```python def progress_view(request): result = my_task.delay(10) return render(request, 'display_progress.html', context={'task_id': result.task_id}) ``` Then in the page you want to show the progress bar you just do the following. #### Add the following HTML wherever you want your progress bar to appear: **display_progress.html** ```html
 
Waiting for progress to start...
``` #### Import the javascript file. **display_progress.html** ```html ``` #### Initialize the progress bar: ```javascript // vanilla JS version document.addEventListener("DOMContentLoaded", function () { var progressUrl = "{% url 'celery_progress:task_status' task_id %}"; CeleryProgressBar.initProgressBar(progressUrl); }); ``` or ```javascript // JQuery $(function () { var progressUrl = "{% url 'celery_progress:task_status' task_id %}"; CeleryProgressBar.initProgressBar(progressUrl) }); ``` ### Displaying the result of a task If you'd like you can also display the result of your task on the front end. To do that follow the steps below. Result handling can also be customized. #### Initialize the result block: This is all that's needed to render the result on the page. **display_progress.html** ```html
``` But more likely you will want to customize how the result looks, which can be done as below: ```javascript // JQuery var progressUrl = "{% url 'celery_progress:task_status' task_id %}"; function customResult(resultElement, result) { $( resultElement ).append( $('

').text('Sum of all seconds is ' + result) ); } $(function () { CeleryProgressBar.initProgressBar(progressUrl, { onResult: customResult, }) }); ``` ## Customization The `initProgressBar` function takes an optional object of options. The following options are supported: | Option | What it does | Default Value | |--------|--------------|---------------| | pollInterval | How frequently to poll for progress (in milliseconds) | 500 | | progressBarId | Override the ID used for the progress bar | 'progress-bar' | | progressBarMessageId | Override the ID used for the progress bar message | 'progress-bar-message' | | progressBarElement | Override the *element* used for the progress bar. If specified, progressBarId will be ignored. | document.getElementById(progressBarId) | | progressBarMessageElement | Override the *element* used for the progress bar message. If specified, progressBarMessageId will be ignored. | document.getElementById(progressBarMessageId) | | resultElementId | Override the ID used for the result | 'celery-result' | | resultElement | Override the *element* used for the result. If specified, resultElementId will be ignored. | document.getElementById(resultElementId) | | onProgress | function to call when progress is updated | onProgressDefault | | onSuccess | function to call when progress successfully completes | onSuccessDefault | | onError | function to call on a known error with no specified handler | onErrorDefault | | onRetry | function to call when a task attempts to retry | onRetryDefault | | onIgnored | function to call when a task result is ignored | onIgnoredDefault | | onTaskError | function to call when progress completes with an error | onError | | onNetworkError | function to call on a network error (ignored by WebSocket) | onError | | onHttpError | function to call on a non-200 response (ignored by WebSocket) | onError | | onDataError | function to call on a response that's not JSON or has invalid schema due to a programming error | onError | | onResult | function to call when returned non empty result | CeleryProgressBar.onResultDefault | | barColors | dictionary containing color values for various progress bar states. Colors that are not specified will defer to defaults | barColorsDefault | The `barColors` option allows you to customize the color of each progress bar state by passing a dictionary of key-value pairs of `state: #hexcode`. The defaults are shown below. | State | Hex Code | Image Color | |-------|----------|:-------------:| | success | #76ce60 | ![#76ce60](https://placehold.it/15/76ce60/000000?text=+) | | error | #dc4f63 | ![#dc4f63](https://placehold.it/15/dc4f63/000000?text=+) | | progress | #68a9ef | ![#68a9ef](https://placehold.it/15/68a9ef/000000?text=+) | | ignored | #7a7a7a | ![#7a7a7a](https://placehold.it/15/7a7a7a/000000?text=+) | # WebSocket Support Additionally, this library offers WebSocket support using [Django Channels](https://channels.readthedocs.io/en/latest/) courtesy of [EJH2](https://github.com/EJH2/). A working example project leveraging WebSockets is [available here](https://github.com/EJH2/cp_ws-example). To use WebSockets, install with `pip install celery-progress[websockets,redis]` or `pip install celery-progress[websockets,rabbitmq]` (depending on broker dependencies). See `WebSocketProgressRecorder` and `websockets.js` for details. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1625482152.0 celery-progress-0.1.1/README.md0000664000175000017500000001574600000000000016073 0ustar00czueczue00000000000000# Celery Progress Bars for Django Drop in, dependency-free progress bars for your Django/Celery applications. Super simple setup. Lots of customization available. ## Demo [Celery Progress Bar demo on Build With Django](https://buildwithdjango.com/projects/celery-progress/) ### Github demo application: build a download progress bar for Django Starting with Celery can be challenging, [eeintech](https://github.com/eeintech) built a complete [Django demo application](https://github.com/eeintech/django-celery-progress-demo) along with a [step-by-step guide](https://eeinte.ch/stream/progress-bar-django-using-celery/) to get you started on building your own progress bar! ## Installation If you haven't already, make sure you have properly [set up celery in your project](https://docs.celeryproject.org/en/stable/getting-started/first-steps-with-celery.html#first-steps). Then install this library: ```bash pip install celery-progress ``` ## Usage ### Prerequisites First add `celery_progress` to your `INSTALLED_APPS` in `settings.py`. Then add the following url config to your main `urls.py`: ```python from django.urls import re_path, include re_path(r'^celery-progress/', include('celery_progress.urls')), # the endpoint is configurable ``` ### Recording Progress In your task you should add something like this: ```python from celery import shared_task from celery_progress.backend import ProgressRecorder import time @shared_task(bind=True) def my_task(self, seconds): progress_recorder = ProgressRecorder(self) result = 0 for i in range(seconds): time.sleep(1) result += i progress_recorder.set_progress(i + 1, seconds) return result ``` You can add an optional progress description like this: ```python progress_recorder.set_progress(i + 1, seconds, description='my progress description') ``` ### Displaying progress In the view where you call the task you need to get the task ID like so: **views.py** ```python def progress_view(request): result = my_task.delay(10) return render(request, 'display_progress.html', context={'task_id': result.task_id}) ``` Then in the page you want to show the progress bar you just do the following. #### Add the following HTML wherever you want your progress bar to appear: **display_progress.html** ```html

 
Waiting for progress to start...
``` #### Import the javascript file. **display_progress.html** ```html ``` #### Initialize the progress bar: ```javascript // vanilla JS version document.addEventListener("DOMContentLoaded", function () { var progressUrl = "{% url 'celery_progress:task_status' task_id %}"; CeleryProgressBar.initProgressBar(progressUrl); }); ``` or ```javascript // JQuery $(function () { var progressUrl = "{% url 'celery_progress:task_status' task_id %}"; CeleryProgressBar.initProgressBar(progressUrl) }); ``` ### Displaying the result of a task If you'd like you can also display the result of your task on the front end. To do that follow the steps below. Result handling can also be customized. #### Initialize the result block: This is all that's needed to render the result on the page. **display_progress.html** ```html
``` But more likely you will want to customize how the result looks, which can be done as below: ```javascript // JQuery var progressUrl = "{% url 'celery_progress:task_status' task_id %}"; function customResult(resultElement, result) { $( resultElement ).append( $('

').text('Sum of all seconds is ' + result) ); } $(function () { CeleryProgressBar.initProgressBar(progressUrl, { onResult: customResult, }) }); ``` ## Customization The `initProgressBar` function takes an optional object of options. The following options are supported: | Option | What it does | Default Value | |--------|--------------|---------------| | pollInterval | How frequently to poll for progress (in milliseconds) | 500 | | progressBarId | Override the ID used for the progress bar | 'progress-bar' | | progressBarMessageId | Override the ID used for the progress bar message | 'progress-bar-message' | | progressBarElement | Override the *element* used for the progress bar. If specified, progressBarId will be ignored. | document.getElementById(progressBarId) | | progressBarMessageElement | Override the *element* used for the progress bar message. If specified, progressBarMessageId will be ignored. | document.getElementById(progressBarMessageId) | | resultElementId | Override the ID used for the result | 'celery-result' | | resultElement | Override the *element* used for the result. If specified, resultElementId will be ignored. | document.getElementById(resultElementId) | | onProgress | function to call when progress is updated | onProgressDefault | | onSuccess | function to call when progress successfully completes | onSuccessDefault | | onError | function to call on a known error with no specified handler | onErrorDefault | | onRetry | function to call when a task attempts to retry | onRetryDefault | | onIgnored | function to call when a task result is ignored | onIgnoredDefault | | onTaskError | function to call when progress completes with an error | onError | | onNetworkError | function to call on a network error (ignored by WebSocket) | onError | | onHttpError | function to call on a non-200 response (ignored by WebSocket) | onError | | onDataError | function to call on a response that's not JSON or has invalid schema due to a programming error | onError | | onResult | function to call when returned non empty result | CeleryProgressBar.onResultDefault | | barColors | dictionary containing color values for various progress bar states. Colors that are not specified will defer to defaults | barColorsDefault | The `barColors` option allows you to customize the color of each progress bar state by passing a dictionary of key-value pairs of `state: #hexcode`. The defaults are shown below. | State | Hex Code | Image Color | |-------|----------|:-------------:| | success | #76ce60 | ![#76ce60](https://placehold.it/15/76ce60/000000?text=+) | | error | #dc4f63 | ![#dc4f63](https://placehold.it/15/dc4f63/000000?text=+) | | progress | #68a9ef | ![#68a9ef](https://placehold.it/15/68a9ef/000000?text=+) | | ignored | #7a7a7a | ![#7a7a7a](https://placehold.it/15/7a7a7a/000000?text=+) | # WebSocket Support Additionally, this library offers WebSocket support using [Django Channels](https://channels.readthedocs.io/en/latest/) courtesy of [EJH2](https://github.com/EJH2/). A working example project leveraging WebSockets is [available here](https://github.com/EJH2/cp_ws-example). To use WebSockets, install with `pip install celery-progress[websockets,redis]` or `pip install celery-progress[websockets,rabbitmq]` (depending on broker dependencies). See `WebSocketProgressRecorder` and `websockets.js` for details. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1625483600.9365168 celery-progress-0.1.1/celery_progress/0000775000175000017500000000000000000000000020006 5ustar00czueczue00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1625482152.0 celery-progress-0.1.1/celery_progress/__init__.py0000664000175000017500000000000000000000000022105 0ustar00czueczue00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1625482152.0 celery-progress-0.1.1/celery_progress/backend.py0000664000175000017500000001114000000000000021744 0ustar00czueczue00000000000000import datetime import logging from abc import ABCMeta, abstractmethod from decimal import Decimal from celery.result import EagerResult, allow_join_result from celery.backends.base import DisabledBackend logger = logging.getLogger(__name__) PROGRESS_STATE = 'PROGRESS' class AbstractProgressRecorder(object): __metaclass__ = ABCMeta @abstractmethod def set_progress(self, current, total, description=""): pass class ConsoleProgressRecorder(AbstractProgressRecorder): def set_progress(self, current, total, description=""): print('processed {} items of {}. {}'.format(current, total, description)) class ProgressRecorder(AbstractProgressRecorder): def __init__(self, task): self.task = task def set_progress(self, current, total, description=""): percent = 0 if total > 0: percent = (Decimal(current) / Decimal(total)) * Decimal(100) percent = float(round(percent, 2)) state = PROGRESS_STATE meta = { 'pending': False, 'current': current, 'total': total, 'percent': percent, 'description': description } self.task.update_state( state=state, meta=meta ) return state, meta class Progress(object): def __init__(self, result): """ result: an AsyncResult or an object that mimics it to a degree """ self.result = result def get_info(self): response = {'state': self.result.state} if self.result.state in ['SUCCESS', 'FAILURE']: success = self.result.successful() with allow_join_result(): response.update({ 'complete': True, 'success': success, 'progress': _get_completed_progress(), 'result': self.result.get(self.result.id) if success else str(self.result.info), }) elif self.result.state in ['RETRY', 'REVOKED']: if self.result.state == 'RETRY': retry = self.result.info when = str(retry.when) if isinstance(retry.when, datetime.datetime) else str( datetime.datetime.now() + datetime.timedelta(seconds=retry.when)) result = {'when': when, 'message': retry.message or str(retry.exc)} else: result = 'Task ' + str(self.result.info) response.update({ 'complete': True, 'success': False, 'progress': _get_completed_progress(), 'result': result, }) elif self.result.state == 'IGNORED': response.update({ 'complete': True, 'success': None, 'progress': _get_completed_progress(), 'result': str(self.result.info) }) elif self.result.state == PROGRESS_STATE: response.update({ 'complete': False, 'success': None, 'progress': self.result.info, }) elif self.result.state in ['PENDING', 'STARTED']: response.update({ 'complete': False, 'success': None, 'progress': _get_unknown_progress(self.result.state), }) else: logger.error('Task %s has unknown state %s with metadata %s', self.result.id, self.result.state, self.result.info) response.update({ 'complete': True, 'success': False, 'progress': _get_unknown_progress(self.result.state), 'result': 'Unknown state {}'.format(self.result.state), }) return response class KnownResult(EagerResult): """Like EagerResult but supports non-ready states.""" def __init__(self, id, ret_value, state, traceback=None): """ ret_value: result, exception, or progress metadata """ # set backend to get state groups (like READY_STATES in ready()) self.backend = DisabledBackend super().__init__(id, ret_value, state, traceback) def ready(self): return super(EagerResult, self).ready() def __del__(self): # throws an exception if not overridden pass def _get_completed_progress(): return { 'pending': False, 'current': 100, 'total': 100, 'percent': 100, } def _get_unknown_progress(state): return { 'pending': state == 'PENDING', 'current': 0, 'total': 100, 'percent': 0, } ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1625483600.9325168 celery-progress-0.1.1/celery_progress/static/0000775000175000017500000000000000000000000021275 5ustar00czueczue00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1625483600.9365168 celery-progress-0.1.1/celery_progress/static/celery_progress/0000775000175000017500000000000000000000000024504 5ustar00czueczue00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1625482152.0 celery-progress-0.1.1/celery_progress/static/celery_progress/celery_progress.js0000664000175000017500000001567100000000000030263 0ustar00czueczue00000000000000class CeleryProgressBar { constructor(progressUrl, options) { this.progressUrl = progressUrl; options = options || {}; let progressBarId = options.progressBarId || 'progress-bar'; let progressBarMessage = options.progressBarMessageId || 'progress-bar-message'; this.progressBarElement = options.progressBarElement || document.getElementById(progressBarId); this.progressBarMessageElement = options.progressBarMessageElement || document.getElementById(progressBarMessage); this.onProgress = options.onProgress || this.onProgressDefault; this.onSuccess = options.onSuccess || this.onSuccessDefault; this.onError = options.onError || this.onErrorDefault; this.onTaskError = options.onTaskError || this.onTaskErrorDefault; this.onDataError = options.onDataError || this.onError; this.onRetry = options.onRetry || this.onRetryDefault; this.onIgnored = options.onIgnored || this.onIgnoredDefault; let resultElementId = options.resultElementId || 'celery-result'; this.resultElement = options.resultElement || document.getElementById(resultElementId); this.onResult = options.onResult || this.onResultDefault; // HTTP options this.onNetworkError = options.onNetworkError || this.onError; this.onHttpError = options.onHttpError || this.onError; this.pollInterval = options.pollInterval || 500; // Other options let barColorsDefault = { success: '#76ce60', error: '#dc4f63', progress: '#68a9ef', ignored: '#7a7a7a' } this.barColors = Object.assign({}, barColorsDefault, options.barColors); } onSuccessDefault(progressBarElement, progressBarMessageElement, result) { result = this.getMessageDetails(result); progressBarElement.style.backgroundColor = this.barColors.success; progressBarMessageElement.textContent = "Success! " + result; } onResultDefault(resultElement, result) { if (resultElement) { resultElement.textContent = result; } } /** * Default handler for all errors. * @param data - A Response object for HTTP errors, undefined for other errors */ onErrorDefault(progressBarElement, progressBarMessageElement, excMessage, data) { progressBarElement.style.backgroundColor = this.barColors.error; excMessage = excMessage || ''; progressBarMessageElement.textContent = "Uh-Oh, something went wrong! " + excMessage; } onTaskErrorDefault(progressBarElement, progressBarMessageElement, excMessage) { let message = this.getMessageDetails(excMessage); this.onError(progressBarElement, progressBarMessageElement, message); } onRetryDefault(progressBarElement, progressBarMessageElement, excMessage, retryWhen) { retryWhen = new Date(retryWhen); let message = 'Retrying in ' + Math.round((retryWhen.getTime() - Date.now())/1000) + 's: ' + excMessage; this.onError(progressBarElement, progressBarMessageElement, message); } onIgnoredDefault(progressBarElement, progressBarMessageElement, result) { progressBarElement.style.backgroundColor = this.barColors.ignored; progressBarMessageElement.textContent = result || 'Task result ignored!' } onProgressDefault(progressBarElement, progressBarMessageElement, progress) { progressBarElement.style.backgroundColor = this.barColors.progress; progressBarElement.style.width = progress.percent + "%"; var description = progress.description || ""; if (progress.current == 0) { if (progress.pending === true) { progressBarMessageElement.textContent = 'Waiting for task to start...'; } else { progressBarMessageElement.textContent = 'Task started...'; } } else { progressBarMessageElement.textContent = progress.current + ' of ' + progress.total + ' processed. ' + description; } } getMessageDetails(result) { if (this.resultElement) { return '' } else { return result || ''; } } /** * Process update message data. * @return true if the task is complete, false if it's not, undefined if `data` is invalid */ onData(data) { let done = false; if (data.progress) { this.onProgress(this.progressBarElement, this.progressBarMessageElement, data.progress); } if (data.complete === true) { done = true; if (data.success === true) { this.onSuccess(this.progressBarElement, this.progressBarMessageElement, data.result); } else if (data.success === false) { if (data.state === 'RETRY') { this.onRetry(this.progressBarElement, this.progressBarMessageElement, data.result.message, data.result.when); done = false; delete data.result; } else { this.onTaskError(this.progressBarElement, this.progressBarMessageElement, data.result); } } else { if (data.state === 'IGNORED') { this.onIgnored(this.progressBarElement, this.progressBarMessageElement, data.result); delete data.result; } else { done = undefined; this.onDataError(this.progressBarElement, this.progressBarMessageElement, "Data Error"); } } if (data.hasOwnProperty('result')) { this.onResult(this.resultElement, data.result); } } else if (data.complete === undefined) { done = undefined; this.onDataError(this.progressBarElement, this.progressBarMessageElement, "Data Error"); } return done; } async connect() { let response; try { response = await fetch(this.progressUrl); } catch (networkError) { this.onNetworkError(this.progressBarElement, this.progressBarMessageElement, "Network Error"); throw networkError; } if (response.status === 200) { let data; try { data = await response.json(); } catch (parsingError) { this.onDataError(this.progressBarElement, this.progressBarMessageElement, "Parsing Error") throw parsingError; } const complete = this.onData(data); if (complete === false) { setTimeout(this.connect.bind(this), this.pollInterval); } } else { this.onHttpError(this.progressBarElement, this.progressBarMessageElement, "HTTP Code " + response.status, response); } } static initProgressBar(progressUrl, options) { const bar = new this(progressUrl, options); bar.connect(); } } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1625482152.0 celery-progress-0.1.1/celery_progress/static/celery_progress/websockets.js0000664000175000017500000000176600000000000027225 0ustar00czueczue00000000000000class CeleryWebSocketProgressBar extends CeleryProgressBar { constructor(progressUrl, options) { super(progressUrl, options); } async connect() { var ProgressSocket = new WebSocket((location.protocol === 'https:' ? 'wss' : 'ws') + '://' + window.location.host + this.progressUrl); ProgressSocket.onopen = function (event) { ProgressSocket.send(JSON.stringify({'type': 'check_task_completion'})); }; const bar = this; ProgressSocket.onmessage = function (event) { let data; try { data = JSON.parse(event.data); } catch (parsingError) { bar.onDataError(bar.progressBarElement, bar.progressBarMessageElement, "Parsing Error") throw parsingError; } const complete = bar.onData(data); if (complete === true || complete === undefined) { ProgressSocket.close(); } }; } } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1625482152.0 celery-progress-0.1.1/celery_progress/tasks.py0000664000175000017500000000067100000000000021511 0ustar00czueczue00000000000000from celery.signals import task_postrun @task_postrun.connect(retry=True) def task_postrun_handler(**kwargs): """Runs after a task has finished. This will update the result backend to include the IGNORED result state. Necessary for HTTP to properly receive ignored task event.""" if kwargs.pop('state') == 'IGNORED': task = kwargs.pop('task') task.update_state(state='IGNORED', meta=str(kwargs.pop('retval'))) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1625482152.0 celery-progress-0.1.1/celery_progress/urls.py0000664000175000017500000000026300000000000021346 0ustar00czueczue00000000000000from django.urls import re_path from . import views app_name = 'celery_progress' urlpatterns = [ re_path(r'^(?P[\w-]+)/$', views.get_progress, name='task_status') ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1625483252.0 celery-progress-0.1.1/celery_progress/views.py0000664000175000017500000000056400000000000021522 0ustar00czueczue00000000000000import json from django.http import HttpResponse from celery.result import AsyncResult from celery_progress.backend import Progress from django.views.decorators.cache import never_cache @never_cache def get_progress(request, task_id): progress = Progress(AsyncResult(task_id)) return HttpResponse(json.dumps(progress.get_info()), content_type='application/json') ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1625483600.9365168 celery-progress-0.1.1/celery_progress/websockets/0000775000175000017500000000000000000000000022157 5ustar00czueczue00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1625482152.0 celery-progress-0.1.1/celery_progress/websockets/__init__.py0000664000175000017500000000000000000000000024256 0ustar00czueczue00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1625482152.0 celery-progress-0.1.1/celery_progress/websockets/backend.py0000664000175000017500000000320000000000000024113 0ustar00czueczue00000000000000import logging from celery_progress.backend import ProgressRecorder, Progress, KnownResult try: from asgiref.sync import async_to_sync from channels.layers import get_channel_layer except ImportError: channel_layer = None else: channel_layer = get_channel_layer() logger = logging.getLogger(__name__) class WebSocketProgressRecorder(ProgressRecorder): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if not channel_layer: logger.warning( 'Tried to use websocket progress bar, but dependencies were not installed / configured. ' 'Use pip install celery-progress[websockets] and set up channels to enable this feature. ' 'See: https://channels.readthedocs.io/en/latest/ for more details.' ) @staticmethod def push_update(task_id, data, final=False): try: async_to_sync(channel_layer.group_send)( task_id, {'type': 'update_task_progress', 'data': data} ) except AttributeError: # No channel layer to send to, so ignore it pass except RuntimeError as e: # We're sending messages too fast for asgiref to handle, drop it if final and channel_layer: # Send error back to post-run handler for a retry raise e def set_progress(self, current, total, description=""): state, meta = super().set_progress(current, total, description) result = KnownResult(self.task.request.id, meta, state) data = Progress(result).get_info() self.push_update(self.task.request.id, data) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1625482152.0 celery-progress-0.1.1/celery_progress/websockets/consumers.py0000664000175000017500000000222500000000000024550 0ustar00czueczue00000000000000from channels.generic.websocket import AsyncWebsocketConsumer import json from celery.result import AsyncResult from celery_progress.backend import Progress class ProgressConsumer(AsyncWebsocketConsumer): async def connect(self): self.task_id = self.scope['url_route']['kwargs']['task_id'] await self.channel_layer.group_add( self.task_id, self.channel_name ) await self.accept() async def disconnect(self, close_code): await self.channel_layer.group_discard( self.task_id, self.channel_name ) async def receive(self, text_data): text_data_json = json.loads(text_data) task_type = text_data_json['type'] if task_type == 'check_task_completion': await self.channel_layer.group_send( self.task_id, { 'type': 'update_task_progress', 'data': Progress(AsyncResult(self.task_id)).get_info() } ) async def update_task_progress(self, event): data = event['data'] await self.send(text_data=json.dumps(data)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1625482152.0 celery-progress-0.1.1/celery_progress/websockets/routing.py0000664000175000017500000000065500000000000024226 0ustar00czueczue00000000000000from django.conf.urls import url from celery_progress.websockets import consumers try: progress_consumer = consumers.ProgressConsumer.as_asgi() # New in Channels 3, works similar to Django's .as_view() except AttributeError: progress_consumer = consumers.ProgressConsumer # Channels 3 not installed, revert to Channels 2 behavior urlpatterns = [ url(r'^ws/progress/(?P[\w-]+)/?$', progress_consumer), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1625482152.0 celery-progress-0.1.1/celery_progress/websockets/tasks.py0000664000175000017500000000230600000000000023657 0ustar00czueczue00000000000000from celery.signals import task_postrun, task_revoked from .backend import WebSocketProgressRecorder from celery_progress.backend import KnownResult, Progress @task_postrun.connect(retry=True) def task_postrun_handler(task_id, **kwargs): """Runs after a task has finished. This will be used to push a websocket update for completed events. If the websockets version of this package is not installed, this will fail silently.""" result = KnownResult(task_id, kwargs.pop('retval'), kwargs.pop('state')) data = Progress(result).get_info() WebSocketProgressRecorder.push_update(task_id, data=data, final=True) @task_revoked.connect(retry=True) def task_revoked_handler(request, **kwargs): """Runs if a task has been revoked. This will be used to push a websocket update for revoked events. If the websockets version of this package is not installed, this will fail silently.""" _result = ('terminated' if kwargs.pop('terminated') else None) or ('expired' if kwargs.pop('expired') else None) \ or 'revoked' result = KnownResult(request.id, _result, 'REVOKED') data = Progress(result).get_info() WebSocketProgressRecorder.push_update(request.id, data=data, final=True) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1625483600.9365168 celery-progress-0.1.1/celery_progress.egg-info/0000775000175000017500000000000000000000000021500 5ustar00czueczue00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1625483600.0 celery-progress-0.1.1/celery_progress.egg-info/PKG-INFO0000664000175000017500000002011500000000000022574 0ustar00czueczue00000000000000Metadata-Version: 2.1 Name: celery-progress Version: 0.1.1 Summary: Drop in, configurable, dependency-free progress bars for your Django/Celery applications. Home-page: https://github.com/czue/celery-progress Author: Cory Zue Author-email: cory@coryzue.com License: MIT License Platform: UNKNOWN Classifier: Environment :: Web Environment Classifier: Framework :: Django Classifier: Framework :: Django :: 1.11 Classifier: Framework :: Django :: 2.0 Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Description-Content-Type: text/markdown Provides-Extra: websockets Provides-Extra: redis Provides-Extra: rabbitmq License-File: LICENSE # Celery Progress Bars for Django Drop in, dependency-free progress bars for your Django/Celery applications. Super simple setup. Lots of customization available. ## Demo [Celery Progress Bar demo on Build With Django](https://buildwithdjango.com/projects/celery-progress/) ### Github demo application: build a download progress bar for Django Starting with Celery can be challenging, [eeintech](https://github.com/eeintech) built a complete [Django demo application](https://github.com/eeintech/django-celery-progress-demo) along with a [step-by-step guide](https://eeinte.ch/stream/progress-bar-django-using-celery/) to get you started on building your own progress bar! ## Installation If you haven't already, make sure you have properly [set up celery in your project](https://docs.celeryproject.org/en/stable/getting-started/first-steps-with-celery.html#first-steps). Then install this library: ```bash pip install celery-progress ``` ## Usage ### Prerequisites First add `celery_progress` to your `INSTALLED_APPS` in `settings.py`. Then add the following url config to your main `urls.py`: ```python from django.urls import re_path, include re_path(r'^celery-progress/', include('celery_progress.urls')), # the endpoint is configurable ``` ### Recording Progress In your task you should add something like this: ```python from celery import shared_task from celery_progress.backend import ProgressRecorder import time @shared_task(bind=True) def my_task(self, seconds): progress_recorder = ProgressRecorder(self) result = 0 for i in range(seconds): time.sleep(1) result += i progress_recorder.set_progress(i + 1, seconds) return result ``` You can add an optional progress description like this: ```python progress_recorder.set_progress(i + 1, seconds, description='my progress description') ``` ### Displaying progress In the view where you call the task you need to get the task ID like so: **views.py** ```python def progress_view(request): result = my_task.delay(10) return render(request, 'display_progress.html', context={'task_id': result.task_id}) ``` Then in the page you want to show the progress bar you just do the following. #### Add the following HTML wherever you want your progress bar to appear: **display_progress.html** ```html

 
Waiting for progress to start...
``` #### Import the javascript file. **display_progress.html** ```html ``` #### Initialize the progress bar: ```javascript // vanilla JS version document.addEventListener("DOMContentLoaded", function () { var progressUrl = "{% url 'celery_progress:task_status' task_id %}"; CeleryProgressBar.initProgressBar(progressUrl); }); ``` or ```javascript // JQuery $(function () { var progressUrl = "{% url 'celery_progress:task_status' task_id %}"; CeleryProgressBar.initProgressBar(progressUrl) }); ``` ### Displaying the result of a task If you'd like you can also display the result of your task on the front end. To do that follow the steps below. Result handling can also be customized. #### Initialize the result block: This is all that's needed to render the result on the page. **display_progress.html** ```html
``` But more likely you will want to customize how the result looks, which can be done as below: ```javascript // JQuery var progressUrl = "{% url 'celery_progress:task_status' task_id %}"; function customResult(resultElement, result) { $( resultElement ).append( $('

').text('Sum of all seconds is ' + result) ); } $(function () { CeleryProgressBar.initProgressBar(progressUrl, { onResult: customResult, }) }); ``` ## Customization The `initProgressBar` function takes an optional object of options. The following options are supported: | Option | What it does | Default Value | |--------|--------------|---------------| | pollInterval | How frequently to poll for progress (in milliseconds) | 500 | | progressBarId | Override the ID used for the progress bar | 'progress-bar' | | progressBarMessageId | Override the ID used for the progress bar message | 'progress-bar-message' | | progressBarElement | Override the *element* used for the progress bar. If specified, progressBarId will be ignored. | document.getElementById(progressBarId) | | progressBarMessageElement | Override the *element* used for the progress bar message. If specified, progressBarMessageId will be ignored. | document.getElementById(progressBarMessageId) | | resultElementId | Override the ID used for the result | 'celery-result' | | resultElement | Override the *element* used for the result. If specified, resultElementId will be ignored. | document.getElementById(resultElementId) | | onProgress | function to call when progress is updated | onProgressDefault | | onSuccess | function to call when progress successfully completes | onSuccessDefault | | onError | function to call on a known error with no specified handler | onErrorDefault | | onRetry | function to call when a task attempts to retry | onRetryDefault | | onIgnored | function to call when a task result is ignored | onIgnoredDefault | | onTaskError | function to call when progress completes with an error | onError | | onNetworkError | function to call on a network error (ignored by WebSocket) | onError | | onHttpError | function to call on a non-200 response (ignored by WebSocket) | onError | | onDataError | function to call on a response that's not JSON or has invalid schema due to a programming error | onError | | onResult | function to call when returned non empty result | CeleryProgressBar.onResultDefault | | barColors | dictionary containing color values for various progress bar states. Colors that are not specified will defer to defaults | barColorsDefault | The `barColors` option allows you to customize the color of each progress bar state by passing a dictionary of key-value pairs of `state: #hexcode`. The defaults are shown below. | State | Hex Code | Image Color | |-------|----------|:-------------:| | success | #76ce60 | ![#76ce60](https://placehold.it/15/76ce60/000000?text=+) | | error | #dc4f63 | ![#dc4f63](https://placehold.it/15/dc4f63/000000?text=+) | | progress | #68a9ef | ![#68a9ef](https://placehold.it/15/68a9ef/000000?text=+) | | ignored | #7a7a7a | ![#7a7a7a](https://placehold.it/15/7a7a7a/000000?text=+) | # WebSocket Support Additionally, this library offers WebSocket support using [Django Channels](https://channels.readthedocs.io/en/latest/) courtesy of [EJH2](https://github.com/EJH2/). A working example project leveraging WebSockets is [available here](https://github.com/EJH2/cp_ws-example). To use WebSockets, install with `pip install celery-progress[websockets,redis]` or `pip install celery-progress[websockets,rabbitmq]` (depending on broker dependencies). See `WebSocketProgressRecorder` and `websockets.js` for details. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1625483600.0 celery-progress-0.1.1/celery_progress.egg-info/SOURCES.txt0000664000175000017500000000122700000000000023366 0ustar00czueczue00000000000000LICENSE MANIFEST.in README.md setup.py celery_progress/__init__.py celery_progress/backend.py celery_progress/tasks.py celery_progress/urls.py celery_progress/views.py celery_progress.egg-info/PKG-INFO celery_progress.egg-info/SOURCES.txt celery_progress.egg-info/dependency_links.txt celery_progress.egg-info/requires.txt celery_progress.egg-info/top_level.txt celery_progress/static/celery_progress/celery_progress.js celery_progress/static/celery_progress/websockets.js celery_progress/websockets/__init__.py celery_progress/websockets/backend.py celery_progress/websockets/consumers.py celery_progress/websockets/routing.py celery_progress/websockets/tasks.py././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1625483600.0 celery-progress-0.1.1/celery_progress.egg-info/dependency_links.txt0000664000175000017500000000000100000000000025546 0ustar00czueczue00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1625483600.0 celery-progress-0.1.1/celery_progress.egg-info/requires.txt0000664000175000017500000000011500000000000024075 0ustar00czueczue00000000000000 [rabbitmq] channels_rabbitmq [redis] channels_redis [websockets] channels ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1625483600.0 celery-progress-0.1.1/celery_progress.egg-info/top_level.txt0000664000175000017500000000002000000000000024222 0ustar00czueczue00000000000000celery_progress ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1625483600.9365168 celery-progress-0.1.1/setup.cfg0000664000175000017500000000004600000000000016420 0ustar00czueczue00000000000000[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1625483297.0 celery-progress-0.1.1/setup.py0000664000175000017500000000335200000000000016314 0ustar00czueczue00000000000000import os from setuptools import find_packages, setup from glob import glob readme_name = os.path.join(os.path.dirname(__file__), 'README.md') with open(readme_name, 'r') as readme: long_description = readme.read() # allow setup.py to be run from any path os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) setup( name='celery-progress', version='0.1.1', packages=find_packages(), include_package_data=True, license='MIT License', description='Drop in, configurable, dependency-free progress bars for your Django/Celery applications.', long_description=long_description, long_description_content_type="text/markdown", url='https://github.com/czue/celery-progress', author='Cory Zue', author_email='cory@coryzue.com', classifiers=[ 'Environment :: Web Environment', 'Framework :: Django', 'Framework :: Django :: 1.11', 'Framework :: Django :: 2.0', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', ], data_files=[ ('static/celery_progress', glob('celery_progress/static/celery_progress/*', recursive=True)), ], extras_require={ 'websockets': ['channels'], 'redis': ['channels_redis'], 'rabbitmq': ['channels_rabbitmq'] } )