├── .coveragerc ├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── __init__.py ├── _config.yml ├── carrot ├── __init__.py ├── api.py ├── apps.py ├── consumer.py ├── exceptions.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── carrot.py │ │ ├── carrot_daemon.py │ │ └── carrot_send.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_add_task_name.py │ ├── 0003_populate_task_name.py │ ├── 0004_set_unique_task_name.py │ └── __init__.py ├── mocks.py ├── models.py ├── objects.py ├── scheduler.py ├── static │ └── carrot │ │ ├── axios.min.js │ │ ├── lodash.min.js │ │ ├── vue.min.js │ │ ├── vuetify.min.css │ │ ├── vuetify.min.js │ │ ├── vuex.min.js │ │ └── white-carrot.png ├── templates │ └── carrot │ │ └── index.vue ├── test_carrot.py ├── tests.py ├── urls.py ├── utilities.py └── views.py ├── docs ├── README.md ├── _config.yml ├── _layouts │ └── default.html ├── images │ ├── 0.2 │ │ └── task-logging.png │ ├── 0.5 │ │ ├── carrot-monitor.png │ │ └── monitor.png │ ├── 1.0 │ │ ├── create-new.png │ │ ├── monitor.png │ │ ├── task-logging.png │ │ └── with-task-modules.png │ ├── carrot-logo-big.png │ ├── carrot-logo-inline-inverse.png │ ├── carrot-logo.png │ ├── no-task-modules.png │ └── with-task-modules.png ├── monitor.md ├── quick-start.md ├── release-notes.md ├── service.md └── settings.md ├── legacy_docs ├── Makefile ├── __init__.py ├── doc_settings.py └── source │ ├── __init__.py │ ├── _static │ └── carrot.css │ ├── _templates │ └── layout.html │ ├── api.rst │ ├── conf.py │ ├── consumer.rst │ ├── images │ ├── 0.2 │ │ └── task-logging.png │ ├── 0.5 │ │ ├── carrot-monitor.png │ │ └── monitor.png │ ├── 1.0 │ │ ├── create-new.png │ │ ├── monitor.png │ │ ├── task-logging.png │ │ └── with-task-modules.png │ ├── carrot-logo-big.png │ ├── carrot-logo-inline-inverse.png │ ├── no-task-modules.png │ └── with-task-modules.png │ ├── index.rst │ ├── models.rst │ ├── monitor.rst │ ├── objects.rst │ ├── quick-start.rst │ ├── release-notes.rst │ ├── service.rst │ ├── settings.rst │ └── utilities.rst ├── poetry.lock ├── pyproject.toml ├── requirements.txt ├── run_tests.py └── setup.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | */python?.?/* 4 | */site-packages/* 5 | *__init__* 6 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | parallel: true 2 | #coveralls_host: https://coveralls.aperture.com 3 | service_name: travis-ci 4 | repo_token: 43xMnMlTNjdv3qJAZHWyL7qMgEj5d6nyQ 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | .idea/ 4 | dist/ 5 | django_carrot.egg-info/ 6 | legacy_docs/build/ 7 | **/__pycache__/ 8 | /test.sh 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.6" 5 | 6 | install: 7 | - curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python 8 | - source $HOME/.poetry/env 9 | - poetry install 10 | 11 | script: 12 | - coverage run run_tests.py 13 | 14 | after_success: 15 | - coveralls 16 | 17 | before_deploy: 18 | - poetry config repositories.packagr https://api.packagr.app/63cdQSDO/ 19 | - poetry config http-basic.packagr christopherdavies553@gmail.com $PACKAGR_PASSWORD 20 | - poetry config http-basic.pypi chris140957 $PYPI_PASSWORD 21 | 22 | deploy: 23 | - provider: script 24 | script: poetry publish --build 25 | on: 26 | branch: master 27 | 28 | - provider: script 29 | script: poetry publish --build -r packagr 30 | on: 31 | all_branches: true 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017-2018 Christopher Davies 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include carrot/carrot.sh 3 | include docs/source/readme.rst 4 | recursive-include carrot/static * 5 | recursive-include carrot/templates * 6 | recursive-include carrot/templatetags * 7 | 8 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://coveralls.io/repos/github/chris104957/django-carrot/badge.svg?branch=master 2 | :target: https://coveralls.io/github/chris104957/django-carrot?branch=master 3 | 4 | .. image:: https://readthedocs.org/projects/django-carrot/badge/?version=latest 5 | :target: http://django-carrot.readthedocs.io/en/latest/?badge= 6 | 7 | .. image:: https://travis-ci.org/chris104957/django-carrot.svg?branch=master 8 | :target: https://travis-ci.org/chris104957/django-carrot.svg?branch=master 9 | 10 | .. image:: https://coveralls.io/repos/github/chris104957/django-carrot/badge.svg?branch=master 11 | :target: https://coveralls.io/github/chris104957/django-carrot?branch=master) 12 | 13 | .. image:: https://badge.fury.io/py/django-carrot.svg 14 | :target: https://badge.fury.io/py/django-carrot 15 | 16 | .. image:: https://img.shields.io/badge/License-Apache%202.0-blue.svg 17 | :target: https://opensource.org/licenses/Apache-2.0 18 | 19 | .. image:: /docs/source/images/carrot-logo-big.png 20 | :align: center 21 | 22 | **django-carrot** is a lightweight task queue backend for Django projects that uses the RabbitMQ message broker, with 23 | an emphasis on quick and easy configuration and task tracking 24 | 25 | Installation 26 | ------------ 27 | 28 | Install django-carrot: 29 | 30 | .. code-block:: bash 31 | 32 | pip install django-carrot 33 | 34 | Install and run RabbitMQ 35 | 36 | .. code-block:: bash 37 | 38 | brew install rabbitmq 39 | brew services start rabbitmq 40 | 41 | Configuration 42 | ------------- 43 | 44 | 1. Add carrot to your Django project's settings module: 45 | 46 | .. code-block:: python 47 | 48 | INSTALLED_APPS = [ 49 | ... 50 | 'carrot', 51 | ... 52 | ] 53 | 54 | 55 | 2. Apply the carrot migrations to your project's database: 56 | 57 | .. code-block:: python 58 | 59 | python manage.py migrate carrot 60 | 61 | 62 | Usage 63 | ----- 64 | 65 | To start the service: 66 | 67 | .. code-block:: bash 68 | 69 | python manage.py carrot_daemon start 70 | 71 | 72 | To run tasks asynchronously: 73 | 74 | .. code-block:: python 75 | 76 | from carrot.utilities import publish_message 77 | 78 | def my_task(**kwargs): 79 | return 'hello world' 80 | 81 | publish_message(my_task, hello=True) 82 | 83 | 84 | 85 | To schedule tasks to run at a given interval 86 | 87 | .. code-block:: python 88 | 89 | from carrot.utilities import create_scheduled_task 90 | 91 | create_scheduled_task(my_task, {'seconds': 5}, hello=True) 92 | 93 | 94 | .. note:: 95 | The above commands must be made from within the Django environment 96 | 97 | Docker 98 | ------ 99 | 100 | A sample docker config is available `here `_ 101 | 102 | Full documentation 103 | ------------------ 104 | 105 | The full documentation is available `here `_ 106 | 107 | Support 108 | ------- 109 | 110 | If you are having any issues, please `log an issue `_ 111 | 112 | Contributing 113 | ------------ 114 | 115 | Django-carrot uses `Packagr `_ to share development builds. If you'd like access to it, 116 | please send me your email address at christopherdavies553@gmail.com so I can give you access 117 | 118 | License 119 | ------- 120 | 121 | The project is licensed under the Apache license. 122 | 123 | Icons made by Trinh Ho from `www.flaticon.com `_ is licensed by CC 3.0 BY 124 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/__init__.py -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /carrot/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017-2018 Christopher Davies 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | 17 | DEFAULT_BROKER = 'amqp://guest:guest@localhost:5672/' 18 | 19 | __version__ = "1.4.0" 20 | -------------------------------------------------------------------------------- /carrot/api.py: -------------------------------------------------------------------------------- 1 | import json 2 | import ast 3 | import importlib 4 | from inspect import getmembers, isfunction 5 | from django.conf import settings 6 | from rest_framework import viewsets, serializers, pagination, response 7 | from rest_framework.request import Request 8 | from carrot.models import MessageLog, ScheduledTask 9 | from carrot.utilities import purge_queue, requeue_all 10 | from django.contrib.postgres.search import SearchVector 11 | from django.db.models import QuerySet 12 | 13 | 14 | class MessageLogSerializer(serializers.ModelSerializer): 15 | class Meta: 16 | model = MessageLog 17 | fields = 'status', 'exchange', 'queue', 'routing_key', 'uuid', 'priority', 'task', 'task_args', \ 18 | 'content', 'exception', 'traceback', 'output', 'publish_time', 'failure_time', 'completion_time', \ 19 | 'log', 'id', 'virtual_host' 20 | 21 | 22 | class SmallPagination(pagination.PageNumberPagination): 23 | page_size = 50 24 | 25 | 26 | class MessageLogViewset(viewsets.ModelViewSet): 27 | serializer_class = MessageLogSerializer 28 | pagination_class = SmallPagination 29 | 30 | def get_queryset(self) -> QuerySet: 31 | """ 32 | Returns a queryset of `carrot.models.MessageLog` objects. If a `search_term` is provided in the request query 33 | params, then the result is filtered based on this. If using postgres, this is done using SearchVectors for 34 | improved performance 35 | """ 36 | search_term = self.request.query_params.get('search', None) 37 | qs = self.queryset.all() 38 | if search_term: 39 | if settings.DATABASES.get('default', {}).get('ENGINE') == 'django.db.backends.postgresql_psycopg2': 40 | qs = qs.annotate(search=SearchVector('task', 'content', 'task_args')).filter(search=search_term) 41 | else: 42 | qs = ( 43 | qs.filter(task__icontains=search_term) | 44 | qs.filter(content__icontains=search_term) | 45 | qs.filter(task_args__icontains=search_term) 46 | ).distinct() 47 | 48 | return qs 49 | 50 | 51 | class PublishedMessageLogViewSet(MessageLogViewset): 52 | """ 53 | Returns a list of Published `MessageLog` objects 54 | """ 55 | 56 | queryset = MessageLog.objects.filter(status__in=['PUBLISHED', 'IN_PROGRESS'], id__isnull=False) 57 | 58 | def purge(self, request: Request, *args, **kwargs) -> response.Response: 59 | """ 60 | Deletes all items in the pending queue 61 | """ 62 | purge_queue() 63 | return super(PublishedMessageLogViewSet, self).list(request, *args, **kwargs) 64 | 65 | def requeue(self, request: Request, *args, **kwargs) -> response.Response: 66 | """ 67 | Requeues all pending MessageLogs. Useful when stuff gets stuck due to system update 68 | """ 69 | requeue_all() 70 | return super(PublishedMessageLogViewSet, self).list(request, *args, **kwargs) 71 | 72 | 73 | published_message_log_viewset = PublishedMessageLogViewSet.as_view({'get': 'list'}) 74 | purge_messages = PublishedMessageLogViewSet.as_view({'get': 'purge'}) 75 | requeue_pending = PublishedMessageLogViewSet.as_view({'get': 'requeue'}) 76 | 77 | 78 | class FailedMessageLogViewSet(MessageLogViewset): 79 | """ 80 | Returns a list of failed `MessageLog` objects 81 | """ 82 | 83 | queryset = MessageLog.objects.filter(status='FAILED', id__isnull=False) 84 | 85 | def destroy(self, request: Request, *args, **kwargs) -> response.Response: 86 | """ 87 | Deletes all `MessageLog` objects in the queryset 88 | """ 89 | self.queryset.delete() 90 | return response.Response(status=204) 91 | 92 | def retry(self, request: Request, *args, **kwargs) -> response.Response: 93 | """ 94 | Retries all `MessageLog` objects in the queryset 95 | """ 96 | queryset = self.get_queryset() 97 | for task in queryset: 98 | task.requeue() 99 | 100 | return self.list(request, *args, **kwargs) 101 | 102 | 103 | failed_message_log_viewset = FailedMessageLogViewSet.as_view({'get': 'list', 'delete': 'destroy', 'put': 'retry'}) 104 | 105 | 106 | class CompletedMessageLogViewSet(MessageLogViewset): 107 | """ 108 | Returns a list of Completed `MessageLog` objects 109 | """ 110 | queryset = MessageLog.objects.filter(status='COMPLETED', id__isnull=False) 111 | 112 | 113 | completed_message_log_viewset = CompletedMessageLogViewSet.as_view({'get': 'list'}) 114 | 115 | 116 | class MessageLogDetailViewset(MessageLogViewset): 117 | """ 118 | Shows the detail of a single `MessageLog` object 119 | """ 120 | queryset = MessageLog.objects.all() 121 | kwargs: dict = {} 122 | 123 | def destroy(self, request: Request, *args, **kwargs) -> response.Response: 124 | """ 125 | Deletes the given `MessageLog` object 126 | """ 127 | return super(MessageLogDetailViewset, self).destroy(request, *args, **kwargs) 128 | 129 | def retry(self, request: Request, *args, **kwargs) -> response.Response: 130 | """ 131 | Requeue a single task 132 | """ 133 | _object = self.get_object() 134 | new_object = _object.requeue() 135 | self.kwargs = {'pk': new_object.pk} 136 | return self.retrieve(request, *args, **kwargs) 137 | 138 | 139 | detail_message_log_viewset = MessageLogDetailViewset.as_view({'get': 'retrieve', 'delete': 'destroy', 'put': 'retry'}) 140 | 141 | 142 | class ScheduledTaskSerializer(serializers.ModelSerializer): 143 | def validate_task(self, value: str) -> str: 144 | modules = settings.CARROT.get('task_modules', None) 145 | if modules: 146 | task_choices = [] 147 | for module in modules: 148 | try: 149 | mod = importlib.import_module(module) 150 | functions = [o[0] for o in getmembers(mod) if isfunction(o[1]) and not o[0] == 'task'] 151 | 152 | for function in functions: 153 | f = '%s.%s' % (module, function) 154 | task_choices.append(f) 155 | except (ImportError, AttributeError): 156 | pass 157 | if task_choices and not value in task_choices: 158 | raise serializers.ValidationError('This is not a valid selection') 159 | 160 | return value 161 | 162 | def validate_task_args(self, value: str) -> str: 163 | """ 164 | Takes an input string and verifies that it can be interpreted as valid positional arguments, e.g. 165 | 166 | Valid: "1, True, None" 167 | Invalid: "foo, bar" 168 | """ 169 | if value: 170 | for arg in value.split(','): 171 | try: 172 | ast.literal_eval(arg.strip()) 173 | except Exception as err: 174 | raise serializers.ValidationError('Error parsing argument %s: %s' % (arg.strip(), err)) 175 | 176 | return value 177 | 178 | def validate_content(self, value: str) -> str: 179 | """ 180 | Validates that the given input string is valid JSON 181 | """ 182 | if value: 183 | try: 184 | json.loads(value) 185 | except json.JSONDecodeError: 186 | raise serializers.ValidationError('This field must be json serializable') 187 | 188 | return value 189 | 190 | def validate_queue(self, value: str) -> str: 191 | """ 192 | Validates that a queue name has been given and is not blank 193 | """ 194 | if value == '' or not value: 195 | raise serializers.ValidationError('This field is required') 196 | 197 | return value 198 | 199 | class Meta: 200 | model = ScheduledTask 201 | fields = ( 202 | 'task', 'interval_display', 'active', 'id', 'queue', 'exchange', 'routing_key', 'interval_type', 203 | 'interval_count', 'content', 'task_args', 'task_name' 204 | ) 205 | extra_kwargs = { 206 | 'queue': { 207 | 'required': True 208 | }, 209 | 'interval_type': { 210 | 'required': True 211 | }, 212 | 'interval_count': { 213 | 'required': True 214 | }, 215 | } 216 | 217 | 218 | class ScheduledTaskViewset(viewsets.ModelViewSet): 219 | """ 220 | Returns a list of `ScheduledTask` objects 221 | """ 222 | 223 | def validate_args(self, request: Request, *args, **kwargs) -> response.Response: 224 | """ 225 | Validates that the input is a valid Python tuple that can be used as a function's positional arguments 226 | """ 227 | value = request.data.get('args') 228 | errors = [] 229 | if value: 230 | for arg in value.split(','): 231 | try: 232 | ast.literal_eval(arg.strip()) 233 | except Exception as err: 234 | print('Error parsing argument %s: %s' % (arg.strip(), err)) 235 | errors.append('Error parsing argument %s: %s' % (arg.strip(), err)) 236 | 237 | return response.Response({'errors': errors}) 238 | 239 | def get_task_choices(self, request: Request, *args, **kwargs) -> response.Response: 240 | """ 241 | Gets a list of python functions from the task_modules settings in the config 242 | """ 243 | modules = settings.CARROT.get('task_modules', None) 244 | task_choices = [] 245 | if modules: 246 | for module in modules: 247 | try: 248 | mod = importlib.import_module(module) 249 | functions = [o[0] for o in getmembers(mod) if isfunction(o[1]) and not o[0] == 'task'] 250 | 251 | for function in functions: 252 | f = '%s.%s' % (module, function) 253 | task_choices.append(f) 254 | except (ImportError, AttributeError): 255 | pass 256 | 257 | return response.Response(data=task_choices) 258 | 259 | def create(self, request: Request, *args, **kwargs) -> viewsets.ModelViewSet.create: 260 | """ 261 | Create a new `ScheduledTask` object 262 | """ 263 | return super(ScheduledTaskViewset, self).create(request, *args, **kwargs) 264 | 265 | def update(self, request: Request, *args, **kwargs) -> viewsets.ModelViewSet.update: 266 | """ 267 | Update an existing `ScheduledTask` object 268 | """ 269 | return super(ScheduledTaskViewset, self).update(request, *args, **kwargs) 270 | 271 | def run(self, request: Request, *args, **kwargs) -> viewsets.ModelViewSet.retrieve: 272 | """ 273 | Triggers a given scheduled task now 274 | """ 275 | _object = self.get_object() 276 | _object.publish() 277 | return self.retrieve(request, *args, **kwargs) 278 | 279 | queryset = ScheduledTask.objects.all() 280 | serializer_class = ScheduledTaskSerializer 281 | pagination_class = SmallPagination 282 | 283 | 284 | scheduled_task_viewset = ScheduledTaskViewset.as_view({'get': 'list', 'post': 'create'}) 285 | scheduled_task_detail = ScheduledTaskViewset.as_view({'get': 'retrieve', 'patch': 'update', 'delete': 'destroy'}) 286 | task_list = ScheduledTaskViewset.as_view({'get': 'get_task_choices'}) 287 | validate_args = ScheduledTaskViewset.as_view({'post': 'validate_args'}) 288 | run_scheduled_task = ScheduledTaskViewset.as_view({'get': 'run'}) 289 | 290 | 291 | 292 | -------------------------------------------------------------------------------- /carrot/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CarrotConfig(AppConfig): 5 | name = 'carrot' 6 | 7 | -------------------------------------------------------------------------------- /carrot/exceptions.py: -------------------------------------------------------------------------------- 1 | class CarrotConfigException(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /carrot/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/carrot/management/__init__.py -------------------------------------------------------------------------------- /carrot/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/carrot/management/commands/__init__.py -------------------------------------------------------------------------------- /carrot/management/commands/carrot.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from carrot.consumer import ConsumerSet, LOGGING_FORMAT 4 | from carrot.models import ScheduledTask 5 | from carrot.objects import VirtualHost 6 | from carrot.scheduler import ScheduledTaskManager 7 | from django.core.management.base import BaseCommand, CommandParser 8 | from django.conf import settings 9 | from carrot import DEFAULT_BROKER 10 | import sys 11 | import os 12 | import logging 13 | import signal 14 | import psutil 15 | import types 16 | from typing import Optional 17 | 18 | 19 | class Command(BaseCommand): 20 | """ 21 | The main process for creating and running :class:`carrot.consumer.ConsumerSet` objects and starting thes scheduler 22 | """ 23 | pks: list = [] 24 | run = True 25 | help = 'Starts the carrot service.' 26 | scheduler: Optional[ScheduledTaskManager] = None 27 | active_consumer_sets: list = [] 28 | 29 | def __init__(self, 30 | stdout: str = None, 31 | stderr: str = None, 32 | nocolor: bool = False) -> None: 33 | """ 34 | Initiates the Carrot process. All params are passed straight to the base class. SIGINT and SIGTERM signals 35 | bound; the process will exit gracefully on these events 36 | """ 37 | signal.signal(signal.SIGINT, self.exit_gracefully) 38 | signal.signal(signal.SIGTERM, self.exit_gracefully) 39 | super(Command, self).__init__(stdout, stderr, nocolor) 40 | 41 | def exit_gracefully(self, signum: int, frame: types.FrameType) -> None: 42 | self.stdout.write(self.style.WARNING('Shutdown requested')) 43 | self.run = False 44 | 45 | def terminate(self, *args) -> None: 46 | """ 47 | Tells the scheduler (if running) and consumer sets to stop running, and waits for the response 48 | """ 49 | if self.scheduler: 50 | self.scheduler.stop() 51 | self.stdout.write(self.style.SUCCESS('Successfully closed scheduler')) 52 | 53 | self.stdout.write('Terminating running consumer sets (%i)...' % len(self.active_consumer_sets)) 54 | count = 0 55 | for consumer_set in self.active_consumer_sets: 56 | count += 1 57 | consumer_set.stop_consuming() 58 | 59 | self.stdout.write(self.style.SUCCESS('Successfully closed %i consumer sets' % count)) 60 | sys.exit() 61 | 62 | def add_arguments(self, parser: CommandParser) -> None: 63 | """ 64 | Adds the relevant command line arguments 65 | """ 66 | parser.add_argument("-l", "--logfile", type=str, help='The path to the log file', 67 | default='/var/log/carrot.log') 68 | parser.add_argument('--no-scheduler', dest='run_scheduler', action='store_false', default=False, 69 | help='Do not start scheduled tasks (only runs consumer sets)') 70 | parser.set_defaults(run_scheduler=True) 71 | parser.set_defaults(testmode=False) 72 | parser.add_argument('--loglevel', type=str, default='DEBUG', help='The logging level. Must be one of DEBUG, ' 73 | 'INFO, WARNING, ERROR, CRITICAL') 74 | parser.add_argument('--testmode', dest='testmode', action='store_true', default=False, 75 | help='Run in test mode. Prevents the command from running as a service. Should only be ' 76 | 'used when running Carrot\'s tests') 77 | 78 | def handle(self, **options) -> None: 79 | """ 80 | The actual handler process. Performs the following actions: 81 | 82 | - Initiates and starts a new :class:`carrot.objects.ScheduledTaskManager`, which schedules all *active* 83 | :class:`carrot.objects.ScheduledTask` instances to run at the given intervals. This only happens if the 84 | **--no-scheduler** argument has not been provided - otherwise, the service only creates consumer objects 85 | 86 | - Loops through the queues registered in your Django project's settings module, and starts a 87 | new :class:`carrot.objects.ConsumerSet` for them. Each ConsumerSet will contain **n** 88 | :class:`carrot.objects.Consumer` objects, where **n** is the concurrency setting for the given queue (as 89 | defined in the Django settings) 90 | 91 | - Enters into an infinite loop which monitors your database for changes to your database - if any changes 92 | to the :class:`carrot.objects.ScheduledTask` queryset are detected, carrot updates the scheduler 93 | accordingly 94 | 95 | On receiving a **KeyboardInterrupt**, **SystemExit** or SIGTERM, the service first turns off each of the 96 | schedulers in turn (so no new tasks can be published to RabbitMQ), before turning off the Consumers in turn. 97 | The more Consumers/ScheduledTask objects you have, the longer this will take. 98 | 99 | :param options: provided by **argparse** (see above for the full list of available options) 100 | 101 | """ 102 | signal.signal(signal.SIGTERM, self.terminate) 103 | 104 | # check if carrot service is already running, and warn the user if so 105 | running_pids = [] 106 | for q in psutil.process_iter(): 107 | if 'python' in q.name(): 108 | if len(q.cmdline()) > 1 and 'manage.py' in q.cmdline()[1] and 'carrot' in q.cmdline()[2]: 109 | if os.name == 'nt': 110 | if not q._pid == os.getpid(): 111 | running_pids.append(q._pid) 112 | else: 113 | if not q._pid == os.getpgid(0): 114 | running_pids.append(q._pid) 115 | 116 | if running_pids: 117 | self.stdout.write( 118 | self.style.WARNING('WARNING: Carrot service is already running with the following PID. Running more ' 119 | 'than one instance of carrot may lead to a memory leak:\n%s' 120 | % '\n'.join([str(pid) for pid in running_pids]))) 121 | 122 | run_scheduler = options['run_scheduler'] 123 | 124 | try: 125 | queues = [q for q in settings.CARROT['queues'] if q.get('consumable', True)] 126 | 127 | except (AttributeError, KeyError): 128 | queues = [{ 129 | 'name': 'default', 130 | 'host': DEFAULT_BROKER 131 | }] 132 | 133 | if run_scheduler: 134 | self.scheduler = ScheduledTaskManager() 135 | 136 | try: 137 | # scheduler 138 | if self.scheduler: 139 | self.scheduler.start() 140 | self.stdout.write(self.style.SUCCESS('Successfully started scheduler')) 141 | 142 | # logger 143 | loglevel = getattr(logging, options.get('loglevel', 'DEBUG')) 144 | 145 | logger = logging.getLogger('carrot') 146 | logger.setLevel(loglevel) 147 | 148 | file_handler = logging.FileHandler(options['logfile']) 149 | file_handler.setLevel(loglevel) 150 | 151 | stream_handler = logging.StreamHandler() 152 | stream_handler.setLevel(loglevel) 153 | 154 | formatter = logging.Formatter(LOGGING_FORMAT) 155 | file_handler.setFormatter(formatter) 156 | stream_handler.setFormatter(formatter) 157 | 158 | logger.addHandler(file_handler) 159 | logger.addHandler(stream_handler) 160 | 161 | # consumers 162 | for queue in queues: 163 | kwargs = { 164 | 'queue': queue['name'], 165 | 'logger': logger, 166 | 'concurrency': queue.get('concurrency', 1), 167 | } 168 | 169 | if queue.get('consumer_class', None): 170 | kwargs['consumer_class'] = queue.get('consumer_class') 171 | 172 | try: 173 | vhost = VirtualHost(**queue['host']) 174 | except TypeError: 175 | vhost = VirtualHost(url=queue['host']) 176 | 177 | c = ConsumerSet(host=vhost, **kwargs) 178 | c.start_consuming() 179 | self.active_consumer_sets.append(c) 180 | self.stdout.write(self.style.SUCCESS('Successfully started %i consumers for queue %s' 181 | % (c.concurrency, queue['name']))) 182 | 183 | self.stdout.write(self.style.SUCCESS('All queues consumer sets started successfully. Full logs are at %s.' 184 | % options['logfile'])) 185 | 186 | qs = ScheduledTask.objects.filter(active=True) 187 | self.pks = [t.pk for t in qs] 188 | 189 | while True: 190 | time.sleep(1) 191 | if not self.run: 192 | self.terminate() 193 | 194 | if self.scheduler or options['testmode']: 195 | new_qs = ScheduledTask.objects.filter(active=True) 196 | active_pks = {st.pk for st in new_qs} 197 | newly_added = set(self.pks) - active_pks 198 | 199 | if new_qs.count() > len(self.pks) or newly_added: 200 | print('New active scheduled tasks have been added to the queryset') 201 | new_tasks = new_qs.exclude(pk__in=self.pks) or [ScheduledTask()] 202 | for new_task in new_tasks: 203 | print('adding new task %s' % new_task) 204 | if self.scheduler: 205 | self.scheduler.add_task(new_task) 206 | 207 | self.pks = [t.pk for t in new_qs] 208 | 209 | elif new_qs.count() < len(self.pks): 210 | self.pks = [t.pk for t in new_qs] 211 | 212 | if options['testmode']: 213 | print('TESTMODE:', options['testmode']) 214 | raise SystemExit() 215 | 216 | except Exception as err: 217 | try: 218 | self.stderr.write(self.style.ERROR(err)) 219 | except AttributeError: 220 | """ 221 | This attribute error will happen when a pika exceptions.ChannelClosed error happens, as django can't 222 | deal with the error styling for this type of exception. Instead, we write the stderr without the 223 | colorisation 224 | """ 225 | self.stderr.write(str(err)) 226 | 227 | except (KeyboardInterrupt, SystemExit): 228 | # self.terminate() 229 | pass 230 | 231 | -------------------------------------------------------------------------------- /carrot/management/commands/carrot_daemon.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandParser 2 | import sys 3 | import os 4 | import signal 5 | import time 6 | import subprocess 7 | import argparse 8 | from typing import Optional 9 | 10 | 11 | class PidExists(Exception): 12 | pass 13 | 14 | 15 | class MissingPid(Exception): 16 | pass 17 | 18 | 19 | class Command(BaseCommand): 20 | """ 21 | The daemon process for controlling the :class:`carrot.management.commands.carrot` service 22 | """ 23 | pid_file: Optional[str] = None 24 | options: dict = {} 25 | 26 | def delete_pid(self) -> None: 27 | """ 28 | Deletes the pid file, if it exists 29 | """ 30 | if self.pid_file and os.path.exists(self.pid_file): 31 | os.remove(self.pid_file) 32 | 33 | def stop(self, hard_stop: bool = False) -> None: 34 | """ 35 | Attempts to stop the process. Performs the following actions: 36 | 37 | 1. Asserts that the pidfile exists, or raises a :class:`MissingPid` exception 38 | 2. Runs :function:`os.kill` on a loop until an :class:`OSError` is raised. 39 | 3. Deletes the pidfile once the process if no longer running 40 | 41 | If *hard_stop* is used, the process will not wait for the consumers to finish running their current tasks 42 | 43 | :param bool hard_stop: if True, sends a sigkill instead of a sigterm to the consumers 44 | 45 | """ 46 | assert self.pid, MissingPid('PIDFILE does not exist. The process may not be running') 47 | 48 | _signal = signal.SIGKILL if hard_stop else signal.SIGTERM 49 | 50 | while True: 51 | try: 52 | os.kill(self.pid, _signal) 53 | time.sleep(0.1) 54 | except OSError: 55 | break 56 | 57 | self.stdout.write(self.style.SUCCESS('Process has been stopped')) 58 | 59 | self.delete_pid() 60 | 61 | def add_arguments(self, parser: CommandParser) -> None: 62 | """ 63 | This Command inherits the same arguments as :class:`carrot.management.commands.carrot.Command`, with the 64 | addition of one positional argument: **mode** 65 | """ 66 | parser.add_argument('mode') 67 | parser.add_argument("-l", "--logfile", type=str, help='The path to the log file', 68 | default='/var/log/carrot.log') 69 | parser.add_argument("-p", "--pidfile", type=str, help='The path to the pid file', 70 | default='/var/run/carrot.pid') 71 | parser.add_argument('--no-scheduler', dest='run_scheduler', action='store_false', default=False, 72 | help='Do not start scheduled tasks (only runs consumer sets)') 73 | parser.add_argument('--hard', dest='force', action='store_true', default=False, 74 | help='Force stop the consumer (can only be used with stop|restart modes). USE WITH CAUTION') 75 | parser.set_defaults(run_scheduler=True) 76 | parser.set_defaults(testmode=False) 77 | 78 | parser.add_argument('--consumer-class', type=str, help='The consumer class to use', 79 | default='carrot.objects.Consumer') 80 | parser.add_argument('--loglevel', type=str, default='DEBUG', help='The logging level. Must be one of DEBUG, ' 81 | 'INFO, WARNING, ERROR, CRITICAL') 82 | parser.add_argument('--testmode', dest='testmode', action='store_true', default=False, 83 | help='Run in test mode. Prevents the command from running as a service. Should only be ' 84 | 'used when running Carrot\'s tests') 85 | 86 | @property 87 | def pid(self) -> Optional[int]: 88 | """ 89 | Opens and reads the file stored at `self.pidfile`, and returns the content as an integer. If the pidfile doesn't 90 | exist, then None is returned. 91 | """ 92 | if self.pid_file: 93 | try: 94 | with open(self.pid_file, 'r') as pf: 95 | return int(pf.read().strip()) 96 | except IOError: 97 | pass 98 | 99 | return None 100 | 101 | def write_pid(self, pid: int) -> None: 102 | """ 103 | Writes the pid to the pidfile 104 | """ 105 | if self.pid_file: 106 | with open(self.pid_file, 'w') as f: 107 | f.write(str(pid) + '\n') 108 | 109 | def start(self, **kwargs: dict) -> None: 110 | """ 111 | Starts the carrot service as a subprocess and records the pid 112 | """ 113 | if self.pid: 114 | raise PidExists('Process already running!') 115 | 116 | if kwargs: 117 | self.options = kwargs 118 | 119 | options: list = [sys.executable, sys.argv[0], 'carrot', '--verbosity', str(kwargs.get('verbosity', 2)), 120 | '--logfile', self.options['logfile'], '--loglevel', self.options['loglevel']] 121 | 122 | if not self.options['run_scheduler']: 123 | options.append('--no-scheduler') 124 | 125 | if self.options['consumer_class'] != 'carrot.objects.Consumer': 126 | options.append('--consumer-class') 127 | options.append(self.options['consumer_class']) 128 | 129 | proc = subprocess.Popen(options, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) 130 | 131 | self.write_pid(proc.pid) 132 | 133 | return None 134 | 135 | def handle(self, *args, **options) -> None: 136 | """ 137 | The main handler. Initiates :class:`CarrotService`, then handles it based on the options supplied 138 | 139 | :param options: handled by *argparse* 140 | """ 141 | mode = options.pop('mode') 142 | hard_stop = options.pop('force', False) 143 | 144 | if hard_stop: 145 | if mode not in ['stop', 'restart']: 146 | raise argparse.ArgumentError('force', 'This option is only valid for stop|restart modes') 147 | 148 | self.pid_file = options.pop('pidfile') 149 | 150 | if mode not in ['start', 'stop', 'restart', 'status']: 151 | raise argparse.ArgumentError('mode', 'Must be start, stop, restart or status') 152 | 153 | if mode == 'start': 154 | self.stdout.write('Attempting to start the process') 155 | self.start(**options) 156 | self.stdout.write(self.style.SUCCESS('Process started successfully with pid: %s' % self.pid)) 157 | 158 | elif mode == 'stop': 159 | self.stdout.write('Attempting to stop the process. Please wait...') 160 | self.stop(hard_stop) 161 | 162 | elif mode == 'restart': 163 | try: 164 | self.stdout.write('Attempting to stop the process. Please wait...') 165 | self.stop(hard_stop) 166 | except MissingPid: 167 | self.stdout.write(self.style.WARNING('Unable to stop the process because it isn\'t running')) 168 | 169 | self.stdout.write('Attempting to start the process') 170 | self.start(**options) 171 | self.stdout.write(self.style.SUCCESS('Process restarted successfully')) 172 | 173 | elif mode == 'status': 174 | if self.pid: 175 | self.stdout.write(self.style.SUCCESS('Service is running. PID: %i' % self.pid)) 176 | else: 177 | self.stdout.write(self.style.ERROR('Service is NOT running')) 178 | 179 | sys.exit() 180 | -------------------------------------------------------------------------------- /carrot/management/commands/carrot_send.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pprint 3 | from django.core.management.base import BaseCommand, CommandError, CommandParser 4 | from carrot.utilities import publish_message 5 | 6 | 7 | class Command(BaseCommand): 8 | help = 'Queues a job for execution' 9 | 10 | def add_arguments(self, parser: CommandParser) -> None: 11 | parser.add_argument('job_name', type=str) 12 | parser.add_argument('job_args', type=str, nargs='+') 13 | 14 | def handle(self, *args, **options) -> None: 15 | job_name = options['job_name'] 16 | job_args = options['job_args'] 17 | 18 | if job_args: 19 | publish_message(job_name, *job_args) 20 | else: 21 | publish_message(job_name) 22 | -------------------------------------------------------------------------------- /carrot/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.5 on 2018-05-08 12:04 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies: list = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='MessageLog', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('status', models.CharField(choices=[('PUBLISHED', 'Published'), ('IN_PROGRESS', 'In progress'), ('FAILED', 'Failed'), ('COMPLETED', 'Completed')], default='PUBLISHED', max_length=11)), 20 | ('exchange', models.CharField(blank=True, max_length=200, null=True)), 21 | ('queue', models.CharField(blank=True, max_length=200, null=True)), 22 | ('routing_key', models.CharField(blank=True, max_length=200, null=True)), 23 | ('uuid', models.CharField(max_length=200)), 24 | ('priority', models.PositiveIntegerField(default=0)), 25 | ('task', models.CharField(max_length=200)), 26 | ('task_args', models.TextField(blank=True, null=True, verbose_name='Task positional arguments')), 27 | ('content', models.TextField(blank=True, null=True, verbose_name='Task keyword arguments')), 28 | ('exception', models.TextField(blank=True, null=True)), 29 | ('traceback', models.TextField(blank=True, null=True)), 30 | ('output', models.TextField(blank=True, null=True)), 31 | ('publish_time', models.DateTimeField(blank=True, null=True)), 32 | ('failure_time', models.DateTimeField(blank=True, null=True)), 33 | ('completion_time', models.DateTimeField(blank=True, null=True)), 34 | ('log', models.TextField(blank=True, null=True)), 35 | ], 36 | options={ 37 | 'ordering': ('-failure_time', '-completion_time', 'status', '-priority', '-publish_time'), 38 | }, 39 | ), 40 | migrations.CreateModel( 41 | name='ScheduledTask', 42 | fields=[ 43 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 44 | ('interval_type', models.CharField(choices=[('seconds', 'seconds'), ('minutes', 'minutes'), ('hours', 'hours'), ('days', 'days')], default='seconds', max_length=200)), 45 | ('interval_count', models.PositiveIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1)])), 46 | ('exchange', models.CharField(blank=True, max_length=200, null=True)), 47 | ('routing_key', models.CharField(blank=True, max_length=200, null=True)), 48 | ('queue', models.CharField(blank=True, max_length=200, null=True)), 49 | ('task', models.CharField(max_length=200)), 50 | ('task_args', models.TextField(blank=True, null=True, verbose_name='Positional arguments')), 51 | ('content', models.TextField(blank=True, null=True, verbose_name='Keyword arguments')), 52 | ('active', models.BooleanField(default=True)), 53 | ], 54 | ), 55 | ] 56 | -------------------------------------------------------------------------------- /carrot/migrations/0002_add_task_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.4 on 2018-05-30 12:19 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ('carrot', '0001_initial'), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name='scheduledtask', 14 | name='task_name', 15 | field=models.CharField(blank=True, max_length=200, null=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /carrot/migrations/0003_populate_task_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.4 on 2018-05-30 12:19 2 | 3 | from django.db import migrations 4 | 5 | 6 | def add_task_name(apps, schema_editor): 7 | """ 8 | This function loops through all ScheduledTask objects and sets the task_name to a unique value, based on the 9 | existing task attribute, e.g. 10 | myapp.mytasks.hello-world 11 | myapp.mytasks.hello-world-1 12 | myapp.mytasks.hello-world-2 13 | """ 14 | ScheduledTask = apps.get_model("carrot", "ScheduledTask") 15 | db_alias = schema_editor.connection.alias 16 | 17 | index = 0 18 | for t in ScheduledTask.objects.using(db_alias).filter(task_name=''): 19 | index += 1 20 | task_name = '%s-%i' % (t.task, index) 21 | t.task_name = task_name 22 | t.save() 23 | 24 | 25 | def remove_task_name(apps, schema_editor): 26 | """ 27 | This doesn't need to be implemented as the task names will be deleted when the field is removed 28 | """ 29 | return 30 | 31 | 32 | class Migration(migrations.Migration): 33 | 34 | dependencies = [ 35 | ('carrot', '0002_add_task_name'), 36 | ] 37 | 38 | operations = [ 39 | migrations.RunPython(add_task_name, remove_task_name), 40 | ] 41 | -------------------------------------------------------------------------------- /carrot/migrations/0004_set_unique_task_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.4 on 2018-05-30 12:19 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('carrot', '0003_populate_task_name'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='scheduledtask', 15 | name='task_name', 16 | field=models.CharField(blank=True, max_length=200, null=True), 17 | ), 18 | ] -------------------------------------------------------------------------------- /carrot/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/carrot/migrations/__init__.py -------------------------------------------------------------------------------- /carrot/mocks.py: -------------------------------------------------------------------------------- 1 | from carrot.objects import DefaultMessageSerializer 2 | 3 | 4 | class MessageSerializer(DefaultMessageSerializer): 5 | failing_method = 'serialize_arguments' 6 | 7 | def serialize_arguments(self, body): 8 | if self.failing_method == 'serialize_arguments': 9 | raise AttributeError('test error message') 10 | super(MessageSerializer, self).serialize_arguments(body) 11 | 12 | def get_task(self, properties, body): 13 | if self.failing_method == 'get_task': 14 | raise AttributeError('test error message') 15 | super(MessageSerializer, self).get_task(properties, body) 16 | 17 | 18 | class IoLoop(object): 19 | def stop(self): 20 | pass 21 | 22 | def start(self): 23 | pass 24 | 25 | 26 | class Channel(object): 27 | def __init__(self, *args, **kwargs): 28 | pass 29 | 30 | def basic_qos(*args, **kwargs): 31 | return 32 | 33 | def queue_purge(self, queue=''): 34 | return 35 | 36 | def add_on_close_callback(self): 37 | return 38 | 39 | @staticmethod 40 | def close(*args, **kwargs): 41 | return 42 | 43 | def exchange_declare(self, callback, exchange=None, **kwargs): 44 | return 45 | 46 | def basic_publish(self, **kwargs): 47 | return 48 | 49 | def queue_declare(self, *args, **kwargs): 50 | return 51 | 52 | def queue_bind(self, *args, **kwargs): 53 | return 54 | 55 | def add_on_cancel_callback(self, *args, **kwargs): 56 | return 57 | 58 | def basic_consume(self, *args, **kwargs): 59 | return 60 | 61 | def basic_cancel(self, *args, **kwargs): 62 | return 63 | 64 | def basic_nack(self, *args, **kwargs): 65 | return 66 | 67 | def basic_ack(self, *args, **kwargs): 68 | return 69 | 70 | 71 | class Connection(object): 72 | def __init__(self, *args, **kwargs): 73 | self.channel = Channel 74 | self.ioloop = IoLoop() 75 | 76 | def connect(self): 77 | return self 78 | 79 | def add_on_close_callback(self, callback): 80 | return 81 | 82 | def basic_qos(self, **kwargs): 83 | return 84 | 85 | @property 86 | def on_channel_open(self): 87 | return 88 | 89 | def add_timeout(self, reconnect_timeout, timeout): 90 | return 91 | 92 | def close(self): 93 | return 94 | 95 | 96 | class Properties(object): 97 | message_id = 1234 98 | delivery_tag = 1 99 | headers = {'type': 'test'} 100 | 101 | 102 | class Consumer(object): 103 | def join(self): 104 | return 105 | 106 | def stop(self): 107 | return 108 | 109 | def start(self): 110 | return 111 | 112 | def __init__(self, *args, **kwargs): 113 | pass -------------------------------------------------------------------------------- /carrot/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.core.validators import MinValueValidator 3 | 4 | # support for both django 1.x/2.x 5 | try: 6 | from django.core.urlresolvers import reverse 7 | except ImportError: 8 | from django.urls import reverse 9 | 10 | from carrot.exceptions import CarrotConfigException 11 | 12 | import json 13 | import os 14 | import sys 15 | 16 | from typing import Optional, Iterable 17 | 18 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 19 | sys.path.append(BASE_DIR + '/carrot') 20 | 21 | 22 | class MessageLog(models.Model): 23 | """ 24 | MessageLogs store information about a carrot task 25 | 26 | Lifecycle: 27 | #. A :class:`carrot.objects.Message` object is created and published. 28 | #. The act of publishing the message creates a MessageLog object with the status 'PUBLISHED'. The task now sits 29 | in the RabbitMQ queue until it has been consumed 30 | #. When a consumer digests the message, the status is updated to 'COMPLETED' if the task completes successfully 31 | or 'FAILED' if it encounters an exception. The output, traceback, exception message and logs are written 32 | back to the MessageLog object 33 | #. If a task has failed, it can be requeued. Requeueing a task will create a new :class:`carrot.objects.Message` 34 | object with the same parameters. In this case, the originally MessageLog object will be deleted 35 | #. If the task has been completed successfully, it will be deleted three days after completion, provided that 36 | the :function:`carrot.helper_tasks.cleanup` has not been disabled 37 | 38 | """ 39 | STATUS_CHOICES = ( 40 | ('PUBLISHED', 'Published'), 41 | ('IN_PROGRESS', 'In progress'), 42 | ('FAILED', 'Failed'), 43 | ('COMPLETED', 'Completed'), 44 | ) #: 45 | 46 | status = models.CharField(max_length=11, choices=STATUS_CHOICES, default='PUBLISHED') 47 | exchange = models.CharField(max_length=200, blank=True, null=True) #: the exchange 48 | queue = models.CharField(max_length=200, blank=True, null=True) 49 | routing_key = models.CharField(max_length=200, blank=True, null=True) 50 | uuid = models.CharField(max_length=200) 51 | priority = models.PositiveIntegerField(default=0) 52 | 53 | task = models.CharField(max_length=200) #: the import path for the task to be executed 54 | task_args = models.TextField(null=True, blank=True, verbose_name='Task positional arguments') 55 | content = models.TextField(null=True, blank=True, verbose_name='Task keyword arguments') 56 | 57 | exception = models.TextField(null=True, blank=True) 58 | traceback = models.TextField(null=True, blank=True) 59 | output = models.TextField(null=True, blank=True) 60 | 61 | publish_time = models.DateTimeField(null=True, blank=True) 62 | failure_time = models.DateTimeField(null=True, blank=True) 63 | completion_time = models.DateTimeField(null=True, blank=True) 64 | 65 | log = models.TextField(blank=True, null=True) 66 | 67 | @property 68 | def virtual_host(self) -> Optional[str]: 69 | from carrot.utilities import get_host_from_name 70 | try: 71 | return str(get_host_from_name(self.queue)) 72 | except CarrotConfigException: 73 | """ 74 | This exception may get raised here when a MessageLog is created with a queue that is later removed from the 75 | Django config. This method now returns `None` in these cases, so as not to break the monitor 76 | 77 | Refer to https://github.com/chris104957/django-carrot/issues/81 78 | """ 79 | return None 80 | 81 | @property 82 | def keywords(self) -> dict: 83 | """ 84 | Used in :class:`carrot.views.MessageView` to display the keyword arguments as a table 85 | """ 86 | return json.loads(self.content or '{}') 87 | 88 | def __str__(self) -> models.CharField: 89 | return self.task 90 | 91 | @property 92 | def positionals(self) -> Iterable: 93 | import ast 94 | if self.task_args == '()': 95 | return () 96 | else: 97 | return [ast.literal_eval(arg.strip()) for arg in self.task_args[1:-1].split(',') if arg != ''] 98 | 99 | def requeue(self) -> 'MessageLog': 100 | """ 101 | Sends a failed MessageLog back to the queue. The original MessageLog is deleted 102 | """ 103 | from carrot.utilities import publish_message 104 | msg = publish_message(self.task, *self.positionals, priority=self.priority, queue=self.queue, 105 | exchange=self.exchange, routing_key=self.routing_key, **self.keywords) 106 | 107 | if self.pk: 108 | self.delete() 109 | 110 | return msg 111 | 112 | class Meta: 113 | ordering = '-failure_time', '-completion_time', 'status', '-priority', '-publish_time', 114 | 115 | 116 | class ScheduledTask(models.Model): 117 | """ 118 | A model for scheduling tasks to run at a certain interval 119 | """ 120 | INTERVAL_CHOICES = ( 121 | ('seconds', 'seconds'), 122 | ('minutes', 'minutes'), 123 | ('hours', 'hours'), 124 | ('days', 'days'), 125 | ) 126 | 127 | interval_type = models.CharField(max_length=200, choices=INTERVAL_CHOICES, default='seconds') 128 | interval_count = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)]) 129 | 130 | exchange = models.CharField(max_length=200, blank=True, null=True) 131 | routing_key = models.CharField(max_length=200, blank=True, null=True) 132 | queue = models.CharField(max_length=200, blank=True, null=True) 133 | task = models.CharField(max_length=200) 134 | task_args = models.TextField(null=True, blank=True, verbose_name='Positional arguments') 135 | content = models.TextField(null=True, blank=True, verbose_name='Keyword arguments') 136 | 137 | active = models.BooleanField(default=True) 138 | 139 | task_name = models.CharField(max_length=200, unique=True) 140 | 141 | def get_absolute_url(self) -> str: 142 | return reverse('edit-scheduled-task', args=[self.pk]) 143 | 144 | @property 145 | def interval_display(self) -> str: 146 | return 'Every %i %s' % (self.interval_count, self.interval_type if self.interval_count > 1 else 147 | self.interval_type[:-1]) 148 | 149 | @property 150 | def multiplier(self) -> int: 151 | if self.interval_type == 'minutes': 152 | return 60 153 | 154 | if self.interval_type == 'hours': 155 | return 60 * 60 156 | 157 | if self.interval_type == 'days': 158 | return 86400 159 | 160 | return 1 161 | 162 | @property 163 | def positional_arguments(self) -> tuple: 164 | if self.task_args: 165 | return tuple([a.strip() for a in self.task_args.split(',') if a]) 166 | else: 167 | return () 168 | 169 | def publish(self, priority: int = 0) -> MessageLog: 170 | from carrot.utilities import publish_message 171 | kwargs = json.loads(self.content or '{}') 172 | if isinstance(kwargs, str): 173 | kwargs = {} 174 | return publish_message(self.task, *self.positional_arguments, priority=priority, queue=self.queue, 175 | exchange=self.exchange or '', routing_key=self.routing_key or self.queue, 176 | **kwargs) 177 | 178 | def __str__(self) -> models.CharField: 179 | return self.task 180 | -------------------------------------------------------------------------------- /carrot/objects.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import json 3 | import logging 4 | import uuid 5 | from typing import Tuple, Callable, Dict, Any, Union 6 | 7 | import pika 8 | from django.utils import timezone 9 | 10 | 11 | 12 | class VirtualHost(object): 13 | """ 14 | A RabbitMQ virtual host 15 | """ 16 | 17 | def __init__(self, 18 | url: str = None, 19 | host: str = 'localhost', 20 | name: str = '%2f', 21 | port: int = 5672, 22 | username: str = 'guest', 23 | password: str = 'guest', 24 | secure: bool = False) -> None: 25 | 26 | self.secure = secure 27 | if not url: 28 | self.host = host 29 | self.name = name 30 | self.port = port 31 | self.username = username 32 | self.password = password 33 | 34 | else: 35 | try: 36 | url = url.replace('amqp://', '') 37 | if '@' in url: 38 | # user:pass@host:port/vhost 39 | credentials, url = url.split('@') 40 | self.username, self.password = credentials.split(':') 41 | 42 | else: 43 | # host:port/vhost 44 | self.username = username 45 | self.password = password 46 | import pika 47 | 48 | try: 49 | url, self.name = url.split('/') 50 | except ValueError: 51 | url = url.split('/')[0] 52 | self.name = name 53 | 54 | self.host, _port = url.split(':') 55 | self.port = int(_port) 56 | 57 | except Exception as err: 58 | raise Exception('Unable to parse the RabbitMQ server. Please check your configuration: %s' % err) 59 | 60 | if not self.name: 61 | self.name = '%2f' 62 | 63 | def __str__(self) -> str: 64 | """ 65 | Returns the broker url 66 | """ 67 | return 'amqp://%s:%s@%s:%s/%s' % (self.username, self.password, self.host, self.port, self.name) 68 | 69 | @property 70 | def blocking_connection(self) -> pika.BlockingConnection: 71 | """ 72 | Connect to the VHOST 73 | """ 74 | credentials = pika.PlainCredentials(username=self.username, password=self.password) 75 | if self.name == '%2f': 76 | vhost = '/' 77 | else: 78 | vhost = self.name 79 | 80 | params = pika.ConnectionParameters(host=self.host, port=self.port, virtual_host=vhost, 81 | credentials=credentials, connection_attempts=10, ssl=self.secure, 82 | heartbeat=1200) 83 | return pika.BlockingConnection(parameters=params) 84 | 85 | 86 | class BaseMessageSerializer(object): 87 | """ 88 | A class that defines how to convert a RabbitMQ message into an executable python function from your Django project, 89 | and back again 90 | 91 | :param Message message: the RabbitMQ message 92 | 93 | """ 94 | content_type = 'application/json' 95 | type_header, message_type = ('',) * 2 96 | task_get_attempts = 20 97 | 98 | @classmethod 99 | def get_task(cls, properties: pika.BasicProperties, body: bytes) -> Callable: 100 | """ 101 | Identifies the python function to be executed from the content of the RabbitMQ message. By default, Carrot 102 | returns the value of the self.type_header header in the properties. 103 | 104 | Once this string has been found, carrot uses importlib to return a callable python function. 105 | 106 | """ 107 | mod = '.'.join(properties.headers[cls.type_header].split('.')[:-1]) 108 | task = properties.headers[cls.type_header].split('.')[-1] 109 | module = importlib.import_module(mod) 110 | func = getattr(module, task) 111 | return func 112 | 113 | def __init__(self, message: 'Message') -> None: 114 | self.message = message 115 | 116 | def publish_kwargs(self) -> dict: 117 | """ 118 | Returns a dictionary of keyword arguments to be passed to channel.basic_publish. In this implementation, the 119 | exchange, routing key and message body are returned 120 | 121 | """ 122 | exchange = self.message.exchange or '' 123 | routing_key = self.message.routing_key or 'default' 124 | body = self.body() 125 | return dict(exchange=exchange, routing_key=routing_key, body=body, mandatory=True) 126 | 127 | def body(self) -> str: 128 | """ 129 | Returns the content to be added to the RabbitMQ message body 130 | 131 | By default, this implementation returns a simple dict in the following format: 132 | 133 | .. code-block:: python 134 | 135 | { 136 | 'args': ('tuple', 'of', 'positional', 'arguments'), 137 | 'kwargs': { 138 | 'keyword1': 'value', 139 | 'keyword2': True 140 | } 141 | } 142 | 143 | """ 144 | args = self.message.task_args 145 | kwargs = self.message.task_kwargs 146 | 147 | data: Dict[str, Any] = {} 148 | 149 | if args: 150 | data['args'] = args 151 | 152 | if kwargs: 153 | data['kwargs'] = kwargs 154 | 155 | return json.dumps(data) 156 | 157 | def properties(self) -> dict: 158 | """ 159 | Returns a dict from which a :class:`pika.BasicProperties` object can be created 160 | 161 | In this implementation, the following is returned: 162 | - headers 163 | - content message_type 164 | - priority 165 | - message id 166 | - message message_type 167 | 168 | """ 169 | headers = {self.type_header: self.message.task} 170 | content_type = self.content_type 171 | priority = self.message.priority 172 | message_id = str(self.message.uuid) 173 | message_type = self.message_type 174 | 175 | return dict(headers=headers, content_type=content_type, priority=priority, message_id=message_id, 176 | type=message_type) 177 | 178 | def publish(self, connection: pika.BlockingConnection, channel: pika.channel.Channel) -> None: 179 | """ 180 | Publishes a message to the channel 181 | """ 182 | kwargs = self.publish_kwargs() 183 | kwargs['properties'] = pika.BasicProperties(**self.properties()) 184 | channel.basic_publish(**kwargs) 185 | connection.close() 186 | 187 | @classmethod 188 | def serialize_arguments(cls, body: str) -> Tuple[tuple, dict]: 189 | """ 190 | Extracts positional and keyword arguments to be sent to a function from the message body 191 | """ 192 | content = json.loads(body) 193 | args = content.get('args', ()) 194 | kwargs = content.get('kwargs', {}) 195 | return args, kwargs 196 | 197 | 198 | class DefaultMessageSerializer(BaseMessageSerializer): 199 | type_header = 'type' 200 | message_type = 'django-carrot message' 201 | 202 | 203 | # noinspection PyUnresolvedReferences 204 | class Message(object): 205 | """ 206 | A message to publish to RabbitMQ. Takes the following parameters: 207 | 208 | .. note:: 209 | Your RabbitMQ queue must support message priority for the *priority* parameter to have any affect. You need to 210 | define the x-max-priority header when creating your RabbitMQ queue to do this. See 211 | `Priority Queue Support `_ for more details. Carrot applies a maximum 212 | priority of **255** by default to all queues it creates automatically. 213 | 214 | .. warning:: 215 | You should not attempt to create instances of this object yourself. You should use the 216 | :func:`carrot.utilities.create_msg` function instead 217 | 218 | """ 219 | 220 | def __init__(self, 221 | task: str, 222 | virtual_host: VirtualHost = None, 223 | queue: str = 'default', 224 | routing_key: str = None, 225 | exchange: str = '', 226 | priority: int = 0, 227 | task_args: tuple = (), 228 | task_kwargs: Union[str, dict] = None) -> None: 229 | 230 | if not task_kwargs or task_kwargs in ['{}', '"{}"']: 231 | task_kwargs = {} 232 | 233 | if not routing_key: 234 | routing_key = queue 235 | 236 | if not virtual_host: 237 | from carrot.utilities import get_host_from_name 238 | virtual_host = get_host_from_name(queue) 239 | 240 | assert isinstance(virtual_host, VirtualHost) 241 | 242 | self.uuid = str(uuid.uuid4()) 243 | 244 | self.virtual_host = virtual_host 245 | self.exchange = exchange 246 | self.queue = queue 247 | self.routing_key = routing_key 248 | self.priority = priority 249 | 250 | self.task = task 251 | self.task_args = task_args 252 | self.task_kwargs = task_kwargs 253 | 254 | self.formatter = DefaultMessageSerializer(self) 255 | 256 | @property 257 | def connection_channel(self) -> Tuple[pika.BlockingConnection, pika.channel.Channel]: 258 | """ 259 | Gets or creates the queue, and returns a tuple containing the object's VirtualHost's blocking connection, 260 | and its channel 261 | """ 262 | connection = self.virtual_host.blocking_connection 263 | channel = connection.channel() 264 | 265 | return connection, channel 266 | 267 | def publish(self, pika_log_level: int = logging.ERROR) -> Any: 268 | """ 269 | Publishes the message to RabbitMQ queue and creates a MessageLog object so the progress of the task can be 270 | tracked in the Django project's database 271 | """ 272 | from carrot.models import MessageLog 273 | logging.getLogger("pika").setLevel(pika_log_level) 274 | connection, channel = self.connection_channel 275 | 276 | if isinstance(self.task_kwargs, str): 277 | try: 278 | json.dumps(self.task_kwargs) 279 | keyword_arguments = self.task_kwargs 280 | except json.decoder.JSONDecodeError: 281 | keyword_arguments = '{}' 282 | else: 283 | keyword_arguments = json.dumps(self.task_kwargs) 284 | 285 | log = MessageLog.objects.create( 286 | status='PUBLISHED', 287 | queue=self.queue, 288 | exchange=self.exchange or '', 289 | routing_key=self.routing_key or self.queue, 290 | uuid=str(self.uuid), 291 | priority=self.priority, 292 | task_args=self.task_args, 293 | content=keyword_arguments, 294 | task=self.task, 295 | publish_time=timezone.now(), 296 | ) 297 | 298 | self.formatter.publish(connection, channel) 299 | return log 300 | -------------------------------------------------------------------------------- /carrot/scheduler.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | from typing import List 4 | 5 | from django.core.exceptions import ObjectDoesNotExist 6 | 7 | from carrot.models import ScheduledTask 8 | 9 | 10 | class ScheduledTaskThread(threading.Thread): 11 | """ 12 | A thread that handles a single :class:`carrot.models.ScheduledTask` object. When started, it waits for the interval 13 | to pass before publishing the task to the required queue 14 | 15 | While waiting for the task to be due for publication, the process continuously monitors the object in the Django 16 | project's database for changes to the interval, task, or arguments, or in case it gets deleted/marked as inactive 17 | and response accordingly 18 | """ 19 | 20 | def __init__(self, 21 | scheduled_task: ScheduledTask, 22 | run_now: bool = False, 23 | **filters) -> None: 24 | threading.Thread.__init__(self) 25 | self.id = scheduled_task.pk 26 | self.queue = scheduled_task.routing_key 27 | self.scheduled_task = scheduled_task 28 | self.run_now = run_now 29 | self.active = True 30 | self.filters = filters 31 | self.inactive_reason = '' 32 | 33 | def run(self) -> None: 34 | """ 35 | Initiates a timer, then once the timer is equal to the ScheduledTask's interval, the scheduler 36 | checks to make sure that the task has not been deactivated/deleted in the mean time, and that the manager 37 | has not been stopped, then publishes it to the queue 38 | """ 39 | interval = self.scheduled_task.multiplier * self.scheduled_task.interval_count 40 | 41 | count = 0 42 | if self.run_now: 43 | self.scheduled_task.publish() 44 | 45 | while True: 46 | while count < interval: 47 | if not self.active: 48 | if self.inactive_reason: 49 | print('Thread stop has been requested because of the following reason: %s.\n Stopping the ' 50 | 'thread' % self.inactive_reason) 51 | 52 | return 53 | 54 | try: 55 | self.scheduled_task = ScheduledTask.objects.get(pk=self.scheduled_task.pk, **self.filters) 56 | interval = self.scheduled_task.multiplier * self.scheduled_task.interval_count 57 | 58 | except ObjectDoesNotExist: 59 | print('Current task has been removed from the queryset. Stopping the thread') 60 | return 61 | 62 | time.sleep(1) 63 | count += 1 64 | 65 | print('Publishing message %s' % self.scheduled_task.task) 66 | self.scheduled_task.publish() 67 | count = 0 68 | 69 | 70 | class ScheduledTaskManager(object): 71 | """ 72 | The main scheduled task manager project. For every active :class:`carrot.models.ScheduledTask`, a 73 | :class:`ScheduledTaskThread` is created and started 74 | 75 | This object exists for the purposes of starting these threads on startup, or when a new ScheduledTask object 76 | gets created, and implements a .stop() method to stop all threads 77 | 78 | """ 79 | 80 | def __init__(self, **options) -> None: 81 | self.threads: List[ScheduledTaskThread] = [] 82 | self.filters = options.pop('filters', {'active': True}) 83 | self.run_now = options.pop('run_now', False) 84 | self.tasks = ScheduledTask.objects.filter(**self.filters) 85 | 86 | def start(self) -> None: 87 | """ 88 | Initiates and starts a scheduler for each given ScheduledTask 89 | """ 90 | print('found %i scheduled tasks to run' % self.tasks.count()) 91 | for t in self.tasks: 92 | print('starting thread for task %s' % t.task) 93 | thread = ScheduledTaskThread(t, self.run_now, **self.filters) 94 | thread.start() 95 | self.threads.append(thread) 96 | 97 | def add_task(self, task: ScheduledTask) -> None: 98 | """ 99 | After the manager has been started, this function can be used to add an additional ScheduledTask starts a 100 | scheduler for it 101 | """ 102 | thread = ScheduledTaskThread(task, self.run_now, **self.filters) 103 | thread.start() 104 | self.threads.append(thread) 105 | 106 | def stop(self) -> None: 107 | """ 108 | Safely stop the manager 109 | """ 110 | print('Attempting to stop %i running threads' % len(self.threads)) 111 | 112 | for t in self.threads: 113 | print('Stopping thread %s' % t) 114 | t.active = False 115 | t.inactive_reason = 'A termination of service was requested' 116 | t.join() 117 | print('thread %s stopped' % t) 118 | -------------------------------------------------------------------------------- /carrot/static/carrot/axios.min.js: -------------------------------------------------------------------------------- 1 | /* axios v0.18.0 | (c) 2018 by Matt Zabriskie */ 2 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.axios=t():e.axios=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){e.exports=n(1)},function(e,t,n){"use strict";function r(e){var t=new s(e),n=i(s.prototype.request,t);return o.extend(n,s.prototype,t),o.extend(n,t),n}var o=n(2),i=n(3),s=n(5),u=n(6),a=r(u);a.Axios=s,a.create=function(e){return r(o.merge(u,e))},a.Cancel=n(23),a.CancelToken=n(24),a.isCancel=n(20),a.all=function(e){return Promise.all(e)},a.spread=n(25),e.exports=a,e.exports.default=a},function(e,t,n){"use strict";function r(e){return"[object Array]"===R.call(e)}function o(e){return"[object ArrayBuffer]"===R.call(e)}function i(e){return"undefined"!=typeof FormData&&e instanceof FormData}function s(e){var t;return t="undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&e.buffer instanceof ArrayBuffer}function u(e){return"string"==typeof e}function a(e){return"number"==typeof e}function c(e){return"undefined"==typeof e}function f(e){return null!==e&&"object"==typeof e}function p(e){return"[object Date]"===R.call(e)}function d(e){return"[object File]"===R.call(e)}function l(e){return"[object Blob]"===R.call(e)}function h(e){return"[object Function]"===R.call(e)}function m(e){return f(e)&&h(e.pipe)}function y(e){return"undefined"!=typeof URLSearchParams&&e instanceof URLSearchParams}function w(e){return e.replace(/^\s*/,"").replace(/\s*$/,"")}function g(){return("undefined"==typeof navigator||"ReactNative"!==navigator.product)&&("undefined"!=typeof window&&"undefined"!=typeof document)}function v(e,t){if(null!==e&&"undefined"!=typeof e)if("object"!=typeof e&&(e=[e]),r(e))for(var n=0,o=e.length;n 6 | * @license MIT 7 | */ 8 | e.exports=function(e){return null!=e&&(n(e)||r(e)||!!e._isBuffer)}},function(e,t,n){"use strict";function r(e){this.defaults=e,this.interceptors={request:new s,response:new s}}var o=n(6),i=n(2),s=n(17),u=n(18);r.prototype.request=function(e){"string"==typeof e&&(e=i.merge({url:arguments[0]},arguments[1])),e=i.merge(o,{method:"get"},this.defaults,e),e.method=e.method.toLowerCase();var t=[u,void 0],n=Promise.resolve(e);for(this.interceptors.request.forEach(function(e){t.unshift(e.fulfilled,e.rejected)}),this.interceptors.response.forEach(function(e){t.push(e.fulfilled,e.rejected)});t.length;)n=n.then(t.shift(),t.shift());return n},i.forEach(["delete","get","head","options"],function(e){r.prototype[e]=function(t,n){return this.request(i.merge(n||{},{method:e,url:t}))}}),i.forEach(["post","put","patch"],function(e){r.prototype[e]=function(t,n,r){return this.request(i.merge(r||{},{method:e,url:t,data:n}))}}),e.exports=r},function(e,t,n){"use strict";function r(e,t){!i.isUndefined(e)&&i.isUndefined(e["Content-Type"])&&(e["Content-Type"]=t)}function o(){var e;return"undefined"!=typeof XMLHttpRequest?e=n(8):"undefined"!=typeof process&&(e=n(8)),e}var i=n(2),s=n(7),u={"Content-Type":"application/x-www-form-urlencoded"},a={adapter:o(),transformRequest:[function(e,t){return s(t,"Content-Type"),i.isFormData(e)||i.isArrayBuffer(e)||i.isBuffer(e)||i.isStream(e)||i.isFile(e)||i.isBlob(e)?e:i.isArrayBufferView(e)?e.buffer:i.isURLSearchParams(e)?(r(t,"application/x-www-form-urlencoded;charset=utf-8"),e.toString()):i.isObject(e)?(r(t,"application/json;charset=utf-8"),JSON.stringify(e)):e}],transformResponse:[function(e){if("string"==typeof e)try{e=JSON.parse(e)}catch(e){}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,validateStatus:function(e){return e>=200&&e<300}};a.headers={common:{Accept:"application/json, text/plain, */*"}},i.forEach(["delete","get","head"],function(e){a.headers[e]={}}),i.forEach(["post","put","patch"],function(e){a.headers[e]=i.merge(u)}),e.exports=a},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t){r.forEach(e,function(n,r){r!==t&&r.toUpperCase()===t.toUpperCase()&&(e[t]=n,delete e[r])})}},function(e,t,n){"use strict";var r=n(2),o=n(9),i=n(12),s=n(13),u=n(14),a=n(10),c="undefined"!=typeof window&&window.btoa&&window.btoa.bind(window)||n(15);e.exports=function(e){return new Promise(function(t,f){var p=e.data,d=e.headers;r.isFormData(p)&&delete d["Content-Type"];var l=new XMLHttpRequest,h="onreadystatechange",m=!1;if("undefined"==typeof window||!window.XDomainRequest||"withCredentials"in l||u(e.url)||(l=new window.XDomainRequest,h="onload",m=!0,l.onprogress=function(){},l.ontimeout=function(){}),e.auth){var y=e.auth.username||"",w=e.auth.password||"";d.Authorization="Basic "+c(y+":"+w)}if(l.open(e.method.toUpperCase(),i(e.url,e.params,e.paramsSerializer),!0),l.timeout=e.timeout,l[h]=function(){if(l&&(4===l.readyState||m)&&(0!==l.status||l.responseURL&&0===l.responseURL.indexOf("file:"))){var n="getAllResponseHeaders"in l?s(l.getAllResponseHeaders()):null,r=e.responseType&&"text"!==e.responseType?l.response:l.responseText,i={data:r,status:1223===l.status?204:l.status,statusText:1223===l.status?"No Content":l.statusText,headers:n,config:e,request:l};o(t,f,i),l=null}},l.onerror=function(){f(a("Network Error",e,null,l)),l=null},l.ontimeout=function(){f(a("timeout of "+e.timeout+"ms exceeded",e,"ECONNABORTED",l)),l=null},r.isStandardBrowserEnv()){var g=n(16),v=(e.withCredentials||u(e.url))&&e.xsrfCookieName?g.read(e.xsrfCookieName):void 0;v&&(d[e.xsrfHeaderName]=v)}if("setRequestHeader"in l&&r.forEach(d,function(e,t){"undefined"==typeof p&&"content-type"===t.toLowerCase()?delete d[t]:l.setRequestHeader(t,e)}),e.withCredentials&&(l.withCredentials=!0),e.responseType)try{l.responseType=e.responseType}catch(t){if("json"!==e.responseType)throw t}"function"==typeof e.onDownloadProgress&&l.addEventListener("progress",e.onDownloadProgress),"function"==typeof e.onUploadProgress&&l.upload&&l.upload.addEventListener("progress",e.onUploadProgress),e.cancelToken&&e.cancelToken.promise.then(function(e){l&&(l.abort(),f(e),l=null)}),void 0===p&&(p=null),l.send(p)})}},function(e,t,n){"use strict";var r=n(10);e.exports=function(e,t,n){var o=n.config.validateStatus;n.status&&o&&!o(n.status)?t(r("Request failed with status code "+n.status,n.config,null,n.request,n)):e(n)}},function(e,t,n){"use strict";var r=n(11);e.exports=function(e,t,n,o,i){var s=new Error(e);return r(s,t,n,o,i)}},function(e,t){"use strict";e.exports=function(e,t,n,r,o){return e.config=t,n&&(e.code=n),e.request=r,e.response=o,e}},function(e,t,n){"use strict";function r(e){return encodeURIComponent(e).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}var o=n(2);e.exports=function(e,t,n){if(!t)return e;var i;if(n)i=n(t);else if(o.isURLSearchParams(t))i=t.toString();else{var s=[];o.forEach(t,function(e,t){null!==e&&"undefined"!=typeof e&&(o.isArray(e)?t+="[]":e=[e],o.forEach(e,function(e){o.isDate(e)?e=e.toISOString():o.isObject(e)&&(e=JSON.stringify(e)),s.push(r(t)+"="+r(e))}))}),i=s.join("&")}return i&&(e+=(e.indexOf("?")===-1?"?":"&")+i),e}},function(e,t,n){"use strict";var r=n(2),o=["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"];e.exports=function(e){var t,n,i,s={};return e?(r.forEach(e.split("\n"),function(e){if(i=e.indexOf(":"),t=r.trim(e.substr(0,i)).toLowerCase(),n=r.trim(e.substr(i+1)),t){if(s[t]&&o.indexOf(t)>=0)return;"set-cookie"===t?s[t]=(s[t]?s[t]:[]).concat([n]):s[t]=s[t]?s[t]+", "+n:n}}),s):s}},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){function e(e){var t=e;return n&&(o.setAttribute("href",t),t=o.href),o.setAttribute("href",t),{href:o.href,protocol:o.protocol?o.protocol.replace(/:$/,""):"",host:o.host,search:o.search?o.search.replace(/^\?/,""):"",hash:o.hash?o.hash.replace(/^#/,""):"",hostname:o.hostname,port:o.port,pathname:"/"===o.pathname.charAt(0)?o.pathname:"/"+o.pathname}}var t,n=/(msie|trident)/i.test(navigator.userAgent),o=document.createElement("a");return t=e(window.location.href),function(n){var o=r.isString(n)?e(n):n;return o.protocol===t.protocol&&o.host===t.host}}():function(){return function(){return!0}}()},function(e,t){"use strict";function n(){this.message="String contains an invalid character"}function r(e){for(var t,r,i=String(e),s="",u=0,a=o;i.charAt(0|u)||(a="=",u%1);s+=a.charAt(63&t>>8-u%1*8)){if(r=i.charCodeAt(u+=.75),r>255)throw new n;t=t<<8|r}return s}var o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";n.prototype=new Error,n.prototype.code=5,n.prototype.name="InvalidCharacterError",e.exports=r},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){return{write:function(e,t,n,o,i,s){var u=[];u.push(e+"="+encodeURIComponent(t)),r.isNumber(n)&&u.push("expires="+new Date(n).toGMTString()),r.isString(o)&&u.push("path="+o),r.isString(i)&&u.push("domain="+i),s===!0&&u.push("secure"),document.cookie=u.join("; ")},read:function(e){var t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove:function(e){this.write(e,"",Date.now()-864e5)}}}():function(){return{write:function(){},read:function(){return null},remove:function(){}}}()},function(e,t,n){"use strict";function r(){this.handlers=[]}var o=n(2);r.prototype.use=function(e,t){return this.handlers.push({fulfilled:e,rejected:t}),this.handlers.length-1},r.prototype.eject=function(e){this.handlers[e]&&(this.handlers[e]=null)},r.prototype.forEach=function(e){o.forEach(this.handlers,function(t){null!==t&&e(t)})},e.exports=r},function(e,t,n){"use strict";function r(e){e.cancelToken&&e.cancelToken.throwIfRequested()}var o=n(2),i=n(19),s=n(20),u=n(6),a=n(21),c=n(22);e.exports=function(e){r(e),e.baseURL&&!a(e.url)&&(e.url=c(e.baseURL,e.url)),e.headers=e.headers||{},e.data=i(e.data,e.headers,e.transformRequest),e.headers=o.merge(e.headers.common||{},e.headers[e.method]||{},e.headers||{}),o.forEach(["delete","get","head","post","put","patch","common"],function(t){delete e.headers[t]});var t=e.adapter||u.adapter;return t(e).then(function(t){return r(e),t.data=i(t.data,t.headers,e.transformResponse),t},function(t){return s(t)||(r(e),t&&t.response&&(t.response.data=i(t.response.data,t.response.headers,e.transformResponse))),Promise.reject(t)})}},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t,n){return r.forEach(n,function(n){e=n(e,t)}),e}},function(e,t){"use strict";e.exports=function(e){return!(!e||!e.__CANCEL__)}},function(e,t){"use strict";e.exports=function(e){return/^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(e)}},function(e,t){"use strict";e.exports=function(e,t){return t?e.replace(/\/+$/,"")+"/"+t.replace(/^\/+/,""):e}},function(e,t){"use strict";function n(e){this.message=e}n.prototype.toString=function(){return"Cancel"+(this.message?": "+this.message:"")},n.prototype.__CANCEL__=!0,e.exports=n},function(e,t,n){"use strict";function r(e){if("function"!=typeof e)throw new TypeError("executor must be a function.");var t;this.promise=new Promise(function(e){t=e});var n=this;e(function(e){n.reason||(n.reason=new o(e),t(n.reason))})}var o=n(23);r.prototype.throwIfRequested=function(){if(this.reason)throw this.reason},r.source=function(){var e,t=new r(function(t){e=t});return{token:t,cancel:e}},e.exports=r},function(e,t){"use strict";e.exports=function(e){return function(t){return e.apply(null,t)}}}])}); 9 | //# sourceMappingURL=axios.min.map -------------------------------------------------------------------------------- /carrot/static/carrot/vuex.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * vuex v3.0.1 3 | * (c) 2017 Evan You 4 | * @license MIT 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.Vuex=e()}(this,function(){"use strict";function t(t){$&&(t._devtoolHook=$,$.emit("vuex:init",t),$.on("vuex:travel-to-state",function(e){t.replaceState(e)}),t.subscribe(function(t,e){$.emit("vuex:mutation",t,e)}))}function e(t,e){Object.keys(t).forEach(function(n){return e(t[n],n)})}function n(t){return null!==t&&"object"==typeof t}function o(t){return t&&"function"==typeof t.then}function i(t,e,n){if(e.update(n),n.modules)for(var o in n.modules){if(!e.getChild(o))return;i(t.concat(o),e.getChild(o),n.modules[o])}}function r(t,e){return e.indexOf(t)<0&&e.push(t),function(){var n=e.indexOf(t);n>-1&&e.splice(n,1)}}function s(t,e){t._actions=Object.create(null),t._mutations=Object.create(null),t._wrappedGetters=Object.create(null),t._modulesNamespaceMap=Object.create(null);var n=t.state;a(t,n,[],t._modules.root,!0),c(t,n,e)}function c(t,n,o){var i=t._vm;t.getters={};var r={};e(t._wrappedGetters,function(e,n){r[n]=function(){return e(t)},Object.defineProperty(t.getters,n,{get:function(){return t._vm[n]},enumerable:!0})});var s=j.config.silent;j.config.silent=!0,t._vm=new j({data:{$$state:n},computed:r}),j.config.silent=s,t.strict&&d(t),i&&(o&&t._withCommit(function(){i._data.$$state=null}),j.nextTick(function(){return i.$destroy()}))}function a(t,e,n,o,i){var r=!n.length,s=t._modules.getNamespace(n);if(o.namespaced&&(t._modulesNamespaceMap[s]=o),!r&&!i){var c=m(e,n.slice(0,-1)),f=n[n.length-1];t._withCommit(function(){j.set(c,f,o.state)})}var d=o.context=u(t,s,n);o.forEachMutation(function(e,n){p(t,s+n,e,d)}),o.forEachAction(function(e,n){var o=e.root?n:s+n,i=e.handler||e;h(t,o,i,d)}),o.forEachGetter(function(e,n){l(t,s+n,e,d)}),o.forEachChild(function(o,r){a(t,e,n.concat(r),o,i)})}function u(t,e,n){var o=""===e,i={dispatch:o?t.dispatch:function(n,o,i){var r=v(n,o,i),s=r.payload,c=r.options,a=r.type;return c&&c.root||(a=e+a),t.dispatch(a,s)},commit:o?t.commit:function(n,o,i){var r=v(n,o,i),s=r.payload,c=r.options,a=r.type;c&&c.root||(a=e+a),t.commit(a,s,c)}};return Object.defineProperties(i,{getters:{get:o?function(){return t.getters}:function(){return f(t,e)}},state:{get:function(){return m(t.state,n)}}}),i}function f(t,e){var n={},o=e.length;return Object.keys(t.getters).forEach(function(i){if(i.slice(0,o)===e){var r=i.slice(o);Object.defineProperty(n,r,{get:function(){return t.getters[i]},enumerable:!0})}}),n}function p(t,e,n,o){(t._mutations[e]||(t._mutations[e]=[])).push(function(e){n.call(t,o.state,e)})}function h(t,e,n,i){(t._actions[e]||(t._actions[e]=[])).push(function(e,r){var s=n.call(t,{dispatch:i.dispatch,commit:i.commit,getters:i.getters,state:i.state,rootGetters:t.getters,rootState:t.state},e,r);return o(s)||(s=Promise.resolve(s)),t._devtoolHook?s.catch(function(e){throw t._devtoolHook.emit("vuex:error",e),e}):s})}function l(t,e,n,o){t._wrappedGetters[e]||(t._wrappedGetters[e]=function(t){return n(o.state,o.getters,t.state,t.getters)})}function d(t){t._vm.$watch(function(){return this._data.$$state},function(){},{deep:!0,sync:!0})}function m(t,e){return e.length?e.reduce(function(t,e){return t[e]},t):t}function v(t,e,o){return n(t)&&t.type&&(o=e,e=t,t=t.type),{type:t,payload:e,options:o}}function _(t){j&&t===j||w(j=t)}function y(t){return Array.isArray(t)?t.map(function(t){return{key:t,val:t}}):Object.keys(t).map(function(e){return{key:e,val:t[e]}})}function g(t){return function(e,n){return"string"!=typeof e?(n=e,e=""):"/"!==e.charAt(e.length-1)&&(e+="/"),t(e,n)}}function b(t,e,n){var o=t._modulesNamespaceMap[n];return o}var w=function(t){function e(){var t=this.$options;t.store?this.$store="function"==typeof t.store?t.store():t.store:t.parent&&t.parent.$store&&(this.$store=t.parent.$store)}if(Number(t.version.split(".")[0])>=2)t.mixin({beforeCreate:e});else{var n=t.prototype._init;t.prototype._init=function(t){void 0===t&&(t={}),t.init=t.init?[e].concat(t.init):e,n.call(this,t)}}},$="undefined"!=typeof window&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__,M=function(t,e){this.runtime=e,this._children=Object.create(null),this._rawModule=t;var n=t.state;this.state=("function"==typeof n?n():n)||{}},O={namespaced:{configurable:!0}};O.namespaced.get=function(){return!!this._rawModule.namespaced},M.prototype.addChild=function(t,e){this._children[t]=e},M.prototype.removeChild=function(t){delete this._children[t]},M.prototype.getChild=function(t){return this._children[t]},M.prototype.update=function(t){this._rawModule.namespaced=t.namespaced,t.actions&&(this._rawModule.actions=t.actions),t.mutations&&(this._rawModule.mutations=t.mutations),t.getters&&(this._rawModule.getters=t.getters)},M.prototype.forEachChild=function(t){e(this._children,t)},M.prototype.forEachGetter=function(t){this._rawModule.getters&&e(this._rawModule.getters,t)},M.prototype.forEachAction=function(t){this._rawModule.actions&&e(this._rawModule.actions,t)},M.prototype.forEachMutation=function(t){this._rawModule.mutations&&e(this._rawModule.mutations,t)},Object.defineProperties(M.prototype,O);var E=function(t){this.register([],t,!1)};E.prototype.get=function(t){return t.reduce(function(t,e){return t.getChild(e)},this.root)},E.prototype.getNamespace=function(t){var e=this.root;return t.reduce(function(t,n){return e=e.getChild(n),t+(e.namespaced?n+"/":"")},"")},E.prototype.update=function(t){i([],this.root,t)},E.prototype.register=function(t,n,o){var i=this;void 0===o&&(o=!0);var r=new M(n,o);0===t.length?this.root=r:this.get(t.slice(0,-1)).addChild(t[t.length-1],r),n.modules&&e(n.modules,function(e,n){i.register(t.concat(n),e,o)})},E.prototype.unregister=function(t){var e=this.get(t.slice(0,-1)),n=t[t.length-1];e.getChild(n).runtime&&e.removeChild(n)};var j,C=function(e){var n=this;void 0===e&&(e={}),!j&&"undefined"!=typeof window&&window.Vue&&_(window.Vue);var o=e.plugins;void 0===o&&(o=[]);var i=e.strict;void 0===i&&(i=!1);var r=e.state;void 0===r&&(r={}),"function"==typeof r&&(r=r()||{}),this._committing=!1,this._actions=Object.create(null),this._actionSubscribers=[],this._mutations=Object.create(null),this._wrappedGetters=Object.create(null),this._modules=new E(e),this._modulesNamespaceMap=Object.create(null),this._subscribers=[],this._watcherVM=new j;var s=this,u=this,f=u.dispatch,p=u.commit;this.dispatch=function(t,e){return f.call(s,t,e)},this.commit=function(t,e,n){return p.call(s,t,e,n)},this.strict=i,a(this,r,[],this._modules.root),c(this,r),o.forEach(function(t){return t(n)}),j.config.devtools&&t(this)},x={state:{configurable:!0}};x.state.get=function(){return this._vm._data.$$state},x.state.set=function(t){},C.prototype.commit=function(t,e,n){var o=this,i=v(t,e,n),r=i.type,s=i.payload,c=(i.options,{type:r,payload:s}),a=this._mutations[r];a&&(this._withCommit(function(){a.forEach(function(t){t(s)})}),this._subscribers.forEach(function(t){return t(c,o.state)}))},C.prototype.dispatch=function(t,e){var n=this,o=v(t,e),i=o.type,r=o.payload,s={type:i,payload:r},c=this._actions[i];if(c)return this._actionSubscribers.forEach(function(t){return t(s,n.state)}),c.length>1?Promise.all(c.map(function(t){return t(r)})):c[0](r)},C.prototype.subscribe=function(t){return r(t,this._subscribers)},C.prototype.subscribeAction=function(t){return r(t,this._actionSubscribers)},C.prototype.watch=function(t,e,n){var o=this;return this._watcherVM.$watch(function(){return t(o.state,o.getters)},e,n)},C.prototype.replaceState=function(t){var e=this;this._withCommit(function(){e._vm._data.$$state=t})},C.prototype.registerModule=function(t,e,n){void 0===n&&(n={}),"string"==typeof t&&(t=[t]),this._modules.register(t,e),a(this,this.state,t,this._modules.get(t),n.preserveState),c(this,this.state)},C.prototype.unregisterModule=function(t){var e=this;"string"==typeof t&&(t=[t]),this._modules.unregister(t),this._withCommit(function(){var n=m(e.state,t.slice(0,-1));j.delete(n,t[t.length-1])}),s(this)},C.prototype.hotUpdate=function(t){this._modules.update(t),s(this,!0)},C.prototype._withCommit=function(t){var e=this._committing;this._committing=!0,t(),this._committing=e},Object.defineProperties(C.prototype,x);var k=g(function(t,e){var n={};return y(e).forEach(function(e){var o=e.key,i=e.val;n[o]=function(){var e=this.$store.state,n=this.$store.getters;if(t){var o=b(this.$store,0,t);if(!o)return;e=o.context.state,n=o.context.getters}return"function"==typeof i?i.call(this,e,n):e[i]},n[o].vuex=!0}),n}),G=g(function(t,e){var n={};return y(e).forEach(function(e){var o=e.key,i=e.val;n[o]=function(){for(var e=[],n=arguments.length;n--;)e[n]=arguments[n];var o=this.$store.commit;if(t){var r=b(this.$store,0,t);if(!r)return;o=r.context.commit}return"function"==typeof i?i.apply(this,[o].concat(e)):o.apply(this.$store,[i].concat(e))}}),n}),S=g(function(t,e){var n={};return y(e).forEach(function(e){var o=e.key,i=e.val;i=t+i,n[o]=function(){if(!t||b(this.$store,0,t))return this.$store.getters[i]},n[o].vuex=!0}),n}),A=g(function(t,e){var n={};return y(e).forEach(function(e){var o=e.key,i=e.val;n[o]=function(){for(var e=[],n=arguments.length;n--;)e[n]=arguments[n];var o=this.$store.dispatch;if(t){var r=b(this.$store,0,t);if(!r)return;o=r.context.dispatch}return"function"==typeof i?i.apply(this,[o].concat(e)):o.apply(this.$store,[i].concat(e))}}),n});return{Store:C,install:_,version:"3.0.1",mapState:k,mapMutations:G,mapGetters:S,mapActions:A,createNamespacedHelpers:function(t){return{mapState:k.bind(null,t),mapGetters:S.bind(null,t),mapMutations:G.bind(null,t),mapActions:A.bind(null,t)}}}}); -------------------------------------------------------------------------------- /carrot/static/carrot/white-carrot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/carrot/static/carrot/white-carrot.png -------------------------------------------------------------------------------- /carrot/test_carrot.py: -------------------------------------------------------------------------------- 1 | from carrot import __version__ 2 | 3 | 4 | def test_version(): 5 | assert __version__ == '1.4.1' 6 | -------------------------------------------------------------------------------- /carrot/tests.py: -------------------------------------------------------------------------------- 1 | import mock 2 | import logging 3 | from carrot.mocks import MessageSerializer, Connection, Properties 4 | from django.test import TestCase, RequestFactory 5 | from django.test.utils import override_settings 6 | 7 | from carrot.consumer import Consumer, ConsumerSet 8 | from carrot.objects import VirtualHost 9 | from carrot.models import MessageLog, ScheduledTask 10 | from carrot.api import (failed_message_log_viewset, detail_message_log_viewset, scheduled_task_detail, 11 | scheduled_task_viewset, task_list, validate_args, run_scheduled_task) 12 | 13 | from carrot.utilities import (get_host_from_name, validate_task, create_scheduled_task, decorate_class_view, 14 | decorate_function_view, purge_queue) 15 | from django.core.exceptions import ObjectDoesNotExist 16 | from carrot.views import MessageList 17 | 18 | logger = logging.getLogger('carrot') 19 | 20 | 21 | def test_task(*args, **kwargs): 22 | logger.info('test') 23 | return 24 | 25 | 26 | def dict_task(*args, **kwargs): 27 | return {'blah': True} 28 | 29 | 30 | def failing_task(*args, **kwargs): 31 | raise Exception('test') 32 | 33 | 34 | def mock_connection(*args, **kwargs): 35 | return Connection 36 | 37 | 38 | def mock_consumer(*args, **kwargs): 39 | from carrot.mocks import Consumer as MockConsumer 40 | return MockConsumer 41 | 42 | 43 | class CarrotTestCase(TestCase): 44 | @mock.patch('pika.SelectConnection', new_callable=mock_connection) 45 | @mock.patch('pika.BlockingConnection', new_callable=mock_connection) 46 | def test_consumer(self, *args): 47 | consumer = Consumer(VirtualHost('amqp://guest:guest@localhost:5672/test'), 'test', logger, 'test') 48 | consumer.task_log = ['blah'] 49 | log = MessageLog.objects.create(task='carrot.tests.test_task', uuid=1234, status='PUBLISHED', task_args='()') 50 | 51 | # consumer.get_task_type({'type': 'carrot.tests.test_task'}, None) 52 | p = Properties() 53 | self.assertEqual(consumer.get_message_log(p, None), log) 54 | 55 | p.message_id = 4321 56 | consumer.get_message_log(p, None) 57 | 58 | consumer.fail(log, 'test error') 59 | 60 | consumer.connection = consumer.connect() 61 | consumer.run() 62 | consumer.reconnect() 63 | 64 | consumer.on_connection_open(consumer.connection) 65 | 66 | consumer.channel = consumer.connection.channel 67 | consumer.on_channel_open(consumer.channel) 68 | 69 | consumer.on_exchange_declare() 70 | consumer.on_queue_declare() 71 | consumer.on_bind() 72 | 73 | p.message_id = 1234 74 | 75 | consumer.on_message(consumer.channel, p, p, b'{}') 76 | 77 | log.status = 'PUBLISHED' 78 | log.save() 79 | 80 | consumer.on_message(consumer.channel, p, p, b'{}') 81 | consumer.on_channel_closed(consumer.channel, 1, 'blah') 82 | 83 | p.headers = {'type':'carrot.tests.test_task'} 84 | log.delete() 85 | log = MessageLog.objects.create(task='carrot.tests.test_task', uuid=1234, status='PUBLISHED', task_args='()') 86 | self.assertEqual(str(log), 'carrot.tests.test_task') 87 | consumer.on_message(consumer.channel, p, p, b'{}') 88 | 89 | log.delete() 90 | 91 | p.headers = {'type': 'carrot.tests.dict_task'} 92 | log = MessageLog.objects.create(task='carrot.tests.dict_task', uuid=1234, status='PUBLISHED', task_args='()') 93 | consumer.on_message(consumer.channel, p, p, b'{}') 94 | 95 | log.delete() 96 | p.headers = {'type':'carrot.tests.failing_task'} 97 | log = MessageLog.objects.create(task='carrot.tests.failing_task', uuid=1234, status='PUBLISHED', task_args='()') 98 | consumer.on_message(consumer.channel, p, p, b'{}') 99 | 100 | log.delete() 101 | log = MessageLog.objects.create(task='carrot.tests.test_task', uuid=1234, status='PUBLISHED', task_args='()') 102 | consumer.serializer = MessageSerializer(log) 103 | consumer.on_message(consumer.channel, p, p, b'{}') 104 | 105 | log.delete() 106 | log = MessageLog.objects.create(task='carrot.tests.test_task', uuid=1234, status='PUBLISHED', task_args='()') 107 | consumer.serializer.failing_method = 'get_task' 108 | consumer.on_message(consumer.channel, p, p, b'{}') 109 | 110 | consumer.active_message_log = log 111 | consumer.on_consumer_cancelled(1) 112 | 113 | consumer.stop() 114 | consumer.on_cancel() 115 | consumer.channel = None 116 | 117 | consumer.stop() 118 | 119 | consumer.close_connection() 120 | consumer.on_channel_closed(consumer.channel, 1, 'blah') 121 | consumer.on_connection_closed(consumer.connection) 122 | 123 | consumer.shutdown_requested = True 124 | 125 | consumer.on_channel_closed(consumer.channel, 1, 'blah') 126 | consumer.on_connection_closed(consumer.connection) 127 | 128 | @mock.patch('carrot.consumer.Consumer', new_callable=mock_consumer) 129 | @mock.patch('pika.BlockingConnection', new_callable=mock_connection) 130 | def test_consumer_set(self, *args): 131 | alt_settings = { 132 | 'queues': [{ 133 | 'name': 'test', 134 | 'durable': True, 135 | 'queue_arguments': {'blah': True}, 136 | 'exchange_arguments': {'blah': True}, 137 | }] 138 | } 139 | with override_settings(CARROT=alt_settings): 140 | cs = ConsumerSet(VirtualHost('amqp://guest:guest@localhost:5672/test'), 'test', logger) 141 | 142 | cs.start_consuming() 143 | 144 | cs.stop_consuming() 145 | 146 | @mock.patch('pika.BlockingConnection', new_callable=mock_connection) 147 | def test_api(self, *args): 148 | MessageLog.objects.create(task='carrot.tests.test_task', uuid=1234, status='FAILED', task_args='()') 149 | 150 | f = RequestFactory() 151 | r = f.delete('/api/message-logs/failed') 152 | 153 | failed_message_log_viewset(r) 154 | self.assertEqual(MessageLog.objects.filter(status='FAILED').count(), 0) 155 | r = f.get('/api/message-logs/failed') 156 | response = failed_message_log_viewset(r) 157 | self.assertEqual(response.data.get('count'), 0) 158 | 159 | MessageLog.objects.create(task='carrot.tests.test_task', uuid=1234, status='FAILED', task_args='()') 160 | r = f.put('/api/message-logs/failed') 161 | failed_message_log_viewset(r) 162 | 163 | log = MessageLog.objects.create(task='carrot.tests.test_task', uuid=1234, status='COMPLETED', task_args='()') 164 | r = f.delete('/api/message-logs/%s/' % log.pk) 165 | detail_message_log_viewset(r, pk=log.pk) 166 | 167 | log = MessageLog.objects.create(task='carrot.tests.test_task', uuid=1234, status='FAILED', task_args='()') 168 | r = f.put('/api/message-logs/%s/' % log.pk) 169 | detail_message_log_viewset(r, pk=log.pk) 170 | 171 | data = { 172 | 'task': 'carrot.tests.test_task', 173 | 'interval_count': 1, 174 | 'active': True, 175 | 'queue': 'test', 176 | 'interval_type': 'hours', 177 | 'task_args': '(True,)', 178 | 'content': '{"blah": true}' 179 | 180 | } 181 | alt_settings = { 182 | 'task_modules': ['carrot.tests', 'invalid.module'] 183 | } 184 | with override_settings(CARROT=alt_settings): 185 | 186 | r = f.post('/api/scheduled-tasks', data) 187 | response = scheduled_task_viewset(r, data) 188 | 189 | data['interval_count'] = 2 190 | data['task'] = 'carrot.tests.something_invalid' 191 | r = f.patch('/api/scheduled-tasks/%s' % response.data.get('pk'), data) 192 | scheduled_task_detail(r, pk=response.data.get('pk')) 193 | 194 | r = f.get('/api/scheduled-tasks/1/run/') 195 | run_scheduled_task(r, pk=1) 196 | 197 | r = f.get('/api/scheduled-tasks/task-choices/') 198 | task_list(r) 199 | 200 | data = {'args': '()'} 201 | r = f.post('/api/scheduled-tasks/validate-args/', data) 202 | validate_args(r, data) 203 | 204 | data = {'args': 'some utter bollocks'} 205 | r = f.post('/api/scheduled-tasks/validate-args/', data) 206 | response = validate_args(r, data) 207 | print(response.data) 208 | self.assertGreater(len(response.data['errors']), 0) 209 | 210 | def test_utilities(self): 211 | with self.assertRaises(Exception): 212 | get_host_from_name('test') 213 | 214 | alt_settings = { 215 | 'queues': [ 216 | { 217 | 'name': 'test', 218 | 'host': 'amqp://guest:guest@localhost:5672/' 219 | } 220 | ] 221 | } 222 | with override_settings(CARROT=alt_settings): 223 | get_host_from_name('test') 224 | 225 | with self.assertRaises(ImportError): 226 | validate_task('some.invalid.task') 227 | 228 | with self.assertRaises(AttributeError): 229 | validate_task('carrot.tests.invalid_function') 230 | 231 | validate_task(test_task) 232 | 233 | task = create_scheduled_task(task='carrot.tests.test_task', interval={'days':1}) 234 | 235 | self.assertEqual(task.multiplier, 86400) 236 | task.interval_type = 'hours' 237 | self.assertEqual(task.multiplier, 3600) 238 | task.interval_type = 'minutes' 239 | self.assertEqual(task.multiplier, 60) 240 | task.interval_type = 'seconds' 241 | self.assertEqual(task.multiplier, 1) 242 | 243 | with mock.patch('carrot.utilities.publish_message'): 244 | task.publish() 245 | 246 | self.assertTrue(isinstance(task, ScheduledTask)) 247 | 248 | with self.assertRaises(AttributeError): 249 | create_scheduled_task(task='carrot.tests.test_task', interval=None) 250 | 251 | decorate_class_view(MessageList, ['django.contrib.auth.decorators.login_required']) 252 | decorate_class_view(MessageList) 253 | 254 | decorate_function_view(failed_message_log_viewset, ['django.contrib.auth.decorators.login_required']) 255 | 256 | def test_purge(self): 257 | MessageLog.objects.create( 258 | status='IN_PROGRESS', 259 | uuid='2c2eef00-689f-4478-ba59-2d17d1fcb23f', 260 | task='some.invalid.task', 261 | ) 262 | 263 | with mock.patch('pika.BlockingConnection', new_callable=mock_connection): 264 | purge_queue() 265 | 266 | with self.assertRaises(ObjectDoesNotExist): 267 | MessageLog.objects.get(uuid='2c2eef00-689f-4478-ba59-2d17d1fcb23f') 268 | 269 | self.assertEqual(MessageLog.objects.filter(status__in=['IN_PROGRESS', 'PUBLISHED']).count(), 0) 270 | 271 | -------------------------------------------------------------------------------- /carrot/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from carrot.views import MessageList 3 | from carrot.utilities import decorate_class_view, decorate_function_view, create_class_view 4 | from django.conf import settings 5 | from carrot.api import ( 6 | published_message_log_viewset, failed_message_log_viewset, completed_message_log_viewset, scheduled_task_viewset, 7 | detail_message_log_viewset, scheduled_task_detail, run_scheduled_task, task_list, validate_args, purge_messages, 8 | MessageLogViewset, requeue_pending 9 | ) 10 | from typing import Any 11 | 12 | try: 13 | decorators = settings.CARROT.get('monitor_authentication', []) 14 | except AttributeError: 15 | decorators = [] 16 | 17 | 18 | def _(v: Any, **kwargs) -> Any: 19 | """ 20 | Decorates a class based view with a custom auth decorator specified in the settings module 21 | """ 22 | return decorate_class_view(v, decorators).as_view(**kwargs) 23 | 24 | 25 | def _f(v: MessageLogViewset) -> Any: 26 | """ 27 | The same as the above _ method, but for function-based views 28 | """ 29 | return decorate_function_view(v, decorators) 30 | 31 | 32 | urlpatterns = [ 33 | url(r'^$', _(MessageList), name='carrot-monitor'), 34 | url(r'^api/message-logs/published/$', _f(published_message_log_viewset), name='published-messagelog'), 35 | url(r'^api/message-logs/failed/$', _f(failed_message_log_viewset)), 36 | url(r'^api/message-logs/purge/$', _f(purge_messages)), 37 | url(r'^api/message-logs/requeue/$', _f(requeue_pending)), 38 | url(r'^api/message-logs/completed/$', _f(completed_message_log_viewset)), 39 | url(r'^api/message-logs/(?P[0-9]+)/$', _f(detail_message_log_viewset)), 40 | url(r'^api/scheduled-tasks/$', _f(scheduled_task_viewset)), 41 | url(r'^api/scheduled-tasks/task-choices/$', _f(task_list)), 42 | url(r'^api/scheduled-tasks/validate-args/$', _f(validate_args)), 43 | url(r'^api/scheduled-tasks/(?P[0-9]+)/$', _f(scheduled_task_detail)), 44 | url(r'^api/scheduled-tasks/(?P[0-9]+)/run/$', _f(run_scheduled_task)), 45 | ] 46 | -------------------------------------------------------------------------------- /carrot/utilities.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains a number of helper functions for performing basic Carrot functions, e.g. publish, schedule and 3 | consume 4 | 5 | Most users should use the functions defined in this module, rather than attempting to subclass the base level objects 6 | 7 | """ 8 | import json 9 | import importlib 10 | from django.conf import settings 11 | from carrot.objects import VirtualHost, Message 12 | from carrot.models import ScheduledTask, MessageLog 13 | from django.utils.decorators import method_decorator 14 | from carrot import DEFAULT_BROKER 15 | from carrot.exceptions import CarrotConfigException 16 | from django.db.utils import IntegrityError 17 | from typing import Dict, List, Union, Callable, Type, Any 18 | 19 | 20 | def get_host_from_name(name: str) -> VirtualHost: 21 | """ 22 | Gets a host object from a given queue name based on the Django configuration 23 | 24 | If no queue name is provided (as may be the case from some callers), this function returns a VirtualHost based on 25 | the CARROT.default_broker value. 26 | 27 | May raise an exception if the given queue name is not registered in the settings. 28 | """ 29 | try: 30 | carrot_settings = settings.CARROT 31 | except AttributeError: 32 | carrot_settings = { 33 | 'default_broker': DEFAULT_BROKER, 34 | 'queues' : [ 35 | { 36 | 'name': 'default', 37 | 'host': DEFAULT_BROKER 38 | } 39 | ] 40 | } 41 | 42 | try: 43 | if not name: 44 | try: 45 | conf = carrot_settings.get('default_broker', {}) 46 | except AttributeError: 47 | conf = {} 48 | 49 | if not conf: 50 | conf = {'url': DEFAULT_BROKER} 51 | elif isinstance(conf, str): 52 | conf = {'url': conf} 53 | 54 | return VirtualHost(**conf) 55 | 56 | queues = carrot_settings.get('queues', []) 57 | queue_host = list(filter(lambda queue: queue['name'] == name, queues))[0]['host'] 58 | try: 59 | vhost = VirtualHost(**queue_host) 60 | except TypeError: 61 | vhost = VirtualHost(url=queue_host) 62 | 63 | return vhost 64 | 65 | except IndexError: 66 | raise CarrotConfigException('Cannot find queue called %s in settings.CARROT queue list' % name) 67 | 68 | 69 | def validate_task(task: Union[str, Callable]) -> str: 70 | """ 71 | Helper function for dealing with task inputs which may either be a callable, or a path to a callable as a string 72 | 73 | In case of a string being provided, this function checks whether the import path leads to a valid callable 74 | 75 | Otherwise, the callable is converted back into a string (as the :class:`carrot.objects.Message` requires a string 76 | input) 77 | 78 | This function is used by the following other utility functions: 79 | - :func:`.create_scheduled_task` 80 | - :func:`.create_message` 81 | 82 | """ 83 | mod, fname = (None,) * 2 84 | 85 | if isinstance(task, str): 86 | try: 87 | fname = task.split('.')[-1] 88 | mod = '.'.join(task.split('.')[:-1]) 89 | module = importlib.import_module(mod) 90 | getattr(module, fname) 91 | except ImportError as err: 92 | raise ImportError('Unable to find the module: %s' % err) 93 | 94 | except AttributeError as err: 95 | raise AttributeError('Unable to find a function called %s in the module %s: %s' % (fname, mod, err)) 96 | else: 97 | # noinspection PyUnresolvedReferences 98 | task = '%s.%s' % (task.__module__, task.__name__) 99 | 100 | return task 101 | 102 | 103 | def create_message(task: Union[str, Callable], 104 | queue: str, 105 | priority: int = 0, 106 | task_args: tuple = (), 107 | exchange: str = '', 108 | routing_key: str = None, 109 | task_kwargs: dict = None 110 | ) -> Message: 111 | """ 112 | Creates a :class:`carrot.objects.Message` object without publishing it 113 | 114 | The task to execute (as a string or a callable) needs to be supplied. All other arguments are optional 115 | """ 116 | 117 | if not task_kwargs: 118 | task_kwargs = {} 119 | 120 | task = validate_task(task) 121 | 122 | vhost = get_host_from_name(queue) 123 | msg = Message(virtual_host=vhost, queue=queue, routing_key=routing_key, exchange=exchange, task=task, 124 | priority=priority, task_args=task_args, task_kwargs=task_kwargs) 125 | 126 | return msg 127 | 128 | 129 | def publish_message(task: Union[str, Callable], 130 | *task_args, 131 | priority: int = 0, 132 | queue: str = None, 133 | exchange: str = '', 134 | routing_key: str = None, 135 | **task_kwargs) -> MessageLog: 136 | """ 137 | Wrapped for :func:`.create_message`, which publishes the task to the queue 138 | 139 | This function is the primary method of publishing tasks to a message queue 140 | """ 141 | if not queue: 142 | queue = 'default' 143 | msg = create_message(task, queue, priority, task_args, exchange, routing_key, task_kwargs) 144 | return msg.publish() 145 | 146 | 147 | def create_scheduled_task(task: Union[str, Callable], 148 | interval: Dict[str, int], 149 | task_name: str = None, 150 | queue: str = None, 151 | **kwargs) -> ScheduledTask: 152 | """ 153 | Helper function for creating a :class:`carrot.models.ScheduledTask` 154 | """ 155 | 156 | if not task_name: 157 | if isinstance(task, str): 158 | task_name = task 159 | else: 160 | raise Exception('You must provide a task_name or task') 161 | 162 | task = validate_task(task) 163 | 164 | try: 165 | assert isinstance(interval, dict) 166 | assert len(interval.items()) == 1 167 | except AssertionError: 168 | raise AttributeError('Interval must be a dict with a single key value pairing, e.g.: {\'seconds\': 5}') 169 | 170 | interval_type, count = list(*interval.items()) 171 | 172 | try: 173 | t = ScheduledTask.objects.create( 174 | queue=queue, 175 | task_name=task_name, 176 | interval_type=interval_type, 177 | interval_count=count, 178 | routing_key=queue, 179 | task=task, 180 | content=json.dumps(kwargs or '{}'), 181 | ) 182 | except IntegrityError: 183 | raise IntegrityError('A ScheduledTask with this task_name already exists. Please specific a unique name using ' 184 | 'the task_name parameter') 185 | 186 | return t 187 | 188 | 189 | def get_mixin(decorator: Callable) -> Type[object]: 190 | """ 191 | Helper function that allows dynamic application of decorators to a class-based views 192 | 193 | :param func decorator: the decorator to apply to the view 194 | """ 195 | 196 | class Mixin(object): 197 | @method_decorator(decorator) 198 | def dispatch(self, request, *args, **kwargs): 199 | return super(Mixin, self).dispatch(request, *args, **kwargs) 200 | 201 | return Mixin 202 | 203 | 204 | def create_class_view(view: Any, decorator: Any) -> object: 205 | """ 206 | Applies a decorator to the dispatch method of a given class based view. Can be chained 207 | """ 208 | mixin: Any = get_mixin(decorator) 209 | base_view: Any = view 210 | 211 | class DecoratedView(mixin, base_view): 212 | pass 213 | 214 | return DecoratedView 215 | 216 | 217 | def decorate_class_view(view_class: object, 218 | decorators: List[str] = None) -> Any: 219 | """ 220 | Loop through a list of string paths to decorator functions, and call :func:`.create_class_view` for each one 221 | """ 222 | if decorators is None: 223 | decorators = [] 224 | 225 | for decorator in decorators: 226 | _module = '.'.join(decorator.split('.')[:-1]) 227 | module = importlib.import_module(_module) 228 | _decorator = getattr(module, decorator.split('.')[-1]) 229 | view_class = create_class_view(view_class, _decorator) 230 | 231 | return view_class 232 | 233 | 234 | def create_function_view(view: Callable, decorator: Callable) -> Callable: 235 | """ 236 | Similar to :func:`.create_class_view`, but attaches a decorator to a function based view, instead of a class-based 237 | one 238 | """ 239 | 240 | @decorator 241 | def wrap(request, *args, **kwargs): 242 | return view(request, *args, **kwargs) 243 | 244 | return wrap 245 | 246 | 247 | def decorate_function_view(view: Any, decorators: List[str] = None) -> Any: 248 | """ 249 | Similar to :func:`.decorate_class_view`, but for function based views 250 | """ 251 | if not decorators: 252 | decorators = [] 253 | 254 | for decorator in decorators: 255 | _module = '.'.join(decorator.split('.')[:-1]) 256 | module = importlib.import_module(_module) 257 | _decorator = getattr(module, decorator.split('.')[-1]) 258 | view = create_function_view(view, _decorator) 259 | 260 | return view 261 | 262 | 263 | # noinspection PyUnresolvedReferences 264 | def purge_queue() -> None: 265 | """ 266 | Deletes all MessageLog objects with status `IN_PROGRESS` or `PUBLISHED` add iterate through and purge all RabbitMQ 267 | queues 268 | """ 269 | queued_messages = MessageLog.objects.filter(status__in=['IN_PROGRESS', 'PUBLISHED']) 270 | queued_messages.delete() 271 | 272 | try: 273 | carrot_settings = settings.CARROT 274 | except AttributeError: 275 | carrot_settings = { 276 | 'default_broker': DEFAULT_BROKER, 277 | } 278 | 279 | queues = carrot_settings.get('queues', [{'name': 'default', 'host': DEFAULT_BROKER}]) 280 | for queue in queues: 281 | if type(queue['host']) is str: 282 | filters = {'url': queue['host']} 283 | else: 284 | filters = queue['host'] 285 | host = VirtualHost(**filters) 286 | channel = host.blocking_connection.channel() 287 | channel.queue_purge(queue=queue['name']) 288 | 289 | 290 | def requeue_all() -> None: 291 | """ 292 | Requeues all pending MessageLogs 293 | """ 294 | logs = MessageLog.objects.filter(status__in=['IN_PROGRESS', 'PUBLISHED']) 295 | 296 | for log in logs: 297 | log.requeue() 298 | -------------------------------------------------------------------------------- /carrot/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import TemplateView 2 | 3 | 4 | class MessageList(TemplateView): 5 | template_name = 'carrot/index.vue' 6 | 7 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # The lightweight task queue for Django 2 | 3 | 4 | django-carrot is a lightweight task queue backend for Django projects that uses the RabbitMQ message broker, with an 5 | emphasis on quick and easy configuration and task tracking 6 | 7 | ## Features 8 | 9 | - Minimal configuration required 10 | - Task scheduling 11 | - Task prioritization 12 | - Detail task level monitoring/logging via django-carrot monitor 13 | - Built in daemon 14 | - Supports Django 2.0 15 | 16 | 17 | ![Carrot monitor](images/1.0/monitor.png "Carrot monitor") 18 | 19 | 20 | Click [here](quick-start.md) to get started 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal 2 | google_analytics: UA-108134557-1 3 | logo: images/carrot-logo.png 4 | title: django-carrot 5 | description: Documentation site for django-carrot 6 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% seo %} 9 | 10 | 13 | 14 | 15 |
16 |
17 | 18 | 19 | {% if site.logo %} 20 | Logo 21 | {% endif %} 22 | 23 |

