├── .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 | --------------------------------------------------------------------------------