diff --git a/.gitignore b/.gitignore index fa6de09..f81937d 100644 --- a/.gitignore +++ b/.gitignore @@ -18,5 +18,6 @@ media /.env /.mypy_cache /.pytest_cache +/*.egg-info/ /static node_modules diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fc4eb69..86a47d3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,30 +1,41 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.1.0 + rev: v4.4.0 hooks: - - id: check-byte-order-marker + - id: check-ast + - id: check-builtin-literals - id: check-case-conflict + - id: check-docstring-first - id: check-executables-have-shebangs - id: check-json - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + - id: check-vcs-permalinks - id: check-yaml + - id: destroyed-symlinks - id: end-of-file-fixer - - id: flake8 + - id: fix-byte-order-marker - id: mixed-line-ending args: - --fix=lf - id: trailing-whitespace + - repo: https://github.com/psf/black + rev: 23.7.0 + hooks: + - id: black + language_version: python3.11 - repo: local hooks: - id: pytest name: Check pytest unit tests pass - entry: pipenv run pytest + entry: poetry run pytest pass_filenames: false language: system types: [python] - id: mypy name: Check mypy static types match - entry: pipenv run mypy . --ignore-missing-imports + entry: poetry run mypy . --ignore-missing-imports pass_filenames: false language: system types: [python] diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 824365d..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,1496 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "48e812f8f32ffe8196d1af1596adfcd6acd6a9110d9cd033ff6c776a4cbd5188" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.9" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "accept-types": { - "hashes": [ - "sha256:c87feccdffb66b02f9343ff387d7fd5c451ccb2e1221fbd37ea0cedef5cf290f", - "sha256:fb27099716d8f0360408c8ca86d69dbfed44455834b70d1506250abe521b535a" - ], - "index": "pypi", - "version": "==0.4.1" - }, - "ago": { - "hashes": [ - "sha256:6f060596a90c6ec33c2e806bcc3146e7e7bfda7e90ca5c11b579c5bf03baf6fa", - "sha256:9d1956edd8103c266d968ae2a7eaf2f23470b6384e655aaaf54d1158408178ad" - ], - "index": "pypi", - "version": "==0.0.93" - }, - "argon2-cffi": { - "hashes": [ - "sha256:8c976986f2c5c0e5000919e6de187906cfd81fb1c72bf9d88c01177e77da7f80", - "sha256:d384164d944190a7dd7ef22c6aa3ff197da12962bd04b17f64d4e93d934dba5b" - ], - "index": "pypi", - "version": "==21.3.0" - }, - "argon2-cffi-bindings": { - "hashes": [ - "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670", - "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f", - "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583", - "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194", - "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c", - "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a", - "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082", - "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5", - "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f", - "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7", - "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d", - "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f", - "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae", - "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", - "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86", - "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367", - "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d", - "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", - "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb", - "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e", - "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351" - ], - "markers": "python_version >= '3.6'", - "version": "==21.2.0" - }, - "asgiref": { - "hashes": [ - "sha256:2f8abc20f7248433085eda803936d98992f1343ddb022065779f37c5da0181d0", - "sha256:88d59c13d634dcffe0510be048210188edd79aeccb6a6c9028cdad6f31d730a9" - ], - "markers": "python_version >= '3.7'", - "version": "==3.5.0" - }, - "async-timeout": { - "hashes": [ - "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15", - "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c" - ], - "markers": "python_version >= '3.6'", - "version": "==4.0.2" - }, - "beautifulsoup4": { - "hashes": [ - "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30", - "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693" - ], - "markers": "python_version >= '3.6'", - "version": "==4.11.1" - }, - "bleach": { - "hashes": [ - "sha256:08a1fe86d253b5c88c92cc3d810fd8048a16d15762e1e5b74d502256e5926aa1", - "sha256:c6d6cc054bdc9c83b48b8083e236e5f00f238428666d2ce2e083eaa5fd568565" - ], - "index": "pypi", - "version": "==5.0.0" - }, - "cachecontrol": { - "hashes": [ - "sha256:2c75d6a8938cb1933c75c50184549ad42728a27e9f6b92fd677c3151aa72555b", - "sha256:a5b9fcc986b184db101aa280b42ecdcdfc524892596f606858e0b7a8b4d9e144" - ], - "index": "pypi", - "version": "==0.12.11" - }, - "certifi": { - "hashes": [ - "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", - "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" - ], - "version": "==2021.10.8" - }, - "cffi": { - "hashes": [ - "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", - "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", - "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", - "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", - "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", - "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", - "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", - "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", - "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", - "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", - "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", - "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", - "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", - "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", - "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", - "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", - "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", - "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", - "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", - "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", - "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", - "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", - "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", - "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", - "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", - "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", - "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", - "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", - "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", - "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", - "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", - "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", - "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", - "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", - "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", - "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", - "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", - "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", - "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", - "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", - "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", - "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", - "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", - "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", - "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", - "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", - "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", - "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", - "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", - "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" - ], - "version": "==1.15.0" - }, - "charset-normalizer": { - "hashes": [ - "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", - "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" - ], - "markers": "python_version >= '3'", - "version": "==2.0.12" - }, - "click": { - "hashes": [ - "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", - "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" - ], - "markers": "python_version >= '3.7'", - "version": "==8.1.3" - }, - "deprecated": { - "hashes": [ - "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d", - "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.2.13" - }, - "django": { - "hashes": [ - "sha256:6d93497a0a9bf6ba0e0b1a29cccdc40efbfc76297255b1309b3a884a688ec4b6", - "sha256:b896ca61edc079eb6bbaa15cf6071eb69d6aac08cce5211583cfb41515644fdf" - ], - "index": "pypi", - "version": "==3.2.13" - }, - "django-activeurl": { - "hashes": [ - "sha256:d2902cfd74e30e5c10c302b8280c849dd23b48ab0b04bab152d2270b904f90ea" - ], - "index": "pypi", - "version": "==0.2.0" - }, - "django-agent-trust": { - "hashes": [ - "sha256:2832fcd6e1e68274fc19dc09fefa1878758715c0a8f5d313f0d5a606e2e66a39", - "sha256:926304f1ae3b915f42c519b9b993c4275dcfe5c3ec85d6a44659f6a59558343a" - ], - "version": "==1.0.4" - }, - "django-analytical": { - "hashes": [ - "sha256:43de3d8ef7734732f58eba4e5e7df0dea37512dbd89727efdfb30c27a96d4ea9", - "sha256:6127c9196c8de3bcb4626f420d2ae670a7703152b1841b1b3e852b31a9a9d44b" - ], - "index": "pypi", - "version": "==3.1.0" - }, - "django-annoying": { - "hashes": [ - "sha256:083b5e26f140f86178fcb47931f69b7ff75181ccd1e318d9c218ae9babc1805e", - "sha256:e230f28fec0559b4fdb621ff202068a78170ada2ac1ec7a73ba822cc1f737791" - ], - "index": "pypi", - "version": "==0.10.6" - }, - "django-appconf": { - "hashes": [ - "sha256:ae9f864ee1958c815a965ed63b3fba4874eec13de10236ba063a788f9a17389d", - "sha256:be3db0be6c81fa84742000b89a81c016d70ae66a7ccb620cdef592b1f1a6aaa4" - ], - "markers": "python_version >= '3.6'", - "version": "==1.0.5" - }, - "django-classy-tags": { - "hashes": [ - "sha256:0e4a03462b29bd94c8da8160d514cb93e4b4c858752437c082a3efeca94b9f51", - "sha256:d222b45502ac99e550a566667839efaef954b9c7366a6493f143862aabeac878" - ], - "markers": "python_version >= '3.7'", - "version": "==3.0.1" - }, - "django-compressor": { - "hashes": [ - "sha256:1db91b6d04293636a68bd1328dc7bb90d636b0295f67b1cc6d4fa102b9fd25f6", - "sha256:b4fe15cc23bf39420b37cb0030572bd0971104ca1ec3764f502c0f179e576dff" - ], - "index": "pypi", - "version": "==4.0" - }, - "django-computed-property": { - "hashes": [ - "sha256:086289d8d21e3fb09e232ff1f61d747d78445cd90b3420899bcec1709c916fff", - "sha256:3dfd93d13c258ced06ec356b276963cc22d87d5c0944a3ee0e1431eabcae16c8" - ], - "index": "pypi", - "version": "==0.3.0" - }, - "django-cors-headers": { - "hashes": [ - "sha256:a22be2befd4069c4fc174f11cf067351df5c061a3a5f94a01650b4e928b0372b", - "sha256:eb98389bf7a2afc5d374806af4a9149697e3a6955b5a2dc2bf049f7d33647456" - ], - "index": "pypi", - "version": "==3.11.0" - }, - "django-debug-toolbar": { - "hashes": [ - "sha256:644bbd5c428d3283aa9115722471769cac1bec189edf3a0c855fd8ff870375a9", - "sha256:6b633b6cfee24f232d73569870f19aa86c819d750e7f3e833f2344a9eb4b4409" - ], - "index": "pypi", - "version": "==3.2.4" - }, - "django-extensions": { - "hashes": [ - "sha256:28e1e1bf49f0e00307ba574d645b0af3564c981a6dfc87209d48cb98f77d0b1a", - "sha256:9238b9e016bb0009d621e05cf56ea8ce5cce9b32e91ad2026996a7377ca28069" - ], - "index": "pypi", - "version": "==3.1.5" - }, - "django-meta": { - "hashes": [ - "sha256:25010157000fa2f1d1ed46e8739f43cfe00e40af199d913b230e52133b650da5", - "sha256:3d69068b1c9e8369427fe0843f4cb6974c24c27dc87750cd915db487ee475fce" - ], - "index": "pypi", - "version": "==2.0.0" - }, - "django-model-utils": { - "hashes": [ - "sha256:a768a25c80514e0ad4e4a6f9c02c44498985f36c5dfdea47b5b1e8cf994beba6", - "sha256:e7a95e102f9c9653427eadab980d5d59e1dea972913b9c9e01ac37f86bba0ddf" - ], - "index": "pypi", - "version": "==4.2.0" - }, - "django-otp": { - "hashes": [ - "sha256:8637be826c0465d0fd1710e4472efe9fc83883853a2141fefdbace9358d20003", - "sha256:f002c71d4ea7f514590be00492980d3c87397b73dc20542e1c4fc00b66f2dda1" - ], - "index": "pypi", - "version": "==1.1.3" - }, - "django-otp-agents": { - "hashes": [ - "sha256:23ae483c1ad0f04e26df66a726ee402e9d72a07c7e804708f2c6fbc775b00d4d", - "sha256:e99addf12fe342628e623aad554c766c0c50bcad79da553197ecbfb49a2929cf" - ], - "index": "pypi", - "version": "==1.0.1" - }, - "django-push": { - "hashes": [ - "sha256:9d73a27f147ea46f5e92d6ab36c19640b11214b43b378693c8961aaf8bea5b60", - "sha256:d5442fcb6d8254a7e837383ce766a72e8fb921f3bcfc2355440c2da8fbcf07b4" - ], - "index": "pypi", - "version": "==1.1" - }, - "django-randomslugfield": { - "hashes": [ - "sha256:8f5866d9383f020fb7f270a218ddc65b6a33d3833633a0c995c68f28cac59efb" - ], - "index": "pypi", - "version": "==0.3.0" - }, - "django-redis": { - "hashes": [ - "sha256:1d037dc02b11ad7aa11f655d26dac3fb1af32630f61ef4428860a2e29ff92026", - "sha256:8a99e5582c79f894168f5865c52bd921213253b7fd64d16733ae4591564465de" - ], - "index": "pypi", - "version": "==5.2.0" - }, - "django-rq": { - "hashes": [ - "sha256:7be1e10e7091555f9f36edf100b0dbb205ea2b98683d74443d2bdf3c6649a03f", - "sha256:f08486602664d73a6e335872c868d79663e380247e6307496d01b8fa770fefd8" - ], - "index": "pypi", - "version": "==2.5.1" - }, - "django-super-favicon": { - "hashes": [ - "sha256:56cb5268ea73ef3cbde5cb01fef02fea2ec00739cdae0566d3102009f052f683" - ], - "index": "pypi", - "version": "==0.6.1" - }, - "docutils": { - "hashes": [ - "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c", - "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06" - ], - "index": "pypi", - "version": "==0.18.1" - }, - "gevent": { - "hashes": [ - "sha256:0082d8a5d23c35812ce0e716a91ede597f6dd2c5ff508a02a998f73598c59397", - "sha256:01928770972181ad8866ee37ea3504f1824587b188fcab782ef1619ce7538766", - "sha256:05c5e8a50cd6868dd36536c92fb4468d18090e801bd63611593c0717bab63692", - "sha256:08b4c17064e28f4eb85604486abc89f442c7407d2aed249cf54544ce5c9baee6", - "sha256:177f93a3a90f46a5009e0841fef561601e5c637ba4332ab8572edd96af650101", - "sha256:22ce1f38fdfe2149ffe8ec2131ca45281791c1e464db34b3b4321ae9d8d2efbb", - "sha256:24d3550fbaeef5fddd794819c2853bca45a86c3d64a056a2c268d981518220d1", - "sha256:2afa3f3ad528155433f6ac8bd64fa5cc303855b97004416ec719a6b1ca179481", - "sha256:2bcec9f80196c751fdcf389ca9f7141e7b0db960d8465ed79be5e685bfcad682", - "sha256:2cfff82f05f14b7f5d9ed53ccb7a609ae8604df522bb05c971bca78ec9d8b2b9", - "sha256:3baeeccc4791ba3f8db27179dff11855a8f9210ddd754f6c9b48e0d2561c2aea", - "sha256:3c012c73e6c61f13c75e3a4869dbe6a2ffa025f103421a6de9c85e627e7477b1", - "sha256:3dad62f55fad839d498c801e139481348991cee6e1c7706041b5fe096cb6a279", - "sha256:542ae891e2aa217d2cf6d8446538fcd2f3263a40eec123b970b899bac391c47a", - "sha256:6a02a88723ed3f0fd92cbf1df3c4cd2fbd87d82b0a4bac3e36a8875923115214", - "sha256:74fc1ef16b86616cfddcc74f7292642b0f72dde4dd95aebf4c45bb236744be54", - "sha256:7909780f0cf18a1fc32aafd8c8e130cdd93c6e285b11263f7f2d1a0f3678bc50", - "sha256:7ccffcf708094564e442ac6fde46f0ae9e40015cb69d995f4b39cc29a7643881", - "sha256:8c21cb5c9f4e14d75b3fe0b143ec875d7dbd1495fad6d49704b00e57e781ee0f", - "sha256:973749bacb7bc4f4181a8fb2a7e0e2ff44038de56d08e856dd54a5ac1d7331b4", - "sha256:9d86438ede1cbe0fde6ef4cc3f72bf2f1ecc9630d8b633ff344a3aeeca272cdd", - "sha256:9f9652d1e4062d4b5b5a0a49ff679fa890430b5f76969d35dccb2df114c55e0f", - "sha256:a5ad4ed8afa0a71e1927623589f06a9b5e8b5e77810be3125cb4d93050d3fd1f", - "sha256:b7709c64afa8bb3000c28bb91ec42c79594a7cb0f322e20427d57f9762366a5b", - "sha256:bb5cb8db753469c7a9a0b8a972d2660fe851aa06eee699a1ca42988afb0aaa02", - "sha256:c43f081cbca41d27fd8fef9c6a32cf83cb979345b20abc07bf68df165cdadb24", - "sha256:cc2fef0f98ee180704cf95ec84f2bc2d86c6c3711bb6b6740d74e0afe708b62c", - "sha256:da8d2d51a49b2a5beb02ad619ca9ddbef806ef4870ba04e5ac7b8b41a5b61db3", - "sha256:e1899b921219fc8959ff9afb94dae36be82e0769ed13d330a393594d478a0b3a", - "sha256:eae3c46f9484eaacd67ffcdf4eaf6ca830f587edd543613b0f5c4eb3c11d052d", - "sha256:ec21f9eaaa6a7b1e62da786132d6788675b314f25f98d9541f1bf00584ed4749", - "sha256:f289fae643a3f1c3b909d6b033e6921b05234a4907e9c9c8c3f1fe403e6ac452", - "sha256:f48b64578c367b91fa793bf8eaaaf4995cb93c8bc45860e473bf868070ad094e" - ], - "index": "pypi", - "version": "==21.12.0" - }, - "greenlet": { - "hashes": [ - "sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3", - "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711", - "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd", - "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073", - "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708", - "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67", - "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23", - "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1", - "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08", - "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd", - "sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2", - "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa", - "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8", - "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40", - "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab", - "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6", - "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc", - "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b", - "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e", - "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963", - "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3", - "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d", - "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d", - "sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe", - "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28", - "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3", - "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e", - "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c", - "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d", - "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0", - "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497", - "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee", - "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713", - "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58", - "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a", - "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06", - "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88", - "sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965", - "sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f", - "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4", - "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5", - "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c", - "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a", - "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1", - "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43", - "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627", - "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b", - "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168", - "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d", - "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5", - "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478", - "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf", - "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce", - "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c", - "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b" - ], - "markers": "platform_python_implementation == 'CPython'", - "version": "==1.1.2" - }, - "gunicorn": { - "extras": [ - "gevent" - ], - "hashes": [ - "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", - "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" - ], - "index": "pypi", - "version": "==20.1.0" - }, - "hiredis": { - "hashes": [ - "sha256:04026461eae67fdefa1949b7332e488224eac9e8f2b5c58c98b54d29af22093e", - "sha256:04927a4c651a0e9ec11c68e4427d917e44ff101f761cd3b5bc76f86aaa431d27", - "sha256:07bbf9bdcb82239f319b1f09e8ef4bdfaec50ed7d7ea51a56438f39193271163", - "sha256:09004096e953d7ebd508cded79f6b21e05dff5d7361771f59269425108e703bc", - "sha256:0adea425b764a08270820531ec2218d0508f8ae15a448568109ffcae050fee26", - "sha256:0b39ec237459922c6544d071cdcf92cbb5bc6685a30e7c6d985d8a3e3a75326e", - "sha256:0d5109337e1db373a892fdcf78eb145ffb6bbd66bb51989ec36117b9f7f9b579", - "sha256:0f41827028901814c709e744060843c77e78a3aca1e0d6875d2562372fcb405a", - "sha256:11d119507bb54e81f375e638225a2c057dda748f2b1deef05c2b1a5d42686048", - "sha256:1233e303645f468e399ec906b6b48ab7cd8391aae2d08daadbb5cad6ace4bd87", - "sha256:139705ce59d94eef2ceae9fd2ad58710b02aee91e7fa0ccb485665ca0ecbec63", - "sha256:1f03d4dadd595f7a69a75709bc81902673fa31964c75f93af74feac2f134cc54", - "sha256:240ce6dc19835971f38caf94b5738092cb1e641f8150a9ef9251b7825506cb05", - "sha256:294a6697dfa41a8cba4c365dd3715abc54d29a86a40ec6405d677ca853307cfb", - "sha256:3d55e36715ff06cdc0ab62f9591607c4324297b6b6ce5b58cb9928b3defe30ea", - "sha256:3dddf681284fe16d047d3ad37415b2e9ccdc6c8986c8062dbe51ab9a358b50a5", - "sha256:3f5f7e3a4ab824e3de1e1700f05ad76ee465f5f11f5db61c4b297ec29e692b2e", - "sha256:508999bec4422e646b05c95c598b64bdbef1edf0d2b715450a078ba21b385bcc", - "sha256:5d2a48c80cf5a338d58aae3c16872f4d452345e18350143b3bf7216d33ba7b99", - "sha256:5dc7a94bb11096bc4bffd41a3c4f2b958257085c01522aa81140c68b8bf1630a", - "sha256:65d653df249a2f95673976e4e9dd7ce10de61cfc6e64fa7eeaa6891a9559c581", - "sha256:7492af15f71f75ee93d2a618ca53fea8be85e7b625e323315169977fae752426", - "sha256:7f0055f1809b911ab347a25d786deff5e10e9cf083c3c3fd2dd04e8612e8d9db", - "sha256:807b3096205c7cec861c8803a6738e33ed86c9aae76cac0e19454245a6bbbc0a", - "sha256:81d6d8e39695f2c37954d1011c0480ef7cf444d4e3ae24bc5e89ee5de360139a", - "sha256:87c7c10d186f1743a8fd6a971ab6525d60abd5d5d200f31e073cd5e94d7e7a9d", - "sha256:8b42c0dc927b8d7c0eb59f97e6e34408e53bc489f9f90e66e568f329bff3e443", - "sha256:a00514362df15af041cc06e97aebabf2895e0a7c42c83c21894be12b84402d79", - "sha256:a39efc3ade8c1fb27c097fd112baf09d7fd70b8cb10ef1de4da6efbe066d381d", - "sha256:a4ee8000454ad4486fb9f28b0cab7fa1cd796fc36d639882d0b34109b5b3aec9", - "sha256:a7928283143a401e72a4fad43ecc85b35c27ae699cf5d54d39e1e72d97460e1d", - "sha256:adf4dd19d8875ac147bf926c727215a0faf21490b22c053db464e0bf0deb0485", - "sha256:ae8427a5e9062ba66fc2c62fb19a72276cf12c780e8db2b0956ea909c48acff5", - "sha256:b4c8b0bc5841e578d5fb32a16e0c305359b987b850a06964bd5a62739d688048", - "sha256:b84f29971f0ad4adaee391c6364e6f780d5aae7e9226d41964b26b49376071d0", - "sha256:c39c46d9e44447181cd502a35aad2bb178dbf1b1f86cf4db639d7b9614f837c6", - "sha256:cb2126603091902767d96bcb74093bd8b14982f41809f85c9b96e519c7e1dc41", - "sha256:dcef843f8de4e2ff5e35e96ec2a4abbdf403bd0f732ead127bd27e51f38ac298", - "sha256:e3447d9e074abf0e3cd85aef8131e01ab93f9f0e86654db7ac8a3f73c63706ce", - "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0", - "sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a" - ], - "index": "pypi", - "version": "==2.0.0" - }, - "html5lib": { - "hashes": [ - "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", - "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.1" - }, - "idna": { - "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" - ], - "markers": "python_version >= '3'", - "version": "==3.3" - }, - "importlib-metadata": { - "hashes": [ - "sha256:1208431ca90a8cca1a6b8af391bb53c1a2db74e5d1cef6ddced95d4b2062edc6", - "sha256:ea4c597ebf37142f827b8f39299579e31685c31d3a438b59f469406afd0f2539" - ], - "markers": "python_version < '3.10'", - "version": "==4.11.3" - }, - "isodate": { - "hashes": [ - "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96", - "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9" - ], - "version": "==0.6.1" - }, - "jinja2": { - "hashes": [ - "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", - "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" - ], - "index": "pypi", - "version": "==3.1.2" - }, - "lxml": { - "hashes": [ - "sha256:078306d19a33920004addeb5f4630781aaeabb6a8d01398045fcde085091a169", - "sha256:0c1978ff1fd81ed9dcbba4f91cf09faf1f8082c9d72eb122e92294716c605428", - "sha256:1010042bfcac2b2dc6098260a2ed022968dbdfaf285fc65a3acf8e4eb1ffd1bc", - "sha256:1d650812b52d98679ed6c6b3b55cbb8fe5a5460a0aef29aeb08dc0b44577df85", - "sha256:20b8a746a026017acf07da39fdb10aa80ad9877046c9182442bf80c84a1c4696", - "sha256:2403a6d6fb61c285969b71f4a3527873fe93fd0abe0832d858a17fe68c8fa507", - "sha256:24f5c5ae618395ed871b3d8ebfcbb36e3f1091fd847bf54c4de623f9107942f3", - "sha256:28d1af847786f68bec57961f31221125c29d6f52d9187c01cd34dc14e2b29430", - "sha256:31499847fc5f73ee17dbe1b8e24c6dafc4e8d5b48803d17d22988976b0171f03", - "sha256:31ba2cbc64516dcdd6c24418daa7abff989ddf3ba6d3ea6f6ce6f2ed6e754ec9", - "sha256:330bff92c26d4aee79c5bc4d9967858bdbe73fdbdbacb5daf623a03a914fe05b", - "sha256:5045ee1ccd45a89c4daec1160217d363fcd23811e26734688007c26f28c9e9e7", - "sha256:52cbf2ff155b19dc4d4100f7442f6a697938bf4493f8d3b0c51d45568d5666b5", - "sha256:530f278849031b0eb12f46cca0e5db01cfe5177ab13bd6878c6e739319bae654", - "sha256:545bd39c9481f2e3f2727c78c169425efbfb3fbba6e7db4f46a80ebb249819ca", - "sha256:5804e04feb4e61babf3911c2a974a5b86f66ee227cc5006230b00ac6d285b3a9", - "sha256:5a58d0b12f5053e270510bf12f753a76aaf3d74c453c00942ed7d2c804ca845c", - "sha256:5f148b0c6133fb928503cfcdfdba395010f997aa44bcf6474fcdd0c5398d9b63", - "sha256:5f7d7d9afc7b293147e2d506a4596641d60181a35279ef3aa5778d0d9d9123fe", - "sha256:60d2f60bd5a2a979df28ab309352cdcf8181bda0cca4529769a945f09aba06f9", - "sha256:6259b511b0f2527e6d55ad87acc1c07b3cbffc3d5e050d7e7bcfa151b8202df9", - "sha256:6268e27873a3d191849204d00d03f65c0e343b3bcb518a6eaae05677c95621d1", - "sha256:627e79894770783c129cc5e89b947e52aa26e8e0557c7e205368a809da4b7939", - "sha256:62f93eac69ec0f4be98d1b96f4d6b964855b8255c345c17ff12c20b93f247b68", - "sha256:6d6483b1229470e1d8835e52e0ff3c6973b9b97b24cd1c116dca90b57a2cc613", - "sha256:6f7b82934c08e28a2d537d870293236b1000d94d0b4583825ab9649aef7ddf63", - "sha256:6fe4ef4402df0250b75ba876c3795510d782def5c1e63890bde02d622570d39e", - "sha256:719544565c2937c21a6f76d520e6e52b726d132815adb3447ccffbe9f44203c4", - "sha256:730766072fd5dcb219dd2b95c4c49752a54f00157f322bc6d71f7d2a31fecd79", - "sha256:74eb65ec61e3c7c019d7169387d1b6ffcfea1b9ec5894d116a9a903636e4a0b1", - "sha256:7993232bd4044392c47779a3c7e8889fea6883be46281d45a81451acfd704d7e", - "sha256:80bbaddf2baab7e6de4bc47405e34948e694a9efe0861c61cdc23aa774fcb141", - "sha256:86545e351e879d0b72b620db6a3b96346921fa87b3d366d6c074e5a9a0b8dadb", - "sha256:891dc8f522d7059ff0024cd3ae79fd224752676447f9c678f2a5c14b84d9a939", - "sha256:8a31f24e2a0b6317f33aafbb2f0895c0bce772980ae60c2c640d82caac49628a", - "sha256:8b99ec73073b37f9ebe8caf399001848fced9c08064effdbfc4da2b5a8d07b93", - "sha256:986b7a96228c9b4942ec420eff37556c5777bfba6758edcb95421e4a614b57f9", - "sha256:a1547ff4b8a833511eeaceacbcd17b043214fcdb385148f9c1bc5556ca9623e2", - "sha256:a2bfc7e2a0601b475477c954bf167dee6d0f55cb167e3f3e7cefad906e7759f6", - "sha256:a3c5f1a719aa11866ffc530d54ad965063a8cbbecae6515acbd5f0fae8f48eaa", - "sha256:a9f1c3489736ff8e1c7652e9dc39f80cff820f23624f23d9eab6e122ac99b150", - "sha256:aa0cf4922da7a3c905d000b35065df6184c0dc1d866dd3b86fd961905bbad2ea", - "sha256:ad4332a532e2d5acb231a2e5d33f943750091ee435daffca3fec0a53224e7e33", - "sha256:b2582b238e1658c4061ebe1b4df53c435190d22457642377fd0cb30685cdfb76", - "sha256:b6fc2e2fb6f532cf48b5fed57567ef286addcef38c28874458a41b7837a57807", - "sha256:b92d40121dcbd74831b690a75533da703750f7041b4bf951befc657c37e5695a", - "sha256:bbab6faf6568484707acc052f4dfc3802bdb0cafe079383fbaa23f1cdae9ecd4", - "sha256:c0b88ed1ae66777a798dc54f627e32d3b81c8009967c63993c450ee4cbcbec15", - "sha256:ce13d6291a5f47c1c8dbd375baa78551053bc6b5e5c0e9bb8e39c0a8359fd52f", - "sha256:db3535733f59e5605a88a706824dfcb9bd06725e709ecb017e165fc1d6e7d429", - "sha256:dd10383f1d6b7edf247d0960a3db274c07e96cf3a3fc7c41c8448f93eac3fb1c", - "sha256:e01f9531ba5420838c801c21c1b0f45dbc9607cb22ea2cf132844453bec863a5", - "sha256:e11527dc23d5ef44d76fef11213215c34f36af1608074561fcc561d983aeb870", - "sha256:e1ab2fac607842ac36864e358c42feb0960ae62c34aa4caaf12ada0a1fb5d99b", - "sha256:e1fd7d2fe11f1cb63d3336d147c852f6d07de0d0020d704c6031b46a30b02ca8", - "sha256:e9f84ed9f4d50b74fbc77298ee5c870f67cb7e91dcdc1a6915cb1ff6a317476c", - "sha256:ec4b4e75fc68da9dc0ed73dcdb431c25c57775383fec325d23a770a64e7ebc87", - "sha256:f10ce66fcdeb3543df51d423ede7e238be98412232fca5daec3e54bcd16b8da0", - "sha256:f63f62fc60e6228a4ca9abae28228f35e1bd3ce675013d1dfb828688d50c6e23", - "sha256:fa56bb08b3dd8eac3a8c5b7d075c94e74f755fd9d8a04543ae8d37b1612dd170", - "sha256:fa9b7c450be85bfc6cd39f6df8c5b8cbd76b5d6fc1f69efec80203f9894b885f" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==4.8.0" - }, - "markdown": { - "hashes": [ - "sha256:76df8ae32294ec39dcf89340382882dfa12975f87f45c3ed1ecdb1e8cefc7006", - "sha256:9923332318f843411e9932237530df53162e29dc7a4e2b91e35764583c46c9a3" - ], - "index": "pypi", - "version": "==3.3.6" - }, - "markupsafe": { - "hashes": [ - "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003", - "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88", - "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5", - "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7", - "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a", - "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603", - "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1", - "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135", - "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247", - "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6", - "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601", - "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77", - "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02", - "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e", - "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63", - "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f", - "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980", - "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b", - "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812", - "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff", - "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96", - "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1", - "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925", - "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a", - "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6", - "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e", - "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f", - "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4", - "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f", - "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3", - "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c", - "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a", - "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417", - "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a", - "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a", - "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37", - "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452", - "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933", - "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a", - "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7" - ], - "markers": "python_version >= '3.7'", - "version": "==2.1.1" - }, - "mf2py": { - "hashes": [ - "sha256:84f1f8f2ff3f1deb1c30be497e7ccd805452996a662fd4a77f09e0105bede2c9" - ], - "index": "pypi", - "version": "==1.1.2" - }, - "mf2util": { - "hashes": [ - "sha256:70b5e08dd553e19b1cb46cd6060ff6a41e57859588364efece6e52d14267c859" - ], - "index": "pypi", - "version": "==0.5.1" - }, - "msgpack": { - "hashes": [ - "sha256:0d8c332f53ffff01953ad25131272506500b14750c1d0ce8614b17d098252fbc", - "sha256:1c58cdec1cb5fcea8c2f1771d7b5fec79307d056874f746690bd2bdd609ab147", - "sha256:2c3ca57c96c8e69c1a0d2926a6acf2d9a522b41dc4253a8945c4c6cd4981a4e3", - "sha256:2f30dd0dc4dfe6231ad253b6f9f7128ac3202ae49edd3f10d311adc358772dba", - "sha256:2f97c0f35b3b096a330bb4a1a9247d0bd7e1f3a2eba7ab69795501504b1c2c39", - "sha256:36a64a10b16c2ab31dcd5f32d9787ed41fe68ab23dd66957ca2826c7f10d0b85", - "sha256:3d875631ecab42f65f9dce6f55ce6d736696ced240f2634633188de2f5f21af9", - "sha256:40fb89b4625d12d6027a19f4df18a4de5c64f6f3314325049f219683e07e678a", - "sha256:47d733a15ade190540c703de209ffbc42a3367600421b62ac0c09fde594da6ec", - "sha256:494471d65b25a8751d19c83f1a482fd411d7ca7a3b9e17d25980a74075ba0e88", - "sha256:51fdc7fb93615286428ee7758cecc2f374d5ff363bdd884c7ea622a7a327a81e", - "sha256:6eef0cf8db3857b2b556213d97dd82de76e28a6524853a9beb3264983391dc1a", - "sha256:6f4c22717c74d44bcd7af353024ce71c6b55346dad5e2cc1ddc17ce8c4507c6b", - "sha256:73a80bd6eb6bcb338c1ec0da273f87420829c266379c8c82fa14c23fb586cfa1", - "sha256:89908aea5f46ee1474cc37fbc146677f8529ac99201bc2faf4ef8edc023c2bf3", - "sha256:8a3a5c4b16e9d0edb823fe54b59b5660cc8d4782d7bf2c214cb4b91a1940a8ef", - "sha256:96acc674bb9c9be63fa8b6dabc3248fdc575c4adc005c440ad02f87ca7edd079", - "sha256:973ad69fd7e31159eae8f580f3f707b718b61141838321c6fa4d891c4a2cca52", - "sha256:9b6f2d714c506e79cbead331de9aae6837c8dd36190d02da74cb409b36162e8a", - "sha256:9c0903bd93cbd34653dd63bbfcb99d7539c372795201f39d16fdfde4418de43a", - "sha256:9fce00156e79af37bb6db4e7587b30d11e7ac6a02cb5bac387f023808cd7d7f4", - "sha256:a598d0685e4ae07a0672b59792d2cc767d09d7a7f39fd9bd37ff84e060b1a996", - "sha256:b0a792c091bac433dfe0a70ac17fc2087d4595ab835b47b89defc8bbabcf5c73", - "sha256:bb87f23ae7d14b7b3c21009c4b1705ec107cb21ee71975992f6aca571fb4a42a", - "sha256:bf1e6bfed4860d72106f4e0a1ab519546982b45689937b40257cfd820650b920", - "sha256:c1ba333b4024c17c7591f0f372e2daa3c31db495a9b2af3cf664aef3c14354f7", - "sha256:c2140cf7a3ec475ef0938edb6eb363fa704159e0bf71dde15d953bacc1cf9d7d", - "sha256:c7e03b06f2982aa98d4ddd082a210c3db200471da523f9ac197f2828e80e7770", - "sha256:d02cea2252abc3756b2ac31f781f7a98e89ff9759b2e7450a1c7a0d13302ff50", - "sha256:da24375ab4c50e5b7486c115a3198d207954fe10aaa5708f7b65105df09109b2", - "sha256:e4c309a68cb5d6bbd0c50d5c71a25ae81f268c2dc675c6f4ea8ab2feec2ac4e2", - "sha256:f01b26c2290cbd74316990ba84a14ac3d599af9cebefc543d241a66e785cf17d", - "sha256:f201d34dc89342fabb2a10ed7c9a9aaaed9b7af0f16a5923f1ae562b31258dea", - "sha256:f74da1e5fcf20ade12c6bf1baa17a2dc3604958922de8dc83cbe3eff22e8b611" - ], - "index": "pypi", - "version": "==1.0.3" - }, - "packaging": { - "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" - ], - "markers": "python_version >= '3.6'", - "version": "==21.3" - }, - "pillow": { - "hashes": [ - "sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e", - "sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595", - "sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512", - "sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c", - "sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477", - "sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a", - "sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4", - "sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e", - "sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5", - "sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378", - "sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a", - "sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652", - "sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7", - "sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a", - "sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a", - "sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6", - "sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165", - "sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160", - "sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331", - "sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b", - "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458", - "sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033", - "sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8", - "sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481", - "sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58", - "sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7", - "sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3", - "sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea", - "sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34", - "sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3", - "sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8", - "sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581", - "sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244", - "sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef", - "sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0", - "sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2", - "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97", - "sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717" - ], - "index": "pypi", - "version": "==9.1.0" - }, - "psycopg2-binary": { - "hashes": [ - "sha256:01310cf4cf26db9aea5158c217caa92d291f0500051a6469ac52166e1a16f5b7", - "sha256:083a55275f09a62b8ca4902dd11f4b33075b743cf0d360419e2051a8a5d5ff76", - "sha256:090f3348c0ab2cceb6dfbe6bf721ef61262ddf518cd6cc6ecc7d334996d64efa", - "sha256:0a29729145aaaf1ad8bafe663131890e2111f13416b60e460dae0a96af5905c9", - "sha256:0c9d5450c566c80c396b7402895c4369a410cab5a82707b11aee1e624da7d004", - "sha256:10bb90fb4d523a2aa67773d4ff2b833ec00857f5912bafcfd5f5414e45280fb1", - "sha256:12b11322ea00ad8db8c46f18b7dfc47ae215e4df55b46c67a94b4effbaec7094", - "sha256:152f09f57417b831418304c7f30d727dc83a12761627bb826951692cc6491e57", - "sha256:15803fa813ea05bef089fa78835118b5434204f3a17cb9f1e5dbfd0b9deea5af", - "sha256:15c4e4cfa45f5a60599d9cec5f46cd7b1b29d86a6390ec23e8eebaae84e64554", - "sha256:183a517a3a63503f70f808b58bfbf962f23d73b6dccddae5aa56152ef2bcb232", - "sha256:1f14c8b0942714eb3c74e1e71700cbbcb415acbc311c730370e70c578a44a25c", - "sha256:1f6b813106a3abdf7b03640d36e24669234120c72e91d5cbaeb87c5f7c36c65b", - "sha256:280b0bb5cbfe8039205c7981cceb006156a675362a00fe29b16fbc264e242834", - "sha256:2d872e3c9d5d075a2e104540965a1cf898b52274a5923936e5bfddb58c59c7c2", - "sha256:2f9ffd643bc7349eeb664eba8864d9e01f057880f510e4681ba40a6532f93c71", - "sha256:3303f8807f342641851578ee7ed1f3efc9802d00a6f83c101d21c608cb864460", - "sha256:35168209c9d51b145e459e05c31a9eaeffa9a6b0fd61689b48e07464ffd1a83e", - "sha256:3a79d622f5206d695d7824cbf609a4f5b88ea6d6dab5f7c147fc6d333a8787e4", - "sha256:404224e5fef3b193f892abdbf8961ce20e0b6642886cfe1fe1923f41aaa75c9d", - "sha256:46f0e0a6b5fa5851bbd9ab1bc805eef362d3a230fbdfbc209f4a236d0a7a990d", - "sha256:47133f3f872faf28c1e87d4357220e809dfd3fa7c64295a4a148bcd1e6e34ec9", - "sha256:526ea0378246d9b080148f2d6681229f4b5964543c170dd10bf4faaab6e0d27f", - "sha256:53293533fcbb94c202b7c800a12c873cfe24599656b341f56e71dd2b557be063", - "sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478", - "sha256:57804fc02ca3ce0dbfbef35c4b3a4a774da66d66ea20f4bda601294ad2ea6092", - "sha256:63638d875be8c2784cfc952c9ac34e2b50e43f9f0a0660b65e2a87d656b3116c", - "sha256:6472a178e291b59e7f16ab49ec8b4f3bdada0a879c68d3817ff0963e722a82ce", - "sha256:68641a34023d306be959101b345732360fc2ea4938982309b786f7be1b43a4a1", - "sha256:6e82d38390a03da28c7985b394ec3f56873174e2c88130e6966cb1c946508e65", - "sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e", - "sha256:7af0dd86ddb2f8af5da57a976d27cd2cd15510518d582b478fbb2292428710b4", - "sha256:7b1e9b80afca7b7a386ef087db614faebbf8839b7f4db5eb107d0f1a53225029", - "sha256:874a52ecab70af13e899f7847b3e074eeb16ebac5615665db33bce8a1009cf33", - "sha256:887dd9aac71765ac0d0bac1d0d4b4f2c99d5f5c1382d8b770404f0f3d0ce8a39", - "sha256:8b344adbb9a862de0c635f4f0425b7958bf5a4b927c8594e6e8d261775796d53", - "sha256:8fc53f9af09426a61db9ba357865c77f26076d48669f2e1bb24d85a22fb52307", - "sha256:91920527dea30175cc02a1099f331aa8c1ba39bf8b7762b7b56cbf54bc5cce42", - "sha256:93cd1967a18aa0edd4b95b1dfd554cf15af657cb606280996d393dadc88c3c35", - "sha256:99485cab9ba0fa9b84f1f9e1fef106f44a46ef6afdeec8885e0b88d0772b49e8", - "sha256:9d29409b625a143649d03d0fd7b57e4b92e0ecad9726ba682244b73be91d2fdb", - "sha256:a29b3ca4ec9defec6d42bf5feb36bb5817ba3c0230dd83b4edf4bf02684cd0ae", - "sha256:a9e1f75f96ea388fbcef36c70640c4efbe4650658f3d6a2967b4cc70e907352e", - "sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f", - "sha256:adf20d9a67e0b6393eac162eb81fb10bc9130a80540f4df7e7355c2dd4af9fba", - "sha256:af9813db73395fb1fc211bac696faea4ca9ef53f32dc0cfa27e4e7cf766dcf24", - "sha256:b1c8068513f5b158cf7e29c43a77eb34b407db29aca749d3eb9293ee0d3103ca", - "sha256:bda845b664bb6c91446ca9609fc69f7db6c334ec5e4adc87571c34e4f47b7ddb", - "sha256:c381bda330ddf2fccbafab789d83ebc6c53db126e4383e73794c74eedce855ef", - "sha256:c3ae8e75eb7160851e59adc77b3a19a976e50622e44fd4fd47b8b18208189d42", - "sha256:d1c1b569ecafe3a69380a94e6ae09a4789bbb23666f3d3a08d06bbd2451f5ef1", - "sha256:def68d7c21984b0f8218e8a15d514f714d96904265164f75f8d3a70f9c295667", - "sha256:dffc08ca91c9ac09008870c9eb77b00a46b3378719584059c034b8945e26b272", - "sha256:e3699852e22aa68c10de06524a3721ade969abf382da95884e6a10ff798f9281", - "sha256:e847774f8ffd5b398a75bc1c18fbb56564cda3d629fe68fd81971fece2d3c67e", - "sha256:ffb7a888a047696e7f8240d649b43fb3644f14f0ee229077e7f6b9f9081635bd" - ], - "index": "pypi", - "version": "==2.9.3" - }, - "pycparser": { - "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.21" - }, - "pyparsing": { - "hashes": [ - "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954", - "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06" - ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.8" - }, - "python-baseconv": { - "hashes": [ - "sha256:0539f8bd0464013b05ad62e0a1673f0ac9086c76b43ebf9f833053527cd9931b" - ], - "index": "pypi", - "version": "==1.2.2" - }, - "python-magic": { - "hashes": [ - "sha256:1a2c81e8f395c744536369790bd75094665e9644110a6623bcc3bbea30f03973", - "sha256:21f5f542aa0330f5c8a64442528542f6215c8e18d2466b399b0d9d39356d83fc" - ], - "index": "pypi", - "version": "==0.4.25" - }, - "python-slugify": { - "hashes": [ - "sha256:272d106cb31ab99b3496ba085e3fea0e9e76dcde967b5e9992500d1f785ce4e1", - "sha256:7b2c274c308b62f4269a9ba701aa69a797e9bca41aeee5b3a9e79e36b6656927" - ], - "index": "pypi", - "version": "==6.1.2" - }, - "pytz": { - "hashes": [ - "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7", - "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c" - ], - "version": "==2022.1" - }, - "pyup-django": { - "hashes": [ - "sha256:f02242b4c7a8926bf9118054429dcaf84e5708a050548abcd3b2b9de4a7570b9", - "sha256:fe84cef39c41d5feb24e307d6c8a55454db50df4c6955fa6a890a42b6e58650e" - ], - "index": "pypi", - "version": "==0.4.0" - }, - "pyyaml": { - "hashes": [ - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" - ], - "index": "pypi", - "version": "==6.0" - }, - "qrcode": { - "hashes": [ - "sha256:375a6ff240ca9bd41adc070428b5dfc1dcfbb0f2507f1ac848f6cded38956578" - ], - "index": "pypi", - "version": "==7.3.1" - }, - "rcssmin": { - "hashes": [ - "sha256:0a6aae7e119509445bf7aa6da6ca0f285cc198273c20f470ad999ff83bbadcf9", - "sha256:1512223b6a687bb747e4e531187bd49a56ed71287e7ead9529cbaa1ca4718a0a", - "sha256:1d7c2719d014e4e4df4e33b75ae8067c7e246cf470eaec8585e06e2efac7586c", - "sha256:2211a5c91ea14a5937b57904c9121f8bfef20987825e55368143da7d25446e3b", - "sha256:27fc400627fd3d328b7fe95af2a01f5d0af6b5af39731af5d071826a1f08e362", - "sha256:30f5522285065cae0164d20068377d84b5d10b414156115f8729b034d0ea5e8b", - "sha256:32ccaebbbd4d56eab08cf26aed36f5d33389b9d1d3ca1fecf53eb6ab77760ddf", - "sha256:352dd3a78eb914bb1cb269ac2b66b3154f2490a52ab605558c681de3fb5194d2", - "sha256:37f1242e34ca273ed2c26cf778854e18dd11b31c6bfca60e23fce146c84667c1", - "sha256:49807735f26f59404194f1e6f93254b6d5b6f7748c2a954f4470a86a40ff4c13", - "sha256:506e33ab4c47051f7deae35b6d8dbb4a5c025f016e90a830929a1ecc7daa1682", - "sha256:6158d0d86cd611c5304d738dc3d6cfeb23864dd78ad0d83a633f443696ac5d77", - "sha256:7085d1b51dd2556f3aae03947380f6e9e1da29fb1eeadfa6766b7f105c54c9ff", - "sha256:7c44002b79f3656348196005b9522ec5e04f182b466f66d72b16be0bd03c13d8", - "sha256:7da63fee37edf204bbd86785edb4d7491642adbfd1d36fd230b7ccbbd8db1a6f", - "sha256:8b659a88850e772c84cfac4520ec223de6807875e173d8ef3248ab7f90876066", - "sha256:c28b9eb20982b45ebe6adef8bd2547e5ed314dafddfff4eba806b0f8c166cfd1", - "sha256:ddff3a41611664c7f1d9e3d8a9c1669e0e155ac0458e586ffa834dc5953e7d9f", - "sha256:f1a37bbd36b050813673e62ae6464467548628690bf4d48a938170e121e8616e", - "sha256:f31c82d06ba2dbf33c20db9550157e80bb0c4cbd24575c098f0831d1d2e3c5df" - ], - "version": "==1.1.0" - }, - "redis": { - "hashes": [ - "sha256:0107dc8e98a4f1d1d4aa00100e044287f77121a1e6d2085545c4b7fa94a7a27f", - "sha256:4e95f4ec5f49e636efcf20061a5a9110c20852f607cfca6865c07aaa8a739ee2" - ], - "markers": "python_version >= '3.6'", - "version": "==4.2.2" - }, - "requests": { - "hashes": [ - "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", - "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==2.27.1" - }, - "rjsmin": { - "hashes": [ - "sha256:05efa485dfddb6418e3b86d8862463aa15641a61f6ae05e7e6de8f116ee77c69", - "sha256:1622fbb6c6a8daaf77da13cc83356539bfe79c1440f9664b02c7f7b150b9a18e", - "sha256:1c93b29fd725e61718299ffe57de93ff32d71b313eaabbfcc7bd32ddb82831d5", - "sha256:2ed83aca637186bafdc894b4b7fc3657e2d74014ccca7d3d69122c1e82675216", - "sha256:38a4474ed52e1575fb9da983ec8657faecd8ab3738508d36e04f87769411fd3d", - "sha256:3b14f4c2933ec194eb816b71a0854ce461b6419a3d852bf360344731ab28c0a6", - "sha256:40e7211a25d9a11ac9ff50446e41268c978555676828af86fa1866615823bfff", - "sha256:41c7c3910f7b8816e37366b293e576ddecf696c5f2197d53cf2c1526ac336646", - "sha256:4387a00777faddf853eebdece9f2e56ebaf243c3f24676a9de6a20c5d4f3d731", - "sha256:54fc30519365841b27556ccc1cb94c5b4413c384ff6d467442fddba66e2e325a", - "sha256:6c395ffc130332cca744f081ed5efd5699038dcb7a5d30c3ff4bc6adb5b30a62", - "sha256:6c529feb6c400984452494c52dd9fdf59185afeacca2afc5174a28ab37751a1b", - "sha256:86c4da7285ddafe6888cb262da563570f28e4a31146b5164a7a6947b1222196b", - "sha256:8944a8a55ac825b8e5ec29f341ecb7574697691ef416506885898d2f780fb4ca", - "sha256:993935654c1311280e69665367d7e6ff694ac9e1609168cf51cae8c0307df0db", - "sha256:99e5597a812b60058baa1457387dc79cca7d273b2a700dc98bfd20d43d60711d", - "sha256:b6a7c8c8d19e154334f640954e43e57283e87bb4a2f6e23295db14eea8e9fc1d", - "sha256:c81229ffe5b0a0d5b3b5d5e6d0431f182572de9e9a077e85dbae5757db0ab75c", - "sha256:d63e193a2f932a786ae82068aa76d1d126fcdff8582094caff9e5e66c4dcc124", - "sha256:e18fe1a610fb105273bb369f61c2b0bd9e66a3f0792e27e4cac44e42ace1968b" - ], - "version": "==1.2.0" - }, - "ronkyuu": { - "hashes": [ - "sha256:06bc8bb80e31b7a1f050d61f7206fe9326249d0aed8860c49cebb73758541eea", - "sha256:bb7d9f69d76b2e1c57a665ebc2baf7002ceaa311782b54652a1e6f97b61f0ed9" - ], - "index": "pypi", - "version": "==0.9" - }, - "rq": { - "hashes": [ - "sha256:62d06b44c3acfa5d1933c5a4ec3fbc2484144a8af60e318d0b8447c5236271e2", - "sha256:92f4cf38b2364c1697b541e77c0fe62b7e5242fa864324f262be126ee2a07e3a" - ], - "markers": "python_version >= '3.5'", - "version": "==1.10.1" - }, - "setuptools": { - "hashes": [ - "sha256:26ead7d1f93efc0f8c804d9fafafbe4a44b179580a7105754b245155f9af05a8", - "sha256:47c7b0c0f8fc10eec4cf1e71c6fdadf8decaa74ffa087e68cd1c20db7ad6a592" - ], - "markers": "python_version >= '3.7'", - "version": "==62.1.0" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "soupsieve": { - "hashes": [ - "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759", - "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d" - ], - "markers": "python_version >= '3.6'", - "version": "==2.3.2.post1" - }, - "sqlparse": { - "hashes": [ - "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae", - "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d" - ], - "markers": "python_version >= '3.5'", - "version": "==0.4.2" - }, - "text-unidecode": { - "hashes": [ - "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", - "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93" - ], - "version": "==1.3" - }, - "urllib3": { - "hashes": [ - "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", - "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.9" - }, - "webencodings": { - "hashes": [ - "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", - "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" - ], - "version": "==0.5.1" - }, - "wrapt": { - "hashes": [ - "sha256:00108411e0f34c52ce16f81f1d308a571df7784932cc7491d1e94be2ee93374b", - "sha256:01f799def9b96a8ec1ef6b9c1bbaf2bbc859b87545efbecc4a78faea13d0e3a0", - "sha256:09d16ae7a13cff43660155383a2372b4aa09109c7127aa3f24c3cf99b891c330", - "sha256:14e7e2c5f5fca67e9a6d5f753d21f138398cad2b1159913ec9e9a67745f09ba3", - "sha256:167e4793dc987f77fd476862d32fa404d42b71f6a85d3b38cbce711dba5e6b68", - "sha256:1807054aa7b61ad8d8103b3b30c9764de2e9d0c0978e9d3fc337e4e74bf25faa", - "sha256:1f83e9c21cd5275991076b2ba1cd35418af3504667affb4745b48937e214bafe", - "sha256:21b1106bff6ece8cb203ef45b4f5778d7226c941c83aaaa1e1f0f4f32cc148cd", - "sha256:22626dca56fd7f55a0733e604f1027277eb0f4f3d95ff28f15d27ac25a45f71b", - "sha256:23f96134a3aa24cc50614920cc087e22f87439053d886e474638c68c8d15dc80", - "sha256:2498762814dd7dd2a1d0248eda2afbc3dd9c11537bc8200a4b21789b6df6cd38", - "sha256:28c659878f684365d53cf59dc9a1929ea2eecd7ac65da762be8b1ba193f7e84f", - "sha256:2eca15d6b947cfff51ed76b2d60fd172c6ecd418ddab1c5126032d27f74bc350", - "sha256:354d9fc6b1e44750e2a67b4b108841f5f5ea08853453ecbf44c81fdc2e0d50bd", - "sha256:36a76a7527df8583112b24adc01748cd51a2d14e905b337a6fefa8b96fc708fb", - "sha256:3a0a4ca02752ced5f37498827e49c414d694ad7cf451ee850e3ff160f2bee9d3", - "sha256:3a71dbd792cc7a3d772ef8cd08d3048593f13d6f40a11f3427c000cf0a5b36a0", - "sha256:3a88254881e8a8c4784ecc9cb2249ff757fd94b911d5df9a5984961b96113fff", - "sha256:47045ed35481e857918ae78b54891fac0c1d197f22c95778e66302668309336c", - "sha256:4775a574e9d84e0212f5b18886cace049a42e13e12009bb0491562a48bb2b758", - "sha256:493da1f8b1bb8a623c16552fb4a1e164c0200447eb83d3f68b44315ead3f9036", - "sha256:4b847029e2d5e11fd536c9ac3136ddc3f54bc9488a75ef7d040a3900406a91eb", - "sha256:59d7d92cee84a547d91267f0fea381c363121d70fe90b12cd88241bd9b0e1763", - "sha256:5a0898a640559dec00f3614ffb11d97a2666ee9a2a6bad1259c9facd01a1d4d9", - "sha256:5a9a1889cc01ed2ed5f34574c90745fab1dd06ec2eee663e8ebeefe363e8efd7", - "sha256:5b835b86bd5a1bdbe257d610eecab07bf685b1af2a7563093e0e69180c1d4af1", - "sha256:5f24ca7953f2643d59a9c87d6e272d8adddd4a53bb62b9208f36db408d7aafc7", - "sha256:61e1a064906ccba038aa3c4a5a82f6199749efbbb3cef0804ae5c37f550eded0", - "sha256:65bf3eb34721bf18b5a021a1ad7aa05947a1767d1aa272b725728014475ea7d5", - "sha256:6807bcee549a8cb2f38f73f469703a1d8d5d990815c3004f21ddb68a567385ce", - "sha256:68aeefac31c1f73949662ba8affaf9950b9938b712fb9d428fa2a07e40ee57f8", - "sha256:6915682f9a9bc4cf2908e83caf5895a685da1fbd20b6d485dafb8e218a338279", - "sha256:6d9810d4f697d58fd66039ab959e6d37e63ab377008ef1d63904df25956c7db0", - "sha256:729d5e96566f44fccac6c4447ec2332636b4fe273f03da128fff8d5559782b06", - "sha256:748df39ed634851350efa87690c2237a678ed794fe9ede3f0d79f071ee042561", - "sha256:763a73ab377390e2af26042f685a26787c402390f682443727b847e9496e4a2a", - "sha256:8323a43bd9c91f62bb7d4be74cc9ff10090e7ef820e27bfe8815c57e68261311", - "sha256:8529b07b49b2d89d6917cfa157d3ea1dfb4d319d51e23030664a827fe5fd2131", - "sha256:87fa943e8bbe40c8c1ba4086971a6fefbf75e9991217c55ed1bcb2f1985bd3d4", - "sha256:88236b90dda77f0394f878324cfbae05ae6fde8a84d548cfe73a75278d760291", - "sha256:891c353e95bb11abb548ca95c8b98050f3620a7378332eb90d6acdef35b401d4", - "sha256:89ba3d548ee1e6291a20f3c7380c92f71e358ce8b9e48161401e087e0bc740f8", - "sha256:8c6be72eac3c14baa473620e04f74186c5d8f45d80f8f2b4eda6e1d18af808e8", - "sha256:9a242871b3d8eecc56d350e5e03ea1854de47b17f040446da0e47dc3e0b9ad4d", - "sha256:9a3ff5fb015f6feb78340143584d9f8a0b91b6293d6b5cf4295b3e95d179b88c", - "sha256:9a5a544861b21e0e7575b6023adebe7a8c6321127bb1d238eb40d99803a0e8bd", - "sha256:9d57677238a0c5411c76097b8b93bdebb02eb845814c90f0b01727527a179e4d", - "sha256:9d8c68c4145041b4eeae96239802cfdfd9ef927754a5be3f50505f09f309d8c6", - "sha256:9d9fcd06c952efa4b6b95f3d788a819b7f33d11bea377be6b8980c95e7d10775", - "sha256:a0057b5435a65b933cbf5d859cd4956624df37b8bf0917c71756e4b3d9958b9e", - "sha256:a65bffd24409454b889af33b6c49d0d9bcd1a219b972fba975ac935f17bdf627", - "sha256:b0ed6ad6c9640671689c2dbe6244680fe8b897c08fd1fab2228429b66c518e5e", - "sha256:b21650fa6907e523869e0396c5bd591cc326e5c1dd594dcdccac089561cacfb8", - "sha256:b3f7e671fb19734c872566e57ce7fc235fa953d7c181bb4ef138e17d607dc8a1", - "sha256:b77159d9862374da213f741af0c361720200ab7ad21b9f12556e0eb95912cd48", - "sha256:bb36fbb48b22985d13a6b496ea5fb9bb2a076fea943831643836c9f6febbcfdc", - "sha256:d066ffc5ed0be00cd0352c95800a519cf9e4b5dd34a028d301bdc7177c72daf3", - "sha256:d332eecf307fca852d02b63f35a7872de32d5ba8b4ec32da82f45df986b39ff6", - "sha256:d808a5a5411982a09fef6b49aac62986274ab050e9d3e9817ad65b2791ed1425", - "sha256:d9bdfa74d369256e4218000a629978590fd7cb6cf6893251dad13d051090436d", - "sha256:db6a0ddc1282ceb9032e41853e659c9b638789be38e5b8ad7498caac00231c23", - "sha256:debaf04f813ada978d7d16c7dfa16f3c9c2ec9adf4656efdc4defdf841fc2f0c", - "sha256:f0408e2dbad9e82b4c960274214af533f856a199c9274bd4aff55d4634dedc33", - "sha256:f2f3bc7cd9c9fcd39143f11342eb5963317bd54ecc98e3650ca22704b69d9653" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.14.0" - }, - "xrd": { - "hashes": [ - "sha256:51d01f732b5b5b7983c5179ffaed864408d95a667b3a6630fe27aa7528274089" - ], - "index": "pypi", - "version": "==0.1" - }, - "zipp": { - "hashes": [ - "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad", - "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099" - ], - "markers": "python_version >= '3.7'", - "version": "==3.8.0" - }, - "zope.event": { - "hashes": [ - "sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42", - "sha256:5e76517f5b9b119acf37ca8819781db6c16ea433f7e2062c4afc2b6fbedb1330" - ], - "version": "==4.5.0" - }, - "zope.interface": { - "hashes": [ - "sha256:08f9636e99a9d5410181ba0729e0408d3d8748026ea938f3b970a0249daa8192", - "sha256:0b465ae0962d49c68aa9733ba92a001b2a0933c317780435f00be7ecb959c702", - "sha256:0cba8477e300d64a11a9789ed40ee8932b59f9ee05f85276dbb4b59acee5dd09", - "sha256:0cee5187b60ed26d56eb2960136288ce91bcf61e2a9405660d271d1f122a69a4", - "sha256:0ea1d73b7c9dcbc5080bb8aaffb776f1c68e807767069b9ccdd06f27a161914a", - "sha256:0f91b5b948686659a8e28b728ff5e74b1be6bf40cb04704453617e5f1e945ef3", - "sha256:15e7d1f7a6ee16572e21e3576d2012b2778cbacf75eb4b7400be37455f5ca8bf", - "sha256:17776ecd3a1fdd2b2cd5373e5ef8b307162f581c693575ec62e7c5399d80794c", - "sha256:194d0bcb1374ac3e1e023961610dc8f2c78a0f5f634d0c737691e215569e640d", - "sha256:1c0e316c9add0db48a5b703833881351444398b04111188069a26a61cfb4df78", - "sha256:205e40ccde0f37496904572035deea747390a8b7dc65146d30b96e2dd1359a83", - "sha256:273f158fabc5ea33cbc936da0ab3d4ba80ede5351babc4f577d768e057651531", - "sha256:2876246527c91e101184f63ccd1d716ec9c46519cc5f3d5375a3351c46467c46", - "sha256:2c98384b254b37ce50eddd55db8d381a5c53b4c10ee66e1e7fe749824f894021", - "sha256:2e5a26f16503be6c826abca904e45f1a44ff275fdb7e9d1b75c10671c26f8b94", - "sha256:334701327f37c47fa628fc8b8d28c7d7730ce7daaf4bda1efb741679c2b087fc", - "sha256:3748fac0d0f6a304e674955ab1365d515993b3a0a865e16a11ec9d86fb307f63", - "sha256:3c02411a3b62668200910090a0dff17c0b25aaa36145082a5a6adf08fa281e54", - "sha256:3dd4952748521205697bc2802e4afac5ed4b02909bb799ba1fe239f77fd4e117", - "sha256:3f24df7124c323fceb53ff6168da70dbfbae1442b4f3da439cd441681f54fe25", - "sha256:469e2407e0fe9880ac690a3666f03eb4c3c444411a5a5fddfdabc5d184a79f05", - "sha256:4de4bc9b6d35c5af65b454d3e9bc98c50eb3960d5a3762c9438df57427134b8e", - "sha256:5208ebd5152e040640518a77827bdfcc73773a15a33d6644015b763b9c9febc1", - "sha256:52de7fc6c21b419078008f697fd4103dbc763288b1406b4562554bd47514c004", - "sha256:5bb3489b4558e49ad2c5118137cfeaf59434f9737fa9c5deefc72d22c23822e2", - "sha256:5dba5f530fec3f0988d83b78cc591b58c0b6eb8431a85edd1569a0539a8a5a0e", - "sha256:5dd9ca406499444f4c8299f803d4a14edf7890ecc595c8b1c7115c2342cadc5f", - "sha256:5f931a1c21dfa7a9c573ec1f50a31135ccce84e32507c54e1ea404894c5eb96f", - "sha256:63b82bb63de7c821428d513607e84c6d97d58afd1fe2eb645030bdc185440120", - "sha256:66c0061c91b3b9cf542131148ef7ecbecb2690d48d1612ec386de9d36766058f", - "sha256:6f0c02cbb9691b7c91d5009108f975f8ffeab5dff8f26d62e21c493060eff2a1", - "sha256:71aace0c42d53abe6fc7f726c5d3b60d90f3c5c055a447950ad6ea9cec2e37d9", - "sha256:7d97a4306898b05404a0dcdc32d9709b7d8832c0c542b861d9a826301719794e", - "sha256:7df1e1c05304f26faa49fa752a8c690126cf98b40b91d54e6e9cc3b7d6ffe8b7", - "sha256:8270252effc60b9642b423189a2fe90eb6b59e87cbee54549db3f5562ff8d1b8", - "sha256:867a5ad16892bf20e6c4ea2aab1971f45645ff3102ad29bd84c86027fa99997b", - "sha256:877473e675fdcc113c138813a5dd440da0769a2d81f4d86614e5d62b69497155", - "sha256:8892f89999ffd992208754851e5a052f6b5db70a1e3f7d54b17c5211e37a98c7", - "sha256:9a9845c4c6bb56e508651f005c4aeb0404e518c6f000d5a1123ab077ab769f5c", - "sha256:a1e6e96217a0f72e2b8629e271e1b280c6fa3fe6e59fa8f6701bec14e3354325", - "sha256:a8156e6a7f5e2a0ff0c5b21d6bcb45145efece1909efcbbbf48c56f8da68221d", - "sha256:a9506a7e80bcf6eacfff7f804c0ad5350c8c95b9010e4356a4b36f5322f09abb", - "sha256:af310ec8335016b5e52cae60cda4a4f2a60a788cbb949a4fbea13d441aa5a09e", - "sha256:b0297b1e05fd128d26cc2460c810d42e205d16d76799526dfa8c8ccd50e74959", - "sha256:bf68f4b2b6683e52bec69273562df15af352e5ed25d1b6641e7efddc5951d1a7", - "sha256:d0c1bc2fa9a7285719e5678584f6b92572a5b639d0e471bb8d4b650a1a910920", - "sha256:d4d9d6c1a455d4babd320203b918ccc7fcbefe308615c521062bc2ba1aa4d26e", - "sha256:db1fa631737dab9fa0b37f3979d8d2631e348c3b4e8325d6873c2541d0ae5a48", - "sha256:dd93ea5c0c7f3e25335ab7d22a507b1dc43976e1345508f845efc573d3d779d8", - "sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4", - "sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==5.4.0" - } - }, - "develop": { - "appdirs": { - "hashes": [ - "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", - "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" - ], - "version": "==1.4.4" - }, - "attrs": { - "hashes": [ - "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", - "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.4.0" - }, - "iniconfig": { - "hashes": [ - "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", - "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" - ], - "version": "==1.1.1" - }, - "jedi": { - "hashes": [ - "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d", - "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab" - ], - "markers": "python_version >= '3.6'", - "version": "==0.18.1" - }, - "mypy": { - "hashes": [ - "sha256:0112752a6ff07230f9ec2f71b0d3d4e088a910fdce454fdb6553e83ed0eced7d", - "sha256:0384d9f3af49837baa92f559d3fa673e6d2652a16550a9ee07fc08c736f5e6f8", - "sha256:1b333cfbca1762ff15808a0ef4f71b5d3eed8528b23ea1c3fb50543c867d68de", - "sha256:1fdeb0a0f64f2a874a4c1f5271f06e40e1e9779bf55f9567f149466fc7a55038", - "sha256:4c653e4846f287051599ed8f4b3c044b80e540e88feec76b11044ddc5612ffed", - "sha256:563514c7dc504698fb66bb1cf897657a173a496406f1866afae73ab5b3cdb334", - "sha256:5b231afd6a6e951381b9ef09a1223b1feabe13625388db48a8690f8daa9b71ff", - "sha256:5ce6a09042b6da16d773d2110e44f169683d8cc8687e79ec6d1181a72cb028d2", - "sha256:5e7647df0f8fc947388e6251d728189cfadb3b1e558407f93254e35abc026e22", - "sha256:6003de687c13196e8a1243a5e4bcce617d79b88f83ee6625437e335d89dfebe2", - "sha256:61504b9a5ae166ba5ecfed9e93357fd51aa693d3d434b582a925338a2ff57fd2", - "sha256:77423570c04aca807508a492037abbd72b12a1fb25a385847d191cd50b2c9605", - "sha256:a4d9898f46446bfb6405383b57b96737dcfd0a7f25b748e78ef3e8c576bba3cb", - "sha256:a952b8bc0ae278fc6316e6384f67bb9a396eb30aced6ad034d3a76120ebcc519", - "sha256:b5b5bd0ffb11b4aba2bb6d31b8643902c48f990cc92fda4e21afac658044f0c0", - "sha256:ca75ecf2783395ca3016a5e455cb322ba26b6d33b4b413fcdedfc632e67941dc", - "sha256:cf9c261958a769a3bd38c3e133801ebcd284ffb734ea12d01457cb09eacf7d7b", - "sha256:dd4d670eee9610bf61c25c940e9ade2d0ed05eb44227275cce88701fee014b1f", - "sha256:e19736af56947addedce4674c0971e5dceef1b5ec7d667fe86bcd2b07f8f9075", - "sha256:eaea21d150fb26d7b4856766e7addcf929119dd19fc832b22e71d942835201ef", - "sha256:eaff8156016487c1af5ffa5304c3e3fd183edcb412f3e9c72db349faf3f6e0eb", - "sha256:ee0a36edd332ed2c5208565ae6e3a7afc0eabb53f5327e281f2ef03a6bc7687a", - "sha256:ef7beb2a3582eb7a9f37beaf38a28acfd801988cde688760aea9e6cc4832b10b" - ], - "index": "pypi", - "version": "==0.950" - }, - "mypy-extensions": { - "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" - ], - "version": "==0.4.3" - }, - "packaging": { - "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" - ], - "markers": "python_version >= '3.6'", - "version": "==21.3" - }, - "parso": { - "hashes": [ - "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0", - "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75" - ], - "markers": "python_version >= '3.6'", - "version": "==0.8.3" - }, - "pluggy": { - "hashes": [ - "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" - ], - "markers": "python_version >= '3.6'", - "version": "==1.0.0" - }, - "prompt-toolkit": { - "hashes": [ - "sha256:62291dad495e665fca0bda814e342c69952086afb0f4094d0893d357e5c78752", - "sha256:bd640f60e8cecd74f0dc249713d433ace2ddc62b65ee07f96d358e0b152b6ea7" - ], - "markers": "python_full_version >= '3.6.2'", - "version": "==3.0.29" - }, - "ptpython": { - "hashes": [ - "sha256:99636899ab0e4d026e2ecc9368269114f387b4bb5411e57f072b0bde724d9f99", - "sha256:eafd4ced27ca5dc370881d4358d1ab5041b32d88d31af8e3c24167fe4af64ed6" - ], - "index": "pypi", - "version": "==3.0.20" - }, - "py": { - "hashes": [ - "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.11.0" - }, - "pygments": { - "hashes": [ - "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb", - "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519" - ], - "markers": "python_version >= '3.6'", - "version": "==2.12.0" - }, - "pyparsing": { - "hashes": [ - "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954", - "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06" - ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.8" - }, - "pytest": { - "hashes": [ - "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c", - "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45" - ], - "markers": "python_version >= '3.7'", - "version": "==7.1.2" - }, - "pytest-django": { - "hashes": [ - "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e", - "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2" - ], - "index": "pypi", - "version": "==4.5.2" - }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version < '3.11'", - "version": "==2.0.1" - }, - "types-bleach": { - "hashes": [ - "sha256:6fcb75ee4b69190fe60340147b66442cecddaefe3c0629433a4240da1ec2dcf6", - "sha256:e1498c512a62117496cf82be3d129972bb89fd1d6482b001cdeb2759ab3c82f5" - ], - "index": "pypi", - "version": "==5.0.2" - }, - "types-markdown": { - "hashes": [ - "sha256:10841332581bd79efdb153fb8f856818ef5cdb40a6e71e5d18505b5db6eba01c", - "sha256:49a406c12be2181346a756086f326d42d5bb3abbfaede8942ccdc9cef367db1f" - ], - "index": "pypi", - "version": "==3.3.14" - }, - "types-python-slugify": { - "hashes": [ - "sha256:a9d44edf034d5ceab8442ce602f976b7880ee935baeff7443964aed93f7e21d4", - "sha256:d0564feb7e21bd9ab646e692ae93eb9bf1b36e9b9bf7bf38d52eab43d29de2f6" - ], - "index": "pypi", - "version": "==5.0.4" - }, - "types-pyyaml": { - "hashes": [ - "sha256:59480cf44595d836aaae050f35e3c39f197f3a833679ef3978d97aa9f2fb7def", - "sha256:7b273a34f32af9910cf9405728c9d2ad3afc4be63e4048091a1a73d76681fe67" - ], - "index": "pypi", - "version": "==6.0.7" - }, - "types-requests": { - "hashes": [ - "sha256:5501ec6bcc164c54a6598e7ee6581827ea0ac0472e9d33b9456d202892f8d94c", - "sha256:e1cde99e92d5fb7afa0ee53924b211f4c47639516434d86dc84d53ec84fcfa8a" - ], - "index": "pypi", - "version": "==2.27.24" - }, - "types-urllib3": { - "hashes": [ - "sha256:40f8fb5e8cd7d57e8aefdee3fdd5e930aa1a1bb4179cdadd55226cea588af790", - "sha256:ff7500641824f881b2c7bde4cc57e6c3abf03d1e005bae83aca752e77213a5da" - ], - "version": "==1.26.13" - }, - "typing-extensions": { - "hashes": [ - "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", - "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" - ], - "markers": "python_version >= '3.7'", - "version": "==4.2.0" - }, - "watchdog": { - "hashes": [ - "sha256:03b43d583df0f18782a0431b6e9e9965c5b3f7cf8ec36a00b930def67942c385", - "sha256:0908bb50f6f7de54d5d31ec3da1654cb7287c6b87bce371954561e6de379d690", - "sha256:0b4a1fe6201c6e5a1926f5767b8664b45f0fcb429b62564a41f490ff1ce1dc7a", - "sha256:177bae28ca723bc00846466016d34f8c1d6a621383b6caca86745918d55c7383", - "sha256:19b36d436578eb437e029c6b838e732ed08054956366f6dd11875434a62d2b99", - "sha256:1d1cf7dfd747dec519486a98ef16097e6c480934ef115b16f18adb341df747a4", - "sha256:1e877c70245424b06c41ac258023ea4bd0c8e4ff15d7c1368f17cd0ae6e351dd", - "sha256:340b875aecf4b0e6672076a6f05cfce6686935559bb6d34cebedee04126a9566", - "sha256:351e09b6d9374d5bcb947e6ac47a608ec25b9d70583e9db00b2fcdb97b00b572", - "sha256:3fd47815353be9c44eebc94cc28fe26b2b0c5bd889dafc4a5a7cbdf924143480", - "sha256:49639865e3db4be032a96695c98ac09eed39bbb43fe876bb217da8f8101689a6", - "sha256:4d0e98ac2e8dd803a56f4e10438b33a2d40390a72750cff4939b4b274e7906fa", - "sha256:6e6ae29b72977f2e1ee3d0b760d7ee47896cb53e831cbeede3e64485e5633cc8", - "sha256:7f14ce6adea2af1bba495acdde0e510aecaeb13b33f7bd2f6324e551b26688ca", - "sha256:81982c7884aac75017a6ecc72f1a4fedbae04181a8665a34afce9539fc1b3fab", - "sha256:81a5861d0158a7e55fe149335fb2bbfa6f48cbcbd149b52dbe2cd9a544034bbd", - "sha256:ae934e34c11aa8296c18f70bf66ed60e9870fcdb4cc19129a04ca83ab23e7055", - "sha256:b26e13e8008dcaea6a909e91d39b629a39635d1a8a7239dd35327c74f4388601", - "sha256:b3750ee5399e6e9c69eae8b125092b871ee9e2fcbd657a92747aea28f9056a5c", - "sha256:b61acffaf5cd5d664af555c0850f9747cc5f2baf71e54bbac164c58398d6ca7b", - "sha256:b9777664848160449e5b4260e0b7bc1ae0f6f4992a8b285db4ec1ef119ffa0e2", - "sha256:bdcbf75580bf4b960fb659bbccd00123d83119619195f42d721e002c1621602f", - "sha256:d802d65262a560278cf1a65ef7cae4e2bc7ecfe19e5451349e4c67e23c9dc420", - "sha256:ed6d9aad09a2a948572224663ab00f8975fae242aa540509737bb4507133fa2d" - ], - "index": "pypi", - "version": "==2.1.7" - }, - "wcwidth": { - "hashes": [ - "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", - "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" - ], - "version": "==0.2.5" - }, - "werkzeug": { - "hashes": [ - "sha256:1ce08e8093ed67d638d63879fd1ba3735817f7a80de3674d293f5984f25fb6e6", - "sha256:72a4b735692dd3135217911cbeaa1be5fa3f62bffb8745c5215420a03dc55255" - ], - "index": "pypi", - "version": "==2.1.2" - } - } -} diff --git a/entries/admin.py b/entries/admin.py index dac64fb..e42b7f0 100644 --- a/entries/admin.py +++ b/entries/admin.py @@ -8,12 +8,10 @@ class SyndicationInline(admin.TabularInline): class EntryAdmin(admin.ModelAdmin): - date_hierarchy = 'created' - list_display = ('title', 'id', 'kind', 'created') - list_filter = ('kind',) - inlines = ( - SyndicationInline, - ) + date_hierarchy = "created" + list_display = ("title", "id", "kind", "created") + list_filter = ("kind",) + inlines = (SyndicationInline,) admin.site.register(Cat) diff --git a/entries/apps.py b/entries/apps.py index 554d2a6..f34a177 100644 --- a/entries/apps.py +++ b/entries/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class EntriesConfig(AppConfig): - name = 'entries' + name = "entries" diff --git a/entries/from_url.py b/entries/from_url.py index d842b83..64cf56f 100644 --- a/entries/from_url.py +++ b/entries/from_url.py @@ -11,24 +11,24 @@ from .models import Entry def from_url(url: str) -> Entry: domain = Site.objects.get_current().domain if not url: - raise error.bad_req('url parameter required') - if '//' not in url: - url = '//' + url - parts = urlparse(url, scheme='https') - if parts.scheme not in ('http', 'https') or parts.netloc != domain: - raise error.bad_req('url does not point to this site') + raise error.bad_req("url parameter required") + if "//" not in url: + url = "//" + url + parts = urlparse(url, scheme="https") + if parts.scheme not in ("http", "https") or parts.netloc != domain: + raise error.bad_req("url does not point to this site") try: match = resolve(parts.path) except Resolver404: - raise error.bad_req('url does not point to a valid page on this site') + raise error.bad_req("url does not point to a valid page on this site") - if match.view_name != 'entries:entry': - raise error.bad_req('url does not point to an entry on this site') + if match.view_name != "entries:entry": + raise error.bad_req("url does not point to an entry on this site") try: - entry = Entry.objects.get(pk=match.kwargs['id']) + entry = Entry.objects.get(pk=match.kwargs["id"]) except Entry.DoesNotExist: - raise error.bad_req('url does not point to an existing entry') + raise error.bad_req("url does not point to an existing entry") return entry diff --git a/entries/jobs.py b/entries/jobs.py index 68a873c..eb4db30 100644 --- a/entries/jobs.py +++ b/entries/jobs.py @@ -7,16 +7,19 @@ from ronkyuu import webmention @job def ping_hub(*urls): for url in urls: - requests.post(settings.PUSH_HUB, data={ - 'hub.mode': 'publish', - 'hub.url': url, - }) + requests.post( + settings.PUSH_HUB, + data={ + "hub.mode": "publish", + "hub.url": url, + }, + ) @job def send_mentions(source, targets=None): if targets is None: - targets = webmention.findMentions(source)['refs'] + targets = webmention.findMentions(source)["refs"] for target in targets: status, endpoint = webmention.discoverEndpoint(target) if endpoint is not None and status == 200: diff --git a/entries/kinds.py b/entries/kinds.py index 4e25466..6378eeb 100644 --- a/entries/kinds.py +++ b/entries/kinds.py @@ -14,62 +14,62 @@ class Entry: return self.index_page() def index_page(self, page=0): - kwargs = {'kind': self} + kwargs = {"kind": self} if page > 1: - kwargs['page'] = page - return reverse('entries:index', kwargs=kwargs) + kwargs["page"] = page + return reverse("entries:index", kwargs=kwargs) @property def entry(self): - return self.plural + '_entry' + return self.plural + "_entry" @property def atom(self): - return reverse('entries:atom_by_kind', kwargs={'kind': self}) + return reverse("entries:atom_by_kind", kwargs={"kind": self}) @property def rss(self): - return reverse('entries:rss_by_kind', kwargs={'kind': self}) + return reverse("entries:rss_by_kind", kwargs={"kind": self}) Note = Entry( - id='note', - icon='fas fa-paper-plane', - plural='notes', + id="note", + icon="fas fa-paper-plane", + plural="notes", ) Article = Entry( - id='article', - icon='fas fa-file-alt', - plural='articles', + id="article", + icon="fas fa-file-alt", + plural="articles", slug=True, ) Photo = Entry( - id='photo', - icon='fas fa-camera', - plural='photos', + id="photo", + icon="fas fa-camera", + plural="photos", ) Reply = Entry( - id='reply', - icon='fas fa-comment', - plural='replies', + id="reply", + icon="fas fa-comment", + plural="replies", on_home=False, ) Like = Entry( - id='like', - icon='fas fa-heart', - plural='likes', + id="like", + icon="fas fa-heart", + plural="likes", on_home=False, ) Repost = Entry( - id='repost', - icon='fas fa-retweet', - plural='reposts', + id="repost", + icon="fas fa-retweet", + plural="reposts", ) all = (Note, Article, Photo) @@ -79,7 +79,7 @@ from_plural = {k.plural: k for k in all} class EntryKindConverter: - regex = '|'.join(k.plural for k in all) + regex = "|".join(k.plural for k in all) def to_python(self, plural): return from_plural[plural] diff --git a/entries/migrations/0001_initial.py b/entries/migrations/0001_initial.py index 1228631..0a230a4 100644 --- a/entries/migrations/0001_initial.py +++ b/entries/migrations/0001_initial.py @@ -8,7 +8,6 @@ import django.db.models.deletion class Migration(migrations.Migration): - initial = True dependencies = [ @@ -17,20 +16,41 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Entry', + name="Entry", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('kind', models.CharField(choices=[('note', 'Note'), ('article', 'Article')], default='note', max_length=30)), - ('name', models.CharField(blank=True, max_length=100)), - ('summary', models.TextField(blank=True)), - ('content', models.TextField()), - ('published', models.DateTimeField()), - ('updated', models.DateTimeField()), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "kind", + models.CharField( + choices=[("note", "Note"), ("article", "Article")], + default="note", + max_length=30, + ), + ), + ("name", models.CharField(blank=True, max_length=100)), + ("summary", models.TextField(blank=True)), + ("content", models.TextField()), + ("published", models.DateTimeField()), + ("updated", models.DateTimeField()), + ( + "author", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'verbose_name_plural': 'entries', - 'ordering': ['-published'], + "verbose_name_plural": "entries", + "ordering": ["-published"], }, ), ] diff --git a/entries/migrations/0002_syndication.py b/entries/migrations/0002_syndication.py index 415ceee..a91a9fd 100644 --- a/entries/migrations/0002_syndication.py +++ b/entries/migrations/0002_syndication.py @@ -7,23 +7,42 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('users', '0005_auto_20171023_0158'), - ('entries', '0001_initial'), + ("users", "0005_auto_20171023_0158"), + ("entries", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Syndication', + name="Syndication", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('url', models.CharField(max_length=255)), - ('entry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='syndications', to='entries.Entry')), - ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.Profile')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("url", models.CharField(max_length=255)), + ( + "entry", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="syndications", + to="entries.Entry", + ), + ), + ( + "profile", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="users.Profile" + ), + ), ], options={ - 'ordering': ['profile'], + "ordering": ["profile"], }, ), ] diff --git a/entries/migrations/0003_remove_entry_summary.py b/entries/migrations/0003_remove_entry_summary.py index aac7f18..d405062 100644 --- a/entries/migrations/0003_remove_entry_summary.py +++ b/entries/migrations/0003_remove_entry_summary.py @@ -6,14 +6,13 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('entries', '0002_syndication'), + ("entries", "0002_syndication"), ] operations = [ migrations.RemoveField( - model_name='entry', - name='summary', + model_name="entry", + name="summary", ), ] diff --git a/entries/migrations/0004_auto_20171027_0846.py b/entries/migrations/0004_auto_20171027_0846.py index f276c63..a267757 100644 --- a/entries/migrations/0004_auto_20171027_0846.py +++ b/entries/migrations/0004_auto_20171027_0846.py @@ -8,20 +8,28 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('entries', '0003_remove_entry_summary'), + ("entries", "0003_remove_entry_summary"), ] operations = [ migrations.AlterField( - model_name='entry', - name='author', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='entries', to=settings.AUTH_USER_MODEL), + model_name="entry", + name="author", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="entries", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( - model_name='entry', - name='kind', - field=models.CharField(choices=[('note', 'note'), ('article', 'article')], db_index=True, default='note', max_length=30), + model_name="entry", + name="kind", + field=models.CharField( + choices=[("note", "note"), ("article", "article")], + db_index=True, + default="note", + max_length=30, + ), ), ] diff --git a/entries/migrations/0005_auto_20171027_1557.py b/entries/migrations/0005_auto_20171027_1557.py index 85ec86b..2b90132 100644 --- a/entries/migrations/0005_auto_20171027_1557.py +++ b/entries/migrations/0005_auto_20171027_1557.py @@ -6,20 +6,24 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('entries', '0004_auto_20171027_0846'), + ("entries", "0004_auto_20171027_0846"), ] operations = [ migrations.AddField( - model_name='entry', - name='photo', - field=models.ImageField(blank=True, upload_to=''), + model_name="entry", + name="photo", + field=models.ImageField(blank=True, upload_to=""), ), migrations.AlterField( - model_name='entry', - name='kind', - field=models.CharField(choices=[('note', 'note'), ('article', 'article'), ('photo', 'photo')], db_index=True, default='note', max_length=30), + model_name="entry", + name="kind", + field=models.CharField( + choices=[("note", "note"), ("article", "article"), ("photo", "photo")], + db_index=True, + default="note", + max_length=30, + ), ), ] diff --git a/entries/migrations/0006_auto_20171102_1200.py b/entries/migrations/0006_auto_20171102_1200.py index af62ba0..c3f735e 100644 --- a/entries/migrations/0006_auto_20171102_1200.py +++ b/entries/migrations/0006_auto_20171102_1200.py @@ -8,34 +8,41 @@ import model_utils.fields class Migration(migrations.Migration): - dependencies = [ - ('entries', '0005_auto_20171027_1557'), + ("entries", "0005_auto_20171027_1557"), ] operations = [ migrations.AlterModelOptions( - name='entry', - options={'ordering': ['-created'], 'verbose_name_plural': 'entries'}, + name="entry", + options={"ordering": ["-created"], "verbose_name_plural": "entries"}, ), migrations.RenameField( - model_name='entry', - old_name='published', - new_name='created', + model_name="entry", + old_name="published", + new_name="created", ), migrations.RenameField( - model_name='entry', - old_name='updated', - new_name='modified', + model_name="entry", + old_name="updated", + new_name="modified", ), migrations.AlterField( - model_name='entry', - name='created', - field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'), + model_name="entry", + name="created", + field=model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), ), migrations.AlterField( - model_name='entry', - name='modified', - field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'), + model_name="entry", + name="modified", + field=model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), ), ] diff --git a/entries/migrations/0007_auto_20171113_0841.py b/entries/migrations/0007_auto_20171113_0841.py index 40fc4f4..d3e8a39 100644 --- a/entries/migrations/0007_auto_20171113_0841.py +++ b/entries/migrations/0007_auto_20171113_0841.py @@ -6,20 +6,31 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('entries', '0006_auto_20171102_1200'), + ("entries", "0006_auto_20171102_1200"), ] operations = [ migrations.AddField( - model_name='entry', - name='cite', + model_name="entry", + name="cite", field=models.CharField(blank=True, max_length=255), ), migrations.AlterField( - model_name='entry', - name='kind', - field=models.CharField(choices=[('note', 'note'), ('article', 'article'), ('photo', 'photo'), ('reply', 'reply'), ('like', 'like'), ('repost', 'repost')], db_index=True, default='note', max_length=30), + model_name="entry", + name="kind", + field=models.CharField( + choices=[ + ("note", "note"), + ("article", "article"), + ("photo", "photo"), + ("reply", "reply"), + ("like", "like"), + ("repost", "repost"), + ], + db_index=True, + default="note", + max_length=30, + ), ), ] diff --git a/entries/migrations/0008_auto_20171116_2116.py b/entries/migrations/0008_auto_20171116_2116.py index c062844..c7aee1b 100644 --- a/entries/migrations/0008_auto_20171116_2116.py +++ b/entries/migrations/0008_auto_20171116_2116.py @@ -6,25 +6,24 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('entries', '0007_auto_20171113_0841'), + ("entries", "0007_auto_20171113_0841"), ] operations = [ migrations.RenameField( - model_name='entry', - old_name='cite', - new_name='in_reply_to', + model_name="entry", + old_name="cite", + new_name="in_reply_to", ), migrations.AddField( - model_name='entry', - name='like_of', + model_name="entry", + name="like_of", field=models.CharField(blank=True, max_length=255), ), migrations.AddField( - model_name='entry', - name='repost_of', + model_name="entry", + name="repost_of", field=models.CharField(blank=True, max_length=255), ), ] diff --git a/entries/migrations/0009_tag.py b/entries/migrations/0009_tag.py index aa61053..48ddc01 100644 --- a/entries/migrations/0009_tag.py +++ b/entries/migrations/0009_tag.py @@ -6,21 +6,28 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('entries', '0008_auto_20171116_2116'), + ("entries", "0008_auto_20171116_2116"), ] operations = [ migrations.CreateModel( - name='Tag', + name="Tag", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255, unique=True)), - ('slug', models.CharField(max_length=255, unique=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255, unique=True)), + ("slug", models.CharField(max_length=255, unique=True)), ], options={ - 'ordering': ('name',), + "ordering": ("name",), }, ), ] diff --git a/entries/migrations/0010_entry_tags.py b/entries/migrations/0010_entry_tags.py index bd36f61..f657ca3 100644 --- a/entries/migrations/0010_entry_tags.py +++ b/entries/migrations/0010_entry_tags.py @@ -6,15 +6,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('entries', '0009_tag'), + ("entries", "0009_tag"), ] operations = [ migrations.AddField( - model_name='entry', - name='tags', - field=models.ManyToManyField(related_name='entries', to='entries.Tag'), + model_name="entry", + name="tags", + field=models.ManyToManyField(related_name="entries", to="entries.Tag"), ), ] diff --git a/entries/migrations/0011_auto_20171120_1108.py b/entries/migrations/0011_auto_20171120_1108.py index 296cacb..36acd85 100644 --- a/entries/migrations/0011_auto_20171120_1108.py +++ b/entries/migrations/0011_auto_20171120_1108.py @@ -9,17 +9,17 @@ class Migration(migrations.Migration): atomic = False dependencies = [ - ('entries', '0010_entry_tags'), + ("entries", "0010_entry_tags"), ] operations = [ migrations.RenameModel( - old_name='Tag', - new_name='Cat', + old_name="Tag", + new_name="Cat", ), migrations.RenameField( - model_name='entry', - old_name='tags', - new_name='cats', + model_name="entry", + old_name="tags", + new_name="cats", ), ] diff --git a/entries/migrations/0012_auto_20180628_2044.py b/entries/migrations/0012_auto_20180628_2044.py index 8769528..dabc60b 100644 --- a/entries/migrations/0012_auto_20180628_2044.py +++ b/entries/migrations/0012_auto_20180628_2044.py @@ -5,25 +5,25 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('entries', '0011_auto_20171120_1108'), + ("entries", "0011_auto_20171120_1108"), ] operations = [ migrations.AlterModelOptions( - name='syndication', - options={'ordering': ['domain']}, + name="syndication", + options={"ordering": ["domain"]}, ), migrations.RemoveField( - model_name='syndication', - name='profile', + model_name="syndication", + name="profile", ), migrations.AddField( - model_name='syndication', - name='domain', + model_name="syndication", + name="domain", field=computed_property.fields.ComputedCharField( - compute_from='calc_domain', default='', editable=False, max_length=255), + compute_from="calc_domain", default="", editable=False, max_length=255 + ), preserve_default=False, ), ] diff --git a/entries/migrations/0013_alter_entry_kind.py b/entries/migrations/0013_alter_entry_kind.py index 6d254e1..a50cf00 100644 --- a/entries/migrations/0013_alter_entry_kind.py +++ b/entries/migrations/0013_alter_entry_kind.py @@ -4,24 +4,19 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('entries', '0012_auto_20180628_2044'), + ("entries", "0012_auto_20180628_2044"), ] operations = [ migrations.AlterField( - model_name='entry', - name='kind', + model_name="entry", + name="kind", field=models.CharField( - choices=[ - ('note', 'note'), - ('article', 'article'), - ('photo', 'photo') - ], + choices=[("note", "note"), ("article", "article"), ("photo", "photo")], db_index=True, - default='note', - max_length=30 + default="note", + max_length=30, ), ), ] diff --git a/entries/models.py b/entries/models.py index e7419c9..e52469b 100644 --- a/entries/models.py +++ b/entries/models.py @@ -17,6 +17,7 @@ from users.models import Site from . import kinds from lemoncurry import requests, utils + ENTRY_KINDS = [(k.id, k.id) for k in kinds.all] @@ -32,38 +33,33 @@ class Cat(models.Model): slug = models.CharField(max_length=255, unique=True) def __str__(self): - return '#' + self.name + return "#" + self.name @property def url(self): - return reverse('entries:cat', args=(self.slug,)) + return reverse("entries:cat", args=(self.slug,)) class Meta: - ordering = ('name',) + ordering = ("name",) class EntryManager(models.Manager): def get_queryset(self): qs = super(EntryManager, self).get_queryset() - return (qs - .select_related('author') - .prefetch_related('cats', 'syndications')) + return qs.select_related("author").prefetch_related("cats", "syndications") class Entry(ModelMeta, TimeStampedModel): objects = EntryManager() kind = models.CharField( - max_length=30, - choices=ENTRY_KINDS, - db_index=True, - default=ENTRY_KINDS[0][0] + max_length=30, choices=ENTRY_KINDS, db_index=True, default=ENTRY_KINDS[0][0] ) name = models.CharField(max_length=100, blank=True) photo = models.ImageField(blank=True) content = models.TextField() - cats = models.ManyToManyField(Cat, related_name='entries') + cats = models.ManyToManyField(Cat, related_name="entries") in_reply_to = models.CharField(max_length=255, blank=True) like_of = models.CharField(max_length=255, blank=True) @@ -71,7 +67,7 @@ class Entry(ModelMeta, TimeStampedModel): author = models.ForeignKey( get_user_model(), - related_name='entries', + related_name="entries", on_delete=models.CASCADE, ) @@ -79,10 +75,7 @@ class Entry(ModelMeta, TimeStampedModel): def reply_context(self): if not self.in_reply_to: return None - return interpret( - requests.mf2(self.in_reply_to).to_dict(), - self.in_reply_to - ) + return interpret(requests.mf2(self.in_reply_to).to_dict(), self.in_reply_to) @property def published(self): @@ -93,35 +86,29 @@ class Entry(ModelMeta, TimeStampedModel): return self.modified _metadata = { - 'description': 'excerpt', - 'image': 'image_url', - 'twitter_creator': 'twitter_creator', - 'og_profile_id': 'og_profile_id', + "description": "excerpt", + "image": "image_url", + "twitter_creator": "twitter_creator", + "og_profile_id": "og_profile_id", } @property def title(self): if self.name: return self.name - return shorten( - utils.to_plain(self.paragraphs[0]), - width=100, - placeholder='…' - ) + return shorten(utils.to_plain(self.paragraphs[0]), width=100, placeholder="…") @property def excerpt(self): try: return utils.to_plain(self.paragraphs[0 if self.name else 1]) except IndexError: - return ' ' + return " " @property def paragraphs(self): lines = self.content.splitlines() - return [ - "\n".join(para) for k, para in groupby(lines, key=bool) if k - ] + return ["\n".join(para) for k, para in groupby(lines, key=bool) if k] @property def twitter_creator(self): @@ -136,31 +123,31 @@ class Entry(ModelMeta, TimeStampedModel): return self.photo.url if self.photo else self.author.avatar_url def __str__(self): - return '{0} {1}: {2}'.format(self.kind, self.id, self.title) + return "{0} {1}: {2}".format(self.kind, self.id, self.title) def get_absolute_url(self): return self.absolute_url @property def absolute_url(self): - base = 'https://' + DjangoSite.objects.get_current().domain + base = "https://" + DjangoSite.objects.get_current().domain return urljoin(base, self.url) @property def affected_urls(self): - base = 'https://' + DjangoSite.objects.get_current().domain + base = "https://" + DjangoSite.objects.get_current().domain kind = kinds.from_id[self.kind] urls = { self.url, - reverse('entries:index', kwargs={'kind': kind}), - reverse('entries:atom_by_kind', kwargs={'kind': kind}), - reverse('entries:rss_by_kind', kwargs={'kind': kind}), + reverse("entries:index", kwargs={"kind": kind}), + reverse("entries:atom_by_kind", kwargs={"kind": kind}), + reverse("entries:rss_by_kind", kwargs={"kind": kind}), } | {cat.url for cat in self.cats.all()} if kind.on_home: urls |= { - reverse('home:index'), - reverse('entries:atom'), - reverse('entries:rss') + reverse("home:index"), + reverse("entries:atom"), + reverse("entries:rss"), } return {urljoin(base, u) for u in urls} @@ -170,7 +157,7 @@ class Entry(ModelMeta, TimeStampedModel): args = [kind, self.id] if kind.slug: args.append(self.slug) - return reverse('entries:entry', args=args) + return reverse("entries:entry", args=args) @property def short_url(self): @@ -182,49 +169,48 @@ class Entry(ModelMeta, TimeStampedModel): @property def json_ld(self): - base = 'https://' + DjangoSite.objects.get_current().domain + base = "https://" + DjangoSite.objects.get_current().domain url = urljoin(base, self.url) posting = { - '@context': 'http://schema.org', - '@type': 'BlogPosting', - '@id': url, - 'url': url, - 'mainEntityOfPage': url, - 'author': { - '@type': 'Person', - 'url': urljoin(base, self.author.url), - 'name': self.author.name, + "@context": "http://schema.org", + "@type": "BlogPosting", + "@id": url, + "url": url, + "mainEntityOfPage": url, + "author": { + "@type": "Person", + "url": urljoin(base, self.author.url), + "name": self.author.name, }, - 'headline': self.title, - 'description': self.excerpt, - 'datePublished': self.created.isoformat(), - 'dateModified': self.modified.isoformat(), + "headline": self.title, + "description": self.excerpt, + "datePublished": self.created.isoformat(), + "dateModified": self.modified.isoformat(), } if self.photo: - posting['image'] = (urljoin(base, self.photo.url), ) + posting["image"] = (urljoin(base, self.photo.url),) return posting class Meta: - verbose_name_plural = 'entries' - ordering = ['-created'] + verbose_name_plural = "entries" + ordering = ["-created"] class Syndication(models.Model): entry = models.ForeignKey( - Entry, - related_name='syndications', - on_delete=models.CASCADE + Entry, related_name="syndications", on_delete=models.CASCADE ) url = models.CharField(max_length=255) domain = ComputedCharField( - compute_from='calc_domain', max_length=255, + compute_from="calc_domain", + max_length=255, ) def calc_domain(self): domain = urlparse(self.url).netloc - if domain.startswith('www.'): + if domain.startswith("www."): domain = domain[4:] return domain @@ -234,7 +220,7 @@ class Syndication(models.Model): try: return Site.objects.get(domain=d) except Site.DoesNotExist: - return Site(name=d, domain=d, icon='fas fa-newspaper') + return Site(name=d, domain=d, icon="fas fa-newspaper") class Meta: - ordering = ['domain'] + ordering = ["domain"] diff --git a/entries/tests/views/feeds.py b/entries/tests/views/feeds.py index 21f6cab..6dd80e3 100644 --- a/entries/tests/views/feeds.py +++ b/entries/tests/views/feeds.py @@ -3,27 +3,27 @@ import pytest @pytest.mark.django_db def test_atom(client): - res = client.get('/atom') + res = client.get("/atom") assert res.status_code == 200 - assert res['Content-Type'] == 'application/atom+xml; charset=utf-8' + assert res["Content-Type"] == "application/atom+xml; charset=utf-8" @pytest.mark.django_db def test_rss(client): - res = client.get('/rss') + res = client.get("/rss") assert res.status_code == 200 - assert res['Content-Type'] == 'application/rss+xml; charset=utf-8' + assert res["Content-Type"] == "application/rss+xml; charset=utf-8" @pytest.mark.django_db def test_atom_by_kind(client): - res = client.get('/notes/atom') + res = client.get("/notes/atom") assert res.status_code == 200 - assert res['Content-Type'] == 'application/atom+xml; charset=utf-8' + assert res["Content-Type"] == "application/atom+xml; charset=utf-8" @pytest.mark.django_db def test_rss_by_kind(client): - res = client.get('/notes/rss') + res = client.get("/notes/rss") assert res.status_code == 200 - assert res['Content-Type'] == 'application/rss+xml; charset=utf-8' + assert res["Content-Type"] == "application/rss+xml; charset=utf-8" diff --git a/entries/urls.py b/entries/urls.py index f40bceb..d015c29 100644 --- a/entries/urls.py +++ b/entries/urls.py @@ -3,47 +3,46 @@ from . import kinds from .views import feeds, lists, perma from lemoncurry import breadcrumbs as crumbs -register_converter(kinds.EntryKindConverter, 'kind') +register_converter(kinds.EntryKindConverter, "kind") def to_pat(*args): - return '^{0}$'.format(''.join(args)) + return "^{0}$".format("".join(args)) def prefix(route): - return app_name + ':' + route + return app_name + ":" + route -id = r'/(?P\d+)' -kind = r'(?P{0})'.format('|'.join(k.plural for k in kinds.all)) -page = r'(?:/page/(?P\d+))?' -slug = r'/(?P[^/]+)' +id = r"/(?P\d+)" +kind = r"(?P{0})".format("|".join(k.plural for k in kinds.all)) +page = r"(?:/page/(?P\d+))?" +slug = r"/(?P[^/]+)" -slug_opt = '(?:' + slug + ')?' +slug_opt = "(?:" + slug + ")?" -app_name = 'entries' +app_name = "entries" urlpatterns = ( - path('atom', feeds.AtomHomeEntries(), name='atom'), - path('rss', feeds.RssHomeEntries(), name='rss'), - path('cats/', lists.by_cat, name='cat'), - path('cats//page/', lists.by_cat, name='cat'), - path('', lists.by_kind, name='index'), - path('/page/', lists.by_kind, name='index'), - path('/atom', feeds.AtomByKind(), name='atom_by_kind'), - path('/rss', feeds.RssByKind(), name='rss_by_kind'), - - path('/', perma.entry, name='entry'), - path('//', perma.entry, name='entry'), + path("atom", feeds.AtomHomeEntries(), name="atom"), + path("rss", feeds.RssHomeEntries(), name="rss"), + path("cats/", lists.by_cat, name="cat"), + path("cats//page/", lists.by_cat, name="cat"), + path("", lists.by_kind, name="index"), + path("/page/", lists.by_kind, name="index"), + path("/atom", feeds.AtomByKind(), name="atom_by_kind"), + path("/rss", feeds.RssByKind(), name="rss_by_kind"), + path("/", perma.entry, name="entry"), + path("//", perma.entry, name="entry"), ) class IndexCrumb(crumbs.Crumb): def __init__(self): - super().__init__(prefix('index'), parent='home:index') + super().__init__(prefix("index"), parent="home:index") @property def kind(self): - return self.match.kwargs['kind'] + return self.match.kwargs["kind"] @property def label(self): @@ -51,9 +50,9 @@ class IndexCrumb(crumbs.Crumb): @property def url(self): - return reverse(prefix('index'), kwargs={'kind': self.kind}) + return reverse(prefix("index"), kwargs={"kind": self.kind}) -crumbs.add(prefix('cat'), parent='home:index') +crumbs.add(prefix("cat"), parent="home:index") crumbs.add(IndexCrumb()) -crumbs.add(prefix('entry'), parent=prefix('index')) +crumbs.add(prefix("entry"), parent=prefix("index")) diff --git a/entries/views/feeds.py b/entries/views/feeds.py index 2a0fac9..837239e 100644 --- a/entries/views/feeds.py +++ b/entries/views/feeds.py @@ -11,8 +11,8 @@ from ..models import Entry class Atom1FeedWithHub(Atom1Feed): def add_root_elements(self, handler): super().add_root_elements(handler) - handler.startElement('link', {'rel': 'hub', 'href': settings.PUSH_HUB}) - handler.endElement('link') + handler.startElement("link", {"rel": "hub", "href": settings.PUSH_HUB}) + handler.endElement("link") class EntriesFeed(Feed): @@ -79,7 +79,7 @@ class RssHomeEntries(EntriesFeed): return Site.objects.get_current().name def link(self): - return reverse('home:index') + return reverse("home:index") def description(self): return "content from {0}".format( diff --git a/entries/views/lists.py b/entries/views/lists.py index c177670..24a8c7e 100644 --- a/entries/views/lists.py +++ b/entries/views/lists.py @@ -5,32 +5,32 @@ from ..models import Entry, Cat from ..pagination import paginate -@render_to('entries/index.html') +@render_to("entries/index.html") def by_kind(request, kind, page=None): entries = Entry.objects.filter(kind=kind.id) entries = paginate(queryset=entries, reverse=kind.index_page, page=page) return { - 'entries': entries, - 'atom': kind.atom, - 'rss': kind.rss, - 'title': kind.plural, + "entries": entries, + "atom": kind.atom, + "rss": kind.rss, + "title": kind.plural, } -@render_to('entries/index.html') +@render_to("entries/index.html") def by_cat(request, slug, page=None): def url(page): - kwargs = {'slug': slug} + kwargs = {"slug": slug} if page > 1: - kwargs['page'] = page - return reverse('entries:cat', kwargs=kwargs) + kwargs["page"] = page + return reverse("entries:cat", kwargs=kwargs) cat = get_object_or_404(Cat, slug=slug) entries = cat.entries.all() entries = paginate(queryset=entries, reverse=url, page=page) return { - 'entries': entries, - 'title': '#' + cat.name, + "entries": entries, + "title": "#" + cat.name, } diff --git a/entries/views/perma.py b/entries/views/perma.py index bb4fbc9..10c5183 100644 --- a/entries/views/perma.py +++ b/entries/views/perma.py @@ -3,12 +3,12 @@ from django.shortcuts import redirect, get_object_or_404 from ..models import Entry -@render_to('entries/entry.html') +@render_to("entries/entry.html") def entry(request, kind, id, slug=None): entry = get_object_or_404(Entry, pk=id) if request.path != entry.url: return redirect(entry.url, permanent=True) return { - 'entry': entry, - 'title': entry.title, + "entry": entry, + "title": entry.title, } diff --git a/gunicorn.py b/gunicorn.py index e34ad90..82d6732 100644 --- a/gunicorn.py +++ b/gunicorn.py @@ -1,5 +1,5 @@ import multiprocessing -proc_name = 'lemoncurry' -worker_class = 'gevent' +proc_name = "lemoncurry" +worker_class = "gevent" workers = multiprocessing.cpu_count() * 2 + 1 diff --git a/home/sitemaps.py b/home/sitemaps.py index a3c33b0..23609be 100644 --- a/home/sitemaps.py +++ b/home/sitemaps.py @@ -3,10 +3,10 @@ from django.urls import reverse class HomeSitemap(sitemaps.Sitemap): - changefreq = 'daily' + changefreq = "daily" def items(self): - return ('home:index',) + return ("home:index",) def location(self, item): return reverse(item) diff --git a/home/urls.py b/home/urls.py index 1f14b68..f7e7e72 100644 --- a/home/urls.py +++ b/home/urls.py @@ -2,9 +2,9 @@ from django.urls import path from . import views -app_name = 'home' +app_name = "home" urlpatterns = [ - path('', views.index, name='index'), - path('page/', views.index, name='index'), - path('robots.txt', views.robots, name='robots.txt'), + path("", views.index, name="index"), + path("page/", views.index, name="index"), + path("robots.txt", views.robots, name="robots.txt"), ] diff --git a/home/views.py b/home/views.py index 056f6ef..b57dc22 100644 --- a/home/views.py +++ b/home/views.py @@ -8,34 +8,31 @@ from urllib.parse import urljoin from entries import kinds, pagination from lemoncurry import breadcrumbs, utils -breadcrumbs.add('home:index', 'home') +breadcrumbs.add("home:index", "home") -@render_to('home/index.html') +@render_to("home/index.html") def index(request, page=None): def url(page): - kwargs = {'page': page} if page != 1 else {} - return reverse('home:index', kwargs=kwargs) + kwargs = {"page": page} if page != 1 else {} + return reverse("home:index", kwargs=kwargs) user = request.user - if not hasattr(user, 'entries'): + if not hasattr(user, "entries"): user = get_object_or_404(User, pk=1) entries = user.entries.filter(kind__in=kinds.on_home) entries = pagination.paginate(queryset=entries, reverse=url, page=page) return { - 'user': user, - 'entries': entries, - 'atom': reverse('entries:atom'), - 'rss': reverse('entries:rss'), + "user": user, + "entries": entries, + "atom": reverse("entries:atom"), + "rss": reverse("entries:rss"), } def robots(request): base = utils.origin(request) - lines = ( - 'User-agent: *', - 'Sitemap: {0}'.format(urljoin(base, reverse('sitemap'))) - ) - return HttpResponse("\n".join(lines) + "\n", content_type='text/plain') + lines = ("User-agent: *", "Sitemap: {0}".format(urljoin(base, reverse("sitemap")))) + return HttpResponse("\n".join(lines) + "\n", content_type="text/plain") diff --git a/lemonauth/migrations/0001_initial.py b/lemonauth/migrations/0001_initial.py index aa17a22..e8c1aa1 100644 --- a/lemonauth/migrations/0001_initial.py +++ b/lemonauth/migrations/0001_initial.py @@ -7,25 +7,36 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True - dependencies = [ - ] # type: List[Tuple[str, str]] + dependencies = [] # type: List[Tuple[str, str]] operations = [ migrations.CreateModel( - name='IndieAuthCode', + name="IndieAuthCode", fields=[ - ('id', models.AutoField(auto_created=True, - primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=64, unique=True)), - ('me', models.CharField(max_length=255)), - ('client_id', models.CharField(max_length=255)), - ('redirect_uri', models.CharField(max_length=255)), - ('response_type', models.CharField(choices=[ - ('id', 'id'), ('code', 'code')], default='id', max_length=4)), - ('scope', models.CharField(blank=True, max_length=200)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("code", models.CharField(max_length=64, unique=True)), + ("me", models.CharField(max_length=255)), + ("client_id", models.CharField(max_length=255)), + ("redirect_uri", models.CharField(max_length=255)), + ( + "response_type", + models.CharField( + choices=[("id", "id"), ("code", "code")], + default="id", + max_length=4, + ), + ), + ("scope", models.CharField(blank=True, max_length=200)), ], ), ] diff --git a/lemonauth/migrations/0002_delete_indieauthcode.py b/lemonauth/migrations/0002_delete_indieauthcode.py index 69ad679..4bf2893 100644 --- a/lemonauth/migrations/0002_delete_indieauthcode.py +++ b/lemonauth/migrations/0002_delete_indieauthcode.py @@ -6,13 +6,12 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('lemonauth', '0001_initial'), + ("lemonauth", "0001_initial"), ] operations = [ migrations.DeleteModel( - name='IndieAuthCode', + name="IndieAuthCode", ), ] diff --git a/lemonauth/migrations/0003_indieauthcode_token.py b/lemonauth/migrations/0003_indieauthcode_token.py index d09d74c..a3e1748 100644 --- a/lemonauth/migrations/0003_indieauthcode_token.py +++ b/lemonauth/migrations/0003_indieauthcode_token.py @@ -9,43 +9,112 @@ import randomslugfield.fields class Migration(migrations.Migration): - initial = True dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('lemonauth', '0002_delete_indieauthcode'), + ("lemonauth", "0002_delete_indieauthcode"), ] operations = [ migrations.CreateModel( - name='IndieAuthCode', + name="IndieAuthCode", fields=[ - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('id', randomslugfield.fields.RandomSlugField(blank=True, editable=False, length=30, max_length=30, primary_key=True, serialize=False, unique=True)), - ('client_id', models.URLField()), - ('scope', models.TextField(blank=True)), - ('redirect_uri', models.URLField()), - ('response_type', model_utils.fields.StatusField(choices=[('id', 'id'), ('code', 'code')], default='id', max_length=100, no_check_for_status=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), + ), + ( + "id", + randomslugfield.fields.RandomSlugField( + blank=True, + editable=False, + length=30, + max_length=30, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("client_id", models.URLField()), + ("scope", models.TextField(blank=True)), + ("redirect_uri", models.URLField()), + ( + "response_type", + model_utils.fields.StatusField( + choices=[("id", "id"), ("code", "code")], + default="id", + max_length=100, + no_check_for_status=True, + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='Token', + name="Token", fields=[ - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('id', randomslugfield.fields.RandomSlugField(blank=True, editable=False, length=30, max_length=30, primary_key=True, serialize=False, unique=True)), - ('client_id', models.URLField()), - ('scope', models.TextField(blank=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), + ), + ( + "id", + randomslugfield.fields.RandomSlugField( + blank=True, + editable=False, + length=30, + max_length=30, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("client_id", models.URLField()), + ("scope", models.TextField(blank=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), ] diff --git a/lemonauth/models.py b/lemonauth/models.py index 607d3cb..7a23244 100644 --- a/lemonauth/models.py +++ b/lemonauth/models.py @@ -17,6 +17,7 @@ class AuthSecret(TimeStampedModel): authorisation codes and tokens in IndieAuth - the two contain many identical fields, but just a few differences. """ + id = RandomSlugField(primary_key=True, length=30) client_id = models.URLField() user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) @@ -27,7 +28,7 @@ class AuthSecret(TimeStampedModel): return self.user.full_url def __contains__(self, scope): - return scope in self.scope.split(' ') + return scope in self.scope.split(" ") class Meta: abstract = True @@ -41,10 +42,11 @@ class IndieAuthCode(AuthSecret): Codes are single-use, and if unused will be expired automatically after thirty seconds. """ + redirect_uri = models.URLField() - RESPONSE_TYPE = Choices('id', 'code') - response_type = StatusField(choices_name='RESPONSE_TYPE') + RESPONSE_TYPE = Choices("id", "code") + response_type = StatusField(choices_name="RESPONSE_TYPE") @property def expired(self): @@ -56,4 +58,5 @@ class Token(AuthSecret): A Token grants a client long-term authorisation - it will not expire unless explicitly revoked by the user. """ + pass diff --git a/lemonauth/tokens.py b/lemonauth/tokens.py index 5e6640e..17d0e5b 100644 --- a/lemonauth/tokens.py +++ b/lemonauth/tokens.py @@ -3,17 +3,17 @@ from .models import IndieAuthCode, Token def auth(request) -> Token: - if 'HTTP_AUTHORIZATION' in request.META: - auth = request.META.get('HTTP_AUTHORIZATION').split(' ') - if auth[0] != 'Bearer': - raise error.bad_req('auth type {0} not supported'.format(auth[0])) + if "HTTP_AUTHORIZATION" in request.META: + auth = request.META.get("HTTP_AUTHORIZATION").split(" ") + if auth[0] != "Bearer": + raise error.bad_req("auth type {0} not supported".format(auth[0])) if len(auth) != 2: - raise error.bad_req('invalid Bearer auth format, must be Bearer ') + raise error.bad_req("invalid Bearer auth format, must be Bearer ") token = auth[1] - elif 'access_token' in request.POST: - token = request.POST.get('access_token') - elif 'access_token' in request.GET: - token = request.GET.get('access_token') + elif "access_token" in request.POST: + token = request.POST.get("access_token") + elif "access_token" in request.GET: + token = request.GET.get("access_token") else: raise error.unauthorized() @@ -28,11 +28,11 @@ def auth(request) -> Token: def gen_auth_code(req): code = IndieAuthCode() code.user = req.user - code.client_id = req.POST['client_id'] - code.redirect_uri = req.POST['redirect_uri'] - code.response_type = req.POST.get('response_type', 'id') - if 'scope' in req.POST: - code.scope = ' '.join(req.POST.getlist('scope')) + code.client_id = req.POST["client_id"] + code.redirect_uri = req.POST["redirect_uri"] + code.response_type = req.POST.get("response_type", "id") + if "scope" in req.POST: + code.scope = " ".join(req.POST.getlist("scope")) code.save() return code.id diff --git a/lemonauth/urls.py b/lemonauth/urls.py index e98672a..a2126b9 100644 --- a/lemonauth/urls.py +++ b/lemonauth/urls.py @@ -1,13 +1,17 @@ from django.urls import path from . import views -app_name = 'lemonauth' +app_name = "lemonauth" urlpatterns = [ - path('login', views.login, name='login'), - path('logout', views.logout, name='logout'), - path('indie', views.IndieView.as_view(), name='indie'), - path('indie/approve', views.indie_approve, name='indie_approve'), - path('token', views.TokenView.as_view(), name='token'), - path('tokens', views.TokensListView.as_view(), name='tokens'), - path('tokens/', views.TokensRevokeView.as_view(), name='tokens_revoke'), + path("login", views.login, name="login"), + path("logout", views.logout, name="logout"), + path("indie", views.IndieView.as_view(), name="indie"), + path("indie/approve", views.indie_approve, name="indie_approve"), + path("token", views.TokenView.as_view(), name="token"), + path("tokens", views.TokensListView.as_view(), name="tokens"), + path( + "tokens/", + views.TokensRevokeView.as_view(), + name="tokens_revoke", + ), ] diff --git a/lemonauth/views/indie.py b/lemonauth/views/indie.py index dbab249..ac2ddc3 100644 --- a/lemonauth/views/indie.py +++ b/lemonauth/views/indie.py @@ -12,120 +12,114 @@ from urllib.parse import urlencode, urljoin, urlunparse, urlparse from .. import tokens from ..models import IndieAuthCode -breadcrumbs.add('lemonauth:indie', parent='home:index') +breadcrumbs.add("lemonauth:indie", parent="home:index") def canonical(url): - if '//' not in url: - url = '//' + url + if "//" not in url: + url = "//" + url (scheme, netloc, path, params, query, fragment) = urlparse(url) - if not scheme or scheme == 'http': - scheme = 'https' + if not scheme or scheme == "http": + scheme = "https" if not path: - path = '/' + path = "/" return urlunparse((scheme, netloc, path, params, query, fragment)) -@method_decorator(csrf_exempt, name='dispatch') +@method_decorator(csrf_exempt, name="dispatch") class IndieView(TemplateView): - template_name = 'lemonauth/indie.html' - required_params = ('client_id', 'redirect_uri') + template_name = "lemonauth/indie.html" + required_params = ("client_id", "redirect_uri") @method_decorator(login_required) @method_decorator(render_to(template_name)) def get(self, request): params = request.GET.dict() - params.setdefault('response_type', 'id') + params.setdefault("response_type", "id") for param in self.required_params: if param not in params: - return utils.bad_req( - 'parameter {0} is required'.format(param) - ) + return utils.bad_req("parameter {0} is required".format(param)) me = request.user.full_url - if 'me' in params: - param_me = canonical(params['me']) + if "me" in params: + param_me = canonical(params["me"]) if me != param_me: return utils.forbid( - 'you are logged in as {}, not as {}'.format(me, param_me) + "you are logged in as {}, not as {}".format(me, param_me) ) - redirect_uri = urljoin(params['client_id'], params['redirect_uri']) + redirect_uri = urljoin(params["client_id"], params["redirect_uri"]) - type = params['response_type'] - if type not in ('id', 'code'): - return utils.bad_req( - 'unknown response_type: {0}'.format(type) - ) + type = params["response_type"] + if type not in ("id", "code"): + return utils.bad_req("unknown response_type: {0}".format(type)) scopes = () - if type == 'code': - if 'scope' not in params: - return utils.bad_req( - 'scopes required for code type' - ) - scopes = params['scope'].split(' ') + if type == "code": + if "scope" not in params: + return utils.bad_req("scopes required for code type") + scopes = params["scope"].split(" ") - client = requests.mf2(params['client_id']) - rels = (client.to_dict()['rel-urls'] - .get(redirect_uri, {}) - .get('rels', ())) - verified = 'redirect_uri' in rels + client = requests.mf2(params["client_id"]) + rels = client.to_dict()["rel-urls"].get(redirect_uri, {}).get("rels", ()) + verified = "redirect_uri" in rels try: - app = client.to_dict(filter_by_type='h-x-app')[0]['properties'] + app = client.to_dict(filter_by_type="h-x-app")[0]["properties"] except IndexError: app = None return { - 'app': app, - 'me': me, - 'redirect_uri': redirect_uri, - 'verified': verified, - 'params': params, - 'scopes': scopes, - 'title': 'indieauth from {client_id}'.format(**params), + "app": app, + "me": me, + "redirect_uri": redirect_uri, + "verified": verified, + "params": params, + "scopes": scopes, + "title": "indieauth from {client_id}".format(**params), } def post(self, request): post = request.POST.dict() try: - code = IndieAuthCode.objects.get(pk=post.get('code')) + code = IndieAuthCode.objects.get(pk=post.get("code")) except IndieAuthCode.DoesNotExist: # if anything at all goes wrong when decoding the auth code, bail # out immediately. - return utils.forbid('invalid auth code') + return utils.forbid("invalid auth code") code.delete() if code.expired: - return utils.forbid('invalid auth code') + return utils.forbid("invalid auth code") - if code.response_type != 'id': - return utils.bad_req( - 'this endpoint only supports response_type=id' - ) - if code.client_id != post.get('client_id'): - return utils.forbid('client id did not match') - if code.redirect_uri != post.get('redirect_uri'): - return utils.forbid('redirect uri did not match') + if code.response_type != "id": + return utils.bad_req("this endpoint only supports response_type=id") + if code.client_id != post.get("client_id"): + return utils.forbid("client id did not match") + if code.redirect_uri != post.get("redirect_uri"): + return utils.forbid("redirect uri did not match") # If we got here, it's valid! Yay! - return utils.choose_type(request, {'me': code.me}, { - 'application/x-www-form-urlencoded': utils.form_encoded_response, - 'application/json': JsonResponse, - }) + return utils.choose_type( + request, + {"me": code.me}, + { + "application/x-www-form-urlencoded": utils.form_encoded_response, + "application/json": JsonResponse, + }, + ) @login_required @require_POST def approve(request): params = { - 'me': urljoin(utils.origin(request), request.user.url), - 'code': tokens.gen_auth_code(request), + "me": urljoin(utils.origin(request), request.user.url), + "code": tokens.gen_auth_code(request), } - if 'state' in request.POST: - params['state'] = request.POST['state'] + if "state" in request.POST: + params["state"] = request.POST["state"] - uri = request.POST['redirect_uri'] - sep = '&' if '?' in uri else '?' + uri = request.POST["redirect_uri"] + sep = "&" if "?" in uri else "?" return redirect(uri + sep + urlencode(params)) diff --git a/lemonauth/views/login.py b/lemonauth/views/login.py index 5e4b192..cffc18b 100644 --- a/lemonauth/views/login.py +++ b/lemonauth/views/login.py @@ -2,11 +2,11 @@ import django.contrib.auth.views from otp_agents.forms import OTPAuthenticationForm from lemoncurry import breadcrumbs -breadcrumbs.add(route='lemonauth:login', label='log in', parent='home:index') +breadcrumbs.add(route="lemonauth:login", label="log in", parent="home:index") login = django.contrib.auth.views.LoginView.as_view( authentication_form=OTPAuthenticationForm, - extra_context={'title': 'log in'}, - template_name='lemonauth/login.html', + extra_context={"title": "log in"}, + template_name="lemonauth/login.html", redirect_authenticated_user=True, ) diff --git a/lemonauth/views/token.py b/lemonauth/views/token.py index 251c016..3884fa1 100644 --- a/lemonauth/views/token.py +++ b/lemonauth/views/token.py @@ -7,41 +7,42 @@ from ..models import IndieAuthCode from lemoncurry import utils -@method_decorator(csrf_exempt, name='dispatch') +@method_decorator(csrf_exempt, name="dispatch") class TokenView(View): def get(self, req): token = tokens.auth(req) res = { - 'me': token.me, - 'client_id': token.client_id, - 'scope': token.scope, + "me": token.me, + "client_id": token.client_id, + "scope": token.scope, } return utils.choose_type(req, res) def post(self, req): post = req.POST try: - code = IndieAuthCode.objects.get(pk=post.get('code')) + code = IndieAuthCode.objects.get(pk=post.get("code")) except IndieAuthCode.DoesNotExist: - return utils.forbid('invalid auth code') + return utils.forbid("invalid auth code") code.delete() if code.expired: - return utils.forbid('invalid auth code') + return utils.forbid("invalid auth code") - if code.response_type != 'code': - return utils.bad_req( - 'this endpoint only supports response_type=code' - ) - if 'client_id' in post and code.client_id != post['client_id']: - return utils.forbid('client id did not match') - if code.redirect_uri != post.get('redirect_uri'): - return utils.forbid('redirect uri did not match') + if code.response_type != "code": + return utils.bad_req("this endpoint only supports response_type=code") + if "client_id" in post and code.client_id != post["client_id"]: + return utils.forbid("client id did not match") + if code.redirect_uri != post.get("redirect_uri"): + return utils.forbid("redirect uri did not match") - if 'me' in post and code.me != post['me']: - return utils.forbid('me did not match') + if "me" in post and code.me != post["me"]: + return utils.forbid("me did not match") - return utils.choose_type(req, { - 'access_token': tokens.gen_token(code), - 'me': code.me, - 'scope': code.scope, - }) + return utils.choose_type( + req, + { + "access_token": tokens.gen_token(code), + "me": code.me, + "scope": code.scope, + }, + ) diff --git a/lemonauth/views/tokens/list.py b/lemonauth/views/tokens/list.py index dba4eb3..52e0760 100644 --- a/lemonauth/views/tokens/list.py +++ b/lemonauth/views/tokens/list.py @@ -20,15 +20,15 @@ class Client: self.id = client_id self.count = 0 self.scopes = set() - apps = mf2(self.id).to_dict(filter_by_type='h-x-app') + apps = mf2(self.id).to_dict(filter_by_type="h-x-app") try: - self.app = apps[0]['properties'] + self.app = apps[0]["properties"] except IndexError: self.app = None class TokensListView(LoginRequiredMixin, TemplateView): - template_name = 'lemonauth/tokens.html' + template_name = "lemonauth/tokens.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -36,6 +36,6 @@ class TokensListView(LoginRequiredMixin, TemplateView): for token in self.request.user.token_set.all(): client = clients[token.client_id] client.count += 1 - client.scopes |= set(token.scope.split(' ')) - context.update({'clients': clients, 'title': 'tokens'}) + client.scopes |= set(token.scope.split(" ")) + context.update({"clients": clients, "title": "tokens"}) return context diff --git a/lemoncurry/breadcrumbs.py b/lemoncurry/breadcrumbs.py index bd88b35..cba1a00 100644 --- a/lemoncurry/breadcrumbs.py +++ b/lemoncurry/breadcrumbs.py @@ -14,7 +14,7 @@ class Crumb: return self._label def __eq__(self, other): - if hasattr(other, 'route'): + if hasattr(other, "route"): return self.route == other.route return self.route == other diff --git a/lemoncurry/debug.py b/lemoncurry/debug.py index d518f52..7e3420f 100644 --- a/lemoncurry/debug.py +++ b/lemoncurry/debug.py @@ -2,6 +2,6 @@ from debug_toolbar.middleware import show_toolbar as core_show_toolbar def show_toolbar(request): - if request.path.endswith('/amp'): + if request.path.endswith("/amp"): return False return core_show_toolbar(request) diff --git a/lemoncurry/jinja2/__init__.py b/lemoncurry/jinja2/__init__.py index 0182a8f..2379b89 100644 --- a/lemoncurry/jinja2/__init__.py +++ b/lemoncurry/jinja2/__init__.py @@ -22,18 +22,22 @@ def environment(**options): lstrip_blocks=True, **options ) - env.filters.update({ - 'ago': ago, - 'friendly_url': friendly_url, - 'markdown': markdown, - }) - env.globals.update({ - 'entry_kinds': entry_kinds, - 'favicons': favicons, - 'package': load_package_json(), - 'settings': settings, - 'static': staticfiles_storage.url, - 'theme_color': theme_color, - 'url': reverse, - }) + env.filters.update( + { + "ago": ago, + "friendly_url": friendly_url, + "markdown": markdown, + } + ) + env.globals.update( + { + "entry_kinds": entry_kinds, + "favicons": favicons, + "package": load_package_json(), + "settings": settings, + "static": staticfiles_storage.url, + "theme_color": theme_color, + "url": reverse, + } + ) return env diff --git a/lemoncurry/jinja2/ago.py b/lemoncurry/jinja2/ago.py index 4794b0e..c061619 100644 --- a/lemoncurry/jinja2/ago.py +++ b/lemoncurry/jinja2/ago.py @@ -6,4 +6,4 @@ def ago(dt: datetime) -> str: # We have to convert the datetime we get to local time first, because ago # just strips the timezone from a timezone-aware datetime. dt = dt.astimezone() - return human(dt, precision=1, past_tense='{}', abbreviate=True) + return human(dt, precision=1, past_tense="{}", abbreviate=True) diff --git a/lemoncurry/jinja2/bleach.py b/lemoncurry/jinja2/bleach.py index 628f3f2..401e9ed 100644 --- a/lemoncurry/jinja2/bleach.py +++ b/lemoncurry/jinja2/bleach.py @@ -3,13 +3,13 @@ from bleach.linkifier import LinkifyFilter from jinja2 import pass_eval_context from markupsafe import Markup -TAGS = ['cite', 'code', 'details', 'p', 'pre', 'img', 'span', 'summary'] +TAGS = ["cite", "code", "details", "p", "pre", "img", "span", "summary"] TAGS.extend(ALLOWED_TAGS) ATTRIBUTES = { - 'a': ['href', 'title', 'class'], - 'details': ['open'], - 'img': ['alt', 'src', 'title'], - 'span': ['class'], + "a": ["href", "title", "class"], + "details": ["open"], + "img": ["alt", "src", "title"], + "span": ["class"], } cleaner = Cleaner(tags=TAGS, attributes=ATTRIBUTES, filters=(LinkifyFilter,)) diff --git a/lemoncurry/jinja2/markdown.py b/lemoncurry/jinja2/markdown.py index 02709c4..37cbb68 100644 --- a/lemoncurry/jinja2/markdown.py +++ b/lemoncurry/jinja2/markdown.py @@ -3,12 +3,14 @@ from markdown import Markdown from .bleach import bleach -md = Markdown(extensions=( - 'extra', - 'sane_lists', - 'smarty', - 'toc', -)) +md = Markdown( + extensions=( + "extra", + "sane_lists", + "smarty", + "toc", + ) +) @pass_eval_context diff --git a/lemoncurry/middleware.py b/lemoncurry/middleware.py index 02221bc..d6d3f3e 100644 --- a/lemoncurry/middleware.py +++ b/lemoncurry/middleware.py @@ -8,7 +8,9 @@ class ResponseException(Exception): class ResponseExceptionMiddleware(MiddlewareMixin): - def process_exception(self, request: HttpRequest, exception: Exception) -> HttpResponse: + def process_exception( + self, request: HttpRequest, exception: Exception + ) -> HttpResponse: if isinstance(exception, ResponseException): return exception.response raise exception diff --git a/lemoncurry/requests.py b/lemoncurry/requests.py index f4fc5eb..a365f88 100644 --- a/lemoncurry/requests.py +++ b/lemoncurry/requests.py @@ -11,7 +11,7 @@ from mf2py import Parser class DjangoCache(BaseCache): @classmethod def key(cls, url): - return 'req:' + sha256(url.encode('utf-8')).hexdigest() + return "req:" + sha256(url.encode("utf-8")).hexdigest() def get(self, url): key = self.key(url) @@ -45,4 +45,4 @@ def get(url): def mf2(url): r = get(url) - return Parser(doc=r.text, url=url, html_parser='html5lib') + return Parser(doc=r.text, url=url, html_parser="html5lib") diff --git a/lemoncurry/settings/base.py b/lemoncurry/settings/base.py index cd3ea1a..b2c5ba5 100644 --- a/lemoncurry/settings/base.py +++ b/lemoncurry/settings/base.py @@ -16,7 +16,7 @@ from typing import List APPEND_SLASH = False ADMINS = [ - ('dani', 'dani@00dani.me'), + ("dani", "dani@00dani.me"), ] BASE_DIR = path.dirname(path.dirname(path.dirname(path.abspath(__file__)))) @@ -26,13 +26,13 @@ BASE_DIR = path.dirname(path.dirname(path.dirname(path.abspath(__file__)))) # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '6riil57g@r^wprf7mdy((+bs&(6l*phcn9&fd$l0@t-kzj+xww' +SECRET_KEY = "6riil57g@r^wprf7mdy((+bs&(6l*phcn9&fd$l0@t-kzj+xww" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] # type: List[str] -INTERNAL_IPS = ['127.0.0.1', '::1'] +ALLOWED_HOSTS: List[str] = [] +INTERNAL_IPS = ["127.0.0.1", "::1"] # Settings to tighten up security - these can safely be on in dev mode too, # since I dev using a local HTTPS server. @@ -50,7 +50,7 @@ CSRF_COOKIE_SECURE = True # Miscellanous headers to protect against attacks. SECURE_CONTENT_TYPE_NOSNIFF = True SECURE_BROWSER_XSS_FILTER = True -X_FRAME_OPTIONS = 'DENY' +X_FRAME_OPTIONS = "DENY" # This technically isn't needed, since nginx doesn't let the app be accessed # over insecure HTTP anyway. Just for completeness! @@ -58,110 +58,106 @@ SECURE_SSL_REDIRECT = True # We run behind nginx, so we need nginx to tell us whether we're using HTTPS or # not. -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # Application definition INSTALLED_APPS = [ - 'lemoncurry', - 'pyup_django', - - 'django.contrib.admin', - 'django.contrib.admindocs', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.humanize', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.sitemaps', - 'django.contrib.messages', - 'django.contrib.staticfiles', - - 'analytical', - 'annoying', - 'compressor', - 'computed_property', - 'corsheaders', - 'debug_toolbar', - 'django_activeurl', - 'django_agent_trust', - 'django_extensions', - 'django_otp', - 'django_otp.plugins.otp_static', - 'django_otp.plugins.otp_totp', - 'django_rq', - 'favicon', - 'meta', - - 'entries', - 'home', - 'lemonauth', - 'lemonshort', - 'micropub', - 'users', - 'webmention', - 'wellknowns', + "lemoncurry", + "pyup_django", + "django.contrib.admin", + "django.contrib.admindocs", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.humanize", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.sitemaps", + "django.contrib.messages", + "django.contrib.staticfiles", + "analytical", + "annoying", + "compressor", + "computed_property", + "corsheaders", + "debug_toolbar", + "django_activeurl", + "django_agent_trust", + "django_extensions", + "django_otp", + "django_otp.plugins.otp_static", + "django_otp.plugins.otp_totp", + "django_rq", + "meta", + "entries", + "home", + "lemonauth", + "lemonshort", + "micropub", + "users", + "webmention", + "wellknowns", ] MIDDLEWARE = [ - 'debug_toolbar.middleware.DebugToolbarMiddleware', - 'django.middleware.http.ConditionalGetMiddleware', - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.admindocs.middleware.XViewMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django_otp.middleware.OTPMiddleware', - 'django_agent_trust.middleware.AgentMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.contrib.sites.middleware.CurrentSiteMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'lemoncurry.middleware.ResponseExceptionMiddleware', + "debug_toolbar.middleware.DebugToolbarMiddleware", + "django.middleware.http.ConditionalGetMiddleware", + "django.middleware.security.SecurityMiddleware", + "django.contrib.admindocs.middleware.XViewMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "corsheaders.middleware.CorsMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django_otp.middleware.OTPMiddleware", + "django_agent_trust.middleware.AgentMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.contrib.sites.middleware.CurrentSiteMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "lemoncurry.middleware.ResponseExceptionMiddleware", ] -ROOT_URLCONF = 'lemoncurry.urls' +ROOT_URLCONF = "lemoncurry.urls" -SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' +SESSION_ENGINE = "django.contrib.sessions.backends.cached_db" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.jinja2.Jinja2', - 'APP_DIRS': True, - 'OPTIONS': { - 'environment': 'lemoncurry.jinja2.environment', + "BACKEND": "django.template.backends.jinja2.Jinja2", + "APP_DIRS": True, + "OPTIONS": { + "environment": "lemoncurry.jinja2.environment", }, }, { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'lemoncurry.wsgi.application' +WSGI_APPLICATION = "lemoncurry.wsgi.application" # Cache # https://docs.djangoproject.com/en/1.11/ref/settings/#std:setting-CACHES CACHES = { - 'default': { - 'BACKEND': 'django_redis.cache.RedisCache', - 'LOCATION': 'redis://127.0.0.1:6380/0', - 'KEY_PREFIX': 'lemoncurry', - 'OPTIONS': { - 'PARSER_CLASS': 'redis.connection.HiredisParser', - 'SERIALIZER': 'lemoncurry.msgpack.MSGPackModernSerializer', + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://127.0.0.1:6380/0", + "KEY_PREFIX": "lemoncurry", + "OPTIONS": { + "PARSER_CLASS": "redis.connection.HiredisParser", + "SERIALIZER": "lemoncurry.msgpack.MSGPackModernSerializer", }, - 'VERSION': 2, + "VERSION": 2, } } @@ -169,51 +165,51 @@ CACHES = { # https://docs.djangoproject.com/en/1.11/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': environ.get('POSTGRES_DB', 'lemoncurry'), - 'USER': environ.get('POSTGRES_USER'), - 'PASSWORD': environ.get('POSTGRES_PASSWORD'), - 'HOST': environ.get('POSTGRES_HOST', 'localhost'), + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": environ.get("POSTGRES_DB", "lemoncurry"), + "USER": environ.get("POSTGRES_USER"), + "PASSWORD": environ.get("POSTGRES_PASSWORD"), + "HOST": environ.get("POSTGRES_HOST", "localhost"), } } -DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" -AUTH_USER_MODEL = 'users.User' +AUTH_USER_MODEL = "users.User" # Password hashers # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators PASSWORD_HASHERS = [ - 'django.contrib.auth.hashers.Argon2PasswordHasher', - 'django.contrib.auth.hashers.PBKDF2PasswordHasher', - 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', - 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', - 'django.contrib.auth.hashers.BCryptPasswordHasher', + "django.contrib.auth.hashers.Argon2PasswordHasher", + "django.contrib.auth.hashers.PBKDF2PasswordHasher", + "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", + "django.contrib.auth.hashers.BCryptSHA256PasswordHasher", + "django.contrib.auth.hashers.BCryptPasswordHasher", ] # Password validation # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators -PW_VALIDATOR_MODULE = 'django.contrib.auth.password_validation' +PW_VALIDATOR_MODULE = "django.contrib.auth.password_validation" AUTH_PASSWORD_VALIDATORS = [ - {'NAME': PW_VALIDATOR_MODULE + '.UserAttributeSimilarityValidator'}, - {'NAME': PW_VALIDATOR_MODULE + '.MinimumLengthValidator'}, - {'NAME': PW_VALIDATOR_MODULE + '.CommonPasswordValidator'}, - {'NAME': PW_VALIDATOR_MODULE + '.NumericPasswordValidator'}, + {"NAME": PW_VALIDATOR_MODULE + ".UserAttributeSimilarityValidator"}, + {"NAME": PW_VALIDATOR_MODULE + ".MinimumLengthValidator"}, + {"NAME": PW_VALIDATOR_MODULE + ".CommonPasswordValidator"}, + {"NAME": PW_VALIDATOR_MODULE + ".NumericPasswordValidator"}, ] -LOGIN_URL = 'lemonauth:login' -LOGIN_REDIRECT_URL = 'home:index' +LOGIN_URL = "lemonauth:login" +LOGIN_REDIRECT_URL = "home:index" LOGOUT_REDIRECT_URL = LOGIN_REDIRECT_URL # Internationalization # https://docs.djangoproject.com/en/1.11/topics/i18n/ -LANGUAGE_CODE = 'en-au' +LANGUAGE_CODE = "en-au" -TIME_ZONE = 'Australia/Sydney' +TIME_ZONE = "Australia/Sydney" USE_I18N = True @@ -225,21 +221,21 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.11/howto/static-files/ -STATIC_URL = '/static/' -STATIC_ROOT = path.join(BASE_DIR, 'static') +STATIC_URL = "/static/" +STATIC_ROOT = path.join(BASE_DIR, "static") STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - 'compressor.finders.CompressorFinder', + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", + "compressor.finders.CompressorFinder", ) -STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' +STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage" COMPRESS_PRECOMPILERS = ( - ('text/stylus', 'npx stylus -u ./lemoncurry/static/lemoncurry/css/theme'), + ("text/stylus", "npx stylus -u ./lemoncurry/static/lemoncurry/css/theme"), ) -MEDIA_URL = STATIC_URL + 'media/' -MEDIA_ROOT = path.join(STATIC_ROOT, 'media') +MEDIA_URL = STATIC_URL + "media/" +MEDIA_ROOT = path.join(STATIC_ROOT, "media") # django-contrib-sites # https://docs.djangoproject.com/en/dev/ref/contrib/sites/ @@ -251,28 +247,25 @@ AGENT_COOKIE_SECURE = True # django-cors-headers CORS_ORIGIN_ALLOW_ALL = True -CORS_URLS_REGEX = r'^/(?!admin|auth/(?:login|logout|indie)).*$' +CORS_URLS_REGEX = r"^/(?!admin|auth/(?:login|logout|indie)).*$" # lemonshort -SHORT_BASE_URL = '/s/' +SHORT_BASE_URL = "/s/" SHORTEN_MODELS = { - 'e': 'entries.entry', + "e": "entries.entry", } # django-meta # https://django-meta.readthedocs.io/en/latest/settings.html -META_SITE_PROTOCOL = 'https' +META_SITE_PROTOCOL = "https" META_USE_SITES = True META_USE_OG_PROPERTIES = True META_USE_TWITTER_PROPERTIES = True # django-push # https://django-push.readthedocs.io/en/latest/publisher.html -PUSH_HUB = 'https://00dani.superfeedr.com/' +PUSH_HUB = "https://00dani.superfeedr.com/" # django-rq # https://github.com/ui/django-rq -RQ_QUEUES = {'default': {'USE_REDIS_CACHE': 'default'}} - -# django-super-favicon -FAVICON_STORAGE = 'django.core.files.storage.DefaultStorage' +RQ_QUEUES = {"default": {"USE_REDIS_CACHE": "default"}} diff --git a/lemoncurry/settings/dev.py b/lemoncurry/settings/dev.py index 999387d..1c4edc1 100644 --- a/lemoncurry/settings/dev.py +++ b/lemoncurry/settings/dev.py @@ -1,7 +1,7 @@ from .base import * -ALLOWED_HOSTS = ['*'] -META_SITE_DOMAIN = '00dani.lo' -META_FB_APPID = '142105433189339' -STATIC_URL = 'https://static.00dani.lo/' -MEDIA_URL = 'https://media.00dani.lo/' +ALLOWED_HOSTS = ["*"] +META_SITE_DOMAIN = "00dani.lo" +META_FB_APPID = "142105433189339" +STATIC_URL = "https://static.00dani.lo/" +MEDIA_URL = "https://media.00dani.lo/" diff --git a/lemoncurry/settings/prod.py b/lemoncurry/settings/prod.py index 31f7c84..a4407ca 100644 --- a/lemoncurry/settings/prod.py +++ b/lemoncurry/settings/prod.py @@ -4,19 +4,19 @@ from os.path import join from .base import * from .base import BASE_DIR, DATABASES -ALLOWED_HOSTS = ['00dani.me'] +ALLOWED_HOSTS = ["00dani.me"] DEBUG = False -SECRET_KEY = environ['DJANGO_SECRET_KEY'] -SERVER_EMAIL = 'lemoncurry@00dani.me' +SECRET_KEY = environ["DJANGO_SECRET_KEY"] +SERVER_EMAIL = "lemoncurry@00dani.me" # Authenticate as an app-specific Postgres user in production. -DATABASES['default']['USER'] = 'lemoncurry' +DATABASES["default"]["USER"] = "lemoncurry" -SHORT_BASE_URL = 'https://nya.as/' +SHORT_BASE_URL = "https://nya.as/" -STATIC_ROOT = join(BASE_DIR, '..', 'static') -MEDIA_ROOT = join(BASE_DIR, '..', 'media') -STATIC_URL = 'https://cdn.00dani.me/' -MEDIA_URL = STATIC_URL + 'm/' -META_SITE_DOMAIN = '00dani.me' -META_FB_APPID = '145311792869199' +STATIC_ROOT = join(BASE_DIR, "..", "static") +MEDIA_ROOT = join(BASE_DIR, "..", "media") +STATIC_URL = "https://cdn.00dani.me/" +MEDIA_URL = STATIC_URL + "m/" +META_SITE_DOMAIN = "00dani.me" +META_FB_APPID = "145311792869199" diff --git a/lemoncurry/settings/test.py b/lemoncurry/settings/test.py index c3eb440..c8dc698 100644 --- a/lemoncurry/settings/test.py +++ b/lemoncurry/settings/test.py @@ -1,8 +1,8 @@ from .base import * -ALLOWED_HOSTS = ['*'] +ALLOWED_HOSTS = ["*"] SECURE_SSL_REDIRECT = False -STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage' +STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage" -MEDIA_URL = '/media/' -STATIC_ROOT = path.join(BASE_DIR, 'media') +MEDIA_URL = "/media/" +STATIC_ROOT = path.join(BASE_DIR, "media") diff --git a/lemoncurry/templatetags/absolute_url.py b/lemoncurry/templatetags/absolute_url.py index 592079b..9724e3c 100644 --- a/lemoncurry/templatetags/absolute_url.py +++ b/lemoncurry/templatetags/absolute_url.py @@ -8,5 +8,5 @@ register = template.Library() @register.simple_tag @register.filter(is_safe=True) def absolute_url(url): - base = 'https://' + Site.objects.get_current().domain + base = "https://" + Site.objects.get_current().domain return urljoin(base, url) diff --git a/lemoncurry/templatetags/bleach.py b/lemoncurry/templatetags/bleach.py index 7b9ffc3..cc410b4 100644 --- a/lemoncurry/templatetags/bleach.py +++ b/lemoncurry/templatetags/bleach.py @@ -5,13 +5,13 @@ from django.utils.safestring import mark_safe from bleach.sanitizer import Cleaner, ALLOWED_TAGS from bleach.linkifier import LinkifyFilter -tags = ['cite', 'code', 'details', 'p', 'pre', 'img', 'span', 'summary'] +tags = ["cite", "code", "details", "p", "pre", "img", "span", "summary"] tags.extend(ALLOWED_TAGS) attributes = { - 'a': ['href', 'title', 'class'], - 'details': ['open'], - 'img': ['alt', 'src', 'title'], - 'span': ['class'], + "a": ["href", "title", "class"], + "details": ["open"], + "img": ["alt", "src", "title"], + "span": ["class"], } register = template.Library() diff --git a/lemoncurry/templatetags/jsonify.py b/lemoncurry/templatetags/jsonify.py index 377ebb7..50864a1 100644 --- a/lemoncurry/templatetags/jsonify.py +++ b/lemoncurry/templatetags/jsonify.py @@ -11,5 +11,5 @@ register = template.Library() @register.filter def jsonify(value): if isinstance(value, QuerySet): - return mark_safe(serialize('json', value)) + return mark_safe(serialize("json", value)) return mark_safe(json.dumps(value, cls=DjangoJSONEncoder)) diff --git a/lemoncurry/templatetags/lemoncurry_tags.py b/lemoncurry/templatetags/lemoncurry_tags.py index 0968f43..bd865f0 100644 --- a/lemoncurry/templatetags/lemoncurry_tags.py +++ b/lemoncurry/templatetags/lemoncurry_tags.py @@ -1,4 +1,3 @@ - from django import template from django.conf import settings from django.contrib.sites.models import Site @@ -40,67 +39,71 @@ def site_name(): return Site.objects.get_current().name -@register.inclusion_tag('lemoncurry/tags/nav.html') +@register.inclusion_tag("lemoncurry/tags/nav.html") def nav_left(request): - items = (MenuItem( - label=k.plural, - icon=k.icon, - url=('entries:index', (k,)) - ) for k in kinds.all) - return {'items': items, 'request': request} + items = ( + MenuItem(label=k.plural, icon=k.icon, url=("entries:index", (k,))) + for k in kinds.all + ) + return {"items": items, "request": request} -@register.inclusion_tag('lemoncurry/tags/nav.html') +@register.inclusion_tag("lemoncurry/tags/nav.html") def nav_right(request): if request.user.is_authenticated: items = ( - MenuItem(label='admin', icon='fas fa-cog', url='admin:index'), - MenuItem(label='log out', icon='fas fa-sign-out-alt', - url='lemonauth:logout'), + MenuItem(label="admin", icon="fas fa-cog", url="admin:index"), + MenuItem( + label="log out", icon="fas fa-sign-out-alt", url="lemonauth:logout" + ), ) else: items = ( - MenuItem(label='log in', icon='fas fa-sign-in-alt', - url='lemonauth:login'), + MenuItem(label="log in", icon="fas fa-sign-in-alt", url="lemonauth:login"), ) - return {'items': items, 'request': request} + return {"items": items, "request": request} -@register.inclusion_tag('lemoncurry/tags/breadcrumbs.html', takes_context=True) +@register.inclusion_tag("lemoncurry/tags/breadcrumbs.html", takes_context=True) def nav_crumbs(context, route): crumbs = breadcrumbs.find(route) current = crumbs.pop() - item_list_element = [{ - '@type': 'ListItem', - 'position': i + 1, - 'item': { - '@id': context['origin'] + crumb.url, - '@type': 'WebPage', - 'name': crumb.label + item_list_element = [ + { + "@type": "ListItem", + "position": i + 1, + "item": { + "@id": context["origin"] + crumb.url, + "@type": "WebPage", + "name": crumb.label, + }, } - } for i, crumb in enumerate(crumbs)] - item_list_element.append({ - '@type': 'ListItem', - 'position': len(item_list_element) + 1, - 'item': { - 'id': context['uri'], - '@type': 'WebPage', - 'name': current.label or context.get('title'), + for i, crumb in enumerate(crumbs) + ] + item_list_element.append( + { + "@type": "ListItem", + "position": len(item_list_element) + 1, + "item": { + "id": context["uri"], + "@type": "WebPage", + "name": current.label or context.get("title"), + }, } - }) + ) breadcrumb_list = { - '@context': 'http://schema.org', - '@type': 'BreadcrumbList', - 'itemListElement': item_list_element + "@context": "http://schema.org", + "@type": "BreadcrumbList", + "itemListElement": item_list_element, } return { - 'breadcrumb_list': breadcrumb_list, - 'crumbs': crumbs, - 'current': current, - 'title': context.get('title'), + "breadcrumb_list": breadcrumb_list, + "crumbs": crumbs, + "current": current, + "title": context.get("title"), } diff --git a/lemoncurry/templatetags/markdown.py b/lemoncurry/templatetags/markdown.py index 1505987..3452a1f 100644 --- a/lemoncurry/templatetags/markdown.py +++ b/lemoncurry/templatetags/markdown.py @@ -3,12 +3,14 @@ from django import template from markdown import Markdown from .bleach import bleach -md = Markdown(extensions=( - 'extra', - 'sane_lists', - 'smarty', - 'toc', -)) +md = Markdown( + extensions=( + "extra", + "sane_lists", + "smarty", + "toc", + ) +) register = template.Library() diff --git a/lemoncurry/tests/breadcrumbs.py b/lemoncurry/tests/breadcrumbs.py index 28d8e14..8372ba9 100644 --- a/lemoncurry/tests/breadcrumbs.py +++ b/lemoncurry/tests/breadcrumbs.py @@ -6,43 +6,43 @@ from .. import breadcrumbs as b @pytest.fixture def nested_crumbs(): - x = b.Crumb('nc.x', label='x') - y = b.Crumb('nc.y', label='y', parent='nc.x') - z = b.Crumb('nc.z', label='z', parent='nc.y') + x = b.Crumb("nc.x", label="x") + y = b.Crumb("nc.y", label="y", parent="nc.x") + z = b.Crumb("nc.z", label="z", parent="nc.y") crumbs = (x, y, z) for crumb in crumbs: b.breadcrumbs[crumb.route] = crumb - yield namedtuple('NestedCrumbs', 'x y z')(*crumbs) + yield namedtuple("NestedCrumbs", "x y z")(*crumbs) for crumb in crumbs: del b.breadcrumbs[crumb.route] @pytest.fixture def crumb_match(nested_crumbs): - return namedtuple('Match', 'view_name')(nested_crumbs.z.route) + return namedtuple("Match", "view_name")(nested_crumbs.z.route) class TestAdd: def test_inserts_a_breadcrumb_without_parent(self): - route = 'tests.add.insert' + route = "tests.add.insert" assert route not in b.breadcrumbs - b.add(route, 'some label') + b.add(route, "some label") assert route in b.breadcrumbs assert b.breadcrumbs[route] == route route = b.breadcrumbs[route] - assert route.label == 'some label' + assert route.label == "some label" assert route.parent is None def test_inserts_a_breadcrumb_with_parent(self): - route = 'tests.add.with_parent' - parent = 'tests.add.insert' + route = "tests.add.with_parent" + parent = "tests.add.insert" assert route not in b.breadcrumbs - b.add(route, 'child label', parent) + b.add(route, "child label", parent) assert route in b.breadcrumbs assert b.breadcrumbs[route] == route route = b.breadcrumbs[route] - assert route.label == 'child label' + assert route.label == "child label" assert route.parent == parent diff --git a/lemoncurry/tests/utils.py b/lemoncurry/tests/utils.py index 5745087..550a49a 100644 --- a/lemoncurry/tests/utils.py +++ b/lemoncurry/tests/utils.py @@ -5,22 +5,22 @@ from .. import utils class TestOrigin: def test_simple_http(self): """should return the correct origin for a vanilla HTTP site""" - req = Mock(scheme='http', site=Mock(domain='lemoncurry.test')) - assert utils.origin(req) == 'http://lemoncurry.test' + req = Mock(scheme="http", site=Mock(domain="lemoncurry.test")) + assert utils.origin(req) == "http://lemoncurry.test" def test_simple_https(self): """should return the correct origin for a vanilla HTTPS site""" - req = Mock(scheme='https', site=Mock(domain='secure.lemoncurry.test')) - assert utils.origin(req) == 'https://secure.lemoncurry.test' + req = Mock(scheme="https", site=Mock(domain="secure.lemoncurry.test")) + assert utils.origin(req) == "https://secure.lemoncurry.test" class TestUri: def test_siteroot(self): """should return correct full URI for requests to the site root""" - req = Mock(scheme='https', path='/', site=Mock(domain='l.test')) - assert utils.uri(req) == 'https://l.test/' + req = Mock(scheme="https", path="/", site=Mock(domain="l.test")) + assert utils.uri(req) == "https://l.test/" def test_path(self): """should return correct full URI for requests with a path""" - req = Mock(scheme='https', path='/notes/23', site=Mock(domain='l.tst')) - assert utils.uri(req) == 'https://l.tst/notes/23' + req = Mock(scheme="https", path="/notes/23", site=Mock(domain="l.tst")) + assert utils.uri(req) == "https://l.tst/notes/23" diff --git a/lemoncurry/theme.py b/lemoncurry/theme.py index b282e13..437dc6e 100644 --- a/lemoncurry/theme.py +++ b/lemoncurry/theme.py @@ -4,12 +4,14 @@ from yaml import safe_load path = join( settings.BASE_DIR, - 'lemoncurry', 'static', - 'base16-materialtheme-scheme', 'material-darker.yaml', + "lemoncurry", + "static", + "base16-materialtheme-scheme", + "material-darker.yaml", ) -with open(path, 'r') as f: +with open(path, "r") as f: theme = safe_load(f) def color(i): - return '#' + theme['base0' + format(i, '1X')] + return "#" + theme["base0" + format(i, "1X")] diff --git a/lemoncurry/urls.py b/lemoncurry/urls.py index 0548105..e2d72bb 100644 --- a/lemoncurry/urls.py +++ b/lemoncurry/urls.py @@ -27,33 +27,37 @@ from entries.sitemaps import EntriesSitemap from home.sitemaps import HomeSitemap sections = { - 'entries': EntriesSitemap, - 'home': HomeSitemap, + "entries": EntriesSitemap, + "home": HomeSitemap, } -maps = {'sitemaps': sections} +maps = {"sitemaps": sections} urlpatterns = ( - path('', include('home.urls')), - path('', include('entries.urls')), - path('', include('users.urls')), - path('.well-known/', include('wellknowns.urls')), - path('admin/doc/', include('django.contrib.admindocs.urls')), - path('admin/', admin.site.urls), - path('auth/', include('lemonauth.urls')), - path('favicon.ico', RedirectView.as_view( - url=settings.MEDIA_URL + 'favicon/favicon.ico')), - path('micropub', include('micropub.urls')), - path('s/', include('lemonshort.urls')), - path('webmention', include('webmention.urls')), - - path('django-rq/', include('django_rq.urls')), - path('sitemap.xml', sitemap.index, maps, name='sitemap'), - path('sitemaps/
.xml', sitemap.sitemap, maps, - name='django.contrib.sitemaps.views.sitemap'), + path("", include("home.urls")), + path("", include("entries.urls")), + path("", include("users.urls")), + path(".well-known/", include("wellknowns.urls")), + path("admin/doc/", include("django.contrib.admindocs.urls")), + path("admin/", admin.site.urls), + path("auth/", include("lemonauth.urls")), + path( + "favicon.ico", + RedirectView.as_view(url=settings.MEDIA_URL + "favicon/favicon.ico"), + ), + path("micropub", include("micropub.urls")), + path("s/", include("lemonshort.urls")), + path("webmention", include("webmention.urls")), + path("django-rq/", include("django_rq.urls")), + path("sitemap.xml", sitemap.index, maps, name="sitemap"), + path( + "sitemaps/
.xml", + sitemap.sitemap, + maps, + name="django.contrib.sitemaps.views.sitemap", + ), ) # type: Tuple[URLPattern, ...] if settings.DEBUG: import debug_toolbar - urlpatterns += ( - path('__debug__/', include(debug_toolbar.urls)), - ) + + urlpatterns += (path("__debug__/", include(debug_toolbar.urls)),) diff --git a/lemoncurry/utils.py b/lemoncurry/utils.py index 1ac70d2..b4b2434 100644 --- a/lemoncurry/utils.py +++ b/lemoncurry/utils.py @@ -20,7 +20,7 @@ class PackageJson: def load(self) -> Dict[str, Any]: if self.data is None: - with open(join(settings.BASE_DIR, 'package.json')) as f: + with open(join(settings.BASE_DIR, "package.json")) as f: self.data = json.load(f) assert self.data is not None return self.data @@ -30,10 +30,10 @@ PACKAGE = PackageJson() def friendly_url(url): - if '//' not in url: - url = '//' + url + if "//" not in url: + url = "//" + url (scheme, netloc, path, params, q, fragment) = urlparse(url) - if path == '/': + if path == "/": return netloc return "{}\u200B{}".format(netloc, path) @@ -43,7 +43,7 @@ def load_package_json() -> Dict[str, Any]: def origin(request): - return '{0}://{1}'.format(request.scheme, request.site.domain) + return "{0}://{1}".format(request.scheme, request.site.domain) def absolute_url(request, url): @@ -56,19 +56,18 @@ def uri(request): def form_encoded_response(content): return HttpResponse( - urlencode(content), - content_type='application/x-www-form-urlencoded' + urlencode(content), content_type="application/x-www-form-urlencoded" ) REPS = { - 'application/x-www-form-urlencoded': form_encoded_response, - 'application/json': JsonResponse, + "application/x-www-form-urlencoded": form_encoded_response, + "application/json": JsonResponse, } def choose_type(request, content, reps=REPS): - accept = request.META.get('HTTP_ACCEPT', '*/*') + accept = request.META.get("HTTP_ACCEPT", "*/*") type = get_best_match(accept, reps.keys()) if type: return reps[type](content) @@ -76,11 +75,11 @@ def choose_type(request, content, reps=REPS): def bad_req(message): - return HttpResponseBadRequest(message, content_type='text/plain') + return HttpResponseBadRequest(message, content_type="text/plain") def forbid(message): - return HttpResponseForbidden(message, content_type='text/plain') + return HttpResponseForbidden(message, content_type="text/plain") def to_plain(md): diff --git a/lemonshort/apps.py b/lemonshort/apps.py index 4edb115..259f20a 100644 --- a/lemonshort/apps.py +++ b/lemonshort/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class LemonshortConfig(AppConfig): - name = 'lemonshort' + name = "lemonshort" diff --git a/lemonshort/convert.py b/lemonshort/convert.py index 936df44..d408610 100644 --- a/lemonshort/convert.py +++ b/lemonshort/convert.py @@ -6,8 +6,9 @@ from string import ascii_lowercase, ascii_uppercase chars = ascii_uppercase + ascii_lowercase conv = BaseConverter(chars) + class AbcIdConverter: - regex = '[a-zA-Z]+' + regex = "[a-zA-Z]+" def to_python(self, value: str) -> int: return int(conv.decode(value)) diff --git a/lemonshort/short_url.py b/lemonshort/short_url.py index 76d7c62..d66461b 100644 --- a/lemonshort/short_url.py +++ b/lemonshort/short_url.py @@ -11,7 +11,7 @@ def short_url(entity): if not prefixes: for k, m in settings.SHORTEN_MODELS.items(): prefixes[apps.get_model(m)] = k - base = '/' - if hasattr(settings, 'SHORT_BASE_URL'): + base = "/" + if hasattr(settings, "SHORT_BASE_URL"): base = settings.SHORT_BASE_URL return base + prefixes[type(entity)] + AbcIdConverter().to_url(entity.id) diff --git a/lemonshort/tests/convert.py b/lemonshort/tests/convert.py index 80d6299..033aa15 100644 --- a/lemonshort/tests/convert.py +++ b/lemonshort/tests/convert.py @@ -3,14 +3,14 @@ from .. import convert def test_to_python(): samples = { - 'A': 0, - 'B': 1, - 'Y': 24, - 'a': 26, - 'b': 27, - 'y': 50, - 'BA': 52, - 'BAB': 2705, + "A": 0, + "B": 1, + "Y": 24, + "a": 26, + "b": 27, + "y": 50, + "BA": 52, + "BAB": 2705, } converter = convert.AbcIdConverter() for abc, id in samples.items(): @@ -19,13 +19,13 @@ def test_to_python(): def test_id_to_abc(): samples = { - 1: 'B', - 24: 'Y', - 26: 'a', - 52: 'BA', - 78: 'Ba', - 104: 'CA', - 130: 'Ca', + 1: "B", + 24: "Y", + 26: "a", + 52: "BA", + 78: "Ba", + 104: "CA", + 130: "Ca", } converter = convert.AbcIdConverter() for id, abc in samples.items(): diff --git a/lemonshort/urls.py b/lemonshort/urls.py index 7fcf49c..385a256 100644 --- a/lemonshort/urls.py +++ b/lemonshort/urls.py @@ -4,10 +4,10 @@ from django.urls import path, register_converter from .convert import AbcIdConverter from .views import unshort -register_converter(AbcIdConverter, 'abc_id') +register_converter(AbcIdConverter, "abc_id") -app_name = 'lemonshort' +app_name = "lemonshort" urlpatterns = tuple( - path('{0!s}'.format(k), unshort, name=m, kwargs={'model': m}) + path("{0!s}".format(k), unshort, name=m, kwargs={"model": m}) for k, m in settings.SHORTEN_MODELS.items() ) diff --git a/micropub/apps.py b/micropub/apps.py index d5259ee..8732a39 100644 --- a/micropub/apps.py +++ b/micropub/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class MicropubConfig(AppConfig): - name = 'micropub' + name = "micropub" diff --git a/micropub/error.py b/micropub/error.py index a5e9373..20a745d 100644 --- a/micropub/error.py +++ b/micropub/error.py @@ -4,33 +4,35 @@ from typing import Optional def forbidden() -> ResponseException: - return res('forbidden', 403) + return res("forbidden", 403) def unauthorized() -> ResponseException: - return res('unauthorized', 401) + return res("unauthorized", 401) def bad_req(msg: str) -> ResponseException: - return res('invalid_request', msg=msg) + return res("invalid_request", msg=msg) def bad_type(type: str) -> ResponseException: - msg = 'unsupported request type {0}'.format(type) - return res('invalid_request', 415, msg) + msg = "unsupported request type {0}".format(type) + return res("invalid_request", 415, msg) def bad_scope(scope: str) -> ResponseException: - return res('insufficient_scope', 401, scope=scope) + return res("insufficient_scope", 401, scope=scope) -def res(error: str, - status: Optional[int]=400, - msg: Optional[str]=None, - scope: Optional[str]=None): - content = {'error': error} +def res( + error: str, + status: Optional[int] = 400, + msg: Optional[str] = None, + scope: Optional[str] = None, +): + content = {"error": error} if msg is not None: - content['error_description'] = msg + content["error_description"] = msg if scope: - content['scope'] = scope + content["scope"] = scope return ResponseException(JsonResponse(content, status=status)) diff --git a/micropub/urls.py b/micropub/urls.py index 546d82f..06acd70 100644 --- a/micropub/urls.py +++ b/micropub/urls.py @@ -2,8 +2,8 @@ from django.urls import path from .views import micropub from .views.media import media -app_name = 'micropub' +app_name = "micropub" urlpatterns = ( - path('', micropub, name='micropub'), - path('/media', media, name='media'), + path("", micropub, name="micropub"), + path("/media", media, name="media"), ) diff --git a/micropub/views/__init__.py b/micropub/views/__init__.py index 7ff7878..96d99e9 100644 --- a/micropub/views/__init__.py +++ b/micropub/views/__init__.py @@ -10,22 +10,22 @@ from .delete import delete from .query import query actions = { - 'create': create, - 'delete': delete, + "create": create, + "delete": delete, } @csrf_exempt -@require_http_methods(['GET', 'HEAD', 'POST']) +@require_http_methods(["GET", "HEAD", "POST"]) def micropub(request): request.token = tokens.auth(request) - if request.method in ('GET', 'HEAD'): + if request.method in ("GET", "HEAD"): return query(request) - action = request.POST.get('action', 'create') - if request.content_type == 'application/json': + action = request.POST.get("action", "create") + if request.content_type == "application/json": request.json = json.load(request) - action = request.json.get('action', 'create') + action = request.json.get("action", "create") if action not in actions: - raise error.bad_req('unknown action: {}'.format(action)) + raise error.bad_req("unknown action: {}".format(action)) return actions[action](request) diff --git a/micropub/views/create.py b/micropub/views/create.py index 23bd21e..35e1357 100644 --- a/micropub/views/create.py +++ b/micropub/views/create.py @@ -14,63 +14,62 @@ def form_to_mf2(request): properties = {} post = request.POST for key in post.keys(): - if key.endswith('[]'): + if key.endswith("[]"): key = key[:-2] - if key == 'access_token': + if key == "access_token": continue - properties[key] = post.getlist(key) + post.getlist(key + '[]') + properties[key] = post.getlist(key) + post.getlist(key + "[]") type = [] - if 'h' in properties: - type = ['h-' + p for p in properties['h']] - del properties['h'] - return {'type': type, 'properties': properties} + if "h" in properties: + type = ["h-" + p for p in properties["h"]] + del properties["h"] + return {"type": type, "properties": properties} def create(request): normalise = { - 'application/json': lambda r: r.json, - 'application/x-www-form-urlencoded': form_to_mf2, + "application/json": lambda r: r.json, + "application/x-www-form-urlencoded": form_to_mf2, } - if 'create' not in request.token: - raise error.bad_scope('create') + if "create" not in request.token: + raise error.bad_scope("create") if request.content_type not in normalise: raise error.unsupported_type(request.content_type) body = normalise[request.content_type](request) - if 'type' not in body: - raise error.bad_req('mf2 object type required') - if body['type'] != ['h-entry']: - raise error.bad_req('only h-entry supported') + if "type" not in body: + raise error.bad_req("mf2 object type required") + if body["type"] != ["h-entry"]: + raise error.bad_req("only h-entry supported") entry = Entry(author=request.token.user) - props = body.get('properties', {}) + props = body.get("properties", {}) kind = Note - if 'name' in props: - entry.name = '\n'.join(props['name']) + if "name" in props: + entry.name = "\n".join(props["name"]) kind = Article - if 'content' in props: - entry.content = '\n'.join( - c if isinstance(c, str) else c['html'] - for c in props['content'] + if "content" in props: + entry.content = "\n".join( + c if isinstance(c, str) else c["html"] for c in props["content"] ) - if 'in-reply-to' in props: - entry.in_reply_to = props['in-reply-to'] + if "in-reply-to" in props: + entry.in_reply_to = props["in-reply-to"] kind = Reply - if 'like-of' in props: - entry.like_of = props['like-of'] + if "like-of" in props: + entry.like_of = props["like-of"] kind = Like - if 'repost-of' in props: - entry.repost_of = props['repost-of'] + if "repost-of" in props: + entry.repost_of = props["repost-of"] kind = Repost - cats = [Cat.objects.from_name(c) for c in props.get('category', [])] + cats = [Cat.objects.from_name(c) for c in props.get("category", [])] entry.kind = kind.id entry.save() entry.cats.set(cats) entry.save() - for url in props.get('syndication', []): + for url in props.get("syndication", []): entry.syndications.create(url=url) base = utils.origin(request) @@ -80,6 +79,6 @@ def create(request): send_mentions.delay(perma) res = HttpResponse(status=201) - res['Location'] = perma - res['Link'] = '<{}>; rel="shortlink"'.format(short) + res["Location"] = perma + res["Link"] = '<{}>; rel="shortlink"'.format(short) return res diff --git a/micropub/views/delete.py b/micropub/views/delete.py index 035da7e..db6e13e 100644 --- a/micropub/views/delete.py +++ b/micropub/views/delete.py @@ -6,24 +6,25 @@ from entries.jobs import ping_hub, send_mentions from .. import error + def delete(request): normalise = { - 'application/json': lambda r: r.json.get('url'), - 'application/x-www-form-urlencoded': lambda r: r.POST.get('url'), + "application/json": lambda r: r.json.get("url"), + "application/x-www-form-urlencoded": lambda r: r.POST.get("url"), } - if 'delete' not in request.token: - raise error.bad_scope('delete') + if "delete" not in request.token: + raise error.bad_scope("delete") if request.content_type not in normalise: raise error.unsupported_type(request.content_type) url = normalise[request.content_type](request) entry = from_url(url) if entry.author != request.token.user: - raise error.forbid('entry belongs to another user') + raise error.forbid("entry belongs to another user") perma = entry.absolute_url pings = entry.affected_urls - mentions = webmention.findMentions(perma)['refs'] + mentions = webmention.findMentions(perma)["refs"] entry.delete() diff --git a/micropub/views/media.py b/micropub/views/media.py index db59972..6e77b93 100644 --- a/micropub/views/media.py +++ b/micropub/views/media.py @@ -11,9 +11,9 @@ from lemoncurry.utils import absolute_url from .. import error ACCEPTED_MEDIA_TYPES = ( - 'image/gif', - 'image/jpeg', - 'image/png', + "image/gif", + "image/jpeg", + "image/png", ) @@ -21,15 +21,13 @@ ACCEPTED_MEDIA_TYPES = ( @require_POST def media(request): token = tokens.auth(request) - if 'file' not in request.FILES: + if "file" not in request.FILES: raise error.bad_req( "a file named 'file' must be provided to the media endpoint" ) - file = request.FILES['file'] + file = request.FILES["file"] if file.content_type not in ACCEPTED_MEDIA_TYPES: - raise error.bad_req( - 'unacceptable file type {0}'.format(file.content_type) - ) + raise error.bad_req("unacceptable file type {0}".format(file.content_type)) mime = None sha = hashlib.sha256() @@ -40,14 +38,15 @@ def media(request): if mime != file.content_type: raise error.bad_req( - 'detected file type {0} did not match specified file type {1}' - .format(mime, file.content_type) + "detected file type {0} did not match specified file type {1}".format( + mime, file.content_type + ) ) - path = 'mp/{0[0]}/{2}.{1}'.format(*mime.split('/'), sha.hexdigest()) + path = "mp/{0[0]}/{2}.{1}".format(*mime.split("/"), sha.hexdigest()) path = store.save(path, file) url = absolute_url(request, store.url(path)) res = HttpResponse(status=201) - res['Location'] = url + res["Location"] = url return res diff --git a/micropub/views/query.py b/micropub/views/query.py index 6b5d2d8..4fc72a2 100644 --- a/micropub/views/query.py +++ b/micropub/views/query.py @@ -7,48 +7,47 @@ from lemoncurry.utils import absolute_url from .. import error - def config(request): config = syndicate_to(request) - config['media-endpoint'] = absolute_url(request, reverse('micropub:media')) + config["media-endpoint"] = absolute_url(request, reverse("micropub:media")) return config def source(request): - if 'url' not in request.GET: - raise error.bad_req('must specify url parameter for source query') - entry = from_url(request.GET['url']) + if "url" not in request.GET: + raise error.bad_req("must specify url parameter for source query") + entry = from_url(request.GET["url"]) props = {} - keys = set(request.GET.getlist('properties') + request.GET.getlist('properties[]')) - if not keys or 'content' in keys: - props['content'] = [entry.content] - if (not keys or 'category' in keys) and entry.cats.exists(): - props['category'] = [cat.name for cat in entry.cats.all()] - if (not keys or 'name' in keys) and entry.name: - props['name'] = [entry.name] - if (not keys or 'syndication' in keys) and entry.syndications.exists(): - props['syndication'] = [synd.url for synd in entry.syndications.all()] + keys = set(request.GET.getlist("properties") + request.GET.getlist("properties[]")) + if not keys or "content" in keys: + props["content"] = [entry.content] + if (not keys or "category" in keys) and entry.cats.exists(): + props["category"] = [cat.name for cat in entry.cats.all()] + if (not keys or "name" in keys) and entry.name: + props["name"] = [entry.name] + if (not keys or "syndication" in keys) and entry.syndications.exists(): + props["syndication"] = [synd.url for synd in entry.syndications.all()] - return {'type': ['h-entry'], 'properties': props} + return {"type": ["h-entry"], "properties": props} def syndicate_to(request): - return {'syndicate-to': []} + return {"syndicate-to": []} queries = { - 'config': config, - 'source': source, - 'syndicate-to': syndicate_to, + "config": config, + "source": source, + "syndicate-to": syndicate_to, } def query(request): - if 'q' not in request.GET: - raise error.bad_req('must specify q parameter') - q = request.GET['q'] + if "q" not in request.GET: + raise error.bad_req("must specify q parameter") + q = request.GET["q"] if q not in queries: - raise error.bad_req('unsupported query {0}'.format(q)) + raise error.bad_req("unsupported query {0}".format(q)) res = queries[q](request) return JsonResponse(res) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..1d07da0 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1317 @@ +[[package]] +category = "main" +description = "Determine the best content to send in an HTTP response" +name = "accept-types" +optional = false +python-versions = "*" +version = "0.4.1" + +[[package]] +category = "main" +description = "ago: Human readable timedeltas" +name = "ago" +optional = false +python-versions = "*" +version = "0.0.95" + +[[package]] +category = "dev" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +name = "appdirs" +optional = false +python-versions = "*" +version = "1.4.4" + +[[package]] +category = "main" +description = "The secure Argon2 password hashing algorithm." +name = "argon2-cffi" +optional = false +python-versions = ">=3.6" +version = "21.3.0" + +[package.dependencies] +argon2-cffi-bindings = "*" + +[package.extras] +dev = ["pre-commit", "cogapp", "tomli", "coverage (>=5.0.2)", "hypothesis", "pytest", "sphinx", "sphinx-notfound-page", "furo"] +docs = ["sphinx", "sphinx-notfound-page", "furo"] +tests = ["coverage (>=5.0.2)", "hypothesis", "pytest"] + +[[package]] +category = "main" +description = "Low-level CFFI bindings for Argon2" +name = "argon2-cffi-bindings" +optional = false +python-versions = ">=3.6" +version = "21.2.0" + +[package.dependencies] +cffi = ">=1.0.1" + +[package.extras] +dev = ["pytest", "cogapp", "pre-commit", "wheel"] +tests = ["pytest"] + +[[package]] +category = "main" +description = "ASGI specs, helper code, and adapters" +name = "asgiref" +optional = false +python-versions = ">=3.7" +version = "3.7.2" + +[package.dependencies] +[package.dependencies.typing-extensions] +python = "<3.11" +version = ">=4" + +[package.extras] +tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] + +[[package]] +category = "main" +description = "Timeout context manager for asyncio programs" +marker = "python_full_version <= \"3.11.2\"" +name = "async-timeout" +optional = false +python-versions = ">=3.6" +version = "4.0.2" + +[[package]] +category = "main" +description = "Screen-scraping library" +name = "beautifulsoup4" +optional = false +python-versions = ">=3.6.0" +version = "4.12.2" + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +category = "main" +description = "An easy safelist-based HTML-sanitizing tool." +name = "bleach" +optional = false +python-versions = ">=3.7" +version = "6.0.0" + +[package.dependencies] +six = ">=1.9.0" +webencodings = "*" + +[package.extras] +css = ["tinycss2 (>=1.1.0,<1.2)"] + +[[package]] +category = "main" +description = "httplib2 caching for requests" +name = "cachecontrol" +optional = false +python-versions = ">=3.7" +version = "0.13.1" + +[package.dependencies] +msgpack = ">=0.5.2" +requests = ">=2.16.0" + +[package.extras] +dev = ["cachecontrol", "build", "mypy", "tox", "pytest-cov", "pytest", "cherrypy", "sphinx", "black", "types-redis", "types-requests"] +filecache = ["filelock (>=3.8.0)"] +redis = ["redis (>=2.10.5)"] + +[[package]] +category = "main" +description = "Python package for providing Mozilla's CA Bundle." +name = "certifi" +optional = false +python-versions = ">=3.6" +version = "2023.7.22" + +[[package]] +category = "main" +description = "Foreign Function Interface for Python calling C code." +name = "cffi" +optional = false +python-versions = "*" +version = "1.15.1" + +[package.dependencies] +pycparser = "*" + +[[package]] +category = "main" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +name = "charset-normalizer" +optional = false +python-versions = ">=3.7.0" +version = "3.2.0" + +[[package]] +category = "main" +description = "Composable command line interface toolkit" +name = "click" +optional = false +python-versions = ">=3.7" +version = "8.1.6" + +[package.dependencies] +colorama = "*" + +[[package]] +category = "main" +description = "Cross-platform colored terminal text." +marker = "platform_system == \"Windows\" or sys_platform == \"win32\"" +name = "colorama" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +version = "0.4.6" + +[[package]] +category = "main" +description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." +name = "django" +optional = false +python-versions = ">=3.6" +version = "3.2.20" + +[package.dependencies] +asgiref = ">=3.3.2,<4" +pytz = "*" +sqlparse = ">=0.2.2" + +[package.extras] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +category = "main" +description = "Easy-to-use active URL highlighting for Django" +name = "django-activeurl" +optional = false +python-versions = "*" +version = "0.2.0" + +[package.dependencies] +django-classy-tags = "*" +django_appconf = "*" +lxml = "*" + +[package.dependencies.django] +python = ">=3" +version = "*" + +[[package]] +category = "main" +description = "A framework for managing agent trust, such as public vs. private computers." +name = "django-agent-trust" +optional = false +python-versions = "*" +version = "1.0.4" + +[package.dependencies] +django = ">=2.2" + +[[package]] +category = "main" +description = "Analytics service integration for Django projects" +name = "django-analytical" +optional = false +python-versions = ">=3.6" +version = "3.1.0" + +[[package]] +category = "main" +description = "This is a django application that tries to eliminate annoying things in the Django framework." +name = "django-annoying" +optional = false +python-versions = "*" +version = "0.10.6" + +[package.dependencies] +Django = ">=1.11" +six = "*" + +[[package]] +category = "main" +description = "A helper class for handling configuration defaults of packaged apps gracefully." +name = "django-appconf" +optional = false +python-versions = ">=3.6" +version = "1.0.5" + +[package.dependencies] +django = "*" + +[[package]] +category = "main" +description = "Class based template tags for Django" +name = "django-classy-tags" +optional = false +python-versions = ">=3.8" +version = "4.1.0" + +[package.dependencies] +django = ">=3.2" + +[[package]] +category = "main" +description = "('Compresses linked and inline JavaScript or CSS into single cached files.',)" +name = "django-compressor" +optional = false +python-versions = "*" +version = "4.4" + +[package.dependencies] +django-appconf = ">=1.0.3" +rcssmin = "1.1.1" +rjsmin = "1.2.1" + +[[package]] +category = "main" +description = "Computed property model fields for Django" +name = "django-computed-property" +optional = false +python-versions = "*" +version = "0.3.0" + +[package.dependencies] +Django = ">=1.8.2" +six = ">=1.11.0" + +[[package]] +category = "main" +description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." +name = "django-cors-headers" +optional = false +python-versions = ">=3.8" +version = "4.2.0" + +[package.dependencies] +Django = ">=3.2" + +[[package]] +category = "main" +description = "A configurable set of panels that display various debug information about the current request/response." +name = "django-debug-toolbar" +optional = false +python-versions = ">=3.8" +version = "4.1.0" + +[package.dependencies] +django = ">=3.2.4" +sqlparse = ">=0.2" + +[[package]] +category = "main" +description = "Extensions for Django" +name = "django-extensions" +optional = false +python-versions = ">=3.6" +version = "3.2.3" + +[package.dependencies] +Django = ">=3.2" + +[[package]] +category = "main" +description = "Pluggable app for handling webpage meta tags and OpenGraph properties" +name = "django-meta" +optional = false +python-versions = ">=3.7" +version = "2.2.0" + +[package.extras] +docs = ["django (<5.0)"] + +[[package]] +category = "main" +description = "Django model mixins and utilities" +name = "django-model-utils" +optional = false +python-versions = ">=3.7" +version = "4.3.1" + +[package.dependencies] +Django = ">=3.2" + +[[package]] +category = "main" +description = "A pluggable framework for adding two-factor authentication to Django using one-time passwords." +name = "django-otp" +optional = false +python-versions = ">=3.7" +version = "1.2.2" + +[package.dependencies] +django = ">=3.2" + +[package.extras] +qrcode = ["qrcode"] + +[[package]] +category = "main" +description = "Integration of django-otp and django-agent-trust." +name = "django-otp-agents" +optional = false +python-versions = "*" +version = "1.0.1" + +[package.dependencies] +django-agent-trust = ">=1.0.1" +django-otp = ">=1.0.1" + +[[package]] +category = "main" +description = "PubSubHubbub (PuSH) support for Django" +name = "django-push" +optional = false +python-versions = "*" +version = "1.1" + +[package.dependencies] +Django = "*" +requests = "*" + +[[package]] +category = "main" +description = "A Django field that automatically generates random slugs." +name = "django-randomslugfield" +optional = false +python-versions = "*" +version = "0.3.0" + +[[package]] +category = "main" +description = "Full featured redis cache backend for Django." +name = "django-redis" +optional = false +python-versions = ">=3.6" +version = "5.3.0" + +[package.dependencies] +Django = ">=3.2" +redis = ">=3,<4.0.0 || >4.0.0,<4.0.1 || >4.0.1" + +[package.extras] +hiredis = ["redis (>=3,<4.0.0 || >4.0.0,<4.0.1 || >4.0.1)"] + +[[package]] +category = "main" +description = "An app that provides django integration for RQ (Redis Queue)" +name = "django-rq" +optional = false +python-versions = "*" +version = "2.8.1" + +[package.dependencies] +django = ">=2.0" +redis = ">=3" +rq = ">=1.14" + +[package.extras] +sentry = ["raven (>=6.1.0)"] +testing = ["mock (>=2.0.0)"] + +[[package]] +category = "main" +description = "Docutils -- Python Documentation Utilities" +name = "docutils" +optional = false +python-versions = ">=3.7" +version = "0.20.1" + +[[package]] +category = "dev" +description = "Backport of PEP 654 (exception groups)" +marker = "python_version < \"3.11\"" +name = "exceptiongroup" +optional = false +python-versions = ">=3.7" +version = "1.1.2" + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +category = "main" +description = "Coroutine-based network library" +name = "gevent" +optional = false +python-versions = ">=3.8" +version = "23.7.0" + +[package.dependencies] +cffi = ">=1.12.2" +"zope.event" = "*" +"zope.interface" = "*" + +[[package.dependencies.greenlet]] +python = "<3.12" +version = ">=2.0.0" + +[[package.dependencies.greenlet]] +python = ">=3.12" +version = ">=3.0a1" + +[package.extras] +dnspython = ["dnspython (>=1.16.0,<2.0)", "idna"] +docs = ["sphinx", "furo", "repoze.sphinx.autointerface", "sphinxcontrib-programoutput", "zope.schema"] +monitor = ["psutil (>=5.7.0)"] +recommended = ["cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "psutil (>=5.7.0)"] +test = ["requests", "objgraph", "setuptools", "cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "coverage (>=5.0)", "psutil (>=5.7.0)"] + +[[package]] +category = "main" +description = "Lightweight in-process concurrent programming" +marker = "platform_python_implementation == \"CPython\" and python_version < \"3.12\"" +name = "greenlet" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +version = "2.0.2" + +[package.extras] +docs = ["sphinx", "docutils (<0.18)"] +test = ["objgraph", "psutil"] + +[[package]] +category = "main" +description = "Lightweight in-process concurrent programming" +marker = "platform_python_implementation == \"CPython\" and python_version >= \"3.12\"" +name = "greenlet" +optional = false +python-versions = ">=3.7" +version = "3.0.0a1" + +[package.extras] +docs = ["sphinx"] +test = ["objgraph", "psutil"] + +[[package]] +category = "main" +description = "WSGI HTTP Server for UNIX" +name = "gunicorn" +optional = false +python-versions = ">=3.5" +version = "21.2.0" + +[package.dependencies] +packaging = "*" + +[package.dependencies.gevent] +optional = true +version = ">=1.4.0" + +[package.extras] +eventlet = ["eventlet (>=0.24.1)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +tornado = ["tornado (>=0.2)"] + +[[package]] +category = "main" +description = "Python wrapper for hiredis" +name = "hiredis" +optional = false +python-versions = ">=3.7" +version = "2.2.3" + +[[package]] +category = "main" +description = "HTML parser based on the WHATWG HTML specification" +name = "html5lib" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "1.1" + +[package.dependencies] +six = ">=1.9" +webencodings = "*" + +[package.extras] +all = ["genshi", "chardet (>=2.2)", "lxml"] +chardet = ["chardet (>=2.2)"] +genshi = ["genshi"] +lxml = ["lxml"] + +[[package]] +category = "main" +description = "Internationalized Domain Names in Applications (IDNA)" +name = "idna" +optional = false +python-versions = ">=3.5" +version = "3.4" + +[[package]] +category = "main" +description = "Read metadata from Python packages" +marker = "python_version < \"3.10\"" +name = "importlib-metadata" +optional = false +python-versions = ">=3.8" +version = "6.8.0" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "sphinx-lint", "jaraco.tidelift (>=1.4)"] +perf = ["ipython"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ruff", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] + +[[package]] +category = "dev" +description = "brain-dead simple config-ini parsing" +name = "iniconfig" +optional = false +python-versions = ">=3.7" +version = "2.0.0" + +[[package]] +category = "dev" +description = "An autocompletion tool for Python that can be used for text editors." +name = "jedi" +optional = false +python-versions = ">=3.6" +version = "0.19.0" + +[package.dependencies] +parso = ">=0.8.3,<0.9.0" + +[package.extras] +docs = ["Jinja2 (2.11.3)", "MarkupSafe (1.1.1)", "Pygments (2.8.1)", "alabaster (0.7.12)", "babel (2.9.1)", "chardet (4.0.0)", "commonmark (0.8.1)", "docutils (0.17.1)", "future (0.18.2)", "idna (2.10)", "imagesize (1.2.0)", "mock (1.0.1)", "packaging (20.9)", "pyparsing (2.4.7)", "pytz (2021.1)", "readthedocs-sphinx-ext (2.1.4)", "recommonmark (0.5.0)", "requests (2.25.1)", "six (1.15.0)", "snowballstemmer (2.1.0)", "sphinx-rtd-theme (0.4.3)", "sphinx (1.8.5)", "sphinxcontrib-serializinghtml (1.1.4)", "sphinxcontrib-websupport (1.2.4)", "urllib3 (1.26.4)"] +qa = ["flake8 (5.0.4)", "mypy (0.971)", "types-setuptools (67.2.0.1)"] +testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] + +[[package]] +category = "main" +description = "A very fast and expressive template engine." +name = "jinja2" +optional = false +python-versions = ">=3.7" +version = "3.1.2" + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +category = "main" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +name = "lxml" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" +version = "4.9.3" + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["beautifulsoup4"] +source = ["Cython (>=0.29.35)"] + +[[package]] +category = "main" +description = "Python implementation of John Gruber's Markdown." +name = "markdown" +optional = false +python-versions = ">=3.7" +version = "3.4.4" + +[package.dependencies] +[package.dependencies.importlib-metadata] +python = "<3.10" +version = ">=4.4" + +[package.extras] +docs = ["mkdocs (>=1.0)", "mkdocs-nature (>=0.4)", "mdx-gh-links (>=0.2)"] +testing = ["coverage", "pyyaml"] + +[[package]] +category = "main" +description = "Safely add untrusted strings to HTML/XML markup." +name = "markupsafe" +optional = false +python-versions = ">=3.7" +version = "2.1.3" + +[[package]] +category = "main" +description = "Python Microformats2 parser" +name = "mf2py" +optional = false +python-versions = ">=2.7" +version = "1.1.3" + +[package.dependencies] +BeautifulSoup4 = ">=4.6.0" +html5lib = ">=1.0.1" +requests = ">=2.18.4" + +[[package]] +category = "main" +description = "Python Microformats2 utilities, a companion to mf2py" +name = "mf2util" +optional = false +python-versions = "*" +version = "0.5.2" + +[[package]] +category = "main" +description = "MessagePack serializer" +name = "msgpack" +optional = false +python-versions = "*" +version = "1.0.5" + +[[package]] +category = "dev" +description = "Optional static typing for Python" +name = "mypy" +optional = false +python-versions = ">=3.7" +version = "1.4.1" + +[package.dependencies] +mypy-extensions = ">=1.0.0" +typing-extensions = ">=4.1.0" + +[package.dependencies.tomli] +python = "<3.11" +version = ">=1.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +category = "dev" +description = "Type system extensions for programs checked with the mypy type checker." +name = "mypy-extensions" +optional = false +python-versions = ">=3.5" +version = "1.0.0" + +[[package]] +category = "main" +description = "Core utilities for Python packages" +name = "packaging" +optional = false +python-versions = ">=3.7" +version = "23.1" + +[[package]] +category = "dev" +description = "A Python Parser" +name = "parso" +optional = false +python-versions = ">=3.6" +version = "0.8.3" + +[package.extras] +qa = ["flake8 (3.8.3)", "mypy (0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + +[[package]] +category = "main" +description = "Python Imaging Library (Fork)" +name = "pillow" +optional = false +python-versions = ">=3.7" +version = "9.5.0" + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +category = "dev" +description = "plugin and hook calling mechanisms for python" +name = "pluggy" +optional = false +python-versions = ">=3.7" +version = "1.2.0" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +category = "dev" +description = "Library for building powerful interactive command lines in Python" +name = "prompt-toolkit" +optional = false +python-versions = ">=3.7.0" +version = "3.0.39" + +[package.dependencies] +wcwidth = "*" + +[[package]] +category = "main" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +name = "psycopg2-binary" +optional = false +python-versions = ">=3.6" +version = "2.9.7" + +[[package]] +category = "dev" +description = "Python REPL build on top of prompt_toolkit" +name = "ptpython" +optional = false +python-versions = ">=3.7" +version = "3.0.23" + +[package.dependencies] +appdirs = "*" +jedi = ">=0.16.0" +prompt-toolkit = ">=3.0.28,<3.1.0" +pygments = "*" + +[package.extras] +all = ["black"] +ptipython = ["ipython"] + +[[package]] +category = "main" +description = "C parser in Python" +name = "pycparser" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.21" + +[[package]] +category = "dev" +description = "Pygments is a syntax highlighting package written in Python." +name = "pygments" +optional = false +python-versions = ">=3.7" +version = "2.16.1" + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +category = "main" +description = "Pure Python library for saving and loading PNG images" +name = "pypng" +optional = false +python-versions = "*" +version = "0.20220715.0" + +[[package]] +category = "dev" +description = "pytest: simple powerful testing with Python" +name = "pytest" +optional = false +python-versions = ">=3.7" +version = "7.4.0" + +[package.dependencies] +colorama = "*" +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" + +[package.dependencies.exceptiongroup] +python = "<3.11" +version = ">=1.0.0rc8" + +[package.dependencies.tomli] +python = "<3.11" +version = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +category = "dev" +description = "A Django plugin for pytest." +name = "pytest-django" +optional = false +python-versions = ">=3.5" +version = "4.5.2" + +[package.dependencies] +pytest = ">=5.4.0" + +[package.extras] +docs = ["sphinx", "sphinx-rtd-theme"] +testing = ["django", "django-configurations (>=2.0)"] + +[[package]] +category = "main" +description = "Convert numbers from base 10 integers to base X strings and back again." +name = "python-baseconv" +optional = false +python-versions = "*" +version = "1.2.2" + +[[package]] +category = "main" +description = "File type identification using libmagic" +name = "python-magic" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.4.27" + +[[package]] +category = "main" +description = "A Python slugify application that also handles Unicode" +name = "python-slugify" +optional = false +python-versions = ">=3.7" +version = "8.0.1" + +[package.dependencies] +text-unidecode = ">=1.3" + +[package.extras] +unidecode = ["Unidecode (>=1.1.1)"] + +[[package]] +category = "main" +description = "World timezone definitions, modern and historical" +name = "pytz" +optional = false +python-versions = "*" +version = "2023.3" + +[[package]] +category = "main" +description = "pyup-django checks your installed dependencies for known security vulnerabilities and displays them in the admin area." +name = "pyup-django" +optional = false +python-versions = "*" +version = "0.4.0" + +[package.dependencies] +packaging = "*" +requests = "*" + +[[package]] +category = "main" +description = "YAML parser and emitter for Python" +name = "pyyaml" +optional = false +python-versions = ">=3.6" +version = "6.0.1" + +[[package]] +category = "main" +description = "QR Code image generator" +name = "qrcode" +optional = false +python-versions = ">=3.7" +version = "7.4.2" + +[package.dependencies] +colorama = "*" +pypng = "*" +typing-extensions = "*" + +[package.extras] +all = ["zest.releaser", "tox", "pytest", "pytest-cov", "pillow (>=9.1.0)"] +dev = ["tox", "pytest", "pytest-cov"] +maintainer = ["zest.releaser"] +pil = ["pillow (>=9.1.0)"] +test = ["coverage", "pytest"] + +[[package]] +category = "main" +description = "CSS Minifier" +name = "rcssmin" +optional = false +python-versions = "*" +version = "1.1.1" + +[[package]] +category = "main" +description = "Python client for Redis database and key-value store" +name = "redis" +optional = false +python-versions = ">=3.7" +version = "4.6.0" + +[package.dependencies] +[package.dependencies.async-timeout] +python = "<=3.11.2" +version = ">=4.0.2" + +[package.extras] +hiredis = ["hiredis (>=1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (20.0.1)", "requests (>=2.26.0)"] + +[[package]] +category = "main" +description = "Python HTTP for Humans." +name = "requests" +optional = false +python-versions = ">=3.7" +version = "2.31.0" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +category = "main" +description = "Javascript Minifier" +name = "rjsmin" +optional = false +python-versions = "*" +version = "1.2.1" + +[[package]] +category = "main" +description = "\"Webmention Manager\"" +name = "ronkyuu" +optional = false +python-versions = ">=3.9" +version = "0.9" + +[[package]] +category = "main" +description = "RQ is a simple, lightweight, library for creating background jobs, and processing them." +name = "rq" +optional = false +python-versions = ">=3.6" +version = "1.15.1" + +[package.dependencies] +click = ">=5.0.0" +redis = ">=4.0.0" + +[[package]] +category = "main" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.16.0" + +[[package]] +category = "main" +description = "A modern CSS selector implementation for Beautiful Soup." +name = "soupsieve" +optional = false +python-versions = ">=3.7" +version = "2.4.1" + +[[package]] +category = "main" +description = "A non-validating SQL parser." +name = "sqlparse" +optional = false +python-versions = ">=3.5" +version = "0.4.4" + +[package.extras] +dev = ["flake8", "build"] +doc = ["sphinx"] +test = ["pytest", "pytest-cov"] + +[[package]] +category = "main" +description = "The most basic Text::Unidecode port" +name = "text-unidecode" +optional = false +python-versions = "*" +version = "1.3" + +[[package]] +category = "dev" +description = "A lil' TOML parser" +marker = "python_version < \"3.11\"" +name = "tomli" +optional = false +python-versions = ">=3.7" +version = "2.0.1" + +[[package]] +category = "dev" +description = "Typing stubs for bleach" +name = "types-bleach" +optional = false +python-versions = "*" +version = "6.0.0.4" + +[[package]] +category = "dev" +description = "Typing stubs for Markdown" +name = "types-markdown" +optional = false +python-versions = "*" +version = "3.4.2.10" + +[[package]] +category = "dev" +description = "Typing stubs for python-slugify" +name = "types-python-slugify" +optional = false +python-versions = "*" +version = "8.0.0.3" + +[[package]] +category = "dev" +description = "Typing stubs for PyYAML" +name = "types-pyyaml" +optional = false +python-versions = "*" +version = "6.0.12.11" + +[[package]] +category = "dev" +description = "Typing stubs for requests" +name = "types-requests" +optional = false +python-versions = "*" +version = "2.31.0.2" + +[package.dependencies] +types-urllib3 = "*" + +[[package]] +category = "dev" +description = "Typing stubs for urllib3" +name = "types-urllib3" +optional = false +python-versions = "*" +version = "1.26.25.14" + +[[package]] +category = "main" +description = "Backported and Experimental Type Hints for Python 3.7+" +name = "typing-extensions" +optional = false +python-versions = ">=3.7" +version = "4.7.1" + +[[package]] +category = "main" +description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "urllib3" +optional = false +python-versions = ">=3.7" +version = "2.0.4" + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +category = "dev" +description = "Filesystem events monitoring" +name = "watchdog" +optional = false +python-versions = ">=3.7" +version = "3.0.0" + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +category = "dev" +description = "Measures the displayed width of unicode strings in a terminal" +name = "wcwidth" +optional = false +python-versions = "*" +version = "0.2.6" + +[[package]] +category = "main" +description = "Character encoding aliases for legacy web content" +name = "webencodings" +optional = false +python-versions = "*" +version = "0.5.1" + +[[package]] +category = "dev" +description = "The comprehensive WSGI web application library." +name = "werkzeug" +optional = false +python-versions = ">=3.8" +version = "2.3.6" + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[[package]] +category = "main" +description = "Package for serializing and deserializing of XRD documents" +name = "xrd" +optional = false +python-versions = "*" +version = "0.1" + +[[package]] +category = "main" +description = "Backport of pathlib-compatible object wrapper for zip files" +marker = "python_version < \"3.10\"" +name = "zipp" +optional = false +python-versions = ">=3.8" +version = "3.16.2" + +[package.extras] +docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "furo", "sphinx-lint", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ruff", "jaraco.itertools", "jaraco.functools", "more-itertools", "big-o", "pytest-ignore-flaky", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] + +[[package]] +category = "main" +description = "Very basic event publishing system" +name = "zope.event" +optional = false +python-versions = ">=3.7" +version = "5.0" + +[package.dependencies] +setuptools = "*" + +[package.extras] +docs = ["sphinx"] +test = ["zope.testrunner"] + +[[package]] +category = "main" +description = "Interfaces for Python" +name = "zope.interface" +optional = false +python-versions = ">=3.7" +version = "6.0" + +[package.dependencies] +setuptools = "*" + +[package.extras] +docs = ["sphinx", "repoze.sphinx.autointerface"] +test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] +testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] + +[metadata] +content-hash = "b55f2c5ce8c55037eb76c7a9a9b3fc428b23a79fd614a3606bd8161d4451fe4f" +python-versions = "^3.9" + +[metadata.files] +accept-types = [] +ago = [] +appdirs = [] +argon2-cffi = [] +argon2-cffi-bindings = [] +asgiref = [] +async-timeout = [] +beautifulsoup4 = [] +bleach = [] +cachecontrol = [] +certifi = [] +cffi = [] +charset-normalizer = [] +click = [] +colorama = [] +django = [] +django-activeurl = [] +django-agent-trust = [] +django-analytical = [] +django-annoying = [] +django-appconf = [] +django-classy-tags = [] +django-compressor = [] +django-computed-property = [] +django-cors-headers = [] +django-debug-toolbar = [] +django-extensions = [] +django-meta = [] +django-model-utils = [] +django-otp = [] +django-otp-agents = [] +django-push = [] +django-randomslugfield = [] +django-redis = [] +django-rq = [] +docutils = [] +exceptiongroup = [] +gevent = [] +greenlet = [] +gunicorn = [] +hiredis = [] +html5lib = [] +idna = [] +importlib-metadata = [] +iniconfig = [] +jedi = [] +jinja2 = [] +lxml = [] +markdown = [] +markupsafe = [] +mf2py = [] +mf2util = [] +msgpack = [] +mypy = [] +mypy-extensions = [] +packaging = [] +parso = [] +pillow = [] +pluggy = [] +prompt-toolkit = [] +psycopg2-binary = [] +ptpython = [] +pycparser = [] +pygments = [] +pypng = [] +pytest = [] +pytest-django = [] +python-baseconv = [] +python-magic = [] +python-slugify = [] +pytz = [] +pyup-django = [] +pyyaml = [] +qrcode = [] +rcssmin = [] +redis = [] +requests = [] +rjsmin = [] +ronkyuu = [] +rq = [] +six = [] +soupsieve = [] +sqlparse = [] +text-unidecode = [] +tomli = [] +types-bleach = [] +types-markdown = [] +types-python-slugify = [] +types-pyyaml = [] +types-requests = [] +types-urllib3 = [] +typing-extensions = [] +urllib3 = [] +watchdog = [] +wcwidth = [] +webencodings = [] +werkzeug = [] +xrd = [] +zipp = [] +"zope.event" = [] +"zope.interface" = [] diff --git a/Pipfile b/pyproject.toml similarity index 64% rename from Pipfile rename to pyproject.toml index 8a070d4..4fc7074 100644 --- a/Pipfile +++ b/pyproject.toml @@ -1,63 +1,66 @@ -[[source]] -url = "https://pypi.python.org/simple" -verify_ssl = true -name = "pypi" +[tool.poetry] +name = "lemoncurry" +version = "1.11.0" +description = "Indieweb-compatible personal website" +authors = ["Danielle McLean "] +license = "MIT" -[requires] -python_version = '3.9' - -[packages] -django = ">=3,<4" -django-compressor = "*" -gunicorn = {extras = ["gevent"]} -"psycopg2-binary" = "*" -pillow = "*" -django-meta = "*" -django-activeurl = "*" -django-otp = "*" -qrcode = "*" -django-otp-agents = "*" -python-slugify = "*" -"mf2py" = "*" -markdown = "*" -bleach = "*" -django-debug-toolbar = "*" -xrd = "*" -django-push = "*" -pyyaml = "*" -django-annoying = "*" +[tool.poetry.dependencies] +python = "^3.9" accept-types = "*" -django-analytical = "*" -django-model-utils = "*" -django-rq = "*" -ronkyuu = "*" -cachecontrol = "*" -hiredis = "*" -"mf2util" = "*" -django-cors-headers = "*" -"argon2-cffi" = "*" -python-baseconv = "*" -django-computed-property = "*" -docutils = "*" -django-super-favicon = "*" -django-redis = "*" -gevent = "*" -django-extensions = "*" -python-magic = "*" -pyup-django = "*" -jinja2 = "*" -msgpack = "*" -django-randomslugfield = "*" ago = "*" +argon2-cffi = "*" +bleach = "*" +cachecontrol = "*" +django = "<4,>=3" +django-activeurl = "*" +django-analytical = "*" +django-annoying = "*" +django-compressor = "*" +django-computed-property = "*" +django-cors-headers = "*" +django-debug-toolbar = "*" +django-extensions = "*" +django-meta = "*" +django-model-utils = "*" +django-otp = "*" +django-otp-agents = "*" +django-push = "*" +django-randomslugfield = "*" +django-redis = "*" +django-rq = "*" +docutils = "*" +gevent = "*" +gunicorn = {extras = ["gevent"], version = "*"} +hiredis = "*" +jinja2 = "*" +markdown = "*" +mf2py = "*" +mf2util = "*" +msgpack = "*" +pillow = "*" +psycopg2-binary = "*" +python-baseconv = "*" +python-magic = "*" +python-slugify = "*" +pyup-django = "*" +pyyaml = "*" +qrcode = "*" +ronkyuu = "*" +xrd = "*" -[dev-packages] +[tool.poetry.dev-dependencies] +mypy = "*" ptpython = "*" pytest-django = "*" -werkzeug = "*" -watchdog = "*" -mypy = "*" -types-pyyaml = "*" -types-requests = "*" -types-python-slugify = "*" types-bleach = "*" types-markdown = "*" +types-python-slugify = "*" +types-pyyaml = "*" +types-requests = "*" +watchdog = "*" +werkzeug = "*" + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/users/admin.py b/users/admin.py index c5f11c4..6cd53dd 100644 --- a/users/admin.py +++ b/users/admin.py @@ -1,14 +1,14 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin -from .models import Key, Profile, Site, User +from .models import PgpKey, Profile, Site, User class SiteAdmin(admin.ModelAdmin): - list_display = ('name', 'icon', 'domain', 'url_template') + list_display = ("name", "icon", "domain", "url_template") -class KeyInline(admin.TabularInline): - model = Key +class PgpKeyInline(admin.TabularInline): + model = PgpKey extra = 1 @@ -19,10 +19,10 @@ class ProfileInline(admin.TabularInline): class UserAdmin(BaseUserAdmin): fieldsets = BaseUserAdmin.fieldsets + ( - ('Profile', {'fields': ('avatar', 'xmpp', 'note')}), + ("Profile", {"fields": ("avatar", "xmpp", "note")}), ) inlines = ( - KeyInline, + PgpKeyInline, ProfileInline, ) diff --git a/users/apps.py b/users/apps.py index 54e430e..2ffced1 100644 --- a/users/apps.py +++ b/users/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class UsersConfig(AppConfig): - name = 'users' - verbose_name = 'Users and Profiles' + name = "users" + verbose_name = "Users and Profiles" diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py index ffec4b2..ba895d4 100644 --- a/users/migrations/0001_initial.py +++ b/users/migrations/0001_initial.py @@ -9,40 +9,127 @@ import django.utils.timezone class Migration(migrations.Migration): - initial = True dependencies = [ - ('auth', '0008_alter_user_username_max_length'), + ("auth", "0008_alter_user_username_max_length"), ] operations = [ migrations.CreateModel( - name='User', + name="User", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('avatar', models.ImageField(upload_to='')), - ('note', models.TextField(blank=True)), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=30, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=30, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ("avatar", models.ImageField(upload_to="")), + ("note", models.TextField(blank=True)), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.Group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), + ), ], options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'abstract': False, + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, }, managers=[ - ('objects', django.contrib.auth.models.UserManager()), + ("objects", django.contrib.auth.models.UserManager()), ], ), ] diff --git a/users/migrations/0002_auto_20171023_0109.py b/users/migrations/0002_auto_20171023_0109.py index 8f521a7..27fbb8f 100644 --- a/users/migrations/0002_auto_20171023_0109.py +++ b/users/migrations/0002_auto_20171023_0109.py @@ -7,15 +7,14 @@ import users.models class Migration(migrations.Migration): - dependencies = [ - ('users', '0001_initial'), + ("users", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='user', - name='avatar', + model_name="user", + name="avatar", field=models.ImageField(upload_to=users.models.avatar_path), ), ] diff --git a/users/migrations/0003_key.py b/users/migrations/0003_key.py index c8e564a..168676e 100644 --- a/users/migrations/0003_key.py +++ b/users/migrations/0003_key.py @@ -8,19 +8,33 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('users', '0002_auto_20171023_0109'), + ("users", "0002_auto_20171023_0109"), ] operations = [ migrations.CreateModel( - name='Key', + name="Key", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('fingerprint', models.CharField(max_length=40)), - ('file', models.FileField(upload_to='keys')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='keys', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("fingerprint", models.CharField(max_length=40)), + ("file", models.FileField(upload_to="keys")), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="keys", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/users/migrations/0004_auto_20171023_0143.py b/users/migrations/0004_auto_20171023_0143.py index 6290295..f895b37 100644 --- a/users/migrations/0004_auto_20171023_0143.py +++ b/users/migrations/0004_auto_20171023_0143.py @@ -8,48 +8,67 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('users', '0003_key'), + ("users", "0003_key"), ] operations = [ migrations.CreateModel( - name='Profile', + name="Profile", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('username', models.CharField(max_length=100)), - ('display_name', models.CharField(blank=True, max_length=100)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("username", models.CharField(max_length=100)), + ("display_name", models.CharField(blank=True, max_length=100)), ], options={ - 'ordering': ('site', 'username'), + "ordering": ("site", "username"), }, ), migrations.CreateModel( - name='Site', + name="Site", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, unique=True)), - ('icon', models.CharField(max_length=100)), - ('url', models.CharField(max_length=100)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100, unique=True)), + ("icon", models.CharField(max_length=100)), + ("url", models.CharField(max_length=100)), ], options={ - 'ordering': ('name',), + "ordering": ("name",), }, ), migrations.AddField( - model_name='profile', - name='site', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.Site'), + model_name="profile", + name="site", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="users.Site" + ), ), migrations.AddField( - model_name='profile', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + model_name="profile", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), ), migrations.AddField( - model_name='user', - name='profiles', - field=models.ManyToManyField(through='users.Profile', to='users.Site'), + model_name="user", + name="profiles", + field=models.ManyToManyField(through="users.Profile", to="users.Site"), ), ] diff --git a/users/migrations/0005_auto_20171023_0158.py b/users/migrations/0005_auto_20171023_0158.py index 4f95d75..edf267e 100644 --- a/users/migrations/0005_auto_20171023_0158.py +++ b/users/migrations/0005_auto_20171023_0158.py @@ -8,19 +8,22 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('users', '0004_auto_20171023_0143'), + ("users", "0004_auto_20171023_0143"), ] operations = [ migrations.RemoveField( - model_name='user', - name='profiles', + model_name="user", + name="profiles", ), migrations.AlterField( - model_name='profile', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='profiles', to=settings.AUTH_USER_MODEL), + model_name="profile", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="profiles", + to=settings.AUTH_USER_MODEL, + ), ), ] diff --git a/users/migrations/0006_auto_20171031_1336.py b/users/migrations/0006_auto_20171031_1336.py index 1cf8516..ef9e1cb 100644 --- a/users/migrations/0006_auto_20171031_1336.py +++ b/users/migrations/0006_auto_20171031_1336.py @@ -6,21 +6,20 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('users', '0005_auto_20171023_0158'), + ("users", "0005_auto_20171023_0158"), ] operations = [ migrations.RenameField( - model_name='site', - old_name='url', - new_name='url_template', + model_name="site", + old_name="url", + new_name="url_template", ), migrations.AddField( - model_name='site', - name='domain', - field=models.CharField(default='', max_length=100), + model_name="site", + name="domain", + field=models.CharField(default="", max_length=100), preserve_default=False, ), ] diff --git a/users/migrations/0007_auto_20171031_1347.py b/users/migrations/0007_auto_20171031_1347.py index a28bb94..8c132e8 100644 --- a/users/migrations/0007_auto_20171031_1347.py +++ b/users/migrations/0007_auto_20171031_1347.py @@ -6,15 +6,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('users', '0006_auto_20171031_1336'), + ("users", "0006_auto_20171031_1336"), ] operations = [ migrations.AlterField( - model_name='site', - name='domain', + model_name="site", + name="domain", field=models.CharField(blank=True, max_length=100), ), ] diff --git a/users/migrations/0008_auto_20171031_1357.py b/users/migrations/0008_auto_20171031_1357.py index eadb686..392d09f 100644 --- a/users/migrations/0008_auto_20171031_1357.py +++ b/users/migrations/0008_auto_20171031_1357.py @@ -6,14 +6,13 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('users', '0007_auto_20171031_1347'), + ("users", "0007_auto_20171031_1347"), ] operations = [ migrations.AlterModelOptions( - name='site', - options={'ordering': ('domain',)}, + name="site", + options={"ordering": ("domain",)}, ), ] diff --git a/users/migrations/0009_user_xmpp.py b/users/migrations/0009_user_xmpp.py index b554f08..7c707f4 100644 --- a/users/migrations/0009_user_xmpp.py +++ b/users/migrations/0009_user_xmpp.py @@ -6,15 +6,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('users', '0008_auto_20171031_1357'), + ("users", "0008_auto_20171031_1357"), ] operations = [ migrations.AddField( - model_name='user', - name='xmpp', + model_name="user", + name="xmpp", field=models.EmailField(blank=True, max_length=254), ), ] diff --git a/users/migrations/0010_auto_20171206_2211.py b/users/migrations/0010_auto_20171206_2211.py index 04a0819..6d982b9 100644 --- a/users/migrations/0010_auto_20171206_2211.py +++ b/users/migrations/0010_auto_20171206_2211.py @@ -6,14 +6,13 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('users', '0009_user_xmpp'), + ("users", "0009_user_xmpp"), ] operations = [ migrations.AlterModelOptions( - name='site', - options={'ordering': ('name',)}, + name="site", + options={"ordering": ("name",)}, ), ] diff --git a/users/migrations/0011_auto_20180124_1311.py b/users/migrations/0011_auto_20180124_1311.py index 20d91e3..65a1661 100644 --- a/users/migrations/0011_auto_20180124_1311.py +++ b/users/migrations/0011_auto_20180124_1311.py @@ -6,15 +6,13 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('users', '0010_auto_20171206_2211'), + ("users", "0010_auto_20171206_2211"), ] operations = [ migrations.AlterModelManagers( - name='user', - managers=[ - ], + name="user", + managers=[], ), ] diff --git a/users/migrations/0012_auto_20180129_1614.py b/users/migrations/0012_auto_20180129_1614.py index f0f4a91..364642f 100644 --- a/users/migrations/0012_auto_20180129_1614.py +++ b/users/migrations/0012_auto_20180129_1614.py @@ -7,16 +7,15 @@ import users.models class Migration(migrations.Migration): - dependencies = [ - ('users', '0011_auto_20180124_1311'), + ("users", "0011_auto_20180124_1311"), ] operations = [ migrations.AlterModelManagers( - name='user', + name="user", managers=[ - ('objects', users.models.UserManager()), + ("objects", users.models.UserManager()), ], ), ] diff --git a/users/migrations/0013_auto_20180323_1200.py b/users/migrations/0013_auto_20180323_1200.py index 953b271..e0f1576 100644 --- a/users/migrations/0013_auto_20180323_1200.py +++ b/users/migrations/0013_auto_20180323_1200.py @@ -5,20 +5,31 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('users', '0012_auto_20180129_1614'), + ("users", "0012_auto_20180129_1614"), ] operations = [ migrations.AddField( - model_name='user', - name='email_md5', - field=computed_property.fields.ComputedCharField(compute_from='calc_email_md5', default='', editable=False, max_length=32, unique=True), + model_name="user", + name="email_md5", + field=computed_property.fields.ComputedCharField( + compute_from="calc_email_md5", + default="", + editable=False, + max_length=32, + unique=True, + ), ), migrations.AddField( - model_name='user', - name='email_sha256', - field=computed_property.fields.ComputedCharField(compute_from='calc_email_sha256', default='', editable=False, max_length=64, unique=True), + model_name="user", + name="email_sha256", + field=computed_property.fields.ComputedCharField( + compute_from="calc_email_sha256", + default="", + editable=False, + max_length=64, + unique=True, + ), ), ] diff --git a/users/migrations/0014_auto_20180711_1248.py b/users/migrations/0014_auto_20180711_1248.py index 2975e47..4d846e5 100644 --- a/users/migrations/0014_auto_20180711_1248.py +++ b/users/migrations/0014_auto_20180711_1248.py @@ -6,58 +6,79 @@ import users.models class Migration(migrations.Migration): - dependencies = [ - ('users', '0013_auto_20180323_1200'), + ("users", "0013_auto_20180323_1200"), ] operations = [ migrations.AlterField( - model_name='profile', - name='display_name', + model_name="profile", + name="display_name", field=models.CharField( - blank=True, help_text='overrides the username for display - useful for sites that use ugly IDs', max_length=100), + blank=True, + help_text="overrides the username for display - useful for sites that use ugly IDs", + max_length=100, + ), ), migrations.AlterField( - model_name='profile', - name='username', + model_name="profile", + name="username", field=models.CharField( - help_text="the user's actual handle or ID on the remote site", max_length=100), + help_text="the user's actual handle or ID on the remote site", + max_length=100, + ), ), migrations.AlterField( - model_name='user', - name='avatar', + model_name="user", + name="avatar", field=models.ImageField( - help_text='an avatar or photo that represents this user', upload_to=users.models.avatar_path), + help_text="an avatar or photo that represents this user", + upload_to=users.models.avatar_path, + ), ), migrations.AlterField( - model_name='user', - name='email_md5', + model_name="user", + name="email_md5", field=computed_property.fields.ComputedCharField( - compute_from='calc_email_md5', editable=False, help_text="MD5 hash of the user's email, used for Libravatar", max_length=32, unique=True), + compute_from="calc_email_md5", + editable=False, + help_text="MD5 hash of the user's email, used for Libravatar", + max_length=32, + unique=True, + ), ), migrations.AlterField( - model_name='user', - name='email_sha256', + model_name="user", + name="email_sha256", field=computed_property.fields.ComputedCharField( - compute_from='calc_email_sha256', editable=False, help_text="SHA-256 hash of the user's email, used for Libravatar", max_length=64, unique=True), + compute_from="calc_email_sha256", + editable=False, + help_text="SHA-256 hash of the user's email, used for Libravatar", + max_length=64, + unique=True, + ), ), migrations.AlterField( - model_name='user', - name='last_name', + model_name="user", + name="last_name", field=models.CharField( - blank=True, max_length=150, verbose_name='last name'), + blank=True, max_length=150, verbose_name="last name" + ), ), migrations.AlterField( - model_name='user', - name='note', + model_name="user", + name="note", field=models.TextField( - blank=True, help_text='a bio or short description provided by the user'), + blank=True, help_text="a bio or short description provided by the user" + ), ), migrations.AlterField( - model_name='user', - name='xmpp', + model_name="user", + name="xmpp", field=models.EmailField( - blank=True, help_text='an XMPP address through which the user may be reached', max_length=254), + blank=True, + help_text="an XMPP address through which the user may be reached", + max_length=254, + ), ), ] diff --git a/users/migrations/0015_user_openid_sha256.py b/users/migrations/0015_user_openid_sha256.py index 3070f11..2cd48a0 100644 --- a/users/migrations/0015_user_openid_sha256.py +++ b/users/migrations/0015_user_openid_sha256.py @@ -5,17 +5,22 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('users', '0014_auto_20180711_1248'), + ("users", "0014_auto_20180711_1248"), ] operations = [ migrations.AddField( - model_name='user', - name='openid_sha256', - field=computed_property.fields.ComputedCharField(compute_from='calc_openid_sha256', default='', editable=False, - help_text="SHA-256 hash of the user's OpenID URL, used for Libravatar", max_length=64, unique=True), + model_name="user", + name="openid_sha256", + field=computed_property.fields.ComputedCharField( + compute_from="calc_openid_sha256", + default="", + editable=False, + help_text="SHA-256 hash of the user's OpenID URL, used for Libravatar", + max_length=64, + unique=True, + ), preserve_default=False, ), ] diff --git a/users/migrations/0016_alter_user_first_name.py b/users/migrations/0016_alter_user_first_name.py index f5375ae..f3eccfa 100644 --- a/users/migrations/0016_alter_user_first_name.py +++ b/users/migrations/0016_alter_user_first_name.py @@ -4,19 +4,16 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('users', '0015_user_openid_sha256'), + ("users", "0015_user_openid_sha256"), ] operations = [ migrations.AlterField( - model_name='user', - name='first_name', + model_name="user", + name="first_name", field=models.CharField( - blank=True, - max_length=150, - verbose_name='first name' + blank=True, max_length=150, verbose_name="first name" ), ), ] diff --git a/users/migrations/0017_rename_key_pgpkey.py b/users/migrations/0017_rename_key_pgpkey.py new file mode 100644 index 0000000..2effa47 --- /dev/null +++ b/users/migrations/0017_rename_key_pgpkey.py @@ -0,0 +1,16 @@ +# Generated by Django 3.2.20 on 2023-08-10 06:37 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0016_alter_user_first_name"), + ] + + operations = [ + migrations.RenameModel( + old_name="Key", + new_name="PgpKey", + ), + ] diff --git a/users/models.py b/users/models.py index f8bfdee..9ddac65 100644 --- a/users/models.py +++ b/users/models.py @@ -10,7 +10,7 @@ from lemoncurry import utils def avatar_path(instance, name): - return 'avatars/{id}/{name}'.format(id=instance.id, name=name) + return "avatars/{id}/{name}".format(id=instance.id, name=name) class Site(models.Model): @@ -19,7 +19,7 @@ class Site(models.Model): domain = models.CharField(max_length=100, blank=True) url_template = models.CharField(max_length=100) - def format(self, username=''): + def format(self, username=""): return self.url_template.format(domain=self.domain, username=username) @property @@ -30,12 +30,14 @@ class Site(models.Model): return self.name class Meta: - ordering = ('name',) + ordering = ("name",) class UserManager(DjangoUserManager): def get_queryset(self): - return super(UserManager, self).get_queryset().prefetch_related('keys', 'profiles') + return ( + super(UserManager, self).get_queryset().prefetch_related("keys", "profiles") + ) class User(ModelMeta, AbstractUser): @@ -44,52 +46,56 @@ class User(ModelMeta, AbstractUser): generated based on all their associated information and may author as many h-entries (:model:`entries.Entry`) as they wish. """ + objects = UserManager() avatar = models.ImageField( - upload_to=avatar_path, - help_text='an avatar or photo that represents this user' + upload_to=avatar_path, help_text="an avatar or photo that represents this user" ) note = models.TextField( - blank=True, - help_text='a bio or short description provided by the user' + blank=True, help_text="a bio or short description provided by the user" ) xmpp = models.EmailField( - blank=True, - help_text='an XMPP address through which the user may be reached' + blank=True, help_text="an XMPP address through which the user may be reached" ) # This is gonna need to change if I ever decide to add multiple-user support ;) - url = '/' + url = "/" email_md5 = ComputedCharField( - compute_from='calc_email_md5', max_length=32, unique=True, - help_text="MD5 hash of the user's email, used for Libravatar" + compute_from="calc_email_md5", + max_length=32, + unique=True, + help_text="MD5 hash of the user's email, used for Libravatar", ) email_sha256 = ComputedCharField( - compute_from='calc_email_sha256', max_length=64, unique=True, - help_text="SHA-256 hash of the user's email, used for Libravatar" + compute_from="calc_email_sha256", + max_length=64, + unique=True, + help_text="SHA-256 hash of the user's email, used for Libravatar", ) openid_sha256 = ComputedCharField( - compute_from='calc_openid_sha256', max_length=64, unique=True, - help_text="SHA-256 hash of the user's OpenID URL, used for Libravatar" + compute_from="calc_openid_sha256", + max_length=64, + unique=True, + help_text="SHA-256 hash of the user's OpenID URL, used for Libravatar", ) @property def calc_email_md5(self): - return md5(self.email.lower().encode('utf-8')).hexdigest() + return md5(self.email.lower().encode("utf-8")).hexdigest() @property def calc_email_sha256(self): - return sha256(self.email.lower().encode('utf-8')).hexdigest() + return sha256(self.email.lower().encode("utf-8")).hexdigest() @property def calc_openid_sha256(self): - return sha256(self.full_url.encode('utf-8')).hexdigest() + return sha256(self.full_url.encode("utf-8")).hexdigest() @property def name(self): - return '{0} {1}'.format(self.first_name, self.last_name) + return "{0} {1}".format(self.first_name, self.last_name) def get_absolute_url(self): return self.absolute_url @@ -100,7 +106,7 @@ class User(ModelMeta, AbstractUser): @property def full_url(self): - base = 'https://' + DjangoSite.objects.get_current().domain + base = "https://" + DjangoSite.objects.get_current().domain return urljoin(base, self.url) @property @@ -114,45 +120,45 @@ class User(ModelMeta, AbstractUser): @cached_property def facebook_id(self): for p in self.profiles.all(): - if p.site.name == 'Facebook': + if p.site.name == "Facebook": return p.username return None @cached_property def twitter_username(self): for p in self.profiles.all(): - if p.site.name == 'Twitter': - return '@' + p.username + if p.site.name == "Twitter": + return "@" + p.username return None @property def json_ld(self): - base = 'https://' + DjangoSite.objects.get_current().domain + base = "https://" + DjangoSite.objects.get_current().domain return { - '@context': 'http://schema.org', - '@type': 'Person', - '@id': self.full_url, - 'url': self.full_url, - 'name': self.name, - 'email': self.email, - 'image': urljoin(base, self.avatar.url), - 'givenName': self.first_name, - 'familyName': self.last_name, - 'sameAs': [profile.url for profile in self.profiles.all()] + "@context": "http://schema.org", + "@type": "Person", + "@id": self.full_url, + "url": self.full_url, + "name": self.name, + "email": self.email, + "image": urljoin(base, self.avatar.url), + "givenName": self.first_name, + "familyName": self.last_name, + "sameAs": [profile.url for profile in self.profiles.all()], } _metadata = { - 'image': 'avatar_url', - 'description': 'description', - 'og_type': 'profile', - 'og_profile_id': 'facebook_id', - 'twitter_creator': 'twitter_username', + "image": "avatar_url", + "description": "description", + "og_type": "profile", + "og_profile_id": "facebook_id", + "twitter_creator": "twitter_username", } class ProfileManager(models.Manager): def get_queryset(self): - return super(ProfileManager, self).get_queryset().select_related('site') + return super(ProfileManager, self).get_queryset().select_related("site") class Profile(models.Model): @@ -163,26 +169,22 @@ class Profile(models.Model): representative h-card. Additionally, :model:`entries.Syndication` is tracked by linking each syndication to a particular profile. """ + objects = ProfileManager() - user = models.ForeignKey( - User, - related_name='profiles', - on_delete=models.CASCADE - ) + user = models.ForeignKey(User, related_name="profiles", on_delete=models.CASCADE) site = models.ForeignKey(Site, on_delete=models.CASCADE) username = models.CharField( - max_length=100, - help_text="the user's actual handle or ID on the remote site" + max_length=100, help_text="the user's actual handle or ID on the remote site" ) display_name = models.CharField( max_length=100, blank=True, - help_text="overrides the username for display - useful for sites that use ugly IDs" + help_text="overrides the username for display - useful for sites that use ugly IDs", ) def __str__(self): if self.site.domain: - return self.name + '@' + self.site.domain + return self.name + "@" + self.site.domain return self.name @property @@ -194,22 +196,19 @@ class Profile(models.Model): return self.site.format(username=self.username) class Meta: - ordering = ('site', 'username') + ordering = ("site", "username") -class Key(models.Model): +class PgpKey(models.Model): """ Represents a PGP key that belongs to a particular :model:`users.User`. Each key will be added to the user's h-card with rel="pgpkey", a format compatible with IndieAuth.com. """ - user = models.ForeignKey( - User, - related_name='keys', - on_delete=models.CASCADE - ) + + user = models.ForeignKey(User, related_name="keys", on_delete=models.CASCADE) fingerprint = models.CharField(max_length=40) - file = models.FileField(upload_to='keys') + file = models.FileField(upload_to="keys") @property def key_id(self): @@ -231,4 +230,4 @@ class Key(models.Model): same way GnuPG does. This can make reading the fingerprint a little friendlier. """ - return " ".join(self.fingerprint[i:i+4] for i in range(0, 40, 4)) + return " ".join(self.fingerprint[i : i + 4] for i in range(0, 40, 4)) diff --git a/users/urls.py b/users/urls.py index b790e3a..ae4ec6b 100644 --- a/users/urls.py +++ b/users/urls.py @@ -2,7 +2,5 @@ from django.urls import re_path from .views import libravatar -app_name = 'users' -urlpatterns = ( - re_path('^avatar/(?P[a-z0-9]+)$', libravatar, name='libravatar'), -) +app_name = "users" +urlpatterns = (re_path("^avatar/(?P[a-z0-9]+)$", libravatar, name="libravatar"),) diff --git a/users/views.py b/users/views.py index 2e700ee..cc4dffb 100644 --- a/users/views.py +++ b/users/views.py @@ -8,16 +8,16 @@ from .models import User def try_libravatar_org(hash, get): - url = 'https://seccdn.libravatar.org/avatar/' + hash + url = "https://seccdn.libravatar.org/avatar/" + hash if get: - url += '?' + get.urlencode() + url += "?" + get.urlencode() return HttpResponseRedirect(url) @cache_page(60 * 15) def libravatar(request, hash): g = request.GET - size = g.get('s', g.get('size', 80)) + size = g.get("s", g.get("size", 80)) try: size = int(size) except ValueError: @@ -30,7 +30,7 @@ def libravatar(request, hash): elif len(hash) == 64: where = Q(email_sha256=hash) | Q(openid_sha256=hash) else: - return utils.bad_req('hash must be either md5 or sha256') + return utils.bad_req("hash must be either md5 or sha256") # If the user doesn't exist or lacks an avatar, see if libravatar.org has # one for them - libravatar.org falls back to Gravatar when possible (only @@ -51,6 +51,6 @@ def libravatar(request, hash): im = im.crop((0, 0, natural_size, natural_size)) im = im.resize((size, size), resample=Image.HAMMING) - response = HttpResponse(content_type='image/'+image_type.lower()) + response = HttpResponse(content_type="image/" + image_type.lower()) im.save(response, image_type) return response diff --git a/webmention/apps.py b/webmention/apps.py index 4d88f81..a2fee31 100644 --- a/webmention/apps.py +++ b/webmention/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class WebmentionConfig(AppConfig): - name = 'webmention' + name = "webmention" diff --git a/webmention/migrations/0001_initial.py b/webmention/migrations/0001_initial.py index ce76889..0c3d9f6 100644 --- a/webmention/migrations/0001_initial.py +++ b/webmention/migrations/0001_initial.py @@ -9,31 +9,71 @@ import model_utils.fields class Migration(migrations.Migration): - initial = True dependencies = [ - ('entries', '0011_auto_20171120_1108'), + ("entries", "0011_auto_20171120_1108"), ] operations = [ migrations.CreateModel( - name='Webmention', + name="Webmention", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('source', models.CharField(max_length=255)), - ('target', models.CharField(max_length=255)), - ('state', models.CharField(choices=[('p', 'pending'), ('v', 'valid'), ('i', 'invalid'), ('d', 'deleted')], default='p', max_length=1)), - ('entry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mentions', to='entries.Entry')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), + ), + ("source", models.CharField(max_length=255)), + ("target", models.CharField(max_length=255)), + ( + "state", + models.CharField( + choices=[ + ("p", "pending"), + ("v", "valid"), + ("i", "invalid"), + ("d", "deleted"), + ], + default="p", + max_length=1, + ), + ), + ( + "entry", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="mentions", + to="entries.Entry", + ), + ), ], options={ - 'default_related_name': 'mentions', + "default_related_name": "mentions", }, ), migrations.AlterUniqueTogether( - name='webmention', - unique_together=set([('source', 'target')]), + name="webmention", + unique_together=set([("source", "target")]), ), ] diff --git a/webmention/models.py b/webmention/models.py index 47690ab..beb65b0 100644 --- a/webmention/models.py +++ b/webmention/models.py @@ -4,15 +4,15 @@ from model_utils.models import TimeStampedModel class State: - PENDING = 'p' - VALID = 'v' - INVALID = 'i' - DELETED = 'd' + PENDING = "p" + VALID = "v" + INVALID = "i" + DELETED = "d" CHOICES = ( - (PENDING, 'pending'), - (VALID, 'valid'), - (INVALID, 'invalid'), - (DELETED, 'deleted'), + (PENDING, "pending"), + (VALID, "valid"), + (INVALID, "invalid"), + (DELETED, "deleted"), ) @@ -20,12 +20,8 @@ class Webmention(TimeStampedModel): entry = models.ForeignKey(Entry, on_delete=models.CASCADE) source = models.CharField(max_length=255) target = models.CharField(max_length=255) - state = models.CharField( - choices=State.CHOICES, - default=State.PENDING, - max_length=1 - ) + state = models.CharField(choices=State.CHOICES, default=State.PENDING, max_length=1) class Meta: - default_related_name = 'mentions' - unique_together = ('source', 'target') + default_related_name = "mentions" + unique_together = ("source", "target") diff --git a/webmention/urls.py b/webmention/urls.py index ac307ca..27c01a5 100644 --- a/webmention/urls.py +++ b/webmention/urls.py @@ -1,8 +1,8 @@ from django.urls import path from . import views -app_name = 'webmention' +app_name = "webmention" urlpatterns = ( - path('s', views.accept, name='accept'), - path('s/', views.status, name='status') + path("s", views.accept, name="accept"), + path("s/", views.status, name="status"), ) diff --git a/webmention/views.py b/webmention/views.py index 5abd688..0207b4c 100644 --- a/webmention/views.py +++ b/webmention/views.py @@ -13,36 +13,36 @@ from .models import State, Webmention @csrf_exempt @require_POST def accept(request): - if 'source' not in request.POST: - return bad_req('missing source url') - source_url = request.POST['source'] + if "source" not in request.POST: + return bad_req("missing source url") + source_url = request.POST["source"] - if 'target' not in request.POST: - return bad_req('missing target url') - target_url = request.POST['target'] + if "target" not in request.POST: + return bad_req("missing target url") + target_url = request.POST["target"] source = urlparse(source_url) target = urlparse(target_url) - if source.scheme not in ('http', 'https'): - return bad_req('unsupported source scheme') - if target.scheme not in ('http', 'https'): - return bad_req('unsupported target scheme') + if source.scheme not in ("http", "https"): + return bad_req("unsupported source scheme") + if target.scheme not in ("http", "https"): + return bad_req("unsupported target scheme") if target.netloc != request.site.domain: - return bad_req('target not on this site') - origin = 'https://' + target.netloc + return bad_req("target not on this site") + origin = "https://" + target.netloc try: match = resolve(target.path) except Resolver404: - return bad_req('target not found') + return bad_req("target not found") - if match.view_name != 'entries:entry': - return bad_req('target does not accept webmentions') + if match.view_name != "entries:entry": + return bad_req("target does not accept webmentions") try: - entry = Entry.objects.get(pk=match.kwargs['id']) + entry = Entry.objects.get(pk=match.kwargs["id"]) except Entry.DoesNotExist: - return bad_req('target not found') + return bad_req("target not found") try: mention = Webmention.objects.get(source=source_url, target=target_url) @@ -54,10 +54,10 @@ def accept(request): mention.entry = entry mention.state = State.PENDING mention.save() - status_url = reverse('webmention:status', kwargs={'id': mention.id}) + status_url = reverse("webmention:status", kwargs={"id": mention.id}) res = HttpResponse(status=201) - res['Location'] = urljoin(origin, status_url) + res["Location"] = urljoin(origin, status_url) return res diff --git a/wellknowns/apps.py b/wellknowns/apps.py index 08ba250..949cc76 100644 --- a/wellknowns/apps.py +++ b/wellknowns/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class WellKnownsConfig(AppConfig): - name = 'wellknowns' + name = "wellknowns" diff --git a/wellknowns/favicons.py b/wellknowns/favicons.py index 27f7a5f..aa0a840 100644 --- a/wellknowns/favicons.py +++ b/wellknowns/favicons.py @@ -3,7 +3,7 @@ from django.core.files.storage import default_storage class Favicon: - def __init__(self, size, rel='icon', mime='image/png'): + def __init__(self, size, rel="icon", mime="image/png"): self.rel = rel self.mime = mime if not isinstance(size, tuple): @@ -12,18 +12,18 @@ class Favicon: @property def url(self): - return default_storage.url('favicon/' + self.filename) + return default_storage.url("favicon/" + self.filename) @property def filename(self): - return 'favicon-{0}.png'.format(*self.size) + return "favicon-{0}.png".format(*self.size) @property def sizes(self): - return 'x'.join(str(s) for s in self.size) + return "x".join(str(s) for s in self.size) -tile_sizes = {'small': 128, 'medium': 270, 'wide': (558, 270), 'large': 558} +tile_sizes = {"small": 128, "medium": 270, "wide": (558, 270), "large": 558} class Tile(Favicon): @@ -33,13 +33,17 @@ class Tile(Favicon): @property def filename(self): - return '{0}tile.png'.format(self.size_name) + return "{0}tile.png".format(self.size_name) sizes = (32, 57, 76, 96, 120, 128, 144, 180, 195, 228) -icons = tuple(chain( - (Favicon(s) for s in sizes), - (Tile(s) for s in tile_sizes.keys()), - (Favicon(152, rel='apple-touch-icon-precomposed'), - Favicon(196, rel='shortcut icon')) -)) +icons = tuple( + chain( + (Favicon(s) for s in sizes), + (Tile(s) for s in tile_sizes.keys()), + ( + Favicon(152, rel="apple-touch-icon-precomposed"), + Favicon(196, rel="shortcut icon"), + ), + ) +) diff --git a/wellknowns/tests/views/host_meta.py b/wellknowns/tests/views/host_meta.py index e8e332f..2a38522 100644 --- a/wellknowns/tests/views/host_meta.py +++ b/wellknowns/tests/views/host_meta.py @@ -5,22 +5,22 @@ import pytest @pytest.mark.django_db def test_host_meta_json(client): - res = client.get('/.well-known/host-meta.json') + res = client.get("/.well-known/host-meta.json") assert res.status_code == 200 - assert res['Content-Type'] == 'application/json' + assert res["Content-Type"] == "application/json" meta = json.loads(res.content) - assert meta.keys() == {'links', 'subject'} - assert meta['subject'] == 'https://example.com' - assert len(meta['links']) == 13 + assert meta.keys() == {"links", "subject"} + assert meta["subject"] == "https://example.com" + assert len(meta["links"]) == 13 @pytest.mark.django_db def test_host_meta_xml(client): - res = client.get('/.well-known/host-meta') + res = client.get("/.well-known/host-meta") assert res.status_code == 200 - assert res['Content-Type'] == 'application/xrd+xml' + assert res["Content-Type"] == "application/xrd+xml" root = etree.XML(res.content) - ns = '{http://docs.oasis-open.org/ns/xri/xrd-1.0}' - assert root.tag == (ns + 'XRD') - assert root.findtext(ns + 'Subject') == 'https://example.com' - assert len(root.findall(ns + 'Link')) == 13 + ns = "{http://docs.oasis-open.org/ns/xri/xrd-1.0}" + assert root.tag == (ns + "XRD") + assert root.findtext(ns + "Subject") == "https://example.com" + assert len(root.findall(ns + "Link")) == 13 diff --git a/wellknowns/tests/views/static.py b/wellknowns/tests/views/static.py index eb7beb2..9f9dcac 100644 --- a/wellknowns/tests/views/static.py +++ b/wellknowns/tests/views/static.py @@ -2,12 +2,12 @@ from ...views import static def test_redirect_to_static(rf): - res = static.redirect_to_static('abcd')(rf.get('/')) + res = static.redirect_to_static("abcd")(rf.get("/")) assert res.status_code == 302 - assert res.url == '/static/wellknowns/abcd' + assert res.url == "/static/wellknowns/abcd" def test_keybase(rf): - res = static.keybase(rf.get('/.well-knowns/keybase.txt')) + res = static.keybase(rf.get("/.well-knowns/keybase.txt")) assert res.status_code == 302 - assert res.url == '/static/wellknowns/keybase.txt' + assert res.url == "/static/wellknowns/keybase.txt" diff --git a/wellknowns/urls.py b/wellknowns/urls.py index 2b1b99f..35957c0 100644 --- a/wellknowns/urls.py +++ b/wellknowns/urls.py @@ -2,11 +2,11 @@ from django.urls import path from . import views -app_name = 'wellknowns' +app_name = "wellknowns" urlpatterns = [ - path('keybase.txt', views.keybase, name='keybase'), - path('host-meta', views.host_meta_xml, name='host-meta'), - path('host-meta.json', views.host_meta_json, name='host-meta.json'), - path('manifest.json', views.manifest, name='manifest'), - path('webfinger', views.webfinger, name='webfinger'), + path("keybase.txt", views.keybase, name="keybase"), + path("host-meta", views.host_meta_xml, name="host-meta"), + path("host-meta.json", views.host_meta_json, name="host-meta.json"), + path("manifest.json", views.manifest, name="manifest"), + path("webfinger", views.webfinger, name="webfinger"), ] diff --git a/wellknowns/views/host_meta.py b/wellknowns/views/host_meta.py index 5fc29f2..6d9050e 100644 --- a/wellknowns/views/host_meta.py +++ b/wellknowns/views/host_meta.py @@ -9,58 +9,55 @@ from xrd import XRD, Attribute, Element, Link def add_links(request, dest): base = origin(request) pkg = load_package_json() - webfinger = reverse('wellknowns:webfinger') + '?resource={uri}' - license = 'https://creativecommons.org/licenses/by-sa/4.0/' + webfinger = reverse("wellknowns:webfinger") + "?resource={uri}" + license = "https://creativecommons.org/licenses/by-sa/4.0/" links = ( Link( - href=urljoin(base, reverse('entries:atom')), - rel='alternate', type_='application/atom+xml', + href=urljoin(base, reverse("entries:atom")), + rel="alternate", + type_="application/atom+xml", ), Link( - href=urljoin(base, reverse('entries:rss')), - rel='alternate', type_='application/rss+xml', + href=urljoin(base, reverse("entries:rss")), + rel="alternate", + type_="application/rss+xml", ), Link( - href=urljoin(base, reverse('lemonauth:indie')), - rel='authorization_endpoint' + href=urljoin(base, reverse("lemonauth:indie")), rel="authorization_endpoint" ), - Link(href=pkg['repository'], type_='text/html', rel='code-repository'), - Link(href=settings.PUSH_HUB, rel='hub'), - Link(href=license, type_='text/html', rel='license'), - Link(href=license+'rdf', type_='application/rdf+xml', rel='license'), + Link(href=pkg["repository"], type_="text/html", rel="code-repository"), + Link(href=settings.PUSH_HUB, rel="hub"), + Link(href=license, type_="text/html", rel="license"), + Link(href=license + "rdf", type_="application/rdf+xml", rel="license"), Link( template=urljoin(base, webfinger), - type_='application/json', rel='lrdd', + type_="application/json", + rel="lrdd", ), Link( - href=urljoin(base, reverse('wellknowns:manifest')), - rel='manifest', type_='application/json', + href=urljoin(base, reverse("wellknowns:manifest")), + rel="manifest", + type_="application/json", ), - Link( - href=urljoin(base, reverse('micropub:micropub')), - rel='micropub' - ), - Link( - href=urljoin(base, reverse('lemonauth:token')), - rel='token_endpoint' - ), - Link(href='https://openid.indieauth.com/openid', rel='openid.server'), - Link(href=base, rel='openid.delegate'), + Link(href=urljoin(base, reverse("micropub:micropub")), rel="micropub"), + Link(href=urljoin(base, reverse("lemonauth:token")), rel="token_endpoint"), + Link(href="https://openid.indieauth.com/openid", rel="openid.server"), + Link(href=base, rel="openid.delegate"), ) dest.extend(links) def host_meta(request): - h = XRD(subject='https://' + request.site.domain) + h = XRD(subject="https://" + request.site.domain) add_links(request, h.links) return h def host_meta_xml(request): return HttpResponse( - host_meta(request).to_xml().toprettyxml(indent=' ', encoding='utf-8'), - content_type='application/xrd+xml', + host_meta(request).to_xml().toprettyxml(indent=" ", encoding="utf-8"), + content_type="application/xrd+xml", ) @@ -71,13 +68,15 @@ def host_meta_json(request): links = [] for l in meta.links: link = { - 'rel': l.rel, 'type': l.type, - 'href': l.href, 'template': l.template, + "rel": l.rel, + "type": l.type, + "href": l.href, + "template": l.template, } for k in list(link.keys()): if not link[k]: del link[k] links.append(link) - meta = {'links': links, 'subject': meta.subject} + meta = {"links": links, "subject": meta.subject} return JsonResponse(meta) diff --git a/wellknowns/views/manifest.py b/wellknowns/views/manifest.py index 2105f56..ae62175 100644 --- a/wellknowns/views/manifest.py +++ b/wellknowns/views/manifest.py @@ -8,23 +8,24 @@ from textwrap import shorten def manifest_icons(base): - return [{'src': i.url, 'type': i.mime, 'sizes': i.sizes} for i in sorted(icons, key=lambda i: i.size)] + return [ + {"src": i.url, "type": i.mime, "sizes": i.sizes} + for i in sorted(icons, key=lambda i: i.size) + ] def manifest(request): base = utils.origin(request) - start_url = reverse('home:index') + '?utm_source=homescreen' + start_url = reverse("home:index") + "?utm_source=homescreen" app = { - 'name': request.site.name, - 'short_name': shorten(request.site.name, width=20, placeholder=''), - 'icons': manifest_icons(base), - - 'display': 'browser', - 'start_url': urljoin(base, start_url), - - 'background_color': color(0), - 'theme_color': color(3), + "name": request.site.name, + "short_name": shorten(request.site.name, width=20, placeholder=""), + "icons": manifest_icons(base), + "display": "browser", + "start_url": urljoin(base, start_url), + "background_color": color(0), + "theme_color": color(3), } - return JsonResponse(app, content_type='application/manifest+json') + return JsonResponse(app, content_type="application/manifest+json") diff --git a/wellknowns/views/static.py b/wellknowns/views/static.py index e09e3c5..3bd8c03 100644 --- a/wellknowns/views/static.py +++ b/wellknowns/views/static.py @@ -3,7 +3,7 @@ from django.templatetags.static import static def redirect_to_static(file): - return RedirectView.as_view(url=static('wellknowns/' + file)) + return RedirectView.as_view(url=static("wellknowns/" + file)) -keybase = redirect_to_static('keybase.txt') +keybase = redirect_to_static("keybase.txt") diff --git a/wellknowns/views/webfinger.py b/wellknowns/views/webfinger.py index 05684a9..ef42687 100644 --- a/wellknowns/views/webfinger.py +++ b/wellknowns/views/webfinger.py @@ -3,9 +3,9 @@ from urllib.parse import urlencode, urlparse from users.models import User -AVATAR = 'http://webfinger.net/rel/avatar' -PROFILE_PAGE = 'http://webfinger.net/rel/profile-page' -BRIDGY_FED = 'https://fed.brid.gy/.well-known/webfinger' +AVATAR = "http://webfinger.net/rel/avatar" +PROFILE_PAGE = "http://webfinger.net/rel/profile-page" +BRIDGY_FED = "https://fed.brid.gy/.well-known/webfinger" def https_resource_matching(resource): @@ -15,10 +15,10 @@ def https_resource_matching(resource): resource, if a user with matching email or XMPP address exists locally. Will throw `User.DoesNotExist` if no such user exists. """ - if resource.scheme == 'mailto': - query = {'email': resource.path} + if resource.scheme == "mailto": + query = {"email": resource.path} else: - query = {'xmpp': resource.path} + query = {"xmpp": resource.path} return User.objects.get(**query).absolute_url @@ -41,19 +41,19 @@ def webfinger(request): original resource will be preserved in the redirect - and likely fail to find anything at Bridgy's end either. """ - if 'resource' not in request.GET: - return HttpResponseBadRequest('resource parameter missing') - resource = request.GET['resource'] + if "resource" not in request.GET: + return HttpResponseBadRequest("resource parameter missing") + resource = request.GET["resource"] try: res = urlparse(resource) except ValueError: - return HttpResponseBadRequest('resource parameter malformed') + return HttpResponseBadRequest("resource parameter malformed") - if res.scheme in ('mailto', 'xmpp'): + if res.scheme in ("mailto", "xmpp"): try: resource = https_resource_matching(res) except User.DoesNotExist: pass - query = urlencode({'resource': resource}) - return HttpResponseRedirect(BRIDGY_FED + '?' + query) + query = urlencode({"resource": resource}) + return HttpResponseRedirect(BRIDGY_FED + "?" + query)