diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..3b227e0 --- /dev/null +++ b/.env.template @@ -0,0 +1,7 @@ +# Upload attachments to the CDN +UPLOADCARE_PUBLIC_KEY +UPLOADCARE_SECRET + +# Sentry +SENTRY_ENVIRONMENT +SENTRY_DSN diff --git a/.gitignore b/.gitignore index 486f201..8010ed3 100644 --- a/.gitignore +++ b/.gitignore @@ -103,4 +103,7 @@ venv.bak/ # mypy .mypy_cache/ -.idea \ No newline at end of file +.idea + +# Local +mydatabase diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..080ddba --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +--- + +- repo: local + hooks: + - id: isort + name: isort + entry: 'pipenv run isort' + language: system + - id: flake8 + name: flake8 + entry: 'pipenv run flake8' + pass_filenames: false + language: system + - id: mypy + name: mypy + entry: 'pipenv run mypy .' + types: [python] + pass_filenames: false + language: system diff --git a/.travis.yml b/.travis.yml index 4874842..5c9f6cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,27 @@ language: python python: - '3.6' cache: pip + +services: + - docker + install: - 'pip install pipenv' - - 'pipenv sync' + - 'pipenv sync --dev' + script: - 'pipenv run flake8' + - 'pipenv run mypy .' + +before_deploy: + - echo $REGISTRY_PASS | docker login -u "$REGISTRY_USER" --password-stdin + - export IMAGE_TAG=`if [ "$TRAVIS_BRANCH" == "master" ]; then echo "latest"; else echo "dev"; fi` + - export IMAGE_NAME="piterpy/postpost" + - docker build -t $IMAGE_NAME:$IMAGE_TAG . + +deploy: + provider: script + script: docker push $IMAGE_NAME:$IMAGE_TAG + on: + condition: $TRAVIS_BRANCH =~ ^master|develop$ + all_branches: true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b8e5344 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.6-alpine + +ENV PYTHONPATH=/app/postpost \ + DJANGO_SETTINGS_MODULE=main.settings \ + PYTHONUNBUFFERED=1 \ + PYTHONFAULTHANDLER=1 \ + PIP_NO_CACHE_DIR=off + +RUN apk --no-cache add gcc build-base linux-headers jpeg-dev zlib-dev postgresql-dev musl-dev && \ + pip install pipenv + +WORKDIR /app +COPY Pipfile Pipfile.lock /app/ +RUN pipenv sync + +COPY . /app + +CMD sh /app/run-app.sh diff --git a/Pipfile b/Pipfile index b688f24..b64c815 100644 --- a/Pipfile +++ b/Pipfile @@ -3,10 +3,15 @@ url = "https://pypi.python.org/simple" verify_ssl = true [dev-packages] +wemake-python-styleguide = "==0.7.1" +# see https://github.com/wemake-services/wemake-python-styleguide/pull/472#issuecomment-460057878 +flake8 = '==3.6.0' +mypy = "==0.701" +pre-commit = "*" +python-decouple = "*" [packages] -django = "==2.1.5" -wemake-python-styleguide = "==0.6.3" +django = "==2.2" django-rest-framework = "*" pillow = "*" drf-writable-nested = "*" @@ -15,6 +20,14 @@ redis = "*" requests = "*" drf-yasg = "*" djangorestframework-camel-case = "*" +django-oauth-toolkit = "*" +django-cors-middleware = "*" +pyuploadcare = "*" +django-filter = "*" +uwsgi = "*" +typing-extensions = "*" +psycopg2-binary = "*" +sentry-sdk = "*" [requires] python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock index 7e24a23..9b84974 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "7cb9ec158649d28880731bc7089c431b35101bf4234eebf2c2012ab369a912ac" + "sha256": "90a503a4e45cdac5509d55a4f710b77ef56a7e43c86f2c9f6f36c0f58cbe4969" }, "pipfile-spec": 6, "requires": { @@ -17,51 +17,30 @@ "default": { "amqp": { "hashes": [ - "sha256:9f181e4aef6562e6f9f45660578fc1556150ca06e836ecb9e733e6ea10b48464", - "sha256:c3d7126bfbc640d076a01f1f4f6e609c0e4348508150c1f61336b0d83c738d2b" + "sha256:043beb485774ca69718a35602089e524f87168268f0d1ae115f28b88d27f92d7", + "sha256:35a3b5006ca00b21aaeec8ceea07130f07b902dd61bfe42815039835f962f5f1" ], - "version": "==2.4.0" - }, - "astor": { - "hashes": [ - "sha256:95c30d87a6c2cf89aa628b87398466840f0ad8652f88eb173125a6df8533fb8d", - "sha256:fb503b9e2fdd05609fbf557b916b4a7824171203701660f0c55bbf5a7a68713e" - ], - "version": "==0.7.1" - }, - "attrs": { - "hashes": [ - "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", - "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" - ], - "version": "==18.2.0" - }, - "bandit": { - "hashes": [ - "sha256:6102b5d6afd9d966df5054e0bdfc2e73a24d0fea400ec25f2e54c134412158d7", - "sha256:9413facfe9de1e1bd291d525c784e1beb1a55c9916b51dae12979af63a69ba4c" - ], - "version": "==1.5.1" + "version": "==2.4.2" }, "billiard": { "hashes": [ - "sha256:42d9a227401ac4fba892918bba0a0c409def5435c4b483267ebfe821afaaba0e" + "sha256:756bf323f250db8bf88462cd042c992ba60d8f5e07fc5636c24ba7d6f4261d84" ], - "version": "==3.5.0.5" + "version": "==3.6.0.0" }, "celery": { "hashes": [ - "sha256:77dab4677e24dc654d42dfbdfed65fa760455b6bb563a0877ecc35f4cfcfc678", - "sha256:ad7a7411772b80a4d6c64f2f7f723200e39fb66cf614a7fdfab76d345acc7b13" + "sha256:4c4532aa683f170f40bd76f928b70bc06ff171a959e06e71bf35f2f9d6031ef9", + "sha256:528e56767ae7e43a16cfef24ee1062491f5754368d38fcfffa861cdb9ef219be" ], - "version": "==4.2.1" + "version": "==4.3.0" }, "certifi": { "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", + "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae" ], - "version": "==2018.11.29" + "version": "==2019.3.9" }, "chardet": { "hashes": [ @@ -86,10 +65,29 @@ }, "django": { "hashes": [ - "sha256:a32c22af23634e1d11425574dce756098e015a165be02e4690179889b207c7a8", - "sha256:d6393918da830530a9516bbbcbf7f1214c3d733738779f06b0f649f49cc698c3" + "sha256:7c3543e4fb070d14e10926189a7fcf42ba919263b7473dceaefce34d54e8a119", + "sha256:a2814bffd1f007805b19194eb0b9a331933b82bd5da1c3ba3d7b7ba16e06dc4b" + ], + "version": "==2.2" + }, + "django-cors-middleware": { + "hashes": [ + "sha256:25d7e3132e9533be83f62767fca9dc92d66ac9aee414559144ccbce2c2913d70" + ], + "version": "==1.3.1" + }, + "django-filter": { + "hashes": [ + "sha256:3dafb7d2810790498895c22a1f31b2375795910680ac9c1432821cbedb1e176d", + "sha256:a3014de317bef0cd43075a0f08dfa1d319a7ccc5733c3901fb860da70b0dda68" ], - "version": "==2.1.5" + "version": "==2.1.0" + }, + "django-oauth-toolkit": { + "hashes": [ + "sha256:ad1b76275950ebbff708222cec57bbdb879f89bac7df6b9dee0f4b9db485c264" + ], + "version": "==1.2.0" }, "django-rest-framework": { "hashes": [ @@ -99,16 +97,17 @@ }, "djangorestframework": { "hashes": [ - "sha256:79c6efbb2514bc50cf25906d7c0a5cfead714c7af667ff4bd110312cd380ae66", - "sha256:a4138613b67e3a223be6c97f53b13d759c5b90d2b433bad670b8ebf95402075f" + "sha256:1d22971a5fc98becdbbad9710ca2a9148dd339f6cbea4c3ddbed2cb84bab94e1", + "sha256:2884763160b997073ff1e937bd820a69d23978902a3ccd0ba53a217e196239f0" ], - "version": "==3.9.1" + "version": "==3.9.3" }, "djangorestframework-camel-case": { "hashes": [ - "sha256:989c5c2d0324069fc1ecea4a5cb8913749d5f2f3c507b38977913ff1b76a719e" + "sha256:4bb2e41fb8a5d3745e20c5ee0842ebc6f6bac602b3286c3dd913b01760a2abb0", + "sha256:5b957f9cf16730f153a0ab4add9ff17fb41b7fceaa7edad29b0536b515bffd16" ], - "version": "==0.2.0" + "version": "==1.0.3" }, "drf-writable-nested": { "hashes": [ @@ -118,16 +117,308 @@ }, "drf-yasg": { "hashes": [ - "sha256:89c84779fb4bfe9c0704bdd40ad70b91fff13fa202696ce580de1c8615414f88", - "sha256:c37adfd3859d04827f971098227a54ef7229a79860860dae7b41abdc17e4e8cf" + "sha256:2c03c569cd1f1711e30efe3a1a56998cf61aa224cb2991e9fdac22aa47fd41e5", + "sha256:dfb96eb36b259c2e0da67515319a25e49f6bdd3a275412f34221cc5236d2b62a" + ], + "version": "==1.15.0" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "inflection": { + "hashes": [ + "sha256:18ea7fb7a7d152853386523def08736aa8c32636b047ade55f7578c4edeb16ca" + ], + "version": "==0.3.1" + }, + "itypes": { + "hashes": [ + "sha256:c6e77bb9fd68a4bfeb9d958fea421802282451a25bac4913ec94db82a899c073" + ], + "version": "==1.1.0" + }, + "jinja2": { + "hashes": [ + "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", + "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b" ], - "version": "==1.12.1" + "version": "==2.10.1" + }, + "kombu": { + "hashes": [ + "sha256:389ba09e03b15b55b1a7371a441c894fd8121d174f5583bbbca032b9ea8c9edd", + "sha256:7b92303af381ef02fad6899fd5f5a9a96031d781356cd8e505fa54ae5ddee181" + ], + "version": "==4.5.0" + }, + "markupsafe": { + "hashes": [ + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" + ], + "version": "==1.1.1" + }, + "oauthlib": { + "hashes": [ + "sha256:0ce32c5d989a1827e3f1148f98b9085ed2370fc939bf524c9c851d8714797298", + "sha256:3e1e14f6cde7e5475128d30e97edc3bfb4dc857cb884d8714ec161fdbb3b358e" + ], + "version": "==3.0.1" + }, + "pillow": { + "hashes": [ + "sha256:15c056bfa284c30a7f265a41ac4cbbc93bdbfc0dfe0613b9cb8a8581b51a9e55", + "sha256:1a4e06ba4f74494ea0c58c24de2bb752818e9d504474ec95b0aa94f6b0a7e479", + "sha256:1c3c707c76be43c9e99cb7e3d5f1bee1c8e5be8b8a2a5eeee665efbf8ddde91a", + "sha256:1fd0b290203e3b0882d9605d807b03c0f47e3440f97824586c173eca0aadd99d", + "sha256:24114e4a6e1870c5a24b1da8f60d0ba77a0b4027907860188ea82bd3508c80eb", + "sha256:258d886a49b6b058cd7abb0ab4b2b85ce78669a857398e83e8b8e28b317b5abb", + "sha256:33c79b6dd6bc7f65079ab9ca5bebffb5f5d1141c689c9c6a7855776d1b09b7e8", + "sha256:367385fc797b2c31564c427430c7a8630db1a00bd040555dfc1d5c52e39fcd72", + "sha256:3c1884ff078fb8bf5f63d7d86921838b82ed4a7d0c027add773c2f38b3168754", + "sha256:44e5240e8f4f8861d748f2a58b3f04daadab5e22bfec896bf5434745f788f33f", + "sha256:46aa988e15f3ea72dddd81afe3839437b755fffddb5e173886f11460be909dce", + "sha256:74d90d499c9c736d52dd6d9b7221af5665b9c04f1767e35f5dd8694324bd4601", + "sha256:809c0a2ce9032cbcd7b5313f71af4bdc5c8c771cb86eb7559afd954cab82ebb5", + "sha256:85d1ef2cdafd5507c4221d201aaf62fc9276f8b0f71bd3933363e62a33abc734", + "sha256:8c3889c7681af77ecfa4431cd42a2885d093ecb811e81fbe5e203abc07e0995b", + "sha256:9218d81b9fca98d2c47d35d688a0cea0c42fd473159dfd5612dcb0483c63e40b", + "sha256:9aa4f3827992288edd37c9df345783a69ef58bd20cc02e64b36e44bcd157bbf1", + "sha256:9d80f44137a70b6f84c750d11019a3419f409c944526a95219bea0ac31f4dd91", + "sha256:b7ebd36128a2fe93991293f997e44be9286503c7530ace6a55b938b20be288d8", + "sha256:c4c78e2c71c257c136cdd43869fd3d5e34fc2162dc22e4a5406b0ebe86958239", + "sha256:c6a842537f887be1fe115d8abb5daa9bc8cc124e455ff995830cc785624a97af", + "sha256:cf0a2e040fdf5a6d95f4c286c6ef1df6b36c218b528c8a9158ec2452a804b9b8", + "sha256:cfd28aad6fc61f7a5d4ee556a997dc6e5555d9381d1390c00ecaf984d57e4232", + "sha256:dca5660e25932771460d4688ccbb515677caaf8595f3f3240ec16c117deff89a", + "sha256:de7aedc85918c2f887886442e50f52c1b93545606317956d65f342bd81cb4fc3", + "sha256:e6c0bbf8e277b74196e3140c35f9a1ae3eafd818f7f2d3a15819c49135d6c062" + ], + "version": "==6.0.0" + }, + "psycopg2-binary": { + "hashes": [ + "sha256:007ca0df127b1862fc010125bc4100b7a630efc6841047bd11afceadb4754611", + "sha256:03c49e02adf0b4d68f422fdbd98f7a7c547beb27e99a75ed02298f85cb48406a", + "sha256:0a1232cdd314e08848825edda06600455ad2a7adaa463ebfb12ece2d09f3370e", + "sha256:131c80d0958c89273d9720b9adf9df1d7600bb3120e16019a7389ab15b079af5", + "sha256:2de34cc3b775724623f86617d2601308083176a495f5b2efc2bbb0da154f483a", + "sha256:2eddc31500f73544a2a54123d4c4b249c3c711d31e64deddb0890982ea37397a", + "sha256:484f6c62bdc166ee0e5be3aa831120423bf399786d1f3b0304526c86180fbc0b", + "sha256:4c2d9369ed40b4a44a8ccd6bc3a7db6272b8314812d2d1091f95c4c836d92e06", + "sha256:70f570b5fa44413b9f30dbc053d17ef3ce6a4100147a10822f8662e58d473656", + "sha256:7a2b5b095f3bd733aab101c89c0e1a3f0dfb4ebdc26f6374805c086ffe29d5b2", + "sha256:804914a669186e2843c1f7fbe12b55aad1b36d40a28274abe6027deffad9433d", + "sha256:8520c03172da18345d012949a53617a963e0191ccb3c666f23276d5326af27b5", + "sha256:90da901fc33ea393fc644607e4a3916b509387e9339ec6ebc7bfded45b7a0ae9", + "sha256:a582416ad123291a82c300d1d872bdc4136d69ad0b41d57dc5ca3df7ef8e3088", + "sha256:ac8c5e20309f4989c296d62cac20ee456b69c41fd1bc03829e27de23b6fa9dd0", + "sha256:b2cf82f55a619879f8557fdaae5cec7a294fac815e0087c4f67026fdf5259844", + "sha256:b59d6f8cfca2983d8fdbe457bf95d2192f7b7efdb2b483bf5fa4e8981b04e8b2", + "sha256:be08168197021d669b9964bd87628fa88f910b1be31e7010901070f2540c05fd", + "sha256:be0f952f1c365061041bad16e27e224e29615d4eb1fb5b7e7760a1d3d12b90b6", + "sha256:c1c9a33e46d7c12b9c96cf2d4349d783e3127163fd96254dcd44663cf0a1d438", + "sha256:d18c89957ac57dd2a2724ecfe9a759912d776f96ecabba23acb9ecbf5c731035", + "sha256:d7e7b0ff21f39433c50397e60bf0995d078802c591ca3b8d99857ea18a7496ee", + "sha256:da0929b2bf0d1f365345e5eb940d8713c1d516312e010135b14402e2a3d2404d", + "sha256:de24a4962e361c512d3e528ded6c7480eab24c655b8ca1f0b761d3b3650d2f07", + "sha256:e45f93ff3f7dae2202248cf413a87aeb330821bf76998b3cf374eda2fc893dd7", + "sha256:f046aeae1f7a845041b8661bb7a52449202b6c5d3fb59eb4724e7ca088811904", + "sha256:f1dc2b7b2748084b890f5d05b65a47cd03188824890e9a60818721fd492249fb", + "sha256:fcbe7cf3a786572b73d2cd5f34ed452a5f5fac47c9c9d1e0642c457a148f9f88" + ], + "version": "==2.8.2" + }, + "python-dateutil": { + "hashes": [ + "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", + "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" + ], + "version": "==2.8.0" + }, + "pytz": { + "hashes": [ + "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", + "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141" + ], + "version": "==2019.1" + }, + "pyuploadcare": { + "hashes": [ + "sha256:98c3e9de8c37d2afc31eff8f33b23f6f4d787b5dc8672775504d5bf76ba21544" + ], + "version": "==2.6.0" + }, + "redis": { + "hashes": [ + "sha256:6946b5dca72e86103edc8033019cc3814c031232d339d5f4533b02ea85685175", + "sha256:8ca418d2ddca1b1a850afa1680a7d2fd1f3322739271de4b704e0d4668449273" + ], + "version": "==3.2.1" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "version": "==2.21.0" + }, + "ruamel.yaml": { + "hashes": [ + "sha256:0939bcb399ad037ef903d74ccf2f8a074f06683bc89133ad19305067d34487c8", + "sha256:119cb8997d65e610de0bfd39b7f89ddfc670c43a5dbec3049b85c7457a027cac", + "sha256:18c0043e32a5f39c60a915015c553f90b367d34ef1b89a3a1e77baa00afec2eb", + "sha256:18c9ec539f8d5a07f5bdc15e41fe13c89d420d3136bbba53a36491c4d544345e", + "sha256:1f1ba73e1d3f1ea74ba687800fd55c3970ea2eff360a3529cb599db41af6ae5a", + "sha256:226f2f42770b8a50009ac0a386b158f24e642f487c981794aed59fdb5cfd232b", + "sha256:2ac8f8cab59c1e0ae7fa952a0e62e6e1b61cd6bef25a542b2ef923e38cc5819c", + "sha256:2b344ab595fe7ed7480b647cd6e6588580f147a62fbfe00fbf72e292f543390d", + "sha256:34e09b287d2ef5227b7a1ebaa2daa7ef9ff662542046c25b9c65c240e0ce3f8d", + "sha256:367e659ea250faa91a8d0a9388e5f3432f5edbbeabe68701daad7fc55a96473a", + "sha256:4b17efa00c14ad76a8bbfe2e31803227709f5908f15a1bd7c7b7003218a9d2e3", + "sha256:4fa15adfd665bc796bfa76b0b3157a28ecf28545bcf36a044ddb8dbb36e9c208", + "sha256:5651db922d10f51a69f4248aaf748dbf06099a6d03e1c3ab793eea6bee827d28", + "sha256:737ad466ac9de17f08d67b0b2d543312726b9ee8fb2172e61922642772fb3bff", + "sha256:786efe76c1092d392f3b1620c2585b3e09bd6a15ed82262a0b003aac58389034", + "sha256:79a1056a289576004a453069d3f716533629497f86fd816090dee0fd2b334b35", + "sha256:88f771085e5f91f641af211fe41d5052a67d388adc43a6deb26d535e40cf1c78", + "sha256:8bf6afc48c79597c58b0d01ce3bbf3769cb92f4c9bd4a903510afe574309ea4a", + "sha256:e5f929b21c90cac257fd7e3d059a5a469a712408d66bb0502159c1fe41ab27ea", + "sha256:eaa73a72adbba81f38147c5bebd34c0fed1813ce7cbaea60529b4c3db58ebff4", + "sha256:eff4f9bfcb428900d93e084808e61a8b4faf8b2bdd8695bec629364e4a7dfe31", + "sha256:fe64f1813251799665b15ca372b9bd1c10651fd5ceab9898caf65a3eeb6c1575" + ], + "version": "==0.15.94" + }, + "sentry-sdk": { + "hashes": [ + "sha256:5818289868755cfea74e61e532b4b0d11d523901041338d473277db91d4d8173", + "sha256:b50948bbb553eef11ba650db858e31f5bb7c8d821a9d7338a01d01487d964e8c" + ], + "version": "==0.7.14" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "sqlparse": { + "hashes": [ + "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177", + "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873" + ], + "version": "==0.3.0" + }, + "typing-extensions": { + "hashes": [ + "sha256:07b2c978670896022a43c4b915df8958bec4a6b84add7f2c87b2b728bda3ba64", + "sha256:f3f0e67e1d42de47b5c67c32c9b26641642e9170fe7e292991793705cd5fef7c", + "sha256:fb2cd053238d33a8ec939190f30cfd736c00653a85a2919415cecf7dc3d9da71" + ], + "version": "==3.7.2" + }, + "uritemplate": { + "hashes": [ + "sha256:01c69f4fe8ed503b2951bef85d996a9d22434d2431584b5b107b2981ff416fbd", + "sha256:1b9c467a940ce9fb9f50df819e8ddd14696f89b9a8cc87ac77952ba416e0a8fd", + "sha256:c02643cebe23fc8adb5e6becffe201185bf06c40bda5c0b4028a93f1527d011d" + ], + "version": "==3.0.0" + }, + "urllib3": { + "hashes": [ + "sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0", + "sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3" + ], + "version": "==1.24.2" + }, + "uwsgi": { + "hashes": [ + "sha256:4972ac538800fb2d421027f49b4a1869b66048839507ccf0aa2fda792d99f583" + ], + "version": "==2.0.18" + }, + "vine": { + "hashes": [ + "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87", + "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af" + ], + "version": "==1.3.0" + } + }, + "develop": { + "aspy.yaml": { + "hashes": [ + "sha256:ae249074803e8b957c83fdd82a99160d0d6d26dff9ba81ba608b42eebd7d8cd3", + "sha256:c7390d79f58eb9157406966201abf26da0d56c07e0ff0deadc39c8f4dbc13482" + ], + "version": "==1.2.0" + }, + "astor": { + "hashes": [ + "sha256:95c30d87a6c2cf89aa628b87398466840f0ad8652f88eb173125a6df8533fb8d", + "sha256:fb503b9e2fdd05609fbf557b916b4a7824171203701660f0c55bbf5a7a68713e" + ], + "version": "==0.7.1" + }, + "attrs": { + "hashes": [ + "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", + "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" + ], + "version": "==18.2.0" + }, + "bandit": { + "hashes": [ + "sha256:6102b5d6afd9d966df5054e0bdfc2e73a24d0fea400ec25f2e54c134412158d7", + "sha256:9413facfe9de1e1bd291d525c784e1beb1a55c9916b51dae12979af63a69ba4c" + ], + "version": "==1.5.1" + }, + "cfgv": { + "hashes": [ + "sha256:6e9f2feea5e84bc71e56abd703140d7a2c250fc5ba38b8702fd6a68ed4e3b2ef", + "sha256:e7f186d4a36c099a9e20b04ac3108bd8bb9b9257e692ce18c8c3764d5cb12172" + ], + "version": "==1.6.0" }, "eradicate": { "hashes": [ - "sha256:f9af01c544ccd8f71bc2f7f3fa39dc363d842cfcb9c730a83676a59026ab5f24" + "sha256:4ffda82aae6fd49dfffa777a857cb758d77502a1f2e0f54c9ac5155a39d2d01a" ], - "version": "==0.2.1" + "version": "==1.0" }, "flake8": { "hashes": [ @@ -138,10 +429,10 @@ }, "flake8-bandit": { "hashes": [ - "sha256:a66c7b42af9530d5e988851ccee02958a51a85d46f1f4609ecc3546948f809b8", - "sha256:f7c3421fd9aebc63689c0693511e16dcad678fd4a0ce624b78ca91ae713eacdc" + "sha256:5eac24fa9fef532e4e4ce599c5b3c5248c5cc435d2927537b529b0a7bcb72467", + "sha256:be5840923ccf06cac6a8893a2f0abc17f03b6b9fdb5284d796f722b69c8f840b" ], - "version": "==1.0.2" + "version": "==2.1.0" }, "flake8-broken-line": { "hashes": [ @@ -180,10 +471,10 @@ }, "flake8-comprehensions": { "hashes": [ - "sha256:b83891fec0e680b07aa1fd92e53eb6993be29a0f3673a09badbe8da307c445e0", - "sha256:e4ccf1627f75f192eb7fde640f5edb81c98d04b1390df9d4145ffd7710bb1ef2" + "sha256:35f826956e87f230415cde9c3b8b454e785736cf5ff0be551c441b41b937f699", + "sha256:f0b61d983d608790abf3664830d68efd3412265c2d10f6a4ba1a353274dbeb64" ], - "version": "==1.4.1" + "version": "==2.1.0" }, "flake8-debugger": { "hashes": [ @@ -200,23 +491,23 @@ }, "flake8-eradicate": { "hashes": [ - "sha256:750e51d4180a253df468a0ebe6ab115a3cf766035269dbbd78589a1d1de7584e", - "sha256:83dfeb5980ada63a73ccdfe64f233fc7ddf9ea702299b9f599dad9c2adc9036e" + "sha256:0953cd3bcae4bfd04d45075234e0b5fd465ff50ecc56cdcaf0027da751632127", + "sha256:c762fbb5c3e3694c9ba656d38477b2dcca6599b8baeee4984d05d655591a6f83" ], - "version": "==0.1.1" + "version": "==0.2.0" }, "flake8-isort": { "hashes": [ - "sha256:3c107c405dd6e3dbdcccb2f84549d76d58a07120cd997a0560fab8b84c305f2a", - "sha256:76d7dd6aec2762c608b442abebb0aaedb72fc75f9a075241a89e4784d8a39900" + "sha256:1e67b6b90a9b980ac3ff73782087752d406ce0a729ed928b92797f9fa188917e", + "sha256:81a8495eefed3f2f63f26cd2d766c7b1191e923a15b9106e6233724056572c68" ], - "version": "==2.6.0" + "version": "==2.7.0" }, "flake8-logging-format": { "hashes": [ - "sha256:464b68b602fb034335b91a21b8968560f29f10e0e0f2618f2a8e2bb0ea01232f" + "sha256:ca5f2b7fc31c3474a0aa77d227e022890f641a025f0ba664418797d979a779f8" ], - "version": "==0.5.0" + "version": "==0.6.0" }, "flake8-pep3101": { "hashes": [ @@ -227,10 +518,10 @@ }, "flake8-per-file-ignores": { "hashes": [ - "sha256:3c4b1d770fa509aaad997ca147bd3533b730c3f6c48290b69a4265072c465522", - "sha256:4ee4f24cbea5e18e1fefdfccb043e819caf483d16d08e39cb6df5d18b0407275" + "sha256:166951535bfb7f373eecdcbe70af867aafcafdcccec88b28a0e14b8b31053b6d", + "sha256:ee826c35263d1f4e5815ff01c3b3134ee078265ce7c1e2b14e506a2cbe4f663a" ], - "version": "==0.6" + "version": "==0.7" }, "flake8-polyfill": { "hashes": [ @@ -279,86 +570,70 @@ ], "version": "==2.1.11" }, - "idna": { + "identify": { "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + "sha256:443f419ca6160773cbaf22dbb302b1e436a386f23129dbb5482b68a147c2eca9", + "sha256:bd7f15fe07112b713fb68fbdde3a34dd774d9062128f2c398104889f783f989d" ], - "version": "==2.8" + "version": "==1.4.2" }, - "inflection": { + "importlib-metadata": { "hashes": [ - "sha256:18ea7fb7a7d152853386523def08736aa8c32636b047ade55f7578c4edeb16ca" + "sha256:46fc60c34b6ed7547e2a723fc8de6dc2e3a1173f8423246b3ce497f064e9c3de", + "sha256:bc136180e961875af88b1ab85b4009f4f1278f8396a60526c0009f503a1a96ca" ], - "version": "==0.3.1" + "version": "==0.9" }, - "isort": { + "importlib-resources": { "hashes": [ - "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", - "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", - "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + "sha256:6e2783b2538bd5a14678284a3962b0660c715e5a0f10243fd5e00a4b5974f50b", + "sha256:d3279fd0f6f847cced9f7acc19bd3e5df54d34f93a2e7bb5f238f81545787078" ], - "version": "==4.3.4" + "markers": "python_version < '3.7'", + "version": "==1.0.2" }, - "itypes": { + "isort": { "hashes": [ - "sha256:c6e77bb9fd68a4bfeb9d958fea421802282451a25bac4913ec94db82a899c073" + "sha256:01cb7e1ca5e6c5b3f235f0385057f70558b70d2f00320208825fa62887292f43", + "sha256:268067462aed7eb2a1e237fcb287852f22077de3fb07964e87e00f829eea2d1a" ], - "version": "==1.1.0" + "version": "==4.3.17" }, - "jinja2": { + "mccabe": { "hashes": [ - "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", - "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" ], - "version": "==2.10" + "version": "==0.6.1" }, - "kombu": { + "mypy": { "hashes": [ - "sha256:1ef049243aa05f29e988ab33444ec7f514375540eaa8e0b2e1f5255e81c5e56d", - "sha256:3c9dca2338c5d893f30c151f5d29bfb81196748ab426d33c362ab51f1e8dbf78" + "sha256:2afe51527b1f6cdc4a5f34fc90473109b22bf7f21086ba3e9451857cf11489e6", + "sha256:56a16df3e0abb145d8accd5dbb70eba6c4bd26e2f89042b491faa78c9635d1e2", + "sha256:5764f10d27b2e93c84f70af5778941b8f4aa1379b2430f85c827e0f5464e8714", + "sha256:5bbc86374f04a3aa817622f98e40375ccb28c4836f36b66706cf3c6ccce86eda", + "sha256:6a9343089f6377e71e20ca734cd8e7ac25d36478a9df580efabfe9059819bf82", + "sha256:6c9851bc4a23dc1d854d3f5dfd5f20a016f8da86bcdbb42687879bb5f86434b0", + "sha256:b8e85956af3fcf043d6f87c91cbe8705073fc67029ba6e22d3468bfee42c4823", + "sha256:b9a0af8fae490306bc112229000aa0c2ccc837b49d29a5c42e088c132a2334dd", + "sha256:bbf643528e2a55df2c1587008d6e3bda5c0445f1240dfa85129af22ae16d7a9a", + "sha256:c46ab3438bd21511db0f2c612d89d8344154c0c9494afc7fbc932de514cf8d15", + "sha256:f7a83d6bd805855ef83ec605eb01ab4fa42bcef254b13631e451cbb44914a9b0" ], - "version": "==4.2.2.post1" + "version": "==0.701" }, - "markupsafe": { + "mypy-extensions": { "hashes": [ - "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", - "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", - "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", - "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", - "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", - "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", - "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", - "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", - "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", - "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", - "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", - "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", - "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", - "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", - "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", - "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", - "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", - "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", - "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", - "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", - "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", - "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", - "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", - "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", - "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", - "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", - "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", - "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" + "sha256:37e0e956f41369209a3d5f34580150bcacfabaa57b33a15c0b25f4b5725e0812", + "sha256:b16cabe759f55e3409a7d231ebd2841378fb0c27a5d1994719e340e4f429ac3e" ], - "version": "==1.1.0" + "version": "==0.4.1" }, - "mccabe": { + "nodeenv": { "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + "sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a" ], - "version": "==0.6.1" + "version": "==1.3.3" }, "pathmatch": { "hashes": [ @@ -368,52 +643,24 @@ }, "pbr": { "hashes": [ - "sha256:f59d71442f9ece3dffc17bc36575768e1ee9967756e6b6535f0ee1f0054c3d68", - "sha256:f6d5b23f226a2ba58e14e49aa3b1bfaf814d0199144b95d78458212444de1387" + "sha256:6901995b9b686cb90cceba67a0f6d4d14ae003cd59bc12beb61549bdfbe3bc89", + "sha256:d950c64aeea5456bbd147468382a5bb77fe692c13c9f00f0219814ce5b642755" ], - "version": "==5.1.1" + "version": "==5.2.0" }, "pep8-naming": { "hashes": [ - "sha256:360308d2c5d2fff8031c1b284820fbdb27a63274c0c1a8ce884d631836da4bdd", - "sha256:624258e0dd06ef32a9daf3c36cc925ff7314da7233209c5b01f7e5cdd3c34826" + "sha256:01cb1dab2f3ce9045133d08449f1b6b93531dceacb9ef04f67087c11c723cea9", + "sha256:0ec891e59eea766efd3059c3d81f1da304d858220678bdc351aab73c533f2fbb" ], - "version": "==0.7.0" + "version": "==0.8.2" }, - "pillow": { + "pre-commit": { "hashes": [ - "sha256:051de330a06c99d6f84bcf582960487835bcae3fc99365185dc2d4f65a390c0e", - "sha256:0ae5289948c5e0a16574750021bd8be921c27d4e3527800dc9c2c1d2abc81bf7", - "sha256:0b1efce03619cdbf8bcc61cfae81fcda59249a469f31c6735ea59badd4a6f58a", - "sha256:163136e09bd1d6c6c6026b0a662976e86c58b932b964f255ff384ecc8c3cefa3", - "sha256:18e912a6ccddf28defa196bd2021fe33600cbe5da1aa2f2e2c6df15f720b73d1", - "sha256:24ec3dea52339a610d34401d2d53d0fb3c7fd08e34b20c95d2ad3973193591f1", - "sha256:267f8e4c0a1d7e36e97c6a604f5b03ef58e2b81c1becb4fccecddcb37e063cc7", - "sha256:3273a28734175feebbe4d0a4cde04d4ed20f620b9b506d26f44379d3c72304e1", - "sha256:4c678e23006798fc8b6f4cef2eaad267d53ff4c1779bd1af8725cc11b72a63f3", - "sha256:4d4bc2e6bb6861103ea4655d6b6f67af8e5336e7216e20fff3e18ffa95d7a055", - "sha256:505738076350a337c1740a31646e1de09a164c62c07db3b996abdc0f9d2e50cf", - "sha256:5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f", - "sha256:5d95cb9f6cced2628f3e4de7e795e98b2659dfcc7176ab4a01a8b48c2c2f488f", - "sha256:7eda4c737637af74bac4b23aa82ea6fbb19002552be85f0b89bc27e3a762d239", - "sha256:801ddaa69659b36abf4694fed5aa9f61d1ecf2daaa6c92541bbbbb775d97b9fe", - "sha256:825aa6d222ce2c2b90d34a0ea31914e141a85edefc07e17342f1d2fdf121c07c", - "sha256:9c215442ff8249d41ff58700e91ef61d74f47dfd431a50253e1a1ca9436b0697", - "sha256:a3d90022f2202bbb14da991f26ca7a30b7e4c62bf0f8bf9825603b22d7e87494", - "sha256:a631fd36a9823638fe700d9225f9698fb59d049c942d322d4c09544dc2115356", - "sha256:a6523a23a205be0fe664b6b8747a5c86d55da960d9586db039eec9f5c269c0e6", - "sha256:a756ecf9f4b9b3ed49a680a649af45a8767ad038de39e6c030919c2f443eb000", - "sha256:b117287a5bdc81f1bac891187275ec7e829e961b8032c9e5ff38b70fd036c78f", - "sha256:ba04f57d1715ca5ff74bb7f8a818bf929a204b3b3c2c2826d1e1cc3b1c13398c", - "sha256:cd878195166723f30865e05d87cbaf9421614501a4bd48792c5ed28f90fd36ca", - "sha256:cee815cc62d136e96cf76771b9d3eb58e0777ec18ea50de5cfcede8a7c429aa8", - "sha256:d1722b7aa4b40cf93ac3c80d3edd48bf93b9208241d166a14ad8e7a20ee1d4f3", - "sha256:d7c1c06246b05529f9984435fc4fa5a545ea26606e7f450bdbe00c153f5aeaad", - "sha256:e9c8066249c040efdda84793a2a669076f92a301ceabe69202446abb4c5c5ef9", - "sha256:f227d7e574d050ff3996049e086e1f18c7bd2d067ef24131e50a1d3fe5831fbc", - "sha256:fc9a12aad714af36cf3ad0275a96a733526571e52710319855628f476dcb144e" - ], - "version": "==5.4.1" + "sha256:2576a2776098f3902ef9540a84696e8e06bf18a337ce43a6a889e7fa5d26c4c5", + "sha256:82f2f2d657d7f9280de9f927ae56886d60b9ef7f3714eae92d12713cd9cb9e11" + ], + "version": "==1.15.2" }, "pycodestyle": { "hashes": [ @@ -437,69 +684,27 @@ ], "version": "==2.0.0" }, - "pytz": { + "python-decouple": { "hashes": [ - "sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9", - "sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c" + "sha256:1317df14b43efee4337a4aa02914bf004f010cd56d6c4bd894e6474ec8c4fe2d" ], - "version": "==2018.9" + "version": "==3.1" }, "pyyaml": { "hashes": [ - "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", - "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", - "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", - "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", - "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", - "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", - "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", - "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", - "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", - "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", - "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" - ], - "version": "==3.13" - }, - "redis": { - "hashes": [ - "sha256:2100750629beff143b6a200a2ea8e719fcf26420adabb81402895e144c5083cf", - "sha256:8e0bdd2de02e829b6225b25646f9fb9daffea99a252610d040409a6738541f0a" - ], - "version": "==3.0.1" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + "sha256:1adecc22f88d38052fb787d959f003811ca858b799590a5eaa70e63dca50308c", + "sha256:436bc774ecf7c103814098159fbb84c2715d25980175292c648f2da143909f95", + "sha256:460a5a4248763f6f37ea225d19d5c205677d8d525f6a83357ca622ed541830c2", + "sha256:5a22a9c84653debfbf198d02fe592c176ea548cccce47553f35f466e15cf2fd4", + "sha256:7a5d3f26b89d688db27822343dfa25c599627bc92093e788956372285c6298ad", + "sha256:9372b04a02080752d9e6f990179a4ab840227c6e2ce15b95e1278456664cf2ba", + "sha256:a5dcbebee834eaddf3fa7366316b880ff4062e4bcc9787b78c7fbb4a26ff2dd1", + "sha256:aee5bab92a176e7cd034e57f46e9df9a9862a71f8f37cad167c6fc74c65f5b4e", + "sha256:c51f642898c0bacd335fc119da60baae0824f2cde95b0330b56c0553439f0673", + "sha256:c68ea4d3ba1705da1e0d85da6684ac657912679a649e8868bd850d2c299cce13", + "sha256:e23d0cc5299223dcc37885dae624f382297717e459ea24053709675a976a3e19" ], - "version": "==2.21.0" - }, - "ruamel.yaml": { - "hashes": [ - "sha256:18078354bfcf00d51bcc17984aded80840379aed36036f078479e191b59bc059", - "sha256:211e6ef2530f44fc3197c713892678e7fbfbc40a1db6741179d6981514be1674", - "sha256:2e8f7cee12a2372cec4480fe81086b1fdab163f4b56e58b5592a105c52973b78", - "sha256:48cc8e948a7ec4917bf94adff2cc1255e98f1eef5e1961889886acc4ff3a7194", - "sha256:4a0c7f970aa0e30bc541f690fbd14aca19de1cab70787180de5083b902ec40b5", - "sha256:5dd0ea7c5c703e8675f3caf2898a50b4dadaa52838f8e104637a452a05e03030", - "sha256:612fb4833f1978ceb7fd7a24d86a5ebd103bcc408394f3af621293194658cf1b", - "sha256:61c421a7a2b8e2886a94fbe29866df6b99451998abaa1584b9fdc9c10c33e40b", - "sha256:6483416847980aa7090b697d177a8754c4f340683cc84abd38da7b850826687d", - "sha256:6622f3b0cae7ed6fe5d3d6a6d8d8cb9413a05b408d69a789a57b77a616bb6562", - "sha256:80b2acde0d1b9d25e5c041960a9149480c15c6d9f4c24b8ddb381b14e9e70ea4", - "sha256:8f9ed94be17f306485df8fd0274a30f130a73f127798657d4dc65b1f89ec7a36", - "sha256:9a6b94cc9b6e738036426498ac9fe8ca05afea4249fb9dec1be32ce4823d5756", - "sha256:a4b11dfe421a9836c723107a4ccc9cab9674de611ba60b8212e85526ea8bf254", - "sha256:a55e55c6ecb5725ba472f9b811940e8d258a32fb36f5793dbc38582d6f377f3f", - "sha256:a736ab1d8c2d5566254a1a2ee38e7c5460520bcccd4a8f0feb25a4463735e5a7", - "sha256:c29d0a3cffa5a25f5259bfeac06ffdc5e7d1fd38a0a26a6664d160192730434f", - "sha256:c33458217a8c352b59c86065c4f05f3f1ac28b01c3e1a422845c306237446bf3", - "sha256:cc9bd3c3fa8a928f7b6e19fe8de13a61deb91f257eccbe0d16114ce8c54cdc81", - "sha256:d63b7c828a7358ce5b03a3e2c2a3e5a7058a954f8919334cb09b3d8541d1fff6", - "sha256:fbd301680a3563e84d667042dac1c5d50ef402ecf1f4b1763507a6877b8181ad", - "sha256:fc67e79e2f5083be6fd1000c4646e13a891585772a503f56f51f845b547fe621" - ], - "version": "==0.15.87" + "version": "==5.1" }, "six": { "hashes": [ @@ -524,17 +729,48 @@ }, "stevedore": { "hashes": [ - "sha256:b92bc7add1a53fb76c634a178978d113330aaf2006f9498d9e2414b31fbfc104", - "sha256:c58b7c231a9c4890cd3c2b5d2b23bd63fa807ff934d68579e3f6c3a1735e8a7c" + "sha256:7be098ff53d87f23d798a7ce7ae5c31f094f3deb92ba18059b1aeb1ca9fec0a0", + "sha256:7d1ce610a87d26f53c087da61f06f9b7f7e552efad2a7f6d2322632b5f932ea2" ], - "version": "==1.30.0" + "version": "==1.30.1" }, "testfixtures": { "hashes": [ - "sha256:969e967df5d8e12012b5c90986428919b1068c20841b0077b3e29e9a928605d3", - "sha256:b6c05222ce8d3c34a1353ff30c73da55f61ef58153229a5664ef7110ec340cdd" + "sha256:015f8220088c40772b48a210f6bcdf328664f66faf83432c4f7aa8e55a7cfdc5", + "sha256:d7558f09801ee53e0ea40eabe8d7d79cbe5b0f8a72871f1d5ae8162004011913" ], - "version": "==6.4.3" + "version": "==6.7.1" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, + "typed-ast": { + "hashes": [ + "sha256:132eae51d6ef3ff4a8c47c393a4ef5ebf0d1aecc96880eb5d6c8ceab7017cc9b", + "sha256:18141c1484ab8784006c839be8b985cfc82a2e9725837b0ecfa0203f71c4e39d", + "sha256:2baf617f5bbbfe73fd8846463f5aeafc912b5ee247f410700245d68525ec584a", + "sha256:3d90063f2cbbe39177e9b4d888e45777012652d6110156845b828908c51ae462", + "sha256:4304b2218b842d610aa1a1d87e1dc9559597969acc62ce717ee4dfeaa44d7eee", + "sha256:4983ede548ffc3541bae49a82675996497348e55bafd1554dc4e4a5d6eda541a", + "sha256:5315f4509c1476718a4825f45a203b82d7fdf2a6f5f0c8f166435975b1c9f7d4", + "sha256:6cdfb1b49d5345f7c2b90d638822d16ba62dc82f7616e9b4caa10b72f3f16649", + "sha256:7b325f12635598c604690efd7a0197d0b94b7d7778498e76e0710cd582fd1c7a", + "sha256:8d3b0e3b8626615826f9a626548057c5275a9733512b137984a68ba1598d3d2f", + "sha256:8f8631160c79f53081bd23446525db0bc4c5616f78d04021e6e434b286493fd7", + "sha256:912de10965f3dc89da23936f1cc4ed60764f712e5fa603a09dd904f88c996760", + "sha256:b010c07b975fe853c65d7bbe9d4ac62f1c69086750a574f6292597763781ba18", + "sha256:c908c10505904c48081a5415a1e295d8403e353e0c14c42b6d67f8f97fae6616", + "sha256:c94dd3807c0c0610f7c76f078119f4ea48235a953512752b9175f9f98f5ae2bd", + "sha256:ce65dee7594a84c466e79d7fb7d3303e7295d16a83c22c7c4037071b059e2c21", + "sha256:eaa9cfcb221a8a4c2889be6f93da141ac777eb8819f077e1d09fb12d00a09a93", + "sha256:f3376bc31bad66d46d44b4e6522c5c21976bf9bca4ef5987bb2bf727f4506cbb", + "sha256:f9202fa138544e13a4ec1a6792c35834250a85958fde1251b6a22e07d1260ae7" + ], + "version": "==1.3.5" }, "typing": { "hashes": [ @@ -552,35 +788,26 @@ ], "version": "==3.7.2" }, - "uritemplate": { - "hashes": [ - "sha256:01c69f4fe8ed503b2951bef85d996a9d22434d2431584b5b107b2981ff416fbd", - "sha256:1b9c467a940ce9fb9f50df819e8ddd14696f89b9a8cc87ac77952ba416e0a8fd", - "sha256:c02643cebe23fc8adb5e6becffe201185bf06c40bda5c0b4028a93f1527d011d" - ], - "version": "==3.0.0" - }, - "urllib3": { + "virtualenv": { "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + "sha256:15ee248d13e4001a691d9583948ad3947bcb8a289775102e4c4aa98a8b7a6d73", + "sha256:bfc98bb9b42a3029ee41b96dc00a34c2f254cbf7716bec824477b2c82741a5c4" ], - "version": "==1.24.1" + "version": "==16.5.0" }, - "vine": { + "wemake-python-styleguide": { "hashes": [ - "sha256:3cd505dcf980223cfaf13423d371f2e7ff99247e38d5985a01ec8264e4f2aca1", - "sha256:ee4813e915d0e1a54e5c1963fde0855337f82655678540a6bc5996bca4165f76" + "sha256:43d0bcbfa7349c80000d7a22e319aff9de6eeabd8b6ff1e3aeb0a4c07453fd04", + "sha256:b68c5b856b7dcc08e881b656a47544e6659643594bece1f945de543d30d31d5a" ], - "version": "==1.2.0" + "version": "==0.7.1" }, - "wemake-python-styleguide": { + "zipp": { "hashes": [ - "sha256:068a57f54dde454b089e3c1e685ce632b27f3562d9988141f8073e56082503e1", - "sha256:d25ba601c2d172f1efbccbc74793f264e51f9c517a1c4e2292d3106365af81a8" + "sha256:139391b239594fd8b91d856bc530fbd2df0892b17dd8d98a91f018715954185f", + "sha256:8047e4575ce8d700370a3301bbfc972896a5845eb62dd535da395b86be95dfad" ], - "version": "==0.6.3" + "version": "==0.4.0" } - }, - "develop": {} + } } diff --git a/README.md b/README.md index 52e2639..d2c3c2c 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,28 @@ python3.6, redis `export PYTHONPATH=/full/path/postpost` -`export DJANGO_SETTINGS_MODULE=main.settings` +Copy environment variables: +`cp .env.template .env` + +Add values to the variables in `.env`, if you have any, like so: +`VAR_NAME=6666aaaa` `pipenv run python manage.py migrate` `pipenv run python manage.py runserver` `pipenv run celery -A main worker -B` + +Add basic user + +`python manage.py createsuperuser` + +Login to [admin interface](http://localhost:8000/admin/oauth2_provider/application/) and create OAuth Application with +these params: + + - User: `1` + - Client type: `Public` + - Grant type: `Resource owner password based` + - Name: e.g. `frontend` + +Congrats! Now, there are your [swagger](http://localhost:8000/api/swagger) and [redoc](http://localhost:8000/api/redoc) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a18c97c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,50 @@ +version: '3' + +services: + api: + container_name: postpost_api + image: piterpy/postpost:$TAG + restart: unless-stopped + ports: + - 8000:8000 + env_file: .env + depends_on: + - celery + - db + + redis: + image: redis:alpine + container_name: redis + volumes: + - redis_data:/data + + celery: + image: piterpy/postpost:$TAG + command: sh /app/wait-for.sh api:8000 -- pipenv run celery -A main worker -B + depends_on: + - redis + - db + env_file: .env + + frontend: + container_name: postpost_frontend + image: piterpy/postpost-frontend:$TAG + restart: unless-stopped + ports: + - 8043:8043 + env_file: .env + + watchtower: + image: v2tec/watchtower + volumes: + - /var/run/docker.sock:/var/run/docker.sock + command: --interval 30 + + db: + image: postgres:alpine + volumes: + - postgres_data:/var/lib/postgresql/data/ + +volumes: + redis_data: + postgres_data: diff --git a/postpost/api/filters.py b/postpost/api/filters.py new file mode 100644 index 0000000..5f7f49d --- /dev/null +++ b/postpost/api/filters.py @@ -0,0 +1,27 @@ +from django_filters import rest_framework as filters + +from api import models + + +class PublicationFilterSet(filters.FilterSet): + """ + Uses for filtering in publication-list endpoint by query-string. + """ + + # scheduled=false is equivalent to scheduled_at__isnull=True + scheduled = filters.BooleanFilter( + field_name='scheduled_at', + lookup_expr='isnull', + exclude=True, + ) + + # If you want change this filtering, first read comment + # for PlatformPost.PLATFORM_TYPES + platform_types = filters.MultipleChoiceFilter( + field_name='platform_posts__platform_type', + choices=models.PlatformPost.PLATFORM_TYPES, + ) + + class Meta(object): + model = models.Publication + fields = ['scheduled', 'platform_types'] diff --git a/postpost/api/migrations/0001_initial.py b/postpost/api/migrations/0001_initial.py index 1b9d036..48acf13 100644 --- a/postpost/api/migrations/0001_initial.py +++ b/postpost/api/migrations/0001_initial.py @@ -1,7 +1,7 @@ # Generated by Django 2.1.4 on 2019-01-20 19:56 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/postpost/api/migrations/0002_auto_20190128_2219.py b/postpost/api/migrations/0002_auto_20190128_2219.py new file mode 100644 index 0000000..ac0570d --- /dev/null +++ b/postpost/api/migrations/0002_auto_20190128_2219.py @@ -0,0 +1,19 @@ +# Generated by Django 2.1.5 on 2019-01-28 22:19 + +import pyuploadcare.dj.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='publication', + name='picture', + field=pyuploadcare.dj.models.ImageField(blank=True, null=True), + ), + ] diff --git a/postpost/api/migrations/0003_attachments.py b/postpost/api/migrations/0003_attachments.py new file mode 100644 index 0000000..fa11e93 --- /dev/null +++ b/postpost/api/migrations/0003_attachments.py @@ -0,0 +1,31 @@ +# Generated by Django 2.1.7 on 2019-03-16 12:35 + +import django.db.models.deletion +import pyuploadcare.dj.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0002_auto_20190128_2219'), + ] + + operations = [ + migrations.CreateModel( + name='Attachment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('attachment', pyuploadcare.dj.models.ImageField(blank=True, null=True)), + ], + ), + migrations.RemoveField( + model_name='publication', + name='picture', + ), + migrations.AddField( + model_name='attachment', + name='parent_publication', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='api.Publication'), + ), + ] diff --git a/postpost/api/models/__init__.py b/postpost/api/models/__init__.py index ffaf91c..aecd29a 100644 --- a/postpost/api/models/__init__.py +++ b/postpost/api/models/__init__.py @@ -1,2 +1,3 @@ +from api.models.attachments import Attachment # noqa: F401 from api.models.platform_settings import PlatformPost # noqa: F401 from api.models.publications import Publication # noqa: F401 diff --git a/postpost/api/models/attachments.py b/postpost/api/models/attachments.py new file mode 100644 index 0000000..1814c9b --- /dev/null +++ b/postpost/api/models/attachments.py @@ -0,0 +1,15 @@ +from django.db import models +from pyuploadcare.dj import models as uploadcare_models + + +class Attachment(models.Model): + """ + Attachment to post. + """ + + parent_publication = models.ForeignKey( + 'Publication', + on_delete=models.CASCADE, + related_name='attachments', + ) + attachment = uploadcare_models.ImageField(blank=True, null=True) diff --git a/postpost/api/models/platform_settings.py b/postpost/api/models/platform_settings.py index adec28c..e5555e1 100644 --- a/postpost/api/models/platform_settings.py +++ b/postpost/api/models/platform_settings.py @@ -1,4 +1,5 @@ from django.db import models +from typing_extensions import Final class PlatformPost(models.Model): @@ -8,20 +9,31 @@ class PlatformPost(models.Model): # Easy for using in filtering without hardcoding database-level constant, like # `PlatformPost.objects.filter(platform_type=PlatformPost.VK_GROUP_TYPE))` - TELEGRAM_CHANNEL_TYPE = 'telegram_channel' - TELEGRAM_SUPERGROUP_TYPE = 'telegram_supergroup' - VK_GROUP_TYPE = 'vk_group' - PLATFORM_TYPES = [ + TELEGRAM_CHANNEL_TYPE: Final = 'telegram_channel' + TELEGRAM_SUPERGROUP_TYPE: Final = 'telegram_supergroup' + VK_GROUP_TYPE: Final = 'vk_group' + + # from boger: + # I think that in the future we will use two variables for describing + # platforms: platform (vk_group, twitter_account, telegram_channel) and + # integration (api, ifttt, buffer). Platform variable will be responsible + # for platform only, because if you want filter `publication to twitter` + # you doesn't care about ifttt or buffer, you want see all twitter + # publication. + # + # Maybe integration type should ruled by PlatformSetting or only on + # frontend-side, I'm not sure. + PLATFORM_TYPES: Final = [ (TELEGRAM_CHANNEL_TYPE, 'Telegram Channel'), (TELEGRAM_SUPERGROUP_TYPE, 'Telegram Supergroup (chat)'), (VK_GROUP_TYPE, 'VK Group (public)'), ] - SCHEDULED_STATUS = 'scheduled' - SENDING_STATUS = 'sending' - FAILED_STATUS = 'failed' - SUCCESS_STATUS = 'success' - PLATFORM_STATUSES = [ + SCHEDULED_STATUS: Final = 'scheduled' + SENDING_STATUS: Final = 'sending' + FAILED_STATUS: Final = 'failed' + SUCCESS_STATUS: Final = 'success' + PLATFORM_STATUSES: Final = [ (SCHEDULED_STATUS, 'Post was scheduled'), (SENDING_STATUS, 'Post is sending'), (FAILED_STATUS, 'Sending was failed'), diff --git a/postpost/api/models/publications.py b/postpost/api/models/publications.py index 1188d62..2cc53d7 100644 --- a/postpost/api/models/publications.py +++ b/postpost/api/models/publications.py @@ -9,7 +9,6 @@ class Publication(models.Model): """ text = models.TextField() - picture = models.ImageField(blank=True, null=True) scheduled_at = models.DateTimeField(blank=True, null=True) diff --git a/postpost/api/serializers.py b/postpost/api/serializers.py deleted file mode 100644 index e6c6e61..0000000 --- a/postpost/api/serializers.py +++ /dev/null @@ -1,99 +0,0 @@ -from typing import Sequence - -from drf_writable_nested import WritableNestedModelSerializer -from rest_framework import serializers - -from api import models - - -class VKGroupSettingsSerializer(serializers.ModelSerializer): - """ - Serializer for vk_group type of PlatformPost. - """ - - class Meta(object): - model = models.PlatformPost - fields = [ - 'id', - 'platform_type', - 'current_status', - 'text', - 'vk_clear_markdown', - ] - - -class TelegramChannelSettingsSerializer(serializers.ModelSerializer): - """ - Serializer for telegram_group type of PlatformPost. - """ - - class Meta(object): - model = models.PlatformPost - fields = [ - 'id', - 'platform_type', - 'current_status', - 'text', - 'telegram_picture_as_link', - 'telegram_markdown', - ] - - -class PlatformSettingsRelatedField(serializers.ModelSerializer): - """ - Special hack field which on the fly change serializer class depending on platform_type. - """ - - serializers_by_type = { - models.PlatformPost.VK_GROUP_TYPE: VKGroupSettingsSerializer, - models.PlatformPost.TELEGRAM_CHANNEL_TYPE: TelegramChannelSettingsSerializer, - models.PlatformPost.TELEGRAM_SUPERGROUP_TYPE: TelegramChannelSettingsSerializer, # FIXME - } - - def to_representation(self, platform_settings: models.PlatformPost) -> dict: # noqa: D102 - serializer = self.serializers_by_type.get(platform_settings.platform_type) - if not serializer: - raise Exception('Unknown type of platform') - return serializer().to_representation(platform_settings) - - def to_internal_value(self, native_values: dict) -> models.PlatformPost: # noqa: D102 - platform_type = native_values.get('platform_type') - serializer = self.serializers_by_type.get(platform_type) - if not serializer: - raise serializers.ValidationError({'platform_type': 'Unknown type of platform'}) - - return serializer().to_internal_value(native_values) - - class Meta(object): - model = models.PlatformPost - fields = '__all__' - - -class PublicationSerializer(WritableNestedModelSerializer): - """ - Serializer for publication with platform posts field. - """ - - platform_posts = PlatformSettingsRelatedField( - many=True, - ) - - class Meta(object): - model = models.Publication - fields = [ - 'id', - 'text', - 'picture', - 'scheduled_at', - 'current_status', - 'platform_posts', - 'created_at', - 'updated_at', - ] - - def validate_platform_posts(self, platform_posts: Sequence[models.PlatformPost]): - """ - In publication must be one or more platform post. - """ - if len(platform_posts) == 0: - raise serializers.ValidationError('Must be set one or more platform settings') diff --git a/postpost/api/serializers/__init__.py b/postpost/api/serializers/__init__.py new file mode 100644 index 0000000..1cc12a5 --- /dev/null +++ b/postpost/api/serializers/__init__.py @@ -0,0 +1,6 @@ +from api.serializers.attachment import AttachmentSerializer # noqa: F401 +from api.serializers.platform_settings_field import PlatformSettingsRelatedField # noqa: F401 +from api.serializers.publication import PublicationSerializer # noqa: F401 +from api.serializers.telegram_settings import TelegramChannelSettingsSerializer # noqa: F401,E501 +from api.serializers.user_registration import UserRegistrationSerializer # noqa: F401 +from api.serializers.vk_settings import VKGroupSettingsSerializer # noqa: F401 diff --git a/postpost/api/serializers/attachment.py b/postpost/api/serializers/attachment.py new file mode 100644 index 0000000..93a8d52 --- /dev/null +++ b/postpost/api/serializers/attachment.py @@ -0,0 +1,15 @@ +from rest_framework import serializers + +from api import models + + +class AttachmentSerializer(serializers.ModelSerializer): + """ + Serializer for attachments. + """ + + class Meta(object): + model = models.Attachment + fields = [ + 'attachment', + ] diff --git a/postpost/api/serializers/platform_settings_field.py b/postpost/api/serializers/platform_settings_field.py new file mode 100644 index 0000000..1825af5 --- /dev/null +++ b/postpost/api/serializers/platform_settings_field.py @@ -0,0 +1,41 @@ +from typing import Dict, Type + +from rest_framework import serializers + +from api import models +from api.serializers.telegram_settings import TelegramChannelSettingsSerializer +from api.serializers.vk_settings import VKGroupSettingsSerializer +from custom_types import JSON + + +class PlatformSettingsRelatedField(serializers.ModelSerializer): + """ + Special hack field which on the fly change serializer class depending on platform_type. + """ + + serializers_by_type: Dict[str, Type[serializers.ModelSerializer]] = { + models.PlatformPost.VK_GROUP_TYPE: VKGroupSettingsSerializer, + models.PlatformPost.TELEGRAM_CHANNEL_TYPE: TelegramChannelSettingsSerializer, + models.PlatformPost.TELEGRAM_SUPERGROUP_TYPE: TelegramChannelSettingsSerializer, # FIXME + } + + def to_representation(self, platform_settings: models.PlatformPost): # noqa: D102 + serializer = self.serializers_by_type.get( + platform_settings.platform_type, + ) + if not serializer: + raise Exception('Unknown type of platform') + representation: JSON = serializer().to_representation(platform_settings) + return representation + + def to_internal_value(self, native_values: JSON) -> models.PlatformPost: # noqa: D102 + platform_type = native_values.get('platform_type') + serializer = self.serializers_by_type.get(platform_type) + if not serializer: + raise serializers.ValidationError({'platform_type': 'Unknown type of platform'}) + + return serializer().to_internal_value(native_values) + + class Meta(object): + model = models.PlatformPost + fields = '__all__' diff --git a/postpost/api/serializers/publication.py b/postpost/api/serializers/publication.py new file mode 100644 index 0000000..b05f7e3 --- /dev/null +++ b/postpost/api/serializers/publication.py @@ -0,0 +1,32 @@ +from drf_writable_nested import WritableNestedModelSerializer + +from api import models +from api.serializers.attachment import AttachmentSerializer +from api.serializers.platform_settings_field import PlatformSettingsRelatedField + + +class PublicationSerializer(WritableNestedModelSerializer): + """ + Serializer for publication with platform posts field. + """ + + platform_posts = PlatformSettingsRelatedField( + many=True, + ) + + attachments = AttachmentSerializer( + many=True, + ) + + class Meta(object): + model = models.Publication + fields = [ + 'id', + 'text', + 'attachments', + 'scheduled_at', + 'current_status', + 'platform_posts', + 'created_at', + 'updated_at', + ] diff --git a/postpost/api/serializers/telegram_settings.py b/postpost/api/serializers/telegram_settings.py new file mode 100644 index 0000000..e81422f --- /dev/null +++ b/postpost/api/serializers/telegram_settings.py @@ -0,0 +1,20 @@ +from rest_framework import serializers + +from api import models + + +class TelegramChannelSettingsSerializer(serializers.ModelSerializer): + """ + Serializer for telegram_group type of PlatformPost. + """ + + class Meta(object): + model = models.PlatformPost + fields = [ + 'id', + 'platform_type', + 'current_status', + 'text', + 'telegram_picture_as_link', + 'telegram_markdown', + ] diff --git a/postpost/api/serializers/user_registration.py b/postpost/api/serializers/user_registration.py new file mode 100644 index 0000000..49139f3 --- /dev/null +++ b/postpost/api/serializers/user_registration.py @@ -0,0 +1,104 @@ +from datetime import timedelta + +from django.contrib.auth import models as contrib_models +from django.utils import timezone +from oauth2_provider.models import AccessToken, Application, RefreshToken +from oauth2_provider.settings import oauth2_settings +from oauthlib import common +from rest_framework import serializers +from rest_framework.validators import UniqueValidator + + +class UserRegistrationSerializer(serializers.ModelSerializer): + """ + Serializer for user registration. + + Check user creds, create User and access/refresh token. + """ + + email = serializers.EmailField( + required=True, + allow_null=False, + validators=[ + UniqueValidator( + queryset=contrib_models.User.objects.all(), + lookup='iexact', + ), + ], + ) + username = serializers.SlugField( + required=True, + min_length=5, + allow_null=False, + validators=[ + UniqueValidator( + queryset=contrib_models.User.objects.all(), + lookup='iexact', + ), + ], + ) + password = serializers.CharField(required=True, allow_null=False, write_only=True, min_length=8) + client_id = serializers.CharField(required=True, allow_null=False, write_only=True) + + access_token = serializers.SerializerMethodField() + refresh_token = serializers.SerializerMethodField() + + def get_access_token(self, _): + """ + Getter for access token. + """ + return self._access_token + + def get_refresh_token(self, _): + """ + Getter for refresh token. + """ + return self._refresh_token + + def create(self, validated_data): + """ + Create User object and generate some oauth stuff like access/refresh token. + """ + try: + application = Application.objects.get(client_id=validated_data['client_id']) + except Application.DoesNotExist: + raise serializers.ValidationError('Invalid client id') + user = contrib_models.User.objects.create_user( + username=validated_data['username'], + email=validated_data['email'], + password=validated_data['password'], + ) + expires = timezone.now() + timedelta(seconds=oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS) + + access_token = AccessToken( + user=user, + scope='', + expires=expires, + token=common.generate_token(), + application=application, + ) + access_token.save() + self._access_token = access_token.token + + refresh_token = RefreshToken( + user=user, + token=common.generate_token(), + application=application, + access_token=access_token, + ) + refresh_token.save() + self._refresh_token = refresh_token.token + + return user + + class Meta(object): + model = contrib_models.User + fields = [ + 'id', + 'email', + 'username', + 'password', + 'client_id', + 'access_token', + 'refresh_token', + ] diff --git a/postpost/api/serializers/vk_settings.py b/postpost/api/serializers/vk_settings.py new file mode 100644 index 0000000..72becc1 --- /dev/null +++ b/postpost/api/serializers/vk_settings.py @@ -0,0 +1,19 @@ +from rest_framework import serializers + +from api import models + + +class VKGroupSettingsSerializer(serializers.ModelSerializer): + """ + Serializer for vk_group type of PlatformPost. + """ + + class Meta(object): + model = models.PlatformPost + fields = [ + 'id', + 'platform_type', + 'current_status', + 'text', + 'vk_clear_markdown', + ] diff --git a/postpost/api/tasks.py b/postpost/api/tasks.py index 63f0967..e928229 100644 --- a/postpost/api/tasks.py +++ b/postpost/api/tasks.py @@ -1,44 +1,63 @@ import logging import os from datetime import datetime +from typing import Dict import requests from celery import shared_task +from celery.local import Proxy from celery.schedules import crontab from celery.task import periodic_task from api.models import PlatformPost +from gates import telegram, vkontakte logger = logging.getLogger(__name__) -@shared_task +@shared_task # type: ignore def send_post_to_telegram_channel(scheduled_post_id: int): """ Celery task which try to send post to telegram and change status of platform post. """ post = PlatformPost.objects.select_related('publication').get(id=scheduled_post_id) # FIXME: переменные вынести в конфиг-файл - # TODO: перенести работу с отправкой сообщений в внешний модуль из тасок - telegram_response = requests.post( - 'https://api.telegram.org/bot{0}/sendMessage'.format( - os.environ['BOT_TOKEN'], - ), - json={ - 'chat_id': os.environ['TELEGRAM_CHANNEL_ID'], - 'text': post.text_for_posting, - }, - ) - if telegram_response.status_code != requests.codes.ok: - logger.error('Error by telegram API: %s', telegram_response.content) + try: + telegram.send_post_to_telegram_chat( + token=os.environ['BOT_TOKEN'], + chat_id=os.environ['TELEGRAM_CHANNEL_ID'], + post=post, + ) + post.current_status = PlatformPost.SUCCESS_STATUS + except requests.HTTPError as error: + logger.error('Error by telegram API: %s', str(error)) post.current_status = PlatformPost.FAILED_STATUS - else: + post.save() + + +@shared_task # type: ignore +def send_post_to_vk_group(scheduled_post_id: int): + """ + Celery task which tries to send post to vk and changes status of platform post. + """ + post = PlatformPost.objects.select_related('publication').get(id=scheduled_post_id) + try: + vkontakte.send_post_to_group( + token=os.environ['VK_TOKEN'], + group_id=os.environ['VK_GROUP_ID'], + api_version=os.environ['VK_API_VERSION'], + post=post, + ) post.current_status = PlatformPost.SUCCESS_STATUS + except vkontakte.VkAPIError as error: + logger.error('Error by vk API: %s', str(error)) + post.current_status = PlatformPost.FAILED_STATUS post.save() -PLATFORM_TASK_MAPPING = { +PLATFORM_TASK_MAPPING: Dict[str, Proxy] = { PlatformPost.TELEGRAM_CHANNEL_TYPE: send_post_to_telegram_channel, + PlatformPost.VK_GROUP_TYPE: send_post_to_vk_group, } @@ -61,5 +80,6 @@ def send_scheduled_posts(): logger.error('Unsupported platform type for posting: %s', post.platform_type) post.current_status = PlatformPost.FAILED_STATUS post.save() - logger.info('Add post with id %s to queue', post.id) - task.delay(post.id) + else: + logger.info('Add post with id %s to queue', post.id) + task.delay(post.id) diff --git a/postpost/api/urls.py b/postpost/api/urls.py index 720a817..6839615 100644 --- a/postpost/api/urls.py +++ b/postpost/api/urls.py @@ -3,6 +3,7 @@ from api import views urlpatterns = [ + path('users/', views.UserRegistration.as_view(), name='users_registration'), path('publications/', views.PublicationList.as_view(), name='publications_list'), path('publications/', views.Publication.as_view(), name='publications_details'), ] diff --git a/postpost/api/views.py b/postpost/api/views.py index e461223..49877d3 100644 --- a/postpost/api/views.py +++ b/postpost/api/views.py @@ -1,6 +1,8 @@ -from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.generics import CreateAPIView, ListCreateAPIView, RetrieveUpdateDestroyAPIView +from rest_framework.permissions import AllowAny, IsAuthenticated -from api import models, serializers +from api import filters, models, serializers class PublicationList(ListCreateAPIView): @@ -8,14 +10,27 @@ class PublicationList(ListCreateAPIView): Very basic view for Publications objects. """ + permission_classes = [IsAuthenticated] queryset = models.Publication.objects.all() serializer_class = serializers.PublicationSerializer + filter_backends = (DjangoFilterBackend,) + filterset_class = filters.PublicationFilterSet class Publication(RetrieveUpdateDestroyAPIView): """ - Very basic view for Publication object. + View for get, delete and change publication entity. """ + permission_classes = [IsAuthenticated] queryset = models.Publication.objects.all() serializer_class = serializers.PublicationSerializer + + +class UserRegistration(CreateAPIView): + """ + Register user and generate access/refresh token immediately. + """ + + permission_classes = [AllowAny] + serializer_class = serializers.UserRegistrationSerializer diff --git a/postpost/custom_types.py b/postpost/custom_types.py new file mode 100644 index 0000000..c023978 --- /dev/null +++ b/postpost/custom_types.py @@ -0,0 +1,12 @@ +import typing + +# Recursive types are not supported yet https://github.com/python/mypy/issues/731 +JSON = typing.Union[ # type: ignore + str, + int, + float, + bool, + None, + typing.Mapping[str, 'JSON'], + typing.List['JSON'], +] diff --git a/postpost/gates/__init__.py b/postpost/gates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/postpost/gates/download_media.py b/postpost/gates/download_media.py new file mode 100644 index 0000000..631a567 --- /dev/null +++ b/postpost/gates/download_media.py @@ -0,0 +1,15 @@ +from io import BytesIO +from typing import IO + +import requests + + +def download_media(url: str) -> IO[bytes]: + """ + Downloads media by the given link and returns IO with it. + """ + response = requests.get(url, allow_redirects=True) + response.raise_for_status() + reader = BytesIO(response.content) + reader.name = url.split('/')[-1] + return reader diff --git a/postpost/gates/telegram.py b/postpost/gates/telegram.py new file mode 100644 index 0000000..2aa2b8b --- /dev/null +++ b/postpost/gates/telegram.py @@ -0,0 +1,93 @@ +from typing import IO, Dict, Union + +import requests + +from api.models import PlatformPost +from custom_types import JSON + + +def send_post_to_telegram_chat(token: str, chat_id: str, post: PlatformPost) -> JSON: + """ + Sends post to telegram chat. + """ + tg = TgAPI(token=token) + # TODO: Add media according to PlatformPost changes + return tg.send_message(chat_id=chat_id, text=post.text_for_posting) + + +class TgAPI(object): + """ + Local mini client for Telegram API. + + Its purpose it to share API token and chat id among the methods. + """ + + def __init__(self, token: str): + """ + Init client. + """ + self._url = 'https://api.telegram.org/bot{0}/'.format(token) + + def send_message( + self, + chat_id: str, + text: str, + disable_notification: bool = False, + disable_web_page_preview: bool = True, + ) -> JSON: + """ + Sends the text message to the chat. + """ + return self._request( + 'sendMessage', + chat_id=chat_id, + text=text, + parse_mode='Markdown', + disable_notification=disable_notification, + disable_web_page_preview=disable_web_page_preview, + ) + + def send_photo( + self, + chat_id: str, + photo: IO[bytes], + disable_notification: bool = False, + ) -> JSON: + """ + Sends the photo to the chat. + """ + return self._request( + 'sendPhoto', + chat_id=chat_id, + media={'photo': photo}, + disable_notification=disable_notification, + ) + + def send_animation( + self, + chat_id: str, + animation: IO[bytes], + disable_notification: bool = False, + ) -> JSON: + """ + Sends the animation to the chat. + """ + return self._request( + 'sendAnimation', + chat_id=chat_id, + media={'animation': animation}, + disable_notification=disable_notification, + ) + + def _request( + self, + method_name: str, + media: Union[Dict[str, IO[bytes]], None] = None, + **kwargs, + ) -> JSON: + """ + Sends request to the Telegram API method with the given payload. + """ + response = requests.post(self._url + method_name, data=kwargs, files=media) + response.raise_for_status() + return response.json() diff --git a/postpost/gates/vkontakte.py b/postpost/gates/vkontakte.py new file mode 100644 index 0000000..399a99c --- /dev/null +++ b/postpost/gates/vkontakte.py @@ -0,0 +1,145 @@ +from typing import IO, List, Union + +import requests + +from api.models import PlatformPost +from custom_types import JSON + + +def get_authorization_url(client_id: int, api_version: float) -> str: + """ + Returns a string with url for authorization. + + By following the url, you will be asked by VK to give access to the application. + After agreement, a blank page will be displayed, and there will be an access token + in the address bar. This token is required by functions bellow. + """ + url = ( + 'https://oauth.vk.com/authorize?client_id={client_id}&response_type=token&' + + 'scope=wall,offline,groups,photos,docs&v={api_version}&' + + 'redirect_uri=https://oauth.vk.com/blank.html' + ).format( + client_id=client_id, api_version=api_version, + ) + return url + + +def send_post_to_group( + token: str, group_id: int, api_version: float, post: PlatformPost, +) -> JSON: + """ + Sends post to vk group on behalf of the group itself. + """ + vk_api = VkAPI(token, api_version) + attachments: List[str] = [] + # TODO: Add attachments uploading according to PlatformPost changes + return vk_api.send_post_to_group_wall(group_id, post.text_for_posting, attachments) + + +class VkAPIError(Exception): + """ + Vk API base exception. + """ + + def __init__(self, method: str, payload: JSON, response: bytes): + """ + Init error. + """ + message = '{0} {1} {2}'.format(method, payload, response) + super().__init__(message) + + +class VkAPI(object): + """ + Local mini client for vk API. + + Its purpose is to share token, api version, and error handling among the api methods. + """ + + def __init__(self, token: str, api_version: float): + """ + Init client. + """ + self._token = token + self._api_version = api_version + self._url = 'https://api.vk.com/method/' + + def send_post_to_group_wall( + self, + group_id: int, + message: str, + attachments: Union[List[str], None] = None, + ) -> JSON: + """ + Sends post to vk group on behalf of the group itself. + """ + payload = { + 'owner_id': -group_id, + 'from_group': 1, + 'message': message, + } + if attachments: + payload['attachment'] = ','.join(attachments) + return self._request( + 'wall.post', + payload=payload, + ) + + def upload_doc(self, doc: IO[bytes]) -> str: + """ + Uploads and saves doc on the server. + """ + upload_url = self._request('docs.getWallUploadServer')['upload_url'] + + uploaded_doc = self._upload_media(upload_url, doc) + + saved_doc = self._request( + 'docs.save', + {'file': uploaded_doc['file']}, + )['doc'] + return 'doc{0}_{1}'.format(saved_doc['owner_id'], saved_doc['id']) + + def upload_photo(self, group_id: int, photo: IO[bytes]) -> str: + """ + Uploads and saves photo in the community wall photos. + """ + upload_url = self._request( + 'photos.getWallUploadServer', + {'group_id': group_id}, + )['upload_url'] + + uploaded_photo = self._upload_media(upload_url, photo) + + saved_photo = self._request( + 'photos.saveWallPhoto', { + 'group_id': group_id, + 'server': uploaded_photo['server'], + 'hash': uploaded_photo['hash'], + 'photo': uploaded_photo['photo'], + }, + )[0] + return 'photo{0}_{1}'.format(saved_photo['owner_id'], saved_photo['id']) + + def _request(self, method: str, payload: JSON = None) -> JSON: + """ + Sends request to the VK API method with the given payload. + """ + if payload is None: + payload = {} + payload.update({ + 'v': self._api_version, + 'access_token': self._token, + }) + response = requests.post(self._url + method, data=payload) + if response.status_code != requests.codes.ok or 'error' in response.json(): + raise VkAPIError(method, payload, response.content) + return response.json()['response'] + + def _upload_media(self, upload_url: str, media: IO[bytes]) -> JSON: + """ + Upload media to the VK server. + """ + response = requests.post(upload_url, files={'file': media}) + if response.status_code != requests.codes.ok or 'error' in response.json(): + raise VkAPIError(upload_url, {'file': media.name}, response.content) + return response.json() diff --git a/postpost/main/settings.py b/postpost/main/settings.py index 4c2979a..2c15279 100644 --- a/postpost/main/settings.py +++ b/postpost/main/settings.py @@ -12,6 +12,10 @@ import os +import sentry_sdk +from decouple import config +from sentry_sdk.integrations.django import DjangoIntegration + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # noqa: Z221 @@ -25,7 +29,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['*'] # Application definition @@ -39,10 +43,14 @@ 'django.contrib.staticfiles', 'rest_framework', 'drf_yasg', + 'oauth2_provider', + 'corsheaders', 'api', + 'pyuploadcare.dj', ] MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', @@ -79,7 +87,7 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + 'NAME': 'mydatabase', }, } @@ -121,7 +129,11 @@ # https://docs.djangoproject.com/en/2.1/howto/static-files/ STATIC_URL = '/static/' -CELERY_BROKER_URL = 'redis://localhost:6379/0' +CELERY_BROKER_URL = 'redis://localhost:6379' +CELERY_RESULT_BACKEND = 'redis://localhost:6379' +CELERY_ACCEPT_CONTENT = ['application/json'] +CELERY_TASK_SERIALIZER = 'json' +CELERY_RESULT_SERIALIZER = 'json' REST_FRAMEWORK = { 'DEFAULT_RENDERER_CLASSES': ( @@ -130,4 +142,37 @@ 'DEFAULT_PARSER_CLASSES': ( 'djangorestframework_camel_case.parser.CamelCaseJSONParser', ), + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', + ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', + ), +} + + +# Secrets +UPLOADCARE = { + 'pub_key': config('UPLOADCARE_PUBLIC_KEY', default=1), + 'secret': config('UPLOADCARE_SECRET', default=1), } + +SWAGGER_SETTINGS = { + 'USE_SESSION_AUTH': False, + 'SECURITY_DEFINITIONS': { + 'API': { + 'type': 'oauth2', + 'authorizationUrl': '/api/oauth/authorize', + 'tokenUrl': '/api/oauth/token/', + 'flow': 'password', + }, + }, +} + +CORS_ORIGIN_ALLOW_ALL = True + +sentry_sdk.init( + dsn=config('SENTRY_DSN', default=None), + environment=config('SENTRY_ENVIRONMENT', default=None), + integrations=[DjangoIntegration()], +) diff --git a/postpost/main/urls.py b/postpost/main/urls.py index 868d92d..4aeb7db 100644 --- a/postpost/main/urls.py +++ b/postpost/main/urls.py @@ -1,19 +1,3 @@ -"""postpost URL Configuration. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/2.1/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) - -""" from django.contrib import admin from django.urls import include, path from drf_yasg import openapi @@ -33,8 +17,9 @@ urlpatterns = [ - path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), - path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), + path('api/swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), + path('api/redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), + path('api/oauth/', include('oauth2_provider.urls', namespace='oauth2_provider')), path('api/', include('api.urls')), path('admin/', admin.site.urls), ] diff --git a/postpost/manage.py b/postpost/manage.py index e526497..35a2801 100755 --- a/postpost/manage.py +++ b/postpost/manage.py @@ -1,4 +1,5 @@ #!/usr/bin/env python + import os import sys @@ -8,8 +9,8 @@ from django.core.management import execute_from_command_line # noqa: Z435 except ImportError as exc: raise ImportError( - 'Couldn\'t import Django. Are you sure it\'s installed and ' - 'available on your PYTHONPATH environment variable? Did you ' + 'Couldn\'t import Django. Are you sure it\'s installed and ' + + 'available on your PYTHONPATH environment variable? Did you ' + 'forget to activate a virtual environment?', ) from exc execute_from_command_line(sys.argv) diff --git a/run-app.sh b/run-app.sh new file mode 100644 index 0000000..ca52506 --- /dev/null +++ b/run-app.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +pipenv run python $PYTHONPATH/manage.py migrate +exec 2>&1 pipenv run uwsgi --ini uwsgi.ini --home $(pipenv --venv) diff --git a/setup.cfg b/setup.cfg index 3f0704b..398d332 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,6 +4,7 @@ include_trailing_comma = true sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER default_section = FIRSTPARTY line_length = 99 +not_skip = __init__.py [flake8] @@ -20,12 +21,16 @@ per-file-ignores = models.py: D106, serializers.py: D106, serializers/*.py: D106, + filters.py: D106, + filters/*.py: D106, # disable for `id` class attribute in model definition "is a python builtin, consider renaming the class attribute" models/*.py: A003, models.py: A003, # disable __init__ with logic, because import all models or admin interfaces from submodules is very conveniently models/__init__.py: Z412, admin/__init__.py: Z412, + # temporarily ignore too many imports + api/serializers.py: Z201, ## Global ignores # I don't like "One-line docstring should fit on one line with quotes" *.py: D200, @@ -44,6 +49,8 @@ per-file-ignores = *.py: Z412, # I know. It isn't problem if I use per-file-ignores for level-project ignores :shrug: *.py: X100, + # Doesn't work properly so far https://github.com/wemake-services/wemake-python-styleguide/issues/492 + *.py: Z323, exclude = # ignore auto-generated migrations @@ -56,10 +63,19 @@ format=pylint [mypy] python_version = 3.6 -warn_return_any = True -warn_unused_configs = True +check_untyped_defs = True +disallow_any_explicit = True +disallow_any_generics = True +disallow_untyped_calls = True +disallow_untyped_decorators = True +ignore_errors = False ignore_missing_imports = True +strict_optional = True +no_implicit_optional = True +warn_unused_ignores = True +warn_redundant_casts = True +warn_unused_configs = True # exception from rules [mypy-*.migrations.*] -ignore_errors = True \ No newline at end of file +ignore_errors = True diff --git a/uwsgi.ini b/uwsgi.ini new file mode 100644 index 0000000..c389834 --- /dev/null +++ b/uwsgi.ini @@ -0,0 +1,9 @@ +[uwsgi] +chdir=$(PYTHONPATH) +module=main.wsgi +master=True +processes=5 +harakiri=10 +max-requests=1000 +vacuum=True +http=0.0.0.0:8000 diff --git a/wait-for.sh b/wait-for.sh new file mode 100644 index 0000000..539a01d --- /dev/null +++ b/wait-for.sh @@ -0,0 +1,79 @@ +#!/bin/sh + +TIMEOUT=15 +QUIET=0 + +echoerr() { + if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi +} + +usage() { + exitcode="$1" + cat << USAGE >&2 +Usage: + $cmdname host:port [-t timeout] [-- command args] + -q | --quiet Do not output any status messages + -t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit "$exitcode" +} + +wait_for() { + for i in `seq $TIMEOUT` ; do + nc -z "$HOST" "$PORT" > /dev/null 2>&1 + + result=$? + if [ $result -eq 0 ] ; then + if [ $# -gt 0 ] ; then + exec "$@" + fi + exit 0 + fi + sleep 1 + done + echo "Operation timed out" >&2 + exit 1 +} + +while [ $# -gt 0 ] +do + case "$1" in + *:* ) + HOST=$(printf "%s\n" "$1"| cut -d : -f 1) + PORT=$(printf "%s\n" "$1"| cut -d : -f 2) + shift 1 + ;; + -q | --quiet) + QUIET=1 + shift 1 + ;; + -t) + TIMEOUT="$2" + if [ "$TIMEOUT" = "" ]; then break; fi + shift 2 + ;; + --timeout=*) + TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + break + ;; + --help) + usage 0 + ;; + *) + echoerr "Unknown argument: $1" + usage 1 + ;; + esac +done + +if [ "$HOST" = "" -o "$PORT" = "" ]; then + echoerr "Error: you need to provide a host and port to test." + usage 2 +fi + +wait_for "$@"