├── .gitattributes
├── .gitignore
├── .project
├── .pydevproject
├── .travis.yml
├── COPYING
├── MANIFEST.in
├── Makefile
├── Pipfile
├── Pipfile.lock
├── README.md
├── dev-requirements.txt
├── djcelery_model
├── __init__.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_auto_20190125_1008.py
│ └── __init__.py
├── models.py
└── south_migrations
│ ├── 0001_initial.py
│ ├── 0002_auto__add_field_modeltaskmeta_state.py
│ └── __init__.py
├── requirements.txt
└── setup.py
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set default behaviour, in case users don't have core.autocrlf set.
2 | * text=auto
3 |
4 | # Explicitly declare text files we want to always be normalized and converted
5 | # to native line endings on checkout.
6 | .gitignore text
7 | .gitattributes text
8 | *.py text
9 | *.md text
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | dist/
3 | *.egg-info
4 | *.pyc
5 | .DS_Store
6 | .vscode
7 |
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | django-celery-model
4 |
5 |
6 |
7 |
8 |
9 | org.python.pydev.PyDevBuilder
10 |
11 |
12 |
13 |
14 |
15 | org.python.pydev.pythonNature
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.pydevproject:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | /${PROJECT_DIR_NAME}
5 |
6 | python 2.7
7 | Default
8 |
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - "2.7"
4 | - "3.6"
5 | - "pypy"
6 | - "pypy3"
7 | # command to install dependencies
8 | install:
9 | - "pip install -r requirements.txt"
10 | - "pip install ."
11 | # command to run tests
12 | script:
13 | - "python -m compileall ."
14 |
--------------------------------------------------------------------------------
/COPYING:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014-2016 Marc Hoersken
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software is furnished to do so,
8 | subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include requirements.txt
2 | include COPYING
3 | include README.md
4 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | all: Pipfile.lock dev-requirements.txt
2 |
3 | Pipfile.lock: Pipfile
4 | pipenv lock --pre --dev
5 |
6 | dev-requirements.txt: Pipfile Pipfile.lock
7 | pipenv lock --pre --dev --requirements > dev-requirements.txt
8 |
9 | build: setup.py
10 | pipenv run python3 setup.py sdist bdist_wheel
11 |
12 | publish: dist
13 | pipenv run twine upload -s -i 2BCE098759303489D895D61D128358963026398E dist/*
14 |
--------------------------------------------------------------------------------
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | name = "pypi"
3 | url = "https://pypi.org/simple"
4 | verify_ssl = true
5 |
6 | [dev-packages]
7 | pylint = "*"
8 | setuptools = "*"
9 | wheel = "*"
10 | twine = "*"
11 |
12 | [packages]
13 | django = ">=1.11"
14 | celery = {extras = ["redis"],version = ">=4.2"}
15 |
16 | [requires]
17 | python_version = "3.7"
18 |
--------------------------------------------------------------------------------
/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "89e40b2426c2d6305354a0c87e65c77894fa80d925a1d5c721b9179698403af0"
5 | },
6 | "pipfile-spec": 6,
7 | "requires": {
8 | "python_version": "3.7"
9 | },
10 | "sources": [
11 | {
12 | "name": "pypi",
13 | "url": "https://pypi.org/simple",
14 | "verify_ssl": true
15 | }
16 | ]
17 | },
18 | "default": {
19 | "amqp": {
20 | "hashes": [
21 | "sha256:6e649ca13a7df3faacdc8bbb280aa9a6602d22fd9d545336077e573a1f4ff3b8",
22 | "sha256:77f1aef9410698d20eaeac5b73a87817365f457a507d82edf292e12cbb83b08d"
23 | ],
24 | "version": "==2.5.2"
25 | },
26 | "asgiref": {
27 | "hashes": [
28 | "sha256:7e06d934a7718bf3975acbf87780ba678957b87c7adc056f13b6215d610695a0",
29 | "sha256:ea448f92fc35a0ef4b1508f53a04c4670255a3f33d22a81c8fc9c872036adbe5"
30 | ],
31 | "version": "==3.2.3"
32 | },
33 | "billiard": {
34 | "hashes": [
35 | "sha256:26fd494dc3251f8ce1f5559744f18aeed427fdaf29a75d7baae26752a5d3816f",
36 | "sha256:f4e09366653aa3cb3ae8ed16423f9ba1665ff426f087bcdbbed86bf3664fe02c"
37 | ],
38 | "version": "==3.6.2.0"
39 | },
40 | "celery": {
41 | "extras": [
42 | "redis"
43 | ],
44 | "hashes": [
45 | "sha256:7c544f37a84a5eadc44cab1aa8c9580dff94636bb81978cdf9bf8012d9ea7d8f",
46 | "sha256:d3363bb5df72d74420986a435449f3c3979285941dff57d5d97ecba352a0e3e2"
47 | ],
48 | "index": "pypi",
49 | "version": "==4.4.0"
50 | },
51 | "django": {
52 | "hashes": [
53 | "sha256:2f1ba1db8648484dd5c238fb62504777b7ad090c81c5f1fd8d5eb5ec21b5f283",
54 | "sha256:c91c91a7ad6ef67a874a4f76f58ba534f9208412692a840e1d125eb5c279cb0a"
55 | ],
56 | "index": "pypi",
57 | "version": "==3.0.3"
58 | },
59 | "importlib-metadata": {
60 | "hashes": [
61 | "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302",
62 | "sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b"
63 | ],
64 | "markers": "python_version < '3.8'",
65 | "version": "==1.5.0"
66 | },
67 | "kombu": {
68 | "hashes": [
69 | "sha256:2a9e7adff14d046c9996752b2c48b6d9185d0b992106d5160e1a179907a5d4ac",
70 | "sha256:67b32ccb6fea030f8799f8fd50dd08e03a4b99464ebc4952d71d8747b1a52ad1"
71 | ],
72 | "version": "==4.6.7"
73 | },
74 | "pytz": {
75 | "hashes": [
76 | "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
77 | "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
78 | ],
79 | "version": "==2019.3"
80 | },
81 | "redis": {
82 | "hashes": [
83 | "sha256:0dcfb335921b88a850d461dc255ff4708294943322bd55de6cfd68972490ca1f",
84 | "sha256:b205cffd05ebfd0a468db74f0eedbff8df1a7bfc47521516ade4692991bb0833"
85 | ],
86 | "version": "==3.4.1"
87 | },
88 | "sqlparse": {
89 | "hashes": [
90 | "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177",
91 | "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873"
92 | ],
93 | "version": "==0.3.0"
94 | },
95 | "vine": {
96 | "hashes": [
97 | "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87",
98 | "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"
99 | ],
100 | "version": "==1.3.0"
101 | },
102 | "zipp": {
103 | "hashes": [
104 | "sha256:5c56e330306215cd3553342cfafc73dda2c60792384117893f3a83f8a1209f50",
105 | "sha256:d65287feb793213ffe11c0f31b81602be31448f38aeb8ffc2eb286c4f6f6657e"
106 | ],
107 | "version": "==2.2.0"
108 | }
109 | },
110 | "develop": {
111 | "astroid": {
112 | "hashes": [
113 | "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a",
114 | "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42"
115 | ],
116 | "version": "==2.3.3"
117 | },
118 | "bleach": {
119 | "hashes": [
120 | "sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16",
121 | "sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa"
122 | ],
123 | "version": "==3.1.0"
124 | },
125 | "certifi": {
126 | "hashes": [
127 | "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
128 | "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
129 | ],
130 | "version": "==2019.11.28"
131 | },
132 | "chardet": {
133 | "hashes": [
134 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
135 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
136 | ],
137 | "version": "==3.0.4"
138 | },
139 | "docutils": {
140 | "hashes": [
141 | "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af",
142 | "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"
143 | ],
144 | "version": "==0.16"
145 | },
146 | "idna": {
147 | "hashes": [
148 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
149 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
150 | ],
151 | "version": "==2.8"
152 | },
153 | "importlib-metadata": {
154 | "hashes": [
155 | "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302",
156 | "sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b"
157 | ],
158 | "markers": "python_version < '3.8'",
159 | "version": "==1.5.0"
160 | },
161 | "isort": {
162 | "hashes": [
163 | "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
164 | "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"
165 | ],
166 | "version": "==4.3.21"
167 | },
168 | "keyring": {
169 | "hashes": [
170 | "sha256:1f393f7466314068961c7e1d508120c092bd71fa54e3d93b76180b526d4abc56",
171 | "sha256:24ae23ab2d6adc59138339e56843e33ec7b0a6b2f06302662477085c6c0aca00"
172 | ],
173 | "version": "==21.1.0"
174 | },
175 | "lazy-object-proxy": {
176 | "hashes": [
177 | "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d",
178 | "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449",
179 | "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08",
180 | "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a",
181 | "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50",
182 | "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd",
183 | "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239",
184 | "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb",
185 | "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea",
186 | "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e",
187 | "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156",
188 | "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142",
189 | "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442",
190 | "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62",
191 | "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db",
192 | "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531",
193 | "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383",
194 | "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a",
195 | "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357",
196 | "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4",
197 | "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"
198 | ],
199 | "version": "==1.4.3"
200 | },
201 | "mccabe": {
202 | "hashes": [
203 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
204 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
205 | ],
206 | "version": "==0.6.1"
207 | },
208 | "pkginfo": {
209 | "hashes": [
210 | "sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb",
211 | "sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32"
212 | ],
213 | "version": "==1.5.0.1"
214 | },
215 | "pygments": {
216 | "hashes": [
217 | "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b",
218 | "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"
219 | ],
220 | "version": "==2.5.2"
221 | },
222 | "pylint": {
223 | "hashes": [
224 | "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd",
225 | "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"
226 | ],
227 | "index": "pypi",
228 | "version": "==2.4.4"
229 | },
230 | "readme-renderer": {
231 | "hashes": [
232 | "sha256:bb16f55b259f27f75f640acf5e00cf897845a8b3e4731b5c1a436e4b8529202f",
233 | "sha256:c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d"
234 | ],
235 | "version": "==24.0"
236 | },
237 | "requests": {
238 | "hashes": [
239 | "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
240 | "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
241 | ],
242 | "version": "==2.22.0"
243 | },
244 | "requests-toolbelt": {
245 | "hashes": [
246 | "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
247 | "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
248 | ],
249 | "version": "==0.9.1"
250 | },
251 | "six": {
252 | "hashes": [
253 | "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
254 | "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
255 | ],
256 | "version": "==1.14.0"
257 | },
258 | "tqdm": {
259 | "hashes": [
260 | "sha256:251ee8440dbda126b8dfa8a7c028eb3f13704898caaef7caa699b35e119301e2",
261 | "sha256:fe231261cfcbc6f4a99165455f8f6b9ef4e1032a6e29bccf168b4bf42012f09c"
262 | ],
263 | "version": "==4.42.1"
264 | },
265 | "twine": {
266 | "hashes": [
267 | "sha256:c1af8ca391e43b0a06bbc155f7f67db0bf0d19d284bfc88d1675da497a946124",
268 | "sha256:d561a5e511f70275e5a485a6275ff61851c16ffcb3a95a602189161112d9f160"
269 | ],
270 | "index": "pypi",
271 | "version": "==3.1.1"
272 | },
273 | "typed-ast": {
274 | "hashes": [
275 | "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
276 | "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
277 | "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
278 | "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
279 | "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
280 | "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
281 | "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
282 | "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
283 | "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
284 | "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
285 | "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
286 | "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
287 | "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
288 | "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
289 | "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
290 | "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
291 | "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
292 | "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
293 | "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
294 | "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
295 | "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
296 | ],
297 | "markers": "implementation_name == 'cpython' and python_version < '3.8'",
298 | "version": "==1.4.1"
299 | },
300 | "urllib3": {
301 | "hashes": [
302 | "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
303 | "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
304 | ],
305 | "version": "==1.25.8"
306 | },
307 | "webencodings": {
308 | "hashes": [
309 | "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
310 | "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
311 | ],
312 | "version": "==0.5.1"
313 | },
314 | "wheel": {
315 | "hashes": [
316 | "sha256:8788e9155fe14f54164c1b9eb0a319d98ef02c160725587ad60f14ddc57b6f96",
317 | "sha256:df277cb51e61359aba502208d680f90c0493adec6f0e848af94948778aed386e"
318 | ],
319 | "index": "pypi",
320 | "version": "==0.34.2"
321 | },
322 | "wrapt": {
323 | "hashes": [
324 | "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"
325 | ],
326 | "version": "==1.11.2"
327 | },
328 | "zipp": {
329 | "hashes": [
330 | "sha256:5c56e330306215cd3553342cfafc73dda2c60792384117893f3a83f8a1209f50",
331 | "sha256:d65287feb793213ffe11c0f31b81602be31448f38aeb8ffc2eb286c4f6f6657e"
332 | ],
333 | "version": "==2.2.0"
334 | }
335 | }
336 | }
337 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [django-celery-model](https://github.com/mback2k/django-celery-model) is an
2 | extension to [Celery](https://github.com/celery/celery) which adds support
3 | for tracking Celery tasks assigned to Django model instances.
4 |
5 | Installation
6 | ------------
7 | Install the latest version from pypi.python.org:
8 |
9 | pip install django-celery-model
10 |
11 | Install the development version by cloning the source from github.com:
12 |
13 | pip install git+https://github.com/mback2k/django-celery-model.git
14 |
15 | Configuration
16 | -------------
17 | Add the package to your `INSTALLED_APPS`:
18 |
19 | INSTALLED_APPS += (
20 | 'djcelery_model',
21 | )
22 |
23 | Make sure that you are receiving Celery events via:
24 |
25 | CELERY_TASK_TRACK_STARTED = True
26 | CELERY_TASK_SEND_SENT_EVENT = True
27 | CELERY_SEND_EVENTS = True
28 |
29 | Example
30 | -------
31 | Add the TaskMixin to your Django model:
32 |
33 | from django.db import models
34 | from django.utils.translation import ugettext_lazy as _
35 | from djcelery_model.models import TaskMixin
36 |
37 | class MyModel(TaskMixin, models.Model):
38 | name = models.CharField(_('Name'), max_length=100)
39 |
40 | Queue an asynchronous task from your Django model instance:
41 |
42 | from .models import MyModel
43 | from .tasks import mytask
44 |
45 | mymodel = MyModel.objects.get(name='test instance')
46 | mymodel.apply_async(mytask, ...)
47 |
48 | Retrieve list of asynchronous tasks assigned to your Django model instance:
49 |
50 | mymodel.tasks.all()
51 | mymodel.tasks.pending()
52 | mymodel.tasks.started()
53 | mymodel.tasks.retrying()
54 | mymodel.tasks.failed()
55 | mymodel.tasks.successful()
56 | mymodel.tasks.running()
57 | mymodel.tasks.ready()
58 |
59 | Check for a running or ready asynchronous task for your Django model instance:
60 |
61 | mymodel.has_running_task
62 | mymodel.has_ready_task
63 |
64 | Handle asynchronous task results for your Django model instance:
65 |
66 | mymodel.get_task_results()
67 | mymodel.get_task_result(task_id)
68 | mymodel.clear_task_results()
69 | mymodel.clear_task_result(task_id)
70 |
71 | Filter your Django model based upon asynchronous tasks:
72 |
73 | MyModel.objects.with_tasks()
74 | MyModel.objects.with_pending_tasks()
75 | MyModel.objects.with_started_tasks()
76 | MyModel.objects.with_retrying_tasks()
77 | MyModel.objects.with_failed_tasks()
78 | MyModel.objects.with_successful_tasks()
79 | MyModel.objects.with_running_tasks()
80 | MyModel.objects.with_ready_tasks()
81 |
82 | MyModel.objects.without_tasks()
83 | MyModel.objects.without_pending_tasks()
84 | MyModel.objects.without_started_tasks()
85 | MyModel.objects.without_retrying_tasks()
86 | MyModel.objects.without_failed_tasks()
87 | MyModel.objects.without_successful_tasks()
88 | MyModel.objects.without_running_tasks()
89 | MyModel.objects.without_ready_tasks()
90 |
91 | License
92 | -------
93 | * Released under MIT License
94 | * Copyright (c) 2014-2019 Marc Hoersken
95 |
--------------------------------------------------------------------------------
/dev-requirements.txt:
--------------------------------------------------------------------------------
1 | -i https://pypi.org/simple
2 | astroid==2.3.3
3 | bleach==3.1.0
4 | certifi==2019.11.28
5 | chardet==3.0.4
6 | docutils==0.16
7 | idna==2.8
8 | importlib-metadata==1.5.0 ; python_version < '3.8'
9 | isort==4.3.21
10 | keyring==21.1.0
11 | lazy-object-proxy==1.4.3
12 | mccabe==0.6.1
13 | pkginfo==1.5.0.1
14 | pygments==2.5.2
15 | pylint==2.4.4
16 | readme-renderer==24.0
17 | requests-toolbelt==0.9.1
18 | requests==2.22.0
19 | six==1.14.0
20 | tqdm==4.42.1
21 | twine==3.1.1
22 | typed-ast==1.4.1 ; implementation_name == 'cpython' and python_version < '3.8'
23 | urllib3==1.25.8
24 | webencodings==0.5.1
25 | wheel==0.34.2
26 | wrapt==1.11.2
27 | zipp==2.2.0
28 |
--------------------------------------------------------------------------------
/djcelery_model/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 | django-celery-model is an extension to Celery which adds support
5 | for tracking Celery tasks assigned to Django model instances.
6 | """
7 | from __future__ import absolute_import
8 | from __future__ import division
9 | from __future__ import print_function
10 | from __future__ import unicode_literals
11 |
12 | __version_info__ = {
13 | 'major': 0,
14 | 'minor': 2,
15 | 'micro': 1,
16 | 'releaselevel': 'final',
17 | }
18 |
19 | def get_version():
20 | """
21 | Return the formatted version information
22 | """
23 | vers = ["%(major)i.%(minor)i" % __version_info__, ]
24 |
25 | if __version_info__['micro']:
26 | vers.append(".%(micro)i" % __version_info__)
27 | if __version_info__['releaselevel'] != 'final':
28 | vers.append('%(releaselevel)s' % __version_info__)
29 | return ''.join(vers)
30 |
31 | __version__ = get_version()
32 |
--------------------------------------------------------------------------------
/djcelery_model/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('contenttypes', '0001_initial'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='ModelTaskMeta',
16 | fields=[
17 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
18 | ('object_id', models.PositiveIntegerField()),
19 | ('task_id', models.CharField(unique=True, max_length=255)),
20 | ('state', models.PositiveIntegerField(default=0, choices=[(0, b'PENDING'), (1, b'STARTED'), (2, b'RETRY'), (3, b'FAILURE'), (4, b'SUCCESS')])),
21 | ('content_type', models.ForeignKey(to='contenttypes.ContentType', on_delete=models.CASCADE)),
22 | ],
23 | options={
24 | },
25 | bases=(models.Model,),
26 | ),
27 | ]
28 |
--------------------------------------------------------------------------------
/djcelery_model/migrations/0002_auto_20190125_1008.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.18 on 2019-01-25 10:08
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 | import django.utils.timezone
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | dependencies = [
12 | ('djcelery_model', '0001_initial'),
13 | ]
14 |
15 | operations = [
16 | migrations.AddField(
17 | model_name='modeltaskmeta',
18 | name='created',
19 | field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
20 | preserve_default=False,
21 | ),
22 | migrations.AddField(
23 | model_name='modeltaskmeta',
24 | name='updated',
25 | field=models.DateTimeField(auto_now=True),
26 | ),
27 | migrations.AlterField(
28 | model_name='modeltaskmeta',
29 | name='state',
30 | field=models.PositiveIntegerField(choices=[(0, 'PENDING'), (1, 'STARTED'), (2, 'RETRY'), (3, 'FAILURE'), (4, 'SUCCESS')], default=0),
31 | ),
32 | ]
33 |
--------------------------------------------------------------------------------
/djcelery_model/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mback2k/django-celery-model/218bb8a6ddc2c6f447d22c2c570e98448df8b78b/djcelery_model/migrations/__init__.py
--------------------------------------------------------------------------------
/djcelery_model/models.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | from __future__ import absolute_import
4 | from __future__ import division
5 | from __future__ import print_function
6 | from __future__ import unicode_literals
7 |
8 | from django.db import models
9 | from django.db.models import Q
10 | from django.db.models.query import QuerySet
11 | from django.contrib.contenttypes.models import ContentType
12 | from django.utils.encoding import python_2_unicode_compatible
13 |
14 | try:
15 | # Django >= 1.7
16 | from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
17 | except ImportError:
18 | from django.contrib.contenttypes.generic import GenericForeignKey, GenericRelation
19 |
20 | from celery.result import AsyncResult
21 | from celery.utils import uuid
22 | from celery import signals
23 |
24 | class ModelTaskMetaState(object):
25 | PENDING = 0
26 | STARTED = 1
27 | RETRY = 2
28 | FAILURE = 3
29 | SUCCESS = 4
30 |
31 | @classmethod
32 | def lookup(cls, state):
33 | return getattr(cls, state)
34 |
35 | class ModelTaskMetaFilterMixin(object):
36 | def pending(self):
37 | return self.filter(state=ModelTaskMetaState.PENDING)
38 |
39 | def started(self):
40 | return self.filter(state=ModelTaskMetaState.STARTED)
41 |
42 | def retrying(self):
43 | return self.filter(state=ModelTaskMetaState.RETRY)
44 |
45 | def failed(self):
46 | return self.filter(state=ModelTaskMetaState.FAILURE)
47 |
48 | def successful(self):
49 | return self.filter(state=ModelTaskMetaState.SUCCESS)
50 |
51 | def running(self):
52 | return self.filter(Q(state=ModelTaskMetaState.PENDING)|
53 | Q(state=ModelTaskMetaState.STARTED)|
54 | Q(state=ModelTaskMetaState.RETRY))
55 |
56 | def ready(self):
57 | return self.filter(Q(state=ModelTaskMetaState.FAILURE)|
58 | Q(state=ModelTaskMetaState.SUCCESS))
59 |
60 | class ModelTaskMetaQuerySet(ModelTaskMetaFilterMixin, QuerySet):
61 | pass
62 |
63 | class ModelTaskMetaManager(ModelTaskMetaFilterMixin, models.Manager):
64 | use_for_related_fields = True
65 |
66 | def get_queryset(self):
67 | return ModelTaskMetaQuerySet(self.model, using=self._db)
68 |
69 | @python_2_unicode_compatible
70 | class ModelTaskMeta(models.Model):
71 | STATES = (
72 | (ModelTaskMetaState.PENDING, 'PENDING'),
73 | (ModelTaskMetaState.STARTED, 'STARTED'),
74 | (ModelTaskMetaState.RETRY, 'RETRY'),
75 | (ModelTaskMetaState.FAILURE, 'FAILURE'),
76 | (ModelTaskMetaState.SUCCESS, 'SUCCESS'),
77 | )
78 |
79 | content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
80 | object_id = models.PositiveIntegerField()
81 | content_object = GenericForeignKey()
82 | task_id = models.CharField(max_length=255, unique=True)
83 | state = models.PositiveIntegerField(choices=STATES,
84 | default=ModelTaskMetaState.PENDING)
85 | created = models.DateTimeField(auto_now_add=True, editable=False)
86 | updated = models.DateTimeField(auto_now=True)
87 |
88 | objects = ModelTaskMetaManager()
89 |
90 | def __str__(self):
91 | return '%s: %s' % (self.task_id, dict(self.STATES)[self.state])
92 |
93 | @property
94 | def result(self):
95 | return ModelAsyncResult(self.task_id)
96 |
97 |
98 | class ModelAsyncResult(AsyncResult):
99 | def forget(self):
100 | ModelTaskMeta.objects.filter(task_id=self.id).delete()
101 | return super(ModelAsyncResult, self).forget()
102 |
103 |
104 | class TaskFilterMixin(object):
105 | def with_tasks(self):
106 | return self.filter(tasks__state__isnull=False)
107 |
108 | def with_pending_tasks(self):
109 | return self.filter(tasks__state=ModelTaskMetaState.PENDING)
110 |
111 | def with_started_tasks(self):
112 | return self.filter(tasks__state=ModelTaskMetaState.STARTED)
113 |
114 | def with_retrying_tasks(self):
115 | return self.filter(tasks__state=ModelTaskMetaState.RETRY)
116 |
117 | def with_failed_tasks(self):
118 | return self.filter(tasks__state=ModelTaskMetaState.FAILURE)
119 |
120 | def with_successful_tasks(self):
121 | return self.filter(tasks__state=ModelTaskMetaState.SUCCESS)
122 |
123 | def with_running_tasks(self):
124 | return self.filter(Q(tasks__state=ModelTaskMetaState.PENDING)|
125 | Q(tasks__state=ModelTaskMetaState.STARTED)|
126 | Q(tasks__state=ModelTaskMetaState.RETRY))
127 |
128 | def with_ready_tasks(self):
129 | return self.filter(Q(tasks__state=ModelTaskMetaState.FAILURE)|
130 | Q(tasks__state=ModelTaskMetaState.SUCCESS))
131 |
132 | def without_tasks(self):
133 | return self.exclude(tasks__state__isnull=False)
134 |
135 | def without_pending_tasks(self):
136 | return self.exclude(tasks__state=ModelTaskMetaState.PENDING)
137 |
138 | def without_started_tasks(self):
139 | return self.exclude(tasks__state=ModelTaskMetaState.STARTED)
140 |
141 | def without_retrying_tasks(self):
142 | return self.exclude(tasks__state=ModelTaskMetaState.RETRY)
143 |
144 | def without_failed_tasks(self):
145 | return self.exclude(tasks__state=ModelTaskMetaState.FAILURE)
146 |
147 | def without_successful_tasks(self):
148 | return self.exclude(tasks__state=ModelTaskMetaState.SUCCESS)
149 |
150 | def without_running_tasks(self):
151 | return self.exclude(Q(tasks__state=ModelTaskMetaState.PENDING)|
152 | Q(tasks__state=ModelTaskMetaState.STARTED)|
153 | Q(tasks__state=ModelTaskMetaState.RETRY))
154 |
155 | def without_ready_tasks(self):
156 | return self.exclude(Q(tasks__state=ModelTaskMetaState.FAILURE)|
157 | Q(tasks__state=ModelTaskMetaState.SUCCESS))
158 |
159 | class TaskQuerySet(TaskFilterMixin, QuerySet):
160 | pass
161 |
162 | class TaskManager(TaskFilterMixin, models.Manager):
163 | use_for_related_fields = True
164 |
165 | def get_queryset(self):
166 | return TaskQuerySet(self.model, using=self._db)
167 |
168 | class TaskMixin(models.Model):
169 | tasks = GenericRelation(ModelTaskMeta)
170 |
171 | objects = TaskManager()
172 |
173 | class Meta:
174 | abstract = True
175 |
176 | @property
177 | def has_running_task(self):
178 | return self.tasks.running().exists()
179 |
180 | @property
181 | def has_ready_task(self):
182 | return self.tasks.ready().exists()
183 |
184 | def apply_async(self, task, *args, **kwargs):
185 | if 'task_id' in kwargs:
186 | task_id = kwargs['task_id']
187 | else:
188 | task_id = kwargs['task_id'] = uuid()
189 | forget_if_ready(AsyncResult(task_id))
190 | try:
191 | taskmeta = ModelTaskMeta.objects.get(task_id=task_id)
192 | taskmeta.content_object = self
193 | except ModelTaskMeta.DoesNotExist:
194 | taskmeta = ModelTaskMeta(task_id=task_id, content_object=self)
195 | taskmeta.save()
196 | return task.apply_async(*args, **kwargs)
197 |
198 | def get_task_results(self):
199 | return map(lambda x: x.result, self.tasks.all())
200 |
201 | def get_task_result(self, task_id):
202 | return self.tasks.get(task_id=task_id).result
203 |
204 | def clear_task_results(self):
205 | for task_result in self.get_task_results():
206 | forget_if_ready(task_result)
207 |
208 | def clear_task_result(self, task_id):
209 | task_result = self.get_task_result(task_id)
210 | forget_if_ready(task_result)
211 |
212 |
213 | def forget_if_ready(async_result):
214 | if async_result and async_result.ready():
215 | async_result.forget()
216 |
217 |
218 | @signals.after_task_publish.connect
219 | def handle_after_task_publish(sender=None, body=None, **kwargs):
220 | if body and 'id' in body:
221 | queryset = ModelTaskMeta.objects.filter(task_id=body['id'])
222 | queryset.update(state=ModelTaskMetaState.PENDING)
223 |
224 | @signals.task_prerun.connect
225 | def handle_task_prerun(sender=None, task_id=None, **kwargs):
226 | if task_id:
227 | queryset = ModelTaskMeta.objects.filter(task_id=task_id)
228 | queryset.update(state=ModelTaskMetaState.STARTED)
229 |
230 | @signals.task_postrun.connect
231 | def handle_task_postrun(sender=None, task_id=None, state=None, **kwargs):
232 | if task_id and state:
233 | queryset = ModelTaskMeta.objects.filter(task_id=task_id)
234 | queryset.update(state=ModelTaskMetaState.lookup(state))
235 |
236 | @signals.task_failure.connect
237 | def handle_task_failure(sender=None, task_id=None, **kwargs):
238 | if task_id:
239 | queryset = ModelTaskMeta.objects.filter(task_id=task_id)
240 | queryset.update(state=ModelTaskMetaState.FAILURE)
241 |
242 | @signals.task_revoked.connect
243 | def handle_task_revoked(sender=None, request=None, **kwargs):
244 | if request and request.id:
245 | queryset = ModelTaskMeta.objects.filter(task_id=request.id)
246 | queryset.delete()
247 |
--------------------------------------------------------------------------------
/djcelery_model/south_migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import datetime
3 | from south.db import db
4 | from south.v2 import SchemaMigration
5 | from django.db import models
6 |
7 |
8 | class Migration(SchemaMigration):
9 |
10 | def forwards(self, orm):
11 | # Adding model 'ModelTaskMeta'
12 | db.create_table(u'djcelery_model_modeltaskmeta', (
13 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
14 | ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])),
15 | ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()),
16 | ('task_id', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)),
17 | ))
18 | db.send_create_signal(u'djcelery_model', ['ModelTaskMeta'])
19 |
20 |
21 | def backwards(self, orm):
22 | # Deleting model 'ModelTaskMeta'
23 | db.delete_table(u'djcelery_model_modeltaskmeta')
24 |
25 |
26 | models = {
27 | u'contenttypes.contenttype': {
28 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
29 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
30 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
31 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
32 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
33 | },
34 | u'djcelery_model.modeltaskmeta': {
35 | 'Meta': {'object_name': 'ModelTaskMeta'},
36 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
37 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
38 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
39 | 'task_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
40 | }
41 | }
42 |
43 | complete_apps = ['djcelery_model']
--------------------------------------------------------------------------------
/djcelery_model/south_migrations/0002_auto__add_field_modeltaskmeta_state.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import datetime
3 | from south.db import db
4 | from south.v2 import SchemaMigration
5 | from django.db import models
6 |
7 |
8 | class Migration(SchemaMigration):
9 |
10 | def forwards(self, orm):
11 | # Adding field 'ModelTaskMeta.state'
12 | db.add_column(u'djcelery_model_modeltaskmeta', 'state',
13 | self.gf('django.db.models.fields.PositiveIntegerField')(default=0),
14 | keep_default=False)
15 |
16 |
17 | def backwards(self, orm):
18 | # Deleting field 'ModelTaskMeta.state'
19 | db.delete_column(u'djcelery_model_modeltaskmeta', 'state')
20 |
21 |
22 | models = {
23 | u'contenttypes.contenttype': {
24 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
25 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
26 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
27 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
28 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
29 | },
30 | u'djcelery_model.modeltaskmeta': {
31 | 'Meta': {'object_name': 'ModelTaskMeta'},
32 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
33 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
34 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
35 | 'state': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
36 | 'task_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
37 | }
38 | }
39 |
40 | complete_apps = ['djcelery_model']
--------------------------------------------------------------------------------
/djcelery_model/south_migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mback2k/django-celery-model/218bb8a6ddc2c6f447d22c2c570e98448df8b78b/djcelery_model/south_migrations/__init__.py
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | django>=1.11
2 | celery>=4.2
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | from __future__ import absolute_import
4 | from __future__ import division
5 | from __future__ import print_function
6 | from __future__ import unicode_literals
7 |
8 | from setuptools import setup, find_packages
9 | from djcelery_model import __version__ as version
10 | from djcelery_model import __doc__ as doc
11 | import os
12 |
13 | def read_file(filename):
14 | """Read a file into a string"""
15 | path = os.path.abspath(os.path.dirname(__file__))
16 | filepath = os.path.join(path, filename)
17 | try:
18 | with open(filepath, 'r') as fh:
19 | return fh.read()
20 | except IOError:
21 | return ''
22 |
23 | setup(
24 | name='django-celery-model',
25 | version=version,
26 | author='Marc Hoersken',
27 | author_email='info@marc-hoersken.de',
28 | packages=find_packages(),
29 | include_package_data=True,
30 | url='https://github.com/mback2k/django-celery-model',
31 | license='MIT',
32 | description=' '.join(doc.splitlines()).strip(),
33 | install_requires=read_file('requirements.txt').splitlines(),
34 | classifiers=[
35 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
36 | 'Intended Audience :: Developers',
37 | 'License :: OSI Approved :: MIT License',
38 | 'Programming Language :: Python',
39 | 'Topic :: Software Development :: Libraries :: Python Modules',
40 | 'Development Status :: 4 - Beta',
41 | 'Operating System :: OS Independent',
42 | ],
43 | long_description=read_file('README.md'),
44 | long_description_content_type='text/markdown',
45 | )
46 |
--------------------------------------------------------------------------------