{{ site.description | default: site.github.project_tagline }}

24 | 25 | {% if site.github.is_project_page %} 26 |

View the Project on GitHub {{ site.github.repository_nwo }}

27 | {% endif %} 28 | 29 |

View the Project on GitHub {{ site.github.repository_nwo }}

30 | 31 | {% if site.github.is_user_page %} 32 |

View My GitHub Profile

33 | {% endif %} 34 | 35 |

Getting started

36 |

The django-carrot monitor

37 |

Release notes

38 |

The django-carrot service

39 |

Django settings

40 | 41 |
42 |
43 | 44 | {{ content }} 45 | 46 |
47 | 53 |
54 | 55 | {% if site.google_analytics %} 56 | 64 | {% endif %} 65 | 66 | -------------------------------------------------------------------------------- /docs/images/0.2/task-logging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/docs/images/0.2/task-logging.png -------------------------------------------------------------------------------- /docs/images/0.5/carrot-monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/docs/images/0.5/carrot-monitor.png -------------------------------------------------------------------------------- /docs/images/0.5/monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/docs/images/0.5/monitor.png -------------------------------------------------------------------------------- /docs/images/1.0/create-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/docs/images/1.0/create-new.png -------------------------------------------------------------------------------- /docs/images/1.0/monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/docs/images/1.0/monitor.png -------------------------------------------------------------------------------- /docs/images/1.0/task-logging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/docs/images/1.0/task-logging.png -------------------------------------------------------------------------------- /docs/images/1.0/with-task-modules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/docs/images/1.0/with-task-modules.png -------------------------------------------------------------------------------- /docs/images/carrot-logo-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/docs/images/carrot-logo-big.png -------------------------------------------------------------------------------- /docs/images/carrot-logo-inline-inverse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/docs/images/carrot-logo-inline-inverse.png -------------------------------------------------------------------------------- /docs/images/carrot-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/docs/images/carrot-logo.png -------------------------------------------------------------------------------- /docs/images/no-task-modules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/docs/images/no-task-modules.png -------------------------------------------------------------------------------- /docs/images/with-task-modules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/docs/images/with-task-modules.png -------------------------------------------------------------------------------- /docs/monitor.md: -------------------------------------------------------------------------------- 1 | # django-carrot monitor 2 | 3 | 4 | ![django-carrot monitor](/images/1.0/monitor.png "django-carrot monitor") 5 | 6 | 7 | ## Introduction 8 | 9 | django-carrot provides a simple interface for managing `carrot.models.MessageLog` and 10 | `carrot.models.ScheduledTask` objects, known as the **django-carrot monitor**. This interface offers the 11 | following functionality: 12 | 13 | - Monitoring of tasks currently in the queue 14 | - Viewing the log and traceback of failed tasks, and deleting/requeuing them 15 | - Viewing the log for tasks that have completed successfully 16 | - Allows users to view, edit and create scheduled tasks 17 | - On demand publishing of scheduled tasks 18 | 19 | For each task, the monitor displays: 20 | 21 | - Basic information about the task, e.g. the virtualhost and queue it has been published to, the priority, and 22 | the dates/times it was published/completed/failed 23 | - The arguments and keyword arguments the task was called with 24 | - Where applicable, the task log, output and error traceback information 25 | 26 | 27 | ## Configuration 28 | 29 | To enable the django-carrot monitor, simply add the URLs to your project's main urls.py file: 30 | 31 | ```python 32 | from django.urls import path, include 33 | urlpatterns = [ 34 | #... 35 | path('carrot/', include('django-carrot.urls')), 36 | ] 37 | ``` 38 | 39 | You will now be able to see the monitor at the path you have specified, eg: http://localhost:8000/carrot/ 40 | 41 | In order to create scheduled tasks using django-carrot monitor, it is also recommended that you specify your task 42 | modules in your Django project's settings module. This is done as follows: 43 | 44 | ```python 45 | CARROT = { 46 | #... 47 | 'task_modules': ['my_app.my_tasks_module'], 48 | } 49 | ``` 50 | 51 | ### Authentication 52 | 53 | By default, the django-carrot monitor interface is public. However, you can set authentication decorators from your 54 | Django project's settings module: 55 | 56 | ```python 57 | CARROT = { 58 | #... 59 | 'monitor_authentication': ['django.contrib.auth.decorators.login_required'], 60 | } 61 | ``` 62 | 63 | The above uses Django's built it :func:`django.contrib.auth.decorators.login_required` decorator to ensure that all 64 | users are logged in before attempting to access the monitor. You can also specify your own decorators here. 65 | 66 | ## Usage 67 | 68 | Once configured, the monitor can be access from the path ``/carrot``, e.g. ``http://localhost:8000/carrot`` 69 | 70 | The monitor has 4 tabbed views: 71 | 72 | ### Queued tasks 73 | 74 | This view shows all tasks that are currently in the queue and will be processed by the consumer. To see more details about a particular task, click on the relevant row in the list. You will be able to see more details about the task, including where/when it is/was published 75 | 76 | ### Failed tasks 77 | 78 | This view shows all tasks that have failed during processing, along with the full log up to the failure, and a full traceback of the issue. Failed tasks can either be requeued or deleted from the queue, either in bulk or individually 79 | 80 | ### Completed tasks 81 | 82 | Once tasks have been completed, they will appear in this section. At this point, the full log becomes available. You can use the drop down in the monitor to customize the level of visible logging. 83 | 84 | ### Scheduled tasks 85 | 86 | You can manage scheduled tasks in this view. 87 | 88 | Use the **Create new** button to schedule tasks to run at a given interval. The *task*, *queue*, *interval type* and *interval count* fields are mandatory. You can use the *active* slider to temporary prevent a scheduled task from running. 89 | 90 | ![creating scheduled tasks](/images/1.0/create-new.png "creating scheduled tasks") 91 | 92 | 93 | The *positional arguments* field must contain a valid list of python arguments. Here are some valid examples of input for this field: 94 | 95 | ```python 96 | True, 1, 'test', {'foo': 'bar'} 97 | ``` 98 | 99 | 100 | The *keyword arguments* field must contain valid json serializable content. For example: 101 | 102 | ```json 103 | { 104 | "parameter_1": true, 105 | "parameter_2": null, 106 | "parameter_3": ["list", "of", "things"], 107 | "parameter_4": { 108 | "more": "things" 109 | } 110 | } 111 | ``` 112 | 113 | > Keep in mind that: 114 | - The *keyword arguments* input must be JSON serializable - NOT all Python objects are supported by RabbitMQ 115 | - All task lists are refreshed every 10 seconds, or when certain actions are performed, e.g. on task deletion/requeue 116 | - Task logs are not available until a task completes or fails. This is because the task log only gets written to your Django project's database at the end of the process 117 | - *New in 0.5.1*: Scheduled tasks can now be run on demand by selecting the required task and clicking the **Run now** button 118 | - *New in 1.0.0*: Carrot monitor now uses a modern material theme for its interface 119 | 120 | 121 | -------------------------------------------------------------------------------- /docs/quick-start.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | ## Install django-carrot 4 | 5 | 6 | Install with *pip* 7 | 8 | ```bash 9 | pip install django-carrot 10 | ``` 11 | 12 | ### Install RabbitMQ 13 | 14 | Install and start RabbitMQ: 15 | 16 | ```bash 17 | brew install rabbitmq 18 | brew services start rabbitmq 19 | ``` 20 | 21 | 22 | ### Configuring your Django project 23 | 24 | 1. Add carrot to your Django project's settings module: 25 | 26 | ```python 27 | INSTALLED_APPS = [ 28 | #... 29 | 'carrot', 30 | ] 31 | 32 | ``` 33 | 34 | 2. Apply django-carrot's migrations them to your project's database: 35 | 36 | ```bash 37 | python manage.py migrate carrot 38 | ``` 39 | 40 | 41 | For see all configuration options, refer to :ref:`carrot-settings` 42 | 43 | ### Starting the service 44 | 45 | Once you have configured django-carrot, you can start the service using the following django-admin command: 46 | 47 | ```bash 48 | python manage.py carrot_daemon start 49 | ``` 50 | 51 | The daemon can be stopped/restarted as follows: 52 | 53 | 54 | ```bash 55 | python manage.py carrot_daemon stop 56 | python manage.py carrot_daemon restart 57 | ``` 58 | 59 | For the full set of options, refer to [admin-command](service.rst) 60 | 61 | ### Creating and publishing tasks 62 | 63 | 64 | While the service is running, tasks will be consumed from your RabbitMQ queue. To test this, start the django shell: 65 | 66 | ```bash 67 | python manage.py shell 68 | ``` 69 | 70 | And use the provided helper, ``carrot.utilities.publish_message``: 71 | 72 | ```python 73 | from carrot.utilities import publish_message 74 | 75 | def my_task(**kwargs): 76 | return 'hello world' 77 | 78 | publish_message(my_task, hello=True) 79 | ``` 80 | 81 | 82 | The above will publish the :code:`my_task` function to the default carrot queue. Once consumed, it will be 83 | called with the keyword argument *hello=True* 84 | 85 | ### Task logging 86 | 87 | In order to view the task output in :ref:`monitor`, you will need to use Carrot's logger object. This is done 88 | as follows: 89 | 90 | 91 | ```python 92 | from carrot.utilities import publish_message 93 | import logging 94 | 95 | logger = logging.getLogger('carrot') 96 | 97 | def my_task(**kwargs): 98 | logger.debug('hello world') 99 | logger.info('hello world') 100 | logger.warning('hello world') 101 | logger.error('hello world') 102 | logger.critical('hello world') 103 | 104 | publish_message(my_task, hello=True) 105 | ``` 106 | 107 | This will be rendered as follows in the carrot monitor output for this task: 108 | 109 | ![logs in django-carrot monitor](images/1.0/task-logging.png) 110 | 111 | 112 | > By default, Carrot Monitor only shows log entries with a level of *info* or higher. The entry logged with 113 | `logger.debug` only becomes visible if you change the **Log level** drop down 114 | 115 | 116 | ### Scheduling tasks 117 | 118 | Scheduled tasks are stored in your Django project's database as **ScheduledTask** objects. The Carrot service will 119 | publish tasks to your RabbitMQ queue at the required intervals. To scheduled the **my_task** function to run every 5 120 | seconds, use the following code: 121 | 122 | ```python 123 | from carrot.utilities import create_scheduled_task 124 | 125 | create_scheduled_task(my_task, {'seconds': 5}, hello=True) 126 | ``` 127 | 128 | The above will publish the **my_task** function to the queue every 5 seconds 129 | 130 | Tasks can also be scheduled via the :ref:`monitor` 131 | 132 | 133 | ## The Carrot monitor 134 | 135 | 136 | Carrot comes with it's own monitor view which allows you to: 137 | - View the list of queued tasks 138 | - View the traceback of failed tasks, and push them back into the message queue 139 | - View the traceback and output of successfully completed tasks 140 | 141 | To implement it, simply add the carrot url config to your Django project's main url file: 142 | 143 | ```python 144 | urlpatterns = [ 145 | #... 146 | url(r'^carrot/', include('carrot.urls')), 147 | ] 148 | ``` 149 | 150 | For more information, refer to [the carrot monitor](monitor.html) 151 | 152 | ## Docker 153 | 154 | A sample docker config is available [here](https://github.com/chris104957/django-carrot-docker) 155 | 156 | ## Support 157 | 158 | If you are having issues, please [Log an issue](https://github.com/chris104957/django-carrot/issues/new) 159 | 160 | ## License 161 | 162 | The project is licensed under the Apache license. 163 | -------------------------------------------------------------------------------- /docs/release-notes.md: -------------------------------------------------------------------------------- 1 | # release notes 2 | 3 | 4 | ## 1.3.0 5 | 6 | - #96: Add purge button to the monitor 7 | - #95: Adding validation to create_scheduled_task 8 | - #95: Fixing versioning issues 9 | 10 | ## 1.2.2 11 | 12 | - #91: task_name missing in create_scheduled_task 13 | 14 | ## 1.2.1 15 | 16 | - #87: Migration error in migration 0003 17 | 18 | ## 1.2.0 19 | 20 | - #81: Carrot monitor breaks when the queue from a completed message log gets removed from the config 21 | - #79: Add unique task_name field to ScheduledTask object 22 | - #78: Carrot service should warn users when process is already running 23 | - #77: Update the docs to make it clear tasks must be published from within the Django context 24 | 25 | > This release contains new migrations. In order to upgrade from a previous version of carrot, you must apply them 26 | first with `python manage.py migrate carrot` 27 | 28 | ## 1.1.3 29 | 30 | - #75: Add a link to the docker container sample to the docs 31 | 32 | ## 1.1.2 33 | 34 | - Doc updates 35 | 36 | ## 1.1.1 37 | 38 | - #72: Migrations end up inside venv? 39 | 40 | 41 | ## 1.1.0 42 | 43 | - #56: Have Django host VueJS resources instead of CDN 44 | - #66: Switching between monitor views quickly shows tasks in the wrong list 45 | - #67: Simply the version management 46 | - #68: Simplify the readmes 47 | 48 | ## 1.0.0 49 | 50 | ### Monitor material theme 51 | 52 | Added a material theme to the django-carrot monitor: 53 | 54 | ![Carrot monitor](images/1.0/monitor.png "Carrot monitor") 55 | 56 | 57 | ### Failure hooks 58 | 59 | Implemented failure hooks, which run when a task fails. This can be used to re-queue a failed task a certain number 60 | of times before raising an exception. For example: 61 | 62 | 63 | `my_project/my_app/consumer.py` 64 | 65 | ```python 66 | from carrot.utilities import publish_message 67 | 68 | def failure_callback(log, exception): 69 | if log.task == 'myapp.tasks.retry_test': 70 | logger.critical(log.__dict__) 71 | attempt = log.positionals[0] + 1 72 | if attempt <= 5: 73 | log.delete() 74 | publish_message('myapp.tasks.retry_test', attempt) 75 | 76 | 77 | class CustomConsumer(Consumer): 78 | def __init__(self, host, queue, logger, name, durable=True, queue_arguments=None, exchange_arguments=None): 79 | super(CustomConsumer, self).__init__(host, queue, logger, name, durable, queue_arguments, exchange_arguments) 80 | self.add_failure_callback(failure_callback) 81 | ``` 82 | 83 | 84 | `my_project/my_app/tasks.py` 85 | 86 | ```python 87 | def retry_test(attempt): 88 | logger.info('ATTEMPT NUMBER: %i' % attempt) 89 | do_stuff() # this method fails, because it isn't actually defined in this example 90 | ``` 91 | 92 | `my_project/my_project/settings.py` 93 | 94 | ```python 95 | CARROT = { 96 | 'default_broker': vhost, 97 | 'queues': [ 98 | { 99 | 'name': 'default', 100 | 'host': vhost, 101 | 'consumer_class': 'my_project.consumer.CustomConsumer', 102 | } 103 | ] 104 | } 105 | ``` 106 | 107 | ### Bug fixes 108 | 109 | - #43: During high server load periods, messages sometimes get consumed before the associated MessageLog is created -------------------------------------------------------------------------------- /docs/service.md: -------------------------------------------------------------------------------- 1 | # django-carrot service 2 | 3 | Carrot implements two *manage.py* commands in your django app - `carrot` and `carrot_daemon` 4 | 5 | The `carrot` command is the base service which starts consuming messages from your defined RabbitMQ brokers, and 6 | publishing any active scheduled tasks at the required intervals 7 | 8 | `carrot_daemon` is a daemon which can be used the invoke the `carrot` service as a detached process, and allows 9 | users to stop/restart the service safely, and to check the status. `carrot_daemon` can be invoked as follows: 10 | 11 | 12 | ```bash 13 | python manage.py carrot_daemon start 14 | python manage.py carrot_daemon stop 15 | python manage.py carrot_daemon restart 16 | python manage.py carrot_daemon status 17 | ``` 18 | 19 | ### Further options 20 | 21 | 22 | The following additional arguments are also available: 23 | 24 | - --logfile: path to the log file. Defaults to `/var/log/carrot.log` 25 | - --pidfile: path to the pid file. Defaults to `/var/run/carrot.pid` 26 | - --no-scheduler: run the carrot service without the scheduler (only consumes tasks) 27 | - --testmode: Used for running the carrot tests. Not applicable for most users 28 | - --loglevel: The level of logging to use. Defaults to `DEBUG` and shouldn't be changed under most circumstances 29 | 30 | ## Examples 31 | 32 | ### Custom log/pid file paths 33 | 34 | On some systems you may encounter OS errors while trying to run the service with the default log/pid file locations. 35 | This can be fixed by specifying your own values for these paths: 36 | 37 | ```bash 38 | python manage.py carrot_daemon start --logfile carrot.log --pidfile carrot.pid 39 | ``` 40 | 41 | > If you use a custom pid, you must also provide this same argument when attempting to stop, restart or check the 42 | status of the carrot service 43 | 44 | ### Running without the scheduler 45 | 46 | Use the following to disabled `ScheduledTasks` 47 | 48 | ```python 49 | python manage.py carrot_daemon --no-scheduler 50 | ``` 51 | 52 | ### Debugging 53 | 54 | Using the `carrot_daemon` will run in detached mode with no `sys.out` visible. If you are having issues getting the 55 | service working properly, or want to check your broker configuration, you can use the `carrot` command instead, as 56 | follows: 57 | 58 | ```bash 59 | python manage.py carrot 60 | ``` 61 | 62 | You will be able to read the system output using this command, which should help you to resolve any issues 63 | 64 | > The `carrot` command does not accept the `pidfile` or `mode` (e.g. start, stop, restart, status) arguments. No 65 | pid file gets created in this mode, and the process is the equivalent of `carrot_daemon start`. To stop the 66 | process, simply use CTRL+C 67 | 68 | -------------------------------------------------------------------------------- /docs/settings.md: -------------------------------------------------------------------------------- 1 | # django-carrot configuration 2 | 3 | django-carrot is configured via your Django project's settings modules. All possible configuration options are listed in 4 | this page. All configuration options are inserted as follows: 5 | 6 | 7 | 8 | ```python 9 | CARROT = { 10 | # settings go here 11 | } 12 | ``` 13 | 14 | 15 | ## ``default_broker`` 16 | 17 | > default value: `amqp://guest:guest@localhost:5672/` (`str` or `dict`) 18 | 19 | Carrot needs to be able to connect to at least one RabbitMQ broker in order to work. The default broker can either be 20 | provided as a string: 21 | 22 | ```python 23 | CARROT = { 24 | 'default_broker': 'amqp://myusername:mypassword@192.168.0.1:5672/my-virtual-host' 25 | } 26 | ``` 27 | 28 | or alternatively, in the following format: 29 | 30 | ```python 31 | CARROT = { 32 | 'default_broker': { 33 | 'host': '192.168.0.1', # host of your RabbitMQ server 34 | 'port': 5672, # your RabbitMQ port. The default is 5672 35 | 'name': 'my-virtual-host', # the name of your virtual host. Can be omitted if you do not use VHOSTs 36 | 'username': 'my-rabbit-username', # your RabbitMQ username 37 | 'password': 'my-rabbit-password', # your RabbitMQ password 38 | 'secure': False # Use SSL 39 | } 40 | } 41 | ``` 42 | 43 | ## `queues` 44 | 45 | > default value: `[]` (`list`) 46 | 47 | django-carrot will automatically create a queue called `default`. However, you may wish to define your own queues in 48 | order to access additional functionality such as: 49 | 50 | - Sending tasks to different queues 51 | - Increasing the number of consumers attached to each queue 52 | 53 | To define your own queues, add a list of *queues* to your carrot configuration: 54 | 55 | 56 | ```python 57 | CARROT = { 58 | 'queues': [ 59 | { 60 | 'name': 'my-queue-1', 61 | 'host': 'amqp://myusername:mypassword@192.168.0.1:5672/my-virtual-host', 62 | 'concurrency': 5, 63 | }, 64 | { 65 | 'name': 'my-queue-2', 66 | 'host': 'amqp://myusername:mypassword@192.168.0.1:5672/my-virtual-host-2', 67 | 'consumable': False, 68 | }, 69 | ] 70 | } 71 | ``` 72 | 73 | Each queue supports the following configuration options: 74 | 75 | - `name`: the queue name, as a string 76 | - `host`: the queue host. Can either be a URL as a string (as in the above example) or a dict in the following format: 77 | ```python 78 | 'name':'my-queue', 79 | 'host': { 80 | 'host': '192.168.0.1', 81 | 'port': 5672, 82 | 'name': 'my-virtual-host', 83 | 'username': 'my-rabbit-username', 84 | 'password': 'my-rabbit-password', 85 | 'secure': False 86 | } 87 | ``` 88 | - `concurrency`: the number of consumers to be attached to the queue, as an integer. Defaults to `1` 89 | - `consumable`: Whether or not the service should consume messages in this queue, as a Boolean. Defaults to `True` 90 | 91 | ## `task_modules` 92 | 93 | > default value: `[]` (`list`) 94 | 95 | This setting is required while using **django-carrot monitor** and should point at the python module where your tasks 96 | are kept. It will populate the task selection drop down while creating/editing scheduled tasks: 97 | 98 | ![with task modules](images/1.0/with-task-modules.png "with task modules") 99 | 100 | The *task_modules* option is used to enable this functionality. It can be added to the Carrot configuration as follows: 101 | 102 | ```python 103 | CARROT = { 104 | #... 105 | 'task_modules': ['myapp.mymodule', 'myapp.myothermodule',] 106 | } 107 | ``` 108 | 109 | 110 | ## `monitor_authentication` 111 | 112 | > default: `[]` (`list`) 113 | 114 | By default, all views provided by :ref:`carrot-monitor-configuration` are public. If you want to limit access to these 115 | views to certain users of your Django app, you can list the decorators to apply to these views. This is done with the 116 | *monitor_authentication* setting: 117 | 118 | 119 | ```python 120 | CARROT = { 121 | 'monitor_authentication': ['django.contrib.auth.decorators.login_required', 'myapp.mymodule.mydecorator'] 122 | } 123 | ``` 124 | 125 | The above example will apply Django's `login_required` decorator to all of Carrot monitor's views, as well as 126 | whatever custom decorators you specify. 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /legacy_docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/carrot.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/carrot.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/carrot" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/carrot" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /legacy_docs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/legacy_docs/__init__.py -------------------------------------------------------------------------------- /legacy_docs/doc_settings.py: -------------------------------------------------------------------------------- 1 | SECRET_KEY = 'secret_key' 2 | 3 | INSTALLED_APPS = [ 4 | 'carrot', 5 | ] 6 | 7 | 8 | -------------------------------------------------------------------------------- /legacy_docs/source/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/legacy_docs/source/__init__.py -------------------------------------------------------------------------------- /legacy_docs/source/_static/carrot.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=PT+Sans'); 2 | 3 | 4 | .navbar-default { 5 | background-color: #ff8907; 6 | } 7 | 8 | body, h1, h2, h3, h4, h5, h6 { 9 | font-family: 'PT Sans', serif; 10 | } 11 | 12 | h2 { 13 | margin: 15px 0; 14 | } 15 | 16 | .navbar-brand { 17 | padding: 13px 15px; 18 | font-size: 32px; 19 | } 20 | 21 | p { 22 | margin: 20px 0 10px 23 | } 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /legacy_docs/source/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends '!layout.html' %} 2 | 3 | {% block footer %} 4 | 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /legacy_docs/source/api.rst: -------------------------------------------------------------------------------- 1 | django-carrot API 2 | ----------------- 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | service 8 | utilities 9 | consumer 10 | objects 11 | models 12 | -------------------------------------------------------------------------------- /legacy_docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # carrot documentation build configuration file, created by 5 | # sphinx-quickstart on Tue Sep 26 15:41:05 2017. 6 | # 7 | 8 | import sys 9 | import os 10 | import sphinx_bootstrap_theme 11 | import django 12 | from carrot import __version__ 13 | 14 | 15 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 16 | 17 | sys.path.insert(0, os.path.abspath('.')) 18 | sys.path.append(BASE_DIR) 19 | sys.path.append(os.path.dirname(BASE_DIR)) 20 | 21 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'doc_settings') 22 | django.setup() 23 | 24 | extensions = [ 25 | 'sphinx.ext.autodoc', 26 | 'sphinx.ext.doctest', 27 | 'sphinx.ext.intersphinx', 28 | 'sphinx.ext.coverage', 29 | ] 30 | 31 | templates_path = ['_templates'] 32 | 33 | source_suffix = '.rst' 34 | 35 | master_doc = 'index' 36 | 37 | # General information about the project. 38 | project = 'django-carrot' 39 | copyright = '2017-2018, Christopher Davies' 40 | author = 'Christopher Davies' 41 | 42 | version = __version__ 43 | release = __version__ 44 | 45 | language = None 46 | 47 | exclude_patterns = [] 48 | 49 | pygments_style = 'sphinx' 50 | 51 | todo_include_todos = False 52 | 53 | html_theme = 'bootstrap' 54 | html_theme_path = sphinx_bootstrap_theme.get_html_theme_path() 55 | 56 | html_theme_options = { 57 | # Tab name for entire site. (Default: "Site") 58 | 'navbar_site_name': "Topics", 59 | 60 | 'navbar_links': [ 61 | ("Home", "index"), 62 | ("What's new", "release-notes"), 63 | ("Getting started", "quick-start"), 64 | ("Monitor", "monitor"), 65 | ("Configuration", "settings"), 66 | ("API", "api"), 67 | 68 | ], 69 | 70 | # Render the next and previous page links in navbar. (Default: true) 71 | 'navbar_sidebarrel': False, 72 | 73 | 'globaltoc_depth': -1, 74 | 75 | # Render the current pages TOC in the navbar. (Default: true) 76 | 'navbar_pagenav': False, 77 | 78 | # Location of link to source. 79 | 'source_link_position': None, 80 | 81 | # - Bootstrap 3: https://bootswatch.com/3 82 | 'bootswatch_theme': "united", 83 | } 84 | 85 | html_static_path = ['_static'] 86 | 87 | htmlhelp_basename = 'carrotdoc' 88 | 89 | 90 | latex_elements = {} 91 | 92 | latex_documents = [ 93 | (master_doc, 'carrot.tex', 'carrot Documentation', 94 | 'Christopher Davies', 'manual'), 95 | ] 96 | 97 | man_pages = [ 98 | (master_doc, 'carrot', 'carrot Documentation', 99 | [author], 1) 100 | ] 101 | 102 | texinfo_documents = [ 103 | (master_doc, 'carrot', 'carrot Documentation', 104 | author, 'carrot', 'One line description of project.', 105 | 'Miscellaneous'), 106 | ] 107 | 108 | intersphinx_mapping = {'https://docs.python.org/': None} 109 | 110 | 111 | def setup(app): 112 | app.add_stylesheet('carrot.css') 113 | 114 | -------------------------------------------------------------------------------- /legacy_docs/source/consumer.rst: -------------------------------------------------------------------------------- 1 | The django-carrot consumer backend 2 | ================================== 3 | 4 | .. automodule:: carrot.consumer 5 | :members: 6 | -------------------------------------------------------------------------------- /legacy_docs/source/images/0.2/task-logging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/legacy_docs/source/images/0.2/task-logging.png -------------------------------------------------------------------------------- /legacy_docs/source/images/0.5/carrot-monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/legacy_docs/source/images/0.5/carrot-monitor.png -------------------------------------------------------------------------------- /legacy_docs/source/images/0.5/monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/legacy_docs/source/images/0.5/monitor.png -------------------------------------------------------------------------------- /legacy_docs/source/images/1.0/create-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/legacy_docs/source/images/1.0/create-new.png -------------------------------------------------------------------------------- /legacy_docs/source/images/1.0/monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/legacy_docs/source/images/1.0/monitor.png -------------------------------------------------------------------------------- /legacy_docs/source/images/1.0/task-logging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/legacy_docs/source/images/1.0/task-logging.png -------------------------------------------------------------------------------- /legacy_docs/source/images/1.0/with-task-modules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/legacy_docs/source/images/1.0/with-task-modules.png -------------------------------------------------------------------------------- /legacy_docs/source/images/carrot-logo-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/legacy_docs/source/images/carrot-logo-big.png -------------------------------------------------------------------------------- /legacy_docs/source/images/carrot-logo-inline-inverse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/legacy_docs/source/images/carrot-logo-inline-inverse.png -------------------------------------------------------------------------------- /legacy_docs/source/images/no-task-modules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/legacy_docs/source/images/no-task-modules.png -------------------------------------------------------------------------------- /legacy_docs/source/images/with-task-modules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris104957/django-carrot/11c8790d8e68ccd7757cc9187d852acb2f528a8d/legacy_docs/source/images/with-task-modules.png -------------------------------------------------------------------------------- /legacy_docs/source/index.rst: -------------------------------------------------------------------------------- 1 | 2 | .. image:: /images/carrot-logo-big.png 3 | :align: center 4 | 5 | 6 | The lightweight task queue for Django 7 | ===================================== 8 | 9 | django-carrot is a lightweight task queue backend for Django projects that uses the RabbitMQ message broker, with an 10 | emphasis on quick and easy configuration and task tracking 11 | 12 | Features 13 | -------- 14 | - Minimal configuration required 15 | - Task scheduling 16 | - Task prioritization 17 | - Detail task level monitoring/logging via django-carrot monitor 18 | - Built in daemon 19 | - Supports Django 2.0 20 | 21 | .. figure:: /images/1.0/monitor.png 22 | :align: center 23 | :width: 600px 24 | :figclass: align-center 25 | 26 | logs in django-carrot monitor 27 | 28 | 29 | Click `here `_ to get started 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /legacy_docs/source/models.rst: -------------------------------------------------------------------------------- 1 | django-carrot models 2 | ==================== 3 | 4 | .. automodule:: carrot.models 5 | :members: 6 | -------------------------------------------------------------------------------- /legacy_docs/source/monitor.rst: -------------------------------------------------------------------------------- 1 | .. _monitor: 2 | 3 | django-carrot monitor 4 | ===================== 5 | 6 | .. figure:: /images/1.0/monitor.png 7 | :align: center 8 | :height: 400px 9 | :figclass: align-center 10 | 11 | django-carrot monitor 12 | 13 | 14 | Introduction 15 | ------------ 16 | 17 | django-carrot provides a simple interface for managing :class:`carrot.models.MessageLog` and 18 | :class:`carrot.models.ScheduledTask` objects, known as the **django-carrot monitor**. This interface offers the 19 | following functionality: 20 | 21 | - Monitoring of tasks currently in the queue 22 | - Viewing the log and traceback of failed tasks, and deleting/requeuing them 23 | - Viewing the log for tasks that have completed successfully 24 | - Allows users to view, edit and create scheduled tasks 25 | - On demand publishing of scheduled tasks 26 | 27 | For each task, the monitor displays: 28 | 29 | - Basic information about the task, e.g. the virtualhost and queue it has been published to, the priority, and 30 | the dates/times it was published/completed/failed 31 | - The arguments and keyword arguments the task was called with 32 | - Where applicable, the task log, output and error traceback information 33 | 34 | .. _carrot-monitor-configuration: 35 | 36 | Configuration 37 | ------------- 38 | 39 | To enable the django-carrot monitor, simply add the URLs to your project's main urls.py file: 40 | 41 | .. code-block:: python 42 | 43 | urlpatterns = [ 44 | ... 45 | url(r'^django-carrot/', include('django-carrot.urls')), 46 | ] 47 | 48 | You will now be able to see the monitor at the path you have specified, eg: http://localhost:8000/carrot/ 49 | 50 | In order to create scheduled tasks using django-carrot monitor, it is also recommended that you specify your task 51 | modules in your Django project's settings module. This is done as follows: 52 | 53 | .. code-block:: python 54 | 55 | CARROT = { 56 | ... 57 | 'task_modules': ['my_app.my_tasks_module'], 58 | } 59 | 60 | 61 | Authentication 62 | ************** 63 | 64 | By default, the django-carrot monitor interface is public. However, you can set authentication decorators from your 65 | Django project's settings module: 66 | 67 | .. code-block:: python 68 | 69 | CARROT = { 70 | ... 71 | 'monitor_authentication': ['django.contrib.auth.decorators.login_required'], 72 | } 73 | 74 | The above uses Django's built it :func:`django.contrib.auth.decorators.login_required` decorator to ensure that all 75 | users are logged in before attempting to access the monitor. You can also specify your own decorators here. 76 | 77 | Usage 78 | ----- 79 | 80 | Once configured, the monitor can be access from the path ``/carrot``, e.g. ``http://localhost:8000/carrot`` 81 | 82 | The monitor has 4 tabbed views: 83 | 84 | Queued tasks 85 | ************ 86 | 87 | This view shows all tasks that are currently in the queue and will be processed by the consumer. To see more details about a particular task, click on the relevant row in the list. You will be able to see more details about the task, including where/when it is/was published 88 | 89 | Failed tasks 90 | ************ 91 | 92 | This view shows all tasks that have failed during processing, along with the full log up to the failure, and a full traceback of the issue. Failed tasks can either be requeued or deleted from the queue, either in bulk or individually 93 | 94 | Completed tasks 95 | *************** 96 | 97 | Once tasks have been completed, they will appear in this section. At this point, the full log becomes available. You can use the drop down in the monitor to customize the level of visible logging. 98 | 99 | Scheduled tasks 100 | *************** 101 | 102 | You can manage scheduled tasks in this view. 103 | 104 | Use the **Create new** button to schedule tasks to run at a given interval. The *task*, *queue*, *interval type* and *interval count* fields are mandatory. You can use the *active* slider to temporary prevent a scheduled task from running. 105 | 106 | .. figure:: /images/1.0/create-new.png 107 | :align: center 108 | :height: 400px 109 | :figclass: align-center 110 | 111 | creating scheduled tasks 112 | 113 | The *positional arguments* field must contain a valid list of python arguments. Here are some valid examples of input for this field: 114 | 115 | .. code-block:: python 116 | 117 | True, 1, 'test', {'foo': 'bar'} 118 | 119 | 120 | The *keyword arguments* field must contain valid json serializable content. For example: 121 | 122 | .. code-block:: javascript 123 | 124 | { 125 | "parameter_1": true, 126 | "parameter_2": null, 127 | "parameter_3": ["list", "of", "things"], 128 | "parameter_4": { 129 | "more": "things" 130 | } 131 | } 132 | 133 | .. warning:: 134 | The *keyword arguments* input must be JSON, not a Python dict 135 | 136 | .. note:: 137 | - All task lists are refreshed every 10 seconds, or when certain actions are performed, e.g. on task deletion/requeue 138 | - Task logs are not available until a task completes or fails. This is because the task log only gets written to your Django project's database at the end of the process 139 | - *New in 0.5.1*: Scheduled tasks can now be run on demand by selecting the required task and clicking the **Run now** button 140 | - *New in 1.0.0*: Carrot monitor now uses a modern material theme for its interface 141 | 142 | 143 | -------------------------------------------------------------------------------- /legacy_docs/source/objects.rst: -------------------------------------------------------------------------------- 1 | Objects 2 | ======= 3 | 4 | .. automodule:: carrot.objects 5 | :members: 6 | -------------------------------------------------------------------------------- /legacy_docs/source/quick-start.rst: -------------------------------------------------------------------------------- 1 | .. _quick-start: 2 | 3 | 4 | Getting started 5 | =============== 6 | 7 | Install django-carrot 8 | ********************* 9 | 10 | Install with *pip* 11 | 12 | .. code-block:: bash 13 | 14 | pip install django-carrot 15 | 16 | Install RabbitMQ 17 | **************** 18 | 19 | Install and start RabbitMQ: 20 | 21 | .. code-block:: bash 22 | 23 | brew install rabbitmq 24 | brew services start rabbitmq 25 | 26 | Configuring your Django project 27 | ******************************* 28 | 29 | 1. Add carrot to your Django project's settings module: 30 | 31 | .. code-block:: python 32 | 33 | INSTALLED_APPS = [ 34 | ... 35 | 'carrot', 36 | ... 37 | ] 38 | 39 | 2. Apply django-carrot's migrations them to your project's database: 40 | 41 | .. code-block:: bash 42 | 43 | python manage.py migrate carrot 44 | 45 | For see all configuration options, refer to :ref:`carrot-settings` 46 | 47 | Starting the service 48 | ******************** 49 | 50 | Once you have configured django-carrot, you can start the service using the following django-admin command: 51 | 52 | .. code-block:: bash 53 | 54 | python manage.py carrot_daemon start 55 | 56 | The daemon can be stopped/restarted as follows: 57 | 58 | .. code-block:: bash 59 | 60 | python manage.py carrot_daemon stop 61 | python manage.py carrot_daemon restart 62 | 63 | For the full set of options, refer to :ref:`admin-command` 64 | 65 | 66 | Creating and publishing tasks 67 | ***************************** 68 | 69 | While the service is running, tasks will be consumed from your RabbitMQ queue. To test this, start the django shell: 70 | 71 | .. code-block:: bash 72 | 73 | python manage.py shell 74 | 75 | And use the provided helper, ``carrot.utilities.publish_message``: 76 | 77 | .. code-block:: python 78 | 79 | from carrot.utilities import publish_message 80 | 81 | def my_task(**kwargs): 82 | return 'hello world' 83 | 84 | publish_message(my_task, hello=True) 85 | 86 | 87 | The above will publish the :code:`my_task` function to the default carrot queue. Once consumed, it will be 88 | called with the keyword argument *hello=True* 89 | 90 | Task logging 91 | ************ 92 | 93 | In order to view the task output in :ref:`monitor`, you will need to use Carrot's logger object. This is done 94 | as follows: 95 | 96 | .. code-block:: python 97 | 98 | from carrot.utilities import publish_message 99 | import logging 100 | 101 | logger = logging.getLogger('carrot') 102 | 103 | def my_task(**kwargs): 104 | logger.debug('hello world') 105 | logger.info('hello world') 106 | logger.warning('hello world') 107 | logger.error('hello world') 108 | logger.critical('hello world') 109 | 110 | publish_message(my_task, hello=True) 111 | 112 | This will be rendered as follows in the carrot monitor output for this task: 113 | 114 | .. figure:: /images/1.0/task-logging.png 115 | :align: center 116 | :height: 300px 117 | :figclass: align-center 118 | 119 | logs in django-carrot monitor 120 | 121 | .. note:: 122 | By default, Carrot Monitor only shows log entries with a level of *info* or higher. The entry logged with 123 | `logger.debug` only becomes visible if you change the **Log level** drop down 124 | 125 | 126 | Scheduling tasks 127 | **************** 128 | 129 | Scheduled tasks are stored in your Django project's database as **ScheduledTask** objects. The Carrot service will 130 | publish tasks to your RabbitMQ queue at the required intervals. To scheduled the **my_task** function to run every 5 131 | seconds, use the following code: 132 | 133 | .. code-block:: python 134 | 135 | from carrot.utilities import create_scheduled_task 136 | 137 | create_scheduled_task(my_task, {'seconds': 5}, hello=True) 138 | 139 | The above will publish the **my_task** function to the queue every 5 seconds 140 | 141 | Tasks can also be scheduled via the :ref:`monitor` 142 | 143 | 144 | The Carrot monitor 145 | ------------------ 146 | 147 | Carrot comes with it's own monitor view which allows you to: 148 | - View the list of queued tasks 149 | - View the traceback of failed tasks, and push them back into the message queue 150 | - View the traceback and output of successfully completed tasks 151 | 152 | To implement it, simply add the carrot url config to your Django project's main url file: 153 | 154 | .. code-block:: python 155 | 156 | urlpatterns = [ 157 | ... 158 | url(r'^carrot/', include('carrot.urls')), 159 | ] 160 | 161 | For more information, refer to :ref:`monitor` 162 | 163 | Docker 164 | ------ 165 | 166 | A sample docker config is available `here `_ 167 | 168 | Support 169 | ------- 170 | 171 | If you are having issues, please `Log an issue `_ and add the **help wanted** label 172 | 173 | License 174 | ------- 175 | 176 | The project is licensed under the Apache license. 177 | -------------------------------------------------------------------------------- /legacy_docs/source/release-notes.rst: -------------------------------------------------------------------------------- 1 | release notes 2 | ============= 3 | 4 | 1.3.0 5 | ----- 6 | - `Issue #96: Add purge button to the monitor `_ 7 | - `Issue #95: Adding validation to create_scheduled_task `_ 8 | - `Issue #95: Fixing versioning issues `_ 9 | 10 | 1.2.2 11 | ----- 12 | - `Issue #91: task_name missing in create_scheduled_task `_ 13 | 14 | 1.2.1 15 | ----- 16 | - `Issue #87: Migration error in migration 0003 `_ 17 | 18 | 1.2.0 19 | ----- 20 | - `Issue #81: Carrot monitor breaks when the queue from a completed message log gets removed from the config `_ 21 | - `Issue #79: Add unique task_name field to ScheduledTask object `_ 22 | - `Issue #78: Carrot service should warn users when process is already running `_ 23 | - `Issue #77: Update the docs to make it clear tasks must be published from within the Django context `_ 24 | 25 | .. warning:: 26 | This release contains new migrations. In order to upgrade from a previous version of carrot, you must apply them: 27 | 28 | .. code-block:: python 29 | 30 | python manage.py migrate carrot 31 | 32 | 1.1.3 33 | ----- 34 | - `Issue #75: Add a link to the docker container sample to the docs `_ 35 | 36 | 1.1.2 37 | ----- 38 | - Doc updates 39 | 40 | 1.1.1 41 | ----- 42 | 43 | Bug fixes 44 | ********* 45 | - `Issue #72: Migrations end up inside venv? `_ 46 | 47 | 48 | 1.1.0 49 | ----- 50 | 51 | Bug fixes 52 | ********* 53 | 54 | - `Issue #56: Have Django host VueJS resources instead of CDN `_ 55 | - `Issue #66: Switching between monitor views quickly shows tasks in the wrong list `_ 56 | - `Issue #67: Simply the version management `_ 57 | - `Issue #68: Simplify the readmes `_ 58 | 59 | 1.0.0 60 | ----- 61 | 62 | Monitor material theme 63 | ********************** 64 | Added a material theme to the django-carrot monitor: 65 | 66 | .. figure:: /images/monitor.png 67 | :align: center 68 | :height: 400px 69 | :figclass: align-center 70 | 71 | material theme django-carrot monitor 72 | 73 | 74 | Failure hooks 75 | ************* 76 | 77 | Implemented failure hooks, which run when a task fails. This can be used to re-queue a failed task a certain number 78 | of times before raising an exception. For example: 79 | 80 | 81 | ``my_project/my_app/consumer.py`` 82 | 83 | .. code-block:: python 84 | 85 | from carrot.utilities import publish_message 86 | 87 | def failure_callback(log, exception): 88 | if log.task == 'myapp.tasks.retry_test': 89 | logger.critical(log.__dict__) 90 | attempt = log.positionals[0] + 1 91 | if attempt <= 5: 92 | log.delete() 93 | publish_message('myapp.tasks.retry_test', attempt) 94 | 95 | 96 | class CustomConsumer(Consumer): 97 | def __init__(self, host, queue, logger, name, durable=True, queue_arguments=None, exchange_arguments=None): 98 | super(CustomConsumer, self).__init__(host, queue, logger, name, durable, queue_arguments, exchange_arguments) 99 | self.add_failure_callback(failure_callback) 100 | 101 | 102 | ``my_project/my_app/tasks.py`` 103 | 104 | .. code-block:: python 105 | 106 | def retry_test(attempt): 107 | logger.info('ATTEMPT NUMBER: %i' % attempt) 108 | do_stuff() # this method fails, because it isn't actually defined in this example 109 | 110 | ``my_project/my_project/settings.py`` 111 | 112 | .. code-block:: python 113 | 114 | CARROT = { 115 | 'default_broker': vhost, 116 | 'queues': [ 117 | { 118 | 'name': 'default', 119 | 'host': vhost, 120 | 'consumer_class': 'my_project.consumer.CustomConsumer', 121 | } 122 | ] 123 | } 124 | 125 | 126 | Bug fixes 127 | ######### 128 | 129 | - `Issue #43: During high server load periods, messages sometimes get consumed before the associated MessageLog is created `_ -------------------------------------------------------------------------------- /legacy_docs/source/service.rst: -------------------------------------------------------------------------------- 1 | .. _admin-command: 2 | 3 | django-carrot service 4 | ********************* 5 | 6 | Carrot implements two *manage.py* commands in your django app - ``carrot`` and ``carrot_daemon`` 7 | 8 | The ``carrot`` command is the base service which starts consuming messages from your defined RabbitMQ brokers, and 9 | publishing any active scheduled tasks at the required intervals 10 | 11 | ``carrot_daemon`` is a daemon which can be used the invoke the ``carrot`` service as a detached process, and allows 12 | users to stop/restart the service safely, and to check the status. ``carrot_daemon`` can be invoked as follows: 13 | 14 | .. code-block:: bash 15 | 16 | python manage.py carrot_daemon start 17 | python manage.py carrot_daemon stop 18 | python manage.py carrot_daemon restart 19 | python manage.py carrot_daemon status 20 | 21 | Further options 22 | *************** 23 | 24 | The following additional arguments are also available: 25 | 26 | :--logfile: path to the log file. Defaults to ``/var/log/carrot.log`` 27 | :--pidfile: path to the pid file. Defaults to ``/var/run/carrot.pid`` 28 | :--no-scheduler: run the carrot service without the scheduler (only consumes tasks) 29 | :--testmode: Used for running the carrot tests. Not applicable for most users 30 | :--loglevel: The level of logging to use. Defaults to ``DEBUG`` and shouldn't be changed under most circumstances 31 | 32 | examples 33 | ******** 34 | 35 | Custom log/pid file paths 36 | ************************* 37 | 38 | On some systems you may encounter OS errors while trying to run the service with the default log/pid file locations. 39 | This can be fixed by specifying your own values for these paths: 40 | 41 | .. code-block:: bash 42 | 43 | python manage.py carrot_daemon start --logfile carrot.log --pidfile carrot.pid 44 | 45 | .. warning:: 46 | If you use a custom pid, you must also provide this same argument when attempting to stop, restart or check the 47 | status of the carrot service 48 | 49 | Running without the scheduler 50 | ***************************** 51 | 52 | Use the following to disabled ``ScheduledTasks`` 53 | 54 | .. code-block:: bash 55 | 56 | python manage.py carrot_daemon --no-scheduler 57 | 58 | 59 | Debugging 60 | ********* 61 | 62 | Using the ``carrot_daemon`` will run in detached mode with no ``sys.out`` visible. If you are having issues getting the 63 | service working properly, or want to check your broker configuration, you can use the ``carrot`` command instead, as 64 | follows: 65 | 66 | .. code-block:: bash 67 | 68 | python manage.py carrot 69 | 70 | You will be able to read the system output using this command, which should help you to resolve any issues 71 | 72 | .. note:: 73 | The ``carrot`` command does not accept the ``pidfile`` or ``mode`` (e.g. start, stop, restart, status) arguments. No 74 | pid file gets created in this mode, and the process is the equivalent of ``carrot_daemon start``. To stop the 75 | process, simply use CTRL+C 76 | 77 | 78 | Classes and methods 79 | ******************* 80 | 81 | .. automodule:: carrot.management.commands.carrot_daemon 82 | :members: 83 | 84 | 85 | .. automodule:: carrot.management.commands.carrot 86 | :members: 87 | 88 | -------------------------------------------------------------------------------- /legacy_docs/source/settings.rst: -------------------------------------------------------------------------------- 1 | .. _carrot-settings: 2 | 3 | django-carrot configuration 4 | =========================== 5 | 6 | django-carrot is configured via your Django project's settings modules. All possible configuration options are listed in 7 | this page. All configuration options are inserted as follows: 8 | 9 | .. code-block:: python 10 | 11 | CARROT = { 12 | ... 13 | } 14 | 15 | 16 | 17 | 18 | ``default_broker`` 19 | ------------------ 20 | 21 | :default value: ``amqp://guest:guest@localhost:5672/`` 22 | :type: ``str`` or ``dict`` 23 | 24 | 25 | Carrot needs to be able to connect to at least one RabbitMQ broker in order to work. The default broker can either be 26 | provided as a string: 27 | 28 | .. code-block:: python 29 | 30 | CARROT = { 31 | 'default_broker': 'amqp://myusername:mypassword@192.168.0.1:5672/my-virtual-host' 32 | } 33 | 34 | or alternatively, in the following format: 35 | 36 | .. code-block:: python 37 | 38 | CARROT = { 39 | 'default_broker': { 40 | 'host': '192.168.0.1', # host of your RabbitMQ server 41 | 'port': 5672, # your RabbitMQ port. The default is 5672 42 | 'name': 'my-virtual-host', # the name of your virtual host. Can be omitted if you do not use VHOSTs 43 | 'username': 'my-rabbit-username', # your RabbitMQ username 44 | 'password': 'my-rabbit-password', # your RabbitMQ password 45 | 'secure': False # Use SSL 46 | } 47 | } 48 | 49 | 50 | ``queues`` 51 | ---------- 52 | 53 | :default value: ``[]`` 54 | :type: ``list`` 55 | 56 | django-carrot will automatically create a queue called `default`. However, you may wish to define your own queues in 57 | order to access additional functionality such as: 58 | 59 | - Sending tasks to different queues 60 | - Increasing the number of consumers attached to each queue 61 | 62 | To define your own queues, add a list of *queues* to your carrot configuration: 63 | 64 | .. code-block:: python 65 | 66 | CARROT = { 67 | 'queues': [ 68 | { 69 | 'name': 'my-queue-1', 70 | 'host': 'amqp://myusername:mypassword@192.168.0.1:5672/my-virtual-host', 71 | 'concurrency': 5, 72 | }, 73 | { 74 | 'name': 'my-queue-2', 75 | 'host': 'amqp://myusername:mypassword@192.168.0.1:5672/my-virtual-host-2', 76 | 'consumable': False, 77 | }, 78 | ] 79 | } 80 | 81 | Each queue supports the following configuration options: 82 | 83 | :name: 84 | the queue name, as a string 85 | :host: 86 | the queue host. Can either be a URL as a string (as in the above example) or a dict in the following format: 87 | 88 | .. code-block:: python 89 | 90 | 'name':'my-queue', 91 | 'host': { 92 | 'host': '192.168.0.1', 93 | 'port': 5672, 94 | 'name': 'my-virtual-host', 95 | 'username': 'my-rabbit-username', 96 | 'password': 'my-rabbit-password', 97 | 'secure': False 98 | } 99 | 100 | :concurrency: 101 | the number of consumers to be attached to the queue, as an integer. Defaults to ``1`` 102 | 103 | :consumable: 104 | Whether or not the service should consume messages in this queue, as a Boolean. Defaults to ``True`` 105 | 106 | ``task_modules`` 107 | ---------------- 108 | 109 | :default value: ``[]`` 110 | :type: ``list`` 111 | 112 | This setting is required while using **django-carrot monitor** and should point at the python module where your tasks 113 | are kept. It will populate the task selection drop down while creating/editing scheduled tasks: 114 | 115 | .. figure:: /images/1.0/with-task-modules.png 116 | :align: center 117 | :height: 100px 118 | :figclass: align-center 119 | 120 | with task modules 121 | 122 | The *task_modules* option is used to enable this functionality. It can be added to the Carrot configuration as follows: 123 | 124 | .. code-block:: python 125 | 126 | CARROT = { 127 | ... 128 | 'task_modules': ['myapp.mymodule', 'myapp.myothermodule',] 129 | } 130 | 131 | 132 | ``monitor_authentication`` 133 | -------------------------- 134 | :default: ``[]`` 135 | :type: ``list`` 136 | 137 | By default, all views provided by :ref:`carrot-monitor-configuration` are public. If you want to limit access to these 138 | views to certain users of your Django app, you can list the decorators to apply to these views. This is done with the 139 | *monitor_authentication* setting: 140 | 141 | .. code-block:: python 142 | 143 | CARROT = { 144 | 'monitor_authentication': ['django.contrib.auth.decorators.login_required', 'myapp.mymodule.mydecorator'] 145 | } 146 | 147 | The above example will apply Django's :func:`login_required` decorator to all of Carrot monitor's views, as well as 148 | whatever custom decorators you specify. 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /legacy_docs/source/utilities.rst: -------------------------------------------------------------------------------- 1 | django-carrot utilities 2 | ======================= 3 | 4 | .. automodule:: carrot.utilities 5 | :members: 6 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "dev" 3 | description = "Atomic file writes." 4 | name = "atomicwrites" 5 | optional = false 6 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 7 | version = "1.3.0" 8 | 9 | [[package]] 10 | category = "dev" 11 | description = "Classes Without Boilerplate" 12 | name = "attrs" 13 | optional = false 14 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 15 | version = "19.1.0" 16 | 17 | [[package]] 18 | category = "dev" 19 | description = "Python package for providing Mozilla's CA Bundle." 20 | name = "certifi" 21 | optional = false 22 | python-versions = "*" 23 | version = "2019.3.9" 24 | 25 | [[package]] 26 | category = "dev" 27 | description = "Universal encoding detector for Python 2 and 3" 28 | name = "chardet" 29 | optional = false 30 | python-versions = "*" 31 | version = "3.0.4" 32 | 33 | [[package]] 34 | category = "dev" 35 | description = "Cross-platform colored terminal text." 36 | marker = "sys_platform == \"win32\"" 37 | name = "colorama" 38 | optional = false 39 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 40 | version = "0.4.1" 41 | 42 | [[package]] 43 | category = "dev" 44 | description = "Code coverage measurement for Python" 45 | name = "coverage" 46 | optional = false 47 | python-versions = "*" 48 | version = "4.0.3" 49 | 50 | [[package]] 51 | category = "main" 52 | description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." 53 | name = "django" 54 | optional = false 55 | python-versions = ">=3.5" 56 | version = "2.1.7" 57 | 58 | [package.dependencies] 59 | pytz = "*" 60 | 61 | [[package]] 62 | category = "main" 63 | description = "Web APIs for Django, made easy." 64 | name = "djangorestframework" 65 | optional = false 66 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 67 | version = "3.9.2" 68 | 69 | [[package]] 70 | category = "dev" 71 | description = "Internationalized Domain Names in Applications (IDNA)" 72 | name = "idna" 73 | optional = false 74 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 75 | version = "2.8" 76 | 77 | [[package]] 78 | category = "main" 79 | description = "JSON to HTML Table Representation" 80 | name = "json2html" 81 | optional = false 82 | python-versions = "*" 83 | version = "1.2.1" 84 | 85 | [[package]] 86 | category = "dev" 87 | description = "Rolling backport of unittest.mock for all Pythons" 88 | name = "mock" 89 | optional = false 90 | python-versions = "*" 91 | version = "2.0.0" 92 | 93 | [package.dependencies] 94 | pbr = ">=0.11" 95 | six = ">=1.9" 96 | 97 | [[package]] 98 | category = "dev" 99 | description = "More routines for operating on iterables, beyond itertools" 100 | name = "more-itertools" 101 | optional = false 102 | python-versions = ">=3.4" 103 | version = "6.0.0" 104 | 105 | [[package]] 106 | category = "dev" 107 | description = "Python Build Reasonableness" 108 | name = "pbr" 109 | optional = false 110 | python-versions = "*" 111 | version = "5.1.3" 112 | 113 | [[package]] 114 | category = "main" 115 | description = "Pika Python AMQP Client Library" 116 | name = "pika" 117 | optional = false 118 | python-versions = "*" 119 | version = "0.13.1" 120 | 121 | [[package]] 122 | category = "dev" 123 | description = "plugin and hook calling mechanisms for python" 124 | name = "pluggy" 125 | optional = false 126 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 127 | version = "0.9.0" 128 | 129 | [[package]] 130 | category = "main" 131 | description = "Cross-platform lib for process and system monitoring in Python." 132 | name = "psutil" 133 | optional = false 134 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 135 | version = "5.6.1" 136 | 137 | [[package]] 138 | category = "dev" 139 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 140 | name = "py" 141 | optional = false 142 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 143 | version = "1.8.0" 144 | 145 | [[package]] 146 | category = "dev" 147 | description = "pytest: simple powerful testing with Python" 148 | name = "pytest" 149 | optional = false 150 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 151 | version = "3.10.1" 152 | 153 | [package.dependencies] 154 | atomicwrites = ">=1.0" 155 | attrs = ">=17.4.0" 156 | colorama = "*" 157 | more-itertools = ">=4.0.0" 158 | pluggy = ">=0.7" 159 | py = ">=1.5.0" 160 | setuptools = "*" 161 | six = ">=1.10.0" 162 | 163 | [[package]] 164 | category = "dev" 165 | description = "Python interface to coveralls.io API" 166 | name = "python-coveralls" 167 | optional = false 168 | python-versions = "*" 169 | version = "2.9.1" 170 | 171 | [package.dependencies] 172 | PyYAML = "*" 173 | coverage = "4.0.3" 174 | requests = "*" 175 | six = "*" 176 | 177 | [[package]] 178 | category = "main" 179 | description = "World timezone definitions, modern and historical" 180 | name = "pytz" 181 | optional = false 182 | python-versions = "*" 183 | version = "2018.9" 184 | 185 | [[package]] 186 | category = "dev" 187 | description = "YAML parser and emitter for Python" 188 | name = "pyyaml" 189 | optional = false 190 | python-versions = "*" 191 | version = "3.13" 192 | 193 | [[package]] 194 | category = "dev" 195 | description = "Python HTTP for Humans." 196 | name = "requests" 197 | optional = false 198 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 199 | version = "2.21.0" 200 | 201 | [package.dependencies] 202 | certifi = ">=2017.4.17" 203 | chardet = ">=3.0.2,<3.1.0" 204 | idna = ">=2.5,<2.9" 205 | urllib3 = ">=1.21.1,<1.25" 206 | 207 | [[package]] 208 | category = "dev" 209 | description = "Python 2 and 3 compatibility utilities" 210 | name = "six" 211 | optional = false 212 | python-versions = ">=2.6, !=3.0.*, !=3.1.*" 213 | version = "1.12.0" 214 | 215 | [[package]] 216 | category = "main" 217 | description = "Sphinx Bootstrap Theme." 218 | name = "sphinx-bootstrap-theme" 219 | optional = false 220 | python-versions = "*" 221 | version = "0.6.5" 222 | 223 | [package.dependencies] 224 | setuptools = "*" 225 | 226 | [[package]] 227 | category = "dev" 228 | description = "HTTP library with thread-safe connection pooling, file post, and more." 229 | name = "urllib3" 230 | optional = false 231 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" 232 | version = "1.24.1" 233 | 234 | [metadata] 235 | content-hash = "e13da31f8eaf06b961cb0bedb57514af31f47ddeea30b2708e299eec9f073f4e" 236 | python-versions = "^3.7.2" 237 | 238 | [metadata.hashes] 239 | atomicwrites = ["03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", "75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"] 240 | attrs = ["69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", "f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"] 241 | certifi = ["59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", "b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"] 242 | chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] 243 | colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] 244 | coverage = ["00d464797a236f654337181af72b4baea3d35d056ca480e45e9163bb5df496b8", "0a90afa6f5ea08889da9066dca3ce2ef85d47587e3f66ca06a4fa8d3a0053acc", "0ba6c4345e3c197f6a3ba924d155c402ad28c080ac0d79529493eb17582fbc41", "2be3748f45d2eb0259c3c93abccc15c10725ef715bf0817a4c0a1a1dad2abc6a", "50727512afe77e044c7d7f2fd4cd0fe62b06527f965b335a810d956748e0514d", "6c2fd127cd4e2decb0ab41fe3ac2948b87ad2ea0470e24b4be5f7e7fdfef8df3", "6ed521ed3800d8f8911642b9b3c3891780a929db5e572c88c4713c1032530f82", "76a73a48a308fb87a4417d630b0345d36166f489ef17ea5aa8e4596fb50a2296", "7eaa0a33423476ed63317ee0a53cc07c0e36b5a390e3e95b95152e7eb6b3a6f6", "845d0f8a1765074b3256f07ddbce2969e5a5316dfd0eb3289137010d7677326a", "85b1275b6d7a61ccc8024a4e9a4c9e896394776edce1a5d075ec116f91925462", "8e60e720cad3ee6b0a32f475ae4040552c5623870a9ca0d3d4263faa89a8d96b", "93c50475f189cd226e9688b9897a0cd3c4c5d9c90b1733fa8f6445cfc0182c51", "94c1e66610807a7917d967ed6415b9d5fde7487ab2a07bb5e054567865ef6ef0", "964f86394cb4d0fd2bb40ffcddca321acf4323b48d1aa5a93db8b743c8a00f79", "99043494b28d6460035dd9410269cdb437ee460edc7f96f07ab45c57ba95e651", "addf63b5e39d573c459c3930b25176146395c1dc1afce4710067bb5e6dc4ea58", "af2f59ce312523c384a7826821cae0b95f320fee1751387abba4f00eed737166", "af6ed80340e5e1b89fa794f730ce7597651fbda3312e500002688b679c184ef9", "beb96d32ce8cfa47ec6433d95a33e4afaa97c19ac1b4a47ea40a424fedfee7c2", "c00bac0f6b35b82ace069a6a0d88e8fd4cd18d964fc5e47329cd02b212397fbe", "d079e36baceea9707fd50b268305654151011274494a33c608c075808920eda8", "d3188345f1c7161d701fd2ea9150f9bb6e2df890f3ddd6c0aea1f525e21d1544", "e65c78bde155a734f0d624647c4d6e0f47fb4875355a0b95c37d537788737f4f", "e813cba9ff0e3d37ad31dc127fac85d23f9a26d0461ef8042ac4539b2045e781", "e96c13a40df389ce8cbb5ec108e5fb834989d1bedff5d8846e5aa3d270a5f3b6", "ee2338539157cfc35fb1d6757dd799126804df39393c4a6c5fe88b402c8c0ab4"] 245 | django = ["275bec66fd2588dd517ada59b8bfb23d4a9abc5a362349139ddda3c7ff6f5ade", "939652e9d34d7d53d74d5d8ef82a19e5f8bb2de75618f7e5360691b6e9667963"] 246 | djangorestframework = ["8a435df9007c8b7d8e69a21ef06650e3c0cbe0d4b09e55dd1bd74c89a75a9fcd", "f7a266260d656e1cf4ca54d7a7349609dc8af4fe2590edd0ecd7d7643ea94a17"] 247 | idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"] 248 | json2html = ["a6c9cdb4ae09db1fe37e895eb706cb8cafcbf30a168e806d9ce105f3839f0d70"] 249 | mock = ["5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1", "b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba"] 250 | more-itertools = ["0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40", "590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1"] 251 | pbr = ["8257baf496c8522437e8a6cfe0f15e00aedc6c0e0e7c9d55eeeeab31e0853843", "8c361cc353d988e4f5b998555c88098b9d5964c2e11acf7b0d21925a66bb5824"] 252 | pika = ["b0640085f1d6398fd47bb16a17713053e26578192821ea5d928772b8e6a28789", "b785e0d5f74a94781bd7d020862eb137d2b56cef2a21475aadbe5bcc8ec4db15"] 253 | pluggy = ["19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f", "84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746"] 254 | psutil = ["23e9cd90db94fbced5151eaaf9033ae9667c033dffe9e709da761c20138d25b6", "27858d688a58cbfdd4434e1c40f6c79eb5014b709e725c180488ccdf2f721729", "354601a1d1a1322ae5920ba397c58d06c29728a15113598d1a8158647aaa5385", "9c3a768486194b4592c7ae9374faa55b37b9877fd9746fb4028cb0ac38fd4c60", "c1fd45931889dc1812ba61a517630d126f6185f688eac1693171c6524901b7de", "d463a142298112426ebd57351b45c39adb41341b91f033aa903fa4c6f76abecc", "e1494d20ffe7891d07d8cb9a8b306c1a38d48b13575265d090fc08910c56d474", "ec4b4b638b84d42fc48139f9352f6c6587ee1018d55253542ee28db7480cc653", "fa0a570e0a30b9dd618bffbece590ae15726b47f9f1eaf7518dfb35f4d7dcd21"] 255 | py = ["64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", "dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"] 256 | pytest = ["3f193df1cfe1d1609d4c583838bea3d532b18d6160fd3f55c9447fdca30848ec", "e246cf173c01169b9617fc07264b7b1316e78d7a650055235d6d897bc80d9660"] 257 | python-coveralls = ["1748272081e0fc21e2c20c12e5bd18cb13272db1b130758df0d473da0cb31087", "736dda01f64beda240e1500d5f264b969495b05fcb325c7c0eb7ebbfd1210b70"] 258 | pytz = ["32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9", "d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"] 259 | pyyaml = ["3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", "3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", "40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", "558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", "a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", "aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", "bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", "d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", "d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", "e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", "e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"] 260 | requests = ["502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", "7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"] 261 | six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"] 262 | sphinx-bootstrap-theme = ["82936109a05b84029052e7efb1756161c3b4c27ae18512a90234d50fe608cba7"] 263 | urllib3 = ["61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", "de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"] 264 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "django-carrot" 3 | version = "1.5.0" 4 | description = "A RabbitMQ asynchronous task queue for Django." 5 | authors = ["Christoper Davies "] 6 | license = "Apache-2.0" 7 | repository = "https://github.com/chris104957/django-carrot" 8 | homepage = "https://django-carrot.readthedocs.io" 9 | readme = "README.rst" 10 | classifiers = [ 11 | 'Environment :: Web Environment', 12 | 'Development Status :: 5 - Production/Stable', 13 | 'Framework :: Django', 14 | 'Framework :: Django :: 1.9', 15 | 'Framework :: Django :: 1.10', 16 | 'Framework :: Django :: 1.11', 17 | 'Framework :: Django :: 2.0', 18 | 'Intended Audience :: Developers', 19 | 'License :: OSI Approved :: Apache Software License', 20 | 'Operating System :: OS Independent', 21 | 'Programming Language :: Python', 22 | 'Programming Language :: Python :: 3.5', 23 | 'Programming Language :: Python :: 3.6', 24 | 'Topic :: Internet :: WWW/HTTP', 25 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 26 | ] 27 | packages = [ 28 | { include = "carrot" } 29 | ] 30 | 31 | 32 | [tool.poetry.dependencies] 33 | python = "^3.6.3" 34 | sphinx_bootstrap_theme = "^0.6.5" 35 | djangorestframework = "^3.9" 36 | pika = "^0.13.1" 37 | json2html = "^1.2" 38 | django = "^2.1" 39 | psutil = "^5.6" 40 | 41 | [tool.poetry.dev-dependencies] 42 | pytest = "^3.0" 43 | mock = "^2.0" 44 | python-coveralls = "^2.9" 45 | 46 | [build-system] 47 | requires = ["poetry>=0.12"] 48 | build-backend = "poetry.masonry.api" 49 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | psutil>=5.4.5 2 | django>=1.9 3 | json2html==1.2.1 4 | pika>=0.10.0 5 | djangorestframework>=3.6 6 | python-coveralls==2.9.1 7 | pyyaml>=4.2b1 8 | sphinx_bootstrap_theme==0.6.4 9 | mock==2.0.0 -------------------------------------------------------------------------------- /run_tests.py: -------------------------------------------------------------------------------- 1 | import django, sys, os 2 | from django.conf import settings 3 | import argparse 4 | from carrot.objects import VirtualHost 5 | 6 | 7 | def runner(options): 8 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 9 | sys.path.append(BASE_DIR) 10 | 11 | vhost = { 12 | 'host': options.host, 13 | 'port': options.port, 14 | 'name': options.name, 15 | 'username': options.username, 16 | 'password': options.password, 17 | 'secure': options.secure, 18 | } 19 | _vhost = VirtualHost(**vhost) 20 | settings.configure( 21 | DEBUG=True, 22 | DATABASES={ 23 | 'default': { 24 | 'ENGINE': 'django.db.backends.sqlite3', 25 | 'NAME': 'local', 26 | } 27 | }, 28 | ROOT_URLCONF='carrot.urls', 29 | INSTALLED_APPS=( 30 | 'django.contrib.auth', 31 | 'django.contrib.contenttypes', 32 | 'django.contrib.sessions', 33 | 'django.contrib.admin', 34 | 'django.contrib.staticfiles', 35 | 'carrot', 36 | ), 37 | CARROT={ 38 | 'default_broker': str(_vhost), 39 | 'queues': [{ 40 | 'name': 'default', 41 | 'host': str(_vhost), 42 | }], 43 | }, 44 | TEMPLATES=[ 45 | { 46 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 47 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 48 | 'APP_DIRS': True, 49 | 'OPTIONS': { 50 | 'context_processors': [ 51 | 'django.template.context_processors.debug', 52 | 'django.template.context_processors.request', 53 | 'django.contrib.auth.context_processors.auth', 54 | 'django.contrib.messages.context_processors.messages', 55 | ], 56 | }, 57 | }, 58 | ], 59 | STATIC_URL='/static/', 60 | ) 61 | django.setup() 62 | 63 | from django.test.runner import DiscoverRunner 64 | 65 | test_runner = DiscoverRunner(verbosity=0,) 66 | 67 | failures = test_runner.run_tests(['carrot']) 68 | if failures: 69 | sys.exit(failures) 70 | 71 | 72 | def main(): 73 | parser = argparse.ArgumentParser(description='Run the Carrot test suite') 74 | parser.add_argument("-H", '--host', type=str, default='localhost', help='The RabbitMQ host') 75 | parser.add_argument("-p", '--port', type=int, default=5672, help='The port number') 76 | parser.add_argument("-n", '--name', type=str, default='/', help='The virtual host name') 77 | parser.add_argument("-U", '--username', type=str, default='guest', help='Your RabbitMQ username') 78 | parser.add_argument("-P", '--password', type=str, default='guest', help='Your RabbitMQ password') 79 | parser.set_defaults(secure=False) 80 | parser.add_argument('-s', '--secure', dest='secure', action='store_true', default=False, 81 | help='Connect to RabbitMQ host over HTTPS') 82 | 83 | args = parser.parse_args() 84 | runner(args) 85 | 86 | 87 | if __name__ == '__main__': 88 | main() -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import find_packages, setup 3 | from carrot import __version__ 4 | 5 | 6 | def readme(): 7 | with open('README.rst') as f: 8 | return f.read() 9 | 10 | 11 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 12 | 13 | if os.environ.get('TRAVIS_BRANCH') == 'develop': 14 | name = 'django-carrot-dev' 15 | version = os.environ.get('TRAVIS_BUILD_NUMBER') 16 | 17 | else: 18 | name = 'django-carrot' 19 | version = __version__ 20 | 21 | 22 | setup( 23 | name=name, 24 | version=version, 25 | packages=find_packages(), 26 | include_package_data=True, 27 | license='Apache Software License', 28 | description='A RabbitMQ asynchronous task queue for Django.', 29 | long_description=readme(), 30 | author='Christopher Davies', 31 | author_email='christopherdavies553@gmail.com', 32 | url='https://django-carrot.readthedocs.io', 33 | home_page='https://github.com/chris104957/django-carrot', 34 | project_urls={ 35 | 'Documentation': 'https://django-carrot.readthedocs.io', 36 | 'Source': 'https://github.com/chris104957/django-carrot', 37 | }, 38 | 39 | classifiers=[ 40 | 'Environment :: Web Environment', 41 | 'Development Status :: 5 - Production/Stable', 42 | 'Framework :: Django', 43 | 'Framework :: Django :: 1.9', 44 | 'Framework :: Django :: 1.10', 45 | 'Framework :: Django :: 1.11', 46 | 'Framework :: Django :: 2.0', 47 | 'Intended Audience :: Developers', 48 | 'License :: OSI Approved :: Apache Software License', 49 | 'Operating System :: OS Independent', 50 | 'Programming Language :: Python', 51 | 'Programming Language :: Python :: 3.5', 52 | 'Programming Language :: Python :: 3.6', 53 | 'Topic :: Internet :: WWW/HTTP', 54 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 55 | ], 56 | install_requires=['django>=1.9', 'json2html==1.2.1', 'pika>=0.10.0', 'djangorestframework>=3.6', 'psutil>=5.4.5'] 57 | ) 58 | 59 | --------------------------------------------------------------------------------