🎉 First commit, from couchbase generator, basic changes
not tested / updated yet
This commit is contained in:
33
{{cookiecutter.project_slug}}/backend/app/Pipfile
Normal file
33
{{cookiecutter.project_slug}}/backend/app/Pipfile
Normal file
@@ -0,0 +1,33 @@
|
||||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
mypy = "*"
|
||||
black = "*"
|
||||
jupyter = "*"
|
||||
isort = "*"
|
||||
autoflake = "*"
|
||||
flake8 = "*"
|
||||
pytest = "*"
|
||||
|
||||
[packages]
|
||||
fastapi = "*"
|
||||
uvicorn = "*"
|
||||
pyjwt = "*"
|
||||
python-multipart = "*"
|
||||
email-validator = "*"
|
||||
requests = "*"
|
||||
celery = "==4.2.1"
|
||||
passlib = {extras = ["bcrypt"],version = "*"}
|
||||
tenacity = "*"
|
||||
pydantic = "*"
|
||||
emails = "*"
|
||||
raven = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.6"
|
||||
|
||||
[pipenv]
|
||||
allow_prereleases = true
|
||||
904
{{cookiecutter.project_slug}}/backend/app/Pipfile.lock
generated
Normal file
904
{{cookiecutter.project_slug}}/backend/app/Pipfile.lock
generated
Normal file
@@ -0,0 +1,904 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "0560932caf400303d4621f7725b1e723464a3e4fe00b5a3c031739d41a5ce5fe"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.6"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"amqp": {
|
||||
"hashes": [
|
||||
"sha256:16056c952e8029ce8db097edf0d7c2fe2ba9de15d30ba08aee2c5221273d8e23",
|
||||
"sha256:6816eed27521293ee03aa9ace300a07215b11fee4e845588a9b863a7ba30addb"
|
||||
],
|
||||
"version": "==2.4.1"
|
||||
},
|
||||
"bcrypt": {
|
||||
"hashes": [
|
||||
"sha256:0ba875eb67b011add6d8c5b76afbd92166e98b1f1efab9433d5dc0fafc76e203",
|
||||
"sha256:21ed446054c93e209434148ef0b362432bb82bbdaf7beef70a32c221f3e33d1c",
|
||||
"sha256:28a0459381a8021f57230954b9e9a65bb5e3d569d2c253c5cac6cb181d71cf23",
|
||||
"sha256:2aed3091eb6f51c26b7c2fad08d6620d1c35839e7a362f706015b41bd991125e",
|
||||
"sha256:2fa5d1e438958ea90eaedbf8082c2ceb1a684b4f6c75a3800c6ec1e18ebef96f",
|
||||
"sha256:3a73f45484e9874252002793518da060fb11eaa76c30713faa12115db17d1430",
|
||||
"sha256:3e489787638a36bb466cd66780e15715494b6d6905ffdbaede94440d6d8e7dba",
|
||||
"sha256:44636759d222baa62806bbceb20e96f75a015a6381690d1bc2eda91c01ec02ea",
|
||||
"sha256:678c21b2fecaa72a1eded0cf12351b153615520637efcadc09ecf81b871f1596",
|
||||
"sha256:75460c2c3786977ea9768d6c9d8957ba31b5fbeb0aae67a5c0e96aab4155f18c",
|
||||
"sha256:8ac06fb3e6aacb0a95b56eba735c0b64df49651c6ceb1ad1cf01ba75070d567f",
|
||||
"sha256:8fdced50a8b646fff8fa0e4b1c5fd940ecc844b43d1da5a980cb07f2d1b1132f",
|
||||
"sha256:9b2c5b640a2da533b0ab5f148d87fb9989bf9bcb2e61eea6a729102a6d36aef9",
|
||||
"sha256:a9083e7fa9adb1a4de5ac15f9097eb15b04e2c8f97618f1b881af40abce382e1",
|
||||
"sha256:b7e3948b8b1a81c5a99d41da5fb2dc03ddb93b5f96fcd3fd27e643f91efa33e1",
|
||||
"sha256:b998b8ca979d906085f6a5d84f7b5459e5e94a13fc27c28a3514437013b6c2f6",
|
||||
"sha256:dd08c50bc6f7be69cd7ba0769acca28c846ec46b7a8ddc2acf4b9ac6f8a7457e",
|
||||
"sha256:de5badee458544ab8125e63e39afeedfcf3aef6a6e2282ac159c95ae7472d773",
|
||||
"sha256:ede2a87333d24f55a4a7338a6ccdccf3eaa9bed081d1737e0db4dbd1a4f7e6b6"
|
||||
],
|
||||
"version": "==3.1.6"
|
||||
},
|
||||
"billiard": {
|
||||
"hashes": [
|
||||
"sha256:42d9a227401ac4fba892918bba0a0c409def5435c4b483267ebfe821afaaba0e"
|
||||
],
|
||||
"version": "==3.5.0.5"
|
||||
},
|
||||
"cachetools": {
|
||||
"hashes": [
|
||||
"sha256:219b7dc6024195b6f2bc3d3f884d1fef458745cd323b04165378622dcc823852",
|
||||
"sha256:9efcc9fab3b49ab833475702b55edd5ae07af1af7a4c627678980b45e459c460"
|
||||
],
|
||||
"version": "==3.1.0"
|
||||
},
|
||||
"celery": {
|
||||
"hashes": [
|
||||
"sha256:77dab4677e24dc654d42dfbdfed65fa760455b6bb563a0877ecc35f4cfcfc678",
|
||||
"sha256:ad7a7411772b80a4d6c64f2f7f723200e39fb66cf614a7fdfab76d345acc7b13"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.2.1"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
|
||||
"sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
|
||||
],
|
||||
"version": "==2018.11.29"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
"sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743",
|
||||
"sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef",
|
||||
"sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50",
|
||||
"sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f",
|
||||
"sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30",
|
||||
"sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93",
|
||||
"sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257",
|
||||
"sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b",
|
||||
"sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3",
|
||||
"sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e",
|
||||
"sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc",
|
||||
"sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04",
|
||||
"sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6",
|
||||
"sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359",
|
||||
"sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596",
|
||||
"sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b",
|
||||
"sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd",
|
||||
"sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95",
|
||||
"sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5",
|
||||
"sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e",
|
||||
"sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6",
|
||||
"sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca",
|
||||
"sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31",
|
||||
"sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1",
|
||||
"sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2",
|
||||
"sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085",
|
||||
"sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801",
|
||||
"sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4",
|
||||
"sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184",
|
||||
"sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917",
|
||||
"sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
|
||||
"sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
|
||||
],
|
||||
"version": "==1.11.5"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||
],
|
||||
"version": "==7.0"
|
||||
},
|
||||
"cssselect": {
|
||||
"hashes": [
|
||||
"sha256:066d8bc5229af09617e24b3ca4d52f1f9092d9e061931f4184cd572885c23204",
|
||||
"sha256:3b5103e8789da9e936a68d993b70df732d06b8bb9a337a05ed4eb52c17ef7206"
|
||||
],
|
||||
"version": "==1.0.3"
|
||||
},
|
||||
"cssutils": {
|
||||
"hashes": [
|
||||
"sha256:a2fcf06467553038e98fea9cfe36af2bf14063eb147a70958cfcaa8f5786acaf",
|
||||
"sha256:c74dbe19c92f5052774eadb15136263548dd013250f1ed1027988e7fef125c8d"
|
||||
],
|
||||
"version": "==1.0.2"
|
||||
},
|
||||
"dataclasses": {
|
||||
"hashes": [
|
||||
"sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f",
|
||||
"sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84"
|
||||
],
|
||||
"markers": "python_version < '3.7'",
|
||||
"version": "==0.6"
|
||||
},
|
||||
"dnspython": {
|
||||
"hashes": [
|
||||
"sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01",
|
||||
"sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"
|
||||
],
|
||||
"version": "==1.16.0"
|
||||
},
|
||||
"email-validator": {
|
||||
"hashes": [
|
||||
"sha256:ddc4b5b59fa699bb10127adcf7ad4de78fde4ec539a072b104b8bb16da666ae5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.0.3"
|
||||
},
|
||||
"emails": {
|
||||
"hashes": [
|
||||
"sha256:2d93bb09539d65a16cf1f68db4ffd0f7f45067633e950866e8a4ef89a7c290ec",
|
||||
"sha256:fcc02567a528eae6b66d2a5c20ce7a0326e4f6b201bc8ae302f89413164db06a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.5.15"
|
||||
},
|
||||
"fastapi": {
|
||||
"hashes": [
|
||||
"sha256:932d7e3d13ef1541b0eeb78576c98a68f15552c44a40ae4fb5816b39184d2307",
|
||||
"sha256:b6485bfbf585c6cb944a9a12ae0c29408f046c32ff0341bd46c6e2f1502d214d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.2.0"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
"sha256:acca6a44cb52a32ab442b1779adf0875c443c689e9e028f8d831a3769f9c5208",
|
||||
"sha256:f2b1ca39bfed357d1f19ac732913d5f9faa54a5062eca7d2ec3a916cfb7ae4c7"
|
||||
],
|
||||
"version": "==0.8.1"
|
||||
},
|
||||
"httptools": {
|
||||
"hashes": [
|
||||
"sha256:04c7703bbef0e8ca28b09811547352b8c7c20549eab70dc24e536bb24fd2b7c5"
|
||||
],
|
||||
"version": "==0.0.11"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
||||
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
||||
],
|
||||
"version": "==2.8"
|
||||
},
|
||||
"kombu": {
|
||||
"hashes": [
|
||||
"sha256:529df9e0ecc0bad9fc2b376c3ce4796c41b482cf697b78b71aea6ebe7ca353c8",
|
||||
"sha256:7a2cbed551103db9a4e2efafe9b63222e012a61a18a881160ad797b9d4e1d0a1"
|
||||
],
|
||||
"version": "==4.3.0"
|
||||
},
|
||||
"lxml": {
|
||||
"hashes": [
|
||||
"sha256:0537eee4902e8bf4f41bfee8133f7edf96533dd175930a12086d6a40d62376b2",
|
||||
"sha256:0562ec748abd230ab87d73384e08fa784f9b9cee89e28696087d2d22c052cc27",
|
||||
"sha256:09e91831e749fbf0f24608694e4573be0ef51430229450c39c83176cc2e2d353",
|
||||
"sha256:1ae4c0722fc70c0d4fba43ae33c2885f705e96dce1db41f75ae14a2d2749b428",
|
||||
"sha256:1c630c083d782cbaf1f7f37f6cac87bda9cff643cf2803a5f180f30d97955cef",
|
||||
"sha256:2fe74e3836bd8c0fa7467ffae05545233c7f37de1eb765cacfda15ad20c6574a",
|
||||
"sha256:37af783c2667ead34a811037bda56a0b142ac8438f7ed29ae93f82ddb812fbd6",
|
||||
"sha256:3f2d9eafbb0b24a33f56acd16f39fc935756524dcb3172892721c54713964c70",
|
||||
"sha256:47d8365a8ef14097aa4c65730689be51851b4ade677285a3b2daa03b37893e26",
|
||||
"sha256:510e904079bc56ea784677348e151e1156040dbfb736f1d8ea4b9e6d0ab2d9f4",
|
||||
"sha256:58d0851da422bba31c7f652a7e9335313cf94a641aa6d73b8f3c67602f75b593",
|
||||
"sha256:7940d5c2185ffb989203dacbb28e6ae88b4f1bb25d04e17f94b0edd82232bcbd",
|
||||
"sha256:7cf39bb3a905579836f7a8f3a45320d9eb22f16ab0c1e112efb940ced4d057a5",
|
||||
"sha256:9563a23c1456c0ab550c087833bc13fcc61013a66c6420921d5b70550ea312bf",
|
||||
"sha256:95b392952935947e0786a90b75cc33388549dcb19af716b525dae65b186138fc",
|
||||
"sha256:983129f3fd3cef5c3cf067adcca56e30a169656c00fcc6c648629dbb850b27fa",
|
||||
"sha256:a0b75b1f1854771844c647c464533def3e0a899dd094a85d1d4ed72ecaaee93d",
|
||||
"sha256:b5db89cc0ef624f3a81214b7961a99f443b8c91e88188376b6b322fd10d5b118",
|
||||
"sha256:c0a7751ba1a4bfbe7831920d98cee3ce748007eab8dfda74593d44079568219a",
|
||||
"sha256:c0c5a7d4aafcc30c9b6d8613a362567e32e5f5b708dc41bc3a81dac56f8af8bb",
|
||||
"sha256:d4d63d85eacc6cb37b459b16061e1f100d154bee89dc8d8f9a6128a5a538e92e",
|
||||
"sha256:da5e7e941d6e71c9c9a717c93725cda0708c2474f532e3680ac5e39ec57d224d",
|
||||
"sha256:dccad2b3c583f036f43f80ac99ee212c2fa9a45151358d55f13004d095e683b2",
|
||||
"sha256:df46307d39f2aeaafa1d25309b8a8d11738b73e9861f72d4d0a092528f498baa",
|
||||
"sha256:e70b5e1cb48828ddd2818f99b1662cb9226dc6f57d07fc75485405c77da17436",
|
||||
"sha256:ea825562b8cd057cbc9810d496b8b5dec37a1e2fc7b27bc7c1e72ce94462a09a"
|
||||
],
|
||||
"version": "==4.3.1"
|
||||
},
|
||||
"passlib": {
|
||||
"extras": [
|
||||
"bcrypt"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:3d948f64138c25633613f303bcc471126eae67c04d5e3f6b7b8ce6242f8653e0",
|
||||
"sha256:43526aea08fa32c6b6dbbbe9963c4c767285b78147b7437597f992812f69d280"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.7.1"
|
||||
},
|
||||
"premailer": {
|
||||
"hashes": [
|
||||
"sha256:93be4f197e9d2a87a8fe6b5b6a79b64070dbb523108dfaf2a415b4558fc78ec1",
|
||||
"sha256:f45eb4a30485aeccc3ff19771d6614346899ec19a301931af4694f737b6035c3"
|
||||
],
|
||||
"version": "==3.3.0"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
"sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
|
||||
],
|
||||
"version": "==2.19"
|
||||
},
|
||||
"pydantic": {
|
||||
"hashes": [
|
||||
"sha256:9f023811b6cefd203c5fd8fd15a4152f04e79e531b8f676ab1244dfe06ce8024",
|
||||
"sha256:edbb08b561feda505374c0f25e4b54466a0a0c702ed6b2efaabdc3890d1a82e7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.18.2"
|
||||
},
|
||||
"pyjwt": {
|
||||
"hashes": [
|
||||
"sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e",
|
||||
"sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.7.1"
|
||||
},
|
||||
"python-dateutil": {
|
||||
"hashes": [
|
||||
"sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb",
|
||||
"sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"
|
||||
],
|
||||
"version": "==2.8.0"
|
||||
},
|
||||
"python-multipart": {
|
||||
"hashes": [
|
||||
"sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.0.5"
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9",
|
||||
"sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"
|
||||
],
|
||||
"version": "==2018.9"
|
||||
},
|
||||
"raven": {
|
||||
"hashes": [
|
||||
"sha256:3fa6de6efa2493a7c827472e984ce9b020797d0da16f1db67197bcc23c8fae54",
|
||||
"sha256:44a13f87670836e153951af9a3c80405d36b43097db869a36e92809673692ce4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==6.10.0"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
|
||||
"sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.21.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
|
||||
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
|
||||
],
|
||||
"version": "==1.12.0"
|
||||
},
|
||||
"starlette": {
|
||||
"hashes": [
|
||||
"sha256:7cc05c33d00db3b2ddfd7516a737544ed0a34c9dd0ced94076f29b581ce4f532"
|
||||
],
|
||||
"version": "==0.10.1"
|
||||
},
|
||||
"tenacity": {
|
||||
"hashes": [
|
||||
"sha256:24b7f302a1caa1801e58b39ea557129c095966e64e5b1ddad3c93a6cb033e38b",
|
||||
"sha256:a04b97b3bda047f912a75d110dde3e746891b5548b6bec6d157cd100f2d4afac"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.0.3"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
|
||||
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
|
||||
],
|
||||
"version": "==1.24.1"
|
||||
},
|
||||
"uvicorn": {
|
||||
"hashes": [
|
||||
"sha256:e84fc3b1e142cec395fb7c1d1a9f3cdc0d455037b96e1bed54b378db1121aaba"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.4.3"
|
||||
},
|
||||
"uvloop": {
|
||||
"hashes": [
|
||||
"sha256:0ff2e67b693f7d2007466952dbe312075098e8f15364fda27d16e8a7f266d74d",
|
||||
"sha256:2d0029314dc87312ff8d46c3724363d847e5235403eced5d3f98da80a87f4828",
|
||||
"sha256:32dcc003e1973f3db303494f5f63db11091c86a146053773d81ac5484b10c416",
|
||||
"sha256:4301871418f967d0b13409f1bd10ecc7825a7f183282dcc9e19d08532e6cb2e9",
|
||||
"sha256:7639188ff4466d86cfd4418cd784d1198a8cc913279fb8798a4b12a4d42ad341",
|
||||
"sha256:a73649cd043f5d3e3ae471667c790a7ee2295b22fac7bedcae8705158f8ba111",
|
||||
"sha256:afdf34bf507090e4c7f5108a17240982760356b8aae4edd37180ec4f94c36cbb",
|
||||
"sha256:bd7a6db5dbfae0c93e27cb200bb2b9513e21a90a2d4a259b39a9b5446c4d5aa3",
|
||||
"sha256:cc27e903da274f76826848832f62e1ec410a43602e1e0cd4f8db8c619b1ee93e",
|
||||
"sha256:ec521d14ddcdd9f8d0075d7d1f82e9d8806f7f0a047d2e5bc737e9eddf7f930d"
|
||||
],
|
||||
"version": "==0.12.0"
|
||||
},
|
||||
"vine": {
|
||||
"hashes": [
|
||||
"sha256:3cd505dcf980223cfaf13423d371f2e7ff99247e38d5985a01ec8264e4f2aca1",
|
||||
"sha256:ee4813e915d0e1a54e5c1963fde0855337f82655678540a6bc5996bca4165f76"
|
||||
],
|
||||
"version": "==1.2.0"
|
||||
},
|
||||
"websockets": {
|
||||
"hashes": [
|
||||
"sha256:04b42a1b57096ffa5627d6a78ea1ff7fad3bc2c0331ffc17bc32a4024da7fea0",
|
||||
"sha256:08e3c3e0535befa4f0c4443824496c03ecc25062debbcf895874f8a0b4c97c9f",
|
||||
"sha256:10d89d4326045bf5e15e83e9867c85d686b612822e4d8f149cf4840aab5f46e0",
|
||||
"sha256:232fac8a1978fc1dead4b1c2fa27c7756750fb393eb4ac52f6bc87ba7242b2fa",
|
||||
"sha256:4bf4c8097440eff22bc78ec76fe2a865a6e658b6977a504679aaf08f02c121da",
|
||||
"sha256:51642ea3a00772d1e48fb0c492f0d3ae3b6474f34d20eca005a83f8c9c06c561",
|
||||
"sha256:55d86102282a636e195dad68aaaf85b81d0bef449d7e2ef2ff79ac450bb25d53",
|
||||
"sha256:564d2675682bd497b59907d2205031acbf7d3fadf8c763b689b9ede20300b215",
|
||||
"sha256:5d13bf5197a92149dc0badcc2b699267ff65a867029f465accfca8abab95f412",
|
||||
"sha256:5eda665f6789edb9b57b57a159b9c55482cbe5b046d7db458948370554b16439",
|
||||
"sha256:5edb2524d4032be4564c65dc4f9d01e79fe8fad5f966e5b552f4e5164fef0885",
|
||||
"sha256:79691794288bc51e2a3b8de2bc0272ca8355d0b8503077ea57c0716e840ebaef",
|
||||
"sha256:7fcc8681e9981b9b511cdee7c580d5b005f3bb86b65bde2188e04a29f1d63317",
|
||||
"sha256:8e447e05ec88b1b408a4c9cde85aa6f4b04f06aa874b9f0b8e8319faf51b1fee",
|
||||
"sha256:90ea6b3e7787620bb295a4ae050d2811c807d65b1486749414f78cfd6fb61489",
|
||||
"sha256:9e13239952694b8b831088431d15f771beace10edfcf9ef230cefea14f18508f",
|
||||
"sha256:d40f081187f7b54d7a99d8a5c782eaa4edc335a057aa54c85059272ed826dc09",
|
||||
"sha256:e1df1a58ed2468c7b7ce9a2f9752a32ad08eac2bcd56318625c3647c2cd2da6f",
|
||||
"sha256:e98d0cec437097f09c7834a11c69d79fe6241729b23f656cfc227e93294fc242",
|
||||
"sha256:f8d59627702d2ff27cb495ca1abdea8bd8d581de425c56e93bff6517134e0a9b",
|
||||
"sha256:fc30cdf2e949a2225b012a7911d1d031df3d23e99b7eda7dfc982dc4a860dae9"
|
||||
],
|
||||
"version": "==7.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
||||
],
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"atomicwrites": {
|
||||
"hashes": [
|
||||
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
|
||||
"sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
|
||||
],
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
|
||||
"sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
|
||||
],
|
||||
"version": "==18.2.0"
|
||||
},
|
||||
"autoflake": {
|
||||
"hashes": [
|
||||
"sha256:c103e63466f11db3617167a2c68ff6a0cda35b940222920631c6eeec6b67e807"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.2"
|
||||
},
|
||||
"backcall": {
|
||||
"hashes": [
|
||||
"sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4",
|
||||
"sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2"
|
||||
],
|
||||
"version": "==0.1.0"
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
"sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739",
|
||||
"sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==18.9b0"
|
||||
},
|
||||
"bleach": {
|
||||
"hashes": [
|
||||
"sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16",
|
||||
"sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa"
|
||||
],
|
||||
"version": "==3.1.0"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||
],
|
||||
"version": "==7.0"
|
||||
},
|
||||
"decorator": {
|
||||
"hashes": [
|
||||
"sha256:33cd704aea07b4c28b3eb2c97d288a06918275dac0ecebdaf1bc8a48d98adb9e",
|
||||
"sha256:cabb249f4710888a2fc0e13e9a16c343d932033718ff62e1e9bc93a9d3a9122b"
|
||||
],
|
||||
"version": "==4.3.2"
|
||||
},
|
||||
"defusedxml": {
|
||||
"hashes": [
|
||||
"sha256:24d7f2f94f7f3cb6061acb215685e5125fbcdc40a857eff9de22518820b0a4f4",
|
||||
"sha256:702a91ade2968a82beb0db1e0766a6a273f33d4616a6ce8cde475d8e09853b20"
|
||||
],
|
||||
"version": "==0.5.0"
|
||||
},
|
||||
"entrypoints": {
|
||||
"hashes": [
|
||||
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
|
||||
"sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"
|
||||
],
|
||||
"version": "==0.3"
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:c3ba1e130c813191db95c431a18cb4d20a468e98af7a77e2181b68574481ad36",
|
||||
"sha256:fd9ddf503110bf3d8b1d270e8c673aab29ccb3dd6abf29bae1f54e5116ab4a91"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.7.5"
|
||||
},
|
||||
"ipykernel": {
|
||||
"hashes": [
|
||||
"sha256:0aeb7ec277ac42cc2b59ae3d08b10909b2ec161dc6908096210527162b53675d",
|
||||
"sha256:0fc0bf97920d454102168ec2008620066878848fcfca06c22b669696212e292f"
|
||||
],
|
||||
"version": "==5.1.0"
|
||||
},
|
||||
"ipython": {
|
||||
"hashes": [
|
||||
"sha256:6a9496209b76463f1dec126ab928919aaf1f55b38beb9219af3fe202f6bbdd12",
|
||||
"sha256:f69932b1e806b38a7818d9a1e918e5821b685715040b48e59c657b3c7961b742"
|
||||
],
|
||||
"markers": "python_version >= '3.3'",
|
||||
"version": "==7.2.0"
|
||||
},
|
||||
"ipython-genutils": {
|
||||
"hashes": [
|
||||
"sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8",
|
||||
"sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"
|
||||
],
|
||||
"version": "==0.2.0"
|
||||
},
|
||||
"ipywidgets": {
|
||||
"hashes": [
|
||||
"sha256:0f2b5cde9f272cb49d52f3f0889fdd1a7ae1e74f37b48dac35a83152780d2b7b",
|
||||
"sha256:a3e224f430163f767047ab9a042fc55adbcab0c24bbe6cf9f306c4f89fdf0ba3"
|
||||
],
|
||||
"version": "==7.4.2"
|
||||
},
|
||||
"isort": {
|
||||
"hashes": [
|
||||
"sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af",
|
||||
"sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
|
||||
"sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.3.4"
|
||||
},
|
||||
"jedi": {
|
||||
"hashes": [
|
||||
"sha256:571702b5bd167911fe9036e5039ba67f820d6502832285cde8c881ab2b2149fd",
|
||||
"sha256:c8481b5e59d34a5c7c42e98f6625e633f6ef59353abea6437472c7ec2093f191"
|
||||
],
|
||||
"version": "==0.13.2"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
|
||||
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
|
||||
],
|
||||
"version": "==2.10"
|
||||
},
|
||||
"jsonschema": {
|
||||
"hashes": [
|
||||
"sha256:683fe7ed58763ea0be572de5aad47cd3cc1297640916f9a8ccd222b287da7d2f",
|
||||
"sha256:b42d7a292addb57370e6260bcbadb77e00a899fe6ec998c453f45893c41c658b"
|
||||
],
|
||||
"version": "==3.0.0b3"
|
||||
},
|
||||
"jupyter": {
|
||||
"hashes": [
|
||||
"sha256:3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7",
|
||||
"sha256:5b290f93b98ffbc21c0c7e749f054b3267782166d72fa5e3ed1ed4eaf34a2b78",
|
||||
"sha256:d9dc4b3318f310e34c82951ea5d6683f67bed7def4b259fafbfe4f1beb1d8e5f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"jupyter-client": {
|
||||
"hashes": [
|
||||
"sha256:b5f9cb06105c1d2d30719db5ffb3ea67da60919fb68deaefa583deccd8813551",
|
||||
"sha256:c44411eb1463ed77548bc2d5ec0d744c9b81c4a542d9637c7a52824e2121b987"
|
||||
],
|
||||
"version": "==5.2.4"
|
||||
},
|
||||
"jupyter-console": {
|
||||
"hashes": [
|
||||
"sha256:308ce876354924fb6c540b41d5d6d08acfc946984bf0c97777c1ddcb42e0b2f5",
|
||||
"sha256:cc80a97a5c389cbd30252ffb5ce7cefd4b66bde98219edd16bf5cb6f84bb3568"
|
||||
],
|
||||
"version": "==6.0.0"
|
||||
},
|
||||
"jupyter-core": {
|
||||
"hashes": [
|
||||
"sha256:927d713ffa616ea11972534411544589976b2493fc7e09ad946e010aa7eb9970",
|
||||
"sha256:ba70754aa680300306c699790128f6fbd8c306ee5927976cbe48adacf240c0b7"
|
||||
],
|
||||
"version": "==4.4.0"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432",
|
||||
"sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b",
|
||||
"sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9",
|
||||
"sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af",
|
||||
"sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834",
|
||||
"sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd",
|
||||
"sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d",
|
||||
"sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7",
|
||||
"sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b",
|
||||
"sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3",
|
||||
"sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c",
|
||||
"sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2",
|
||||
"sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7",
|
||||
"sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36",
|
||||
"sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1",
|
||||
"sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e",
|
||||
"sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1",
|
||||
"sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c",
|
||||
"sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856",
|
||||
"sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550",
|
||||
"sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492",
|
||||
"sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672",
|
||||
"sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401",
|
||||
"sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6",
|
||||
"sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6",
|
||||
"sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c",
|
||||
"sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd",
|
||||
"sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
||||
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
||||
],
|
||||
"version": "==0.6.1"
|
||||
},
|
||||
"mistune": {
|
||||
"hashes": [
|
||||
"sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e",
|
||||
"sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"
|
||||
],
|
||||
"version": "==0.8.4"
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4",
|
||||
"sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc",
|
||||
"sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"
|
||||
],
|
||||
"version": "==5.0.0"
|
||||
},
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
"sha256:308c274eb8482fbf16006f549137ddc0d69e5a589465e37b99c4564414363ca7",
|
||||
"sha256:e80fd6af34614a0e898a57f14296d0dacb584648f0339c2e000ddbf0f4cc2f8d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.670"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
"sha256:37e0e956f41369209a3d5f34580150bcacfabaa57b33a15c0b25f4b5725e0812",
|
||||
"sha256:b16cabe759f55e3409a7d231ebd2841378fb0c27a5d1994719e340e4f429ac3e"
|
||||
],
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"nbconvert": {
|
||||
"hashes": [
|
||||
"sha256:302554a2e219bc0fc84f3edd3e79953f3767b46ab67626fdec16e38ba3f7efe4",
|
||||
"sha256:5de8fb2284422272a1d45abc77c07b888127550a6d602ce619592a2b08a474ff"
|
||||
],
|
||||
"version": "==5.4.1"
|
||||
},
|
||||
"nbformat": {
|
||||
"hashes": [
|
||||
"sha256:b9a0dbdbd45bb034f4f8893cafd6f652ea08c8c1674ba83f2dc55d3955743b0b",
|
||||
"sha256:f7494ef0df60766b7cabe0a3651556345a963b74dbc16bc7c18479041170d402"
|
||||
],
|
||||
"version": "==4.4.0"
|
||||
},
|
||||
"notebook": {
|
||||
"hashes": [
|
||||
"sha256:3ab2db8bc10e6edbd264c3c4b800bee276c99818386ee0c146d98d7e6bcf0a67",
|
||||
"sha256:d908673a4010787625c8952e91a22adf737db031f2aa0793ad92f6558918a74a"
|
||||
],
|
||||
"version": "==5.7.4"
|
||||
},
|
||||
"pandocfilters": {
|
||||
"hashes": [
|
||||
"sha256:b3dd70e169bb5449e6bc6ff96aea89c5eea8c5f6ab5e207fc2f521a2cf4a0da9"
|
||||
],
|
||||
"version": "==1.4.2"
|
||||
},
|
||||
"parso": {
|
||||
"hashes": [
|
||||
"sha256:6ecf7244be8e7283ec9009c72d074830e7e0e611c974f813d76db0390a4e0dd6",
|
||||
"sha256:8162be7570ffb34ec0b8d215d7f3b6c5fab24f51eb3886d6dee362de96b6db94"
|
||||
],
|
||||
"version": "==0.3.3"
|
||||
},
|
||||
"pexpect": {
|
||||
"hashes": [
|
||||
"sha256:2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba",
|
||||
"sha256:3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b"
|
||||
],
|
||||
"markers": "sys_platform != 'win32'",
|
||||
"version": "==4.6.0"
|
||||
},
|
||||
"pickleshare": {
|
||||
"hashes": [
|
||||
"sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca",
|
||||
"sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"
|
||||
],
|
||||
"version": "==0.7.5"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616",
|
||||
"sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a"
|
||||
],
|
||||
"version": "==0.8.1"
|
||||
},
|
||||
"prometheus-client": {
|
||||
"hashes": [
|
||||
"sha256:e8c11ff5ca53de6c3d91e1510500611cafd1d247a937ec6c588a0a7cc3bef93c"
|
||||
],
|
||||
"version": "==0.5.0"
|
||||
},
|
||||
"prompt-toolkit": {
|
||||
"hashes": [
|
||||
"sha256:88002cc618cacfda8760c4539e76c3b3f148ecdb7035a3d422c7ecdc90c2a3ba",
|
||||
"sha256:c6655a12e9b08edb8cf5aeab4815fd1e1bdea4ad73d3bbf269cf2e0c4eb75d5e",
|
||||
"sha256:df5835fb8f417aa55e5cafadbaeb0cf630a1e824aad16989f9f0493e679ec010"
|
||||
],
|
||||
"version": "==2.0.8"
|
||||
},
|
||||
"ptyprocess": {
|
||||
"hashes": [
|
||||
"sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0",
|
||||
"sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"
|
||||
],
|
||||
"markers": "os_name != 'nt'",
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694",
|
||||
"sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"
|
||||
],
|
||||
"version": "==1.7.0"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
|
||||
"sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
|
||||
],
|
||||
"version": "==2.5.0"
|
||||
},
|
||||
"pyflakes": {
|
||||
"hashes": [
|
||||
"sha256:5e8c00e30c464c99e0b501dc160b13a14af7f27d4dffb529c556e30a159e231d",
|
||||
"sha256:f277f9ca3e55de669fba45b7393a1449009cff5a37d1af10ebb76c52765269cd"
|
||||
],
|
||||
"version": "==2.1.0"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a",
|
||||
"sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d"
|
||||
],
|
||||
"version": "==2.3.1"
|
||||
},
|
||||
"pyrsistent": {
|
||||
"hashes": [
|
||||
"sha256:07f7ae71291af8b0dbad8c2ab630d8223e4a8c4e10fc37badda158c02e753acf"
|
||||
],
|
||||
"version": "==0.14.10"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:65aeaa77ae87c7fc95de56285282546cfa9c886dc8e5dc78313db1c25e21bc07",
|
||||
"sha256:6ac6d467d9f053e95aaacd79f831dbecfe730f419c6c7022cb316b365cd9199d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.2.0"
|
||||
},
|
||||
"python-dateutil": {
|
||||
"hashes": [
|
||||
"sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb",
|
||||
"sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"
|
||||
],
|
||||
"version": "==2.8.0"
|
||||
},
|
||||
"pyzmq": {
|
||||
"hashes": [
|
||||
"sha256:25a0715c8f69cf72f67cfe5a68a3f3ed391c67c063d2257bec0fe7fc2c7f08f8",
|
||||
"sha256:2bab63759632c6b9e0d5bf19cc63c3b01df267d660e0abcf230cf0afaa966349",
|
||||
"sha256:30ab49d99b24bf0908ebe1cdfa421720bfab6f93174e4883075b7ff38cc555ba",
|
||||
"sha256:32c7ca9fc547a91e3c26fc6080b6982e46e79819e706eb414dd78f635a65d946",
|
||||
"sha256:41219ae72b3cc86d97557fe5b1ef5d1adc1057292ec597b50050874a970a39cf",
|
||||
"sha256:4b8c48a9a13cea8f1f16622f9bd46127108af14cd26150461e3eab71e0de3e46",
|
||||
"sha256:55724997b4a929c0d01b43c95051318e26ddbae23565018e138ae2dc60187e59",
|
||||
"sha256:65f0a4afae59d4fc0aad54a917ab599162613a761b760ba167d66cc646ac3786",
|
||||
"sha256:6f88591a8b246f5c285ee6ce5c1bf4f6bd8464b7f090b1333a446b6240a68d40",
|
||||
"sha256:75022a4c60dcd8765bb9ca32f6de75a0ec83b0d96e0309dc479f4c7b21f26cb7",
|
||||
"sha256:76ea493bfab18dcb090d825f3662b5612e2def73dffc196d51a5194b0294a81d",
|
||||
"sha256:7b60c045b80709e4e3c085bab9b691e71761b44c2b42dbb047b8b498e7bc16b3",
|
||||
"sha256:8e6af2f736734aef8ed6f278f9f552ec7f37b1a6b98e59b887484a840757f67d",
|
||||
"sha256:9ac2298e486524331e26390eac14e4627effd3f8e001d4266ed9d8f1d2d31cce",
|
||||
"sha256:9ba650f493a9bc1f24feca1d90fce0e5dd41088a252ac9840131dfbdbf3815ca",
|
||||
"sha256:a02a4a385e394e46012dc83d2e8fd6523f039bb52997c1c34a2e0dd49ed839c1",
|
||||
"sha256:a3ceee84114d9f5711fa0f4db9c652af0e4636c89eabc9b7f03a3882569dd1ed",
|
||||
"sha256:a72b82ac1910f2cf61a49139f4974f994984475f771b0faa730839607eeedddf",
|
||||
"sha256:ab136ac51027e7c484c53138a0fab4a8a51e80d05162eb7b1585583bcfdbad27",
|
||||
"sha256:c095b224300bcac61e6c445e27f9046981b1ac20d891b2f1714da89d34c637c8",
|
||||
"sha256:c5cc52d16c06dc2521340d69adda78a8e1031705924e103c0eb8fc8af861d810",
|
||||
"sha256:d612e9833a89e8177f8c1dc68d7b4ff98d3186cd331acd616b01bbdab67d3a7b",
|
||||
"sha256:e828376a23c66c6fe90dcea24b4b72cd774f555a6ee94081670872918df87a19",
|
||||
"sha256:e9767c7ab2eb552796440168d5c6e23a99ecaade08dda16266d43ad461730192",
|
||||
"sha256:ebf8b800d42d217e4710d1582b0c8bff20cdcb4faad7c7213e52644034300924"
|
||||
],
|
||||
"version": "==17.1.2"
|
||||
},
|
||||
"qtconsole": {
|
||||
"hashes": [
|
||||
"sha256:1ac4a65e81a27b0838330a6d351c2f8435d4013d98a95373e8a41119b2968390",
|
||||
"sha256:bc1ba15f50c29ed50f1268ad823bb6543be263c18dd093b80495e9df63b003ac"
|
||||
],
|
||||
"version": "==4.4.3"
|
||||
},
|
||||
"send2trash": {
|
||||
"hashes": [
|
||||
"sha256:60001cc07d707fe247c94f74ca6ac0d3255aabcb930529690897ca2a39db28b2",
|
||||
"sha256:f1691922577b6fa12821234aeb57599d887c4900b9ca537948d2dac34aea888b"
|
||||
],
|
||||
"version": "==1.5.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
|
||||
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
|
||||
],
|
||||
"version": "==1.12.0"
|
||||
},
|
||||
"terminado": {
|
||||
"hashes": [
|
||||
"sha256:55abf9ade563b8f9be1f34e4233c7b7bde726059947a593322e8a553cc4c067a",
|
||||
"sha256:65011551baff97f5414c67018e908110693143cfbaeb16831b743fe7cad8b927"
|
||||
],
|
||||
"version": "==0.8.1"
|
||||
},
|
||||
"testpath": {
|
||||
"hashes": [
|
||||
"sha256:46c89ebb683f473ffe2aab0ed9f12581d4d078308a3cb3765d79c6b2317b0109",
|
||||
"sha256:b694b3d9288dbd81685c5d2e7140b81365d46c29f5db4bc659de5aa6b98780f8"
|
||||
],
|
||||
"version": "==0.4.2"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
|
||||
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
|
||||
],
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"tornado": {
|
||||
"hashes": [
|
||||
"sha256:00ebd485a52bd7eaa3f35bdf8ab43c109aaa2edc722849b6905c1ffd8c958e82"
|
||||
],
|
||||
"version": "==6.0a1"
|
||||
},
|
||||
"traitlets": {
|
||||
"hashes": [
|
||||
"sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835",
|
||||
"sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"
|
||||
],
|
||||
"version": "==4.3.2"
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
"sha256:035a54ede6ce1380599b2ce57844c6554666522e376bd111eb940fbc7c3dad23",
|
||||
"sha256:037c35f2741ce3a9ac0d55abfcd119133cbd821fffa4461397718287092d9d15",
|
||||
"sha256:049feae7e9f180b64efacbdc36b3af64a00393a47be22fa9cb6794e68d4e73d3",
|
||||
"sha256:19228f7940beafc1ba21a6e8e070e0b0bfd1457902a3a81709762b8b9039b88d",
|
||||
"sha256:2ea681e91e3550a30c2265d2916f40a5f5d89b59469a20f3bad7d07adee0f7a6",
|
||||
"sha256:3a6b0a78af298d82323660df5497bcea0f0a4a25a0b003afd0ce5af049bd1f60",
|
||||
"sha256:5385da8f3b801014504df0852bf83524599df890387a3c2b17b7caa3d78b1773",
|
||||
"sha256:606d8afa07eef77280c2bf84335e24390055b478392e1975f96286d99d0cb424",
|
||||
"sha256:69245b5b23bbf7fb242c9f8f08493e9ecd7711f063259aefffaeb90595d62287",
|
||||
"sha256:6f6d839ab09830d59b7fa8fb6917023d8cb5498ee1f1dbd82d37db78eb76bc99",
|
||||
"sha256:730888475f5ac0e37c1de4bd05eeb799fdb742697867f524dc8a4cd74bcecc23",
|
||||
"sha256:9819b5162ffc121b9e334923c685b0d0826154e41dfe70b2ede2ce29034c71d8",
|
||||
"sha256:9e60ef9426efab601dd9aa120e4ff560f4461cf8442e9c0a2b92548d52800699",
|
||||
"sha256:af5fbdde0690c7da68e841d7fc2632345d570768ea7406a9434446d7b33b0ee1",
|
||||
"sha256:b64efdbdf3bbb1377562c179f167f3bf301251411eb5ac77dec6b7d32bcda463",
|
||||
"sha256:bac5f444c118aeb456fac1b0b5d14c6a71ea2a42069b09c176f75e9bd4c186f6",
|
||||
"sha256:bda9068aafb73859491e13b99b682bd299c1b5fd50644d697533775828a28ee0",
|
||||
"sha256:d659517ca116e6750101a1326107d3479028c5191f0ecee3c7203c50f5b915b0",
|
||||
"sha256:eddd3fb1f3e0f82e5915a899285a39ee34ce18fd25d89582bc89fc9fb16cd2c6"
|
||||
],
|
||||
"version": "==1.3.1"
|
||||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
|
||||
"sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
|
||||
],
|
||||
"version": "==0.1.7"
|
||||
},
|
||||
"webencodings": {
|
||||
"hashes": [
|
||||
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
|
||||
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
|
||||
],
|
||||
"version": "==0.5.1"
|
||||
},
|
||||
"widgetsnbextension": {
|
||||
"hashes": [
|
||||
"sha256:14b2c65f9940c9a7d3b70adbe713dbd38b5ec69724eebaba034d1036cf3d4740",
|
||||
"sha256:fa618be8435447a017fd1bf2c7ae922d0428056cfc7449f7a8641edf76b48265"
|
||||
],
|
||||
"version": "==3.4.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
{{cookiecutter.project_slug}}/backend/app/alembic/README
Executable file
1
{{cookiecutter.project_slug}}/backend/app/alembic/README
Executable file
@@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
||||
76
{{cookiecutter.project_slug}}/backend/app/alembic/env.py
Executable file
76
{{cookiecutter.project_slug}}/backend/app/alembic/env.py
Executable file
@@ -0,0 +1,76 @@
|
||||
from __future__ import with_statement
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from logging.config import fileConfig
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
# target_metadata = None
|
||||
|
||||
from app.db.base import Base # noqa
|
||||
|
||||
target_metadata = Base.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url, target_metadata=target_metadata, literal_binds=True, compare_type=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection, target_metadata=target_metadata, compare_type=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
24
{{cookiecutter.project_slug}}/backend/app/alembic/script.py.mako
Executable file
24
{{cookiecutter.project_slug}}/backend/app/alembic/script.py.mako
Executable file
@@ -0,0 +1,24 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
||||
0
{{cookiecutter.project_slug}}/backend/app/alembic/versions/.keep
Executable file
0
{{cookiecutter.project_slug}}/backend/app/alembic/versions/.keep
Executable file
@@ -0,0 +1,12 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.api_v1.endpoints.role import router as roles_router
|
||||
from app.api.api_v1.endpoints.token import router as token_router
|
||||
from app.api.api_v1.endpoints.user import router as user_router
|
||||
from app.api.api_v1.endpoints.utils import router as utils_router
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(roles_router)
|
||||
api_router.include_router(token_router)
|
||||
api_router.include_router(user_router)
|
||||
api_router.include_router(utils_router)
|
||||
@@ -0,0 +1,25 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from app.core.jwt import get_current_user
|
||||
from app.crud.user import check_if_user_is_active, check_if_user_is_superuser
|
||||
from app.crud.utils import ensure_enums_to_strs
|
||||
from app.models.role import RoleEnum, Roles
|
||||
from app.models.user import UserInDB
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/roles/", response_model=Roles)
|
||||
def route_roles_get(current_user: UserInDB = Depends(get_current_user)):
|
||||
"""
|
||||
Retrieve roles
|
||||
"""
|
||||
if not check_if_user_is_active(current_user):
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
elif not (check_if_user_is_superuser(current_user)):
|
||||
raise HTTPException(
|
||||
status_code=400, detail="The current user does not have enogh privileges"
|
||||
)
|
||||
roles = ensure_enums_to_strs(RoleEnum)
|
||||
return {"roles": roles}
|
||||
@@ -0,0 +1,96 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from app.core import config
|
||||
from app.core.jwt import create_access_token, get_current_user
|
||||
from app.crud.user import (
|
||||
authenticate_user,
|
||||
check_if_user_is_active,
|
||||
check_if_user_is_superuser,
|
||||
get_user,
|
||||
update_user,
|
||||
)
|
||||
from app.db.database import get_default_bucket
|
||||
from app.models.msg import Msg
|
||||
from app.models.token import Token
|
||||
from app.models.user import User, UserInDB, UserInUpdate
|
||||
from app.utils import (
|
||||
generate_password_reset_token,
|
||||
send_reset_password_email,
|
||||
verify_password_reset_token,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/login/access-token", response_model=Token, tags=["login"])
|
||||
def route_login_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
"""
|
||||
OAuth2 compatible token login, get an access token for future requests
|
||||
"""
|
||||
bucket = get_default_bucket()
|
||||
user = authenticate_user(bucket, form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(status_code=400, detail="Incorrect email or password")
|
||||
elif not check_if_user_is_active(user):
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
access_token_expires = timedelta(minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
return {
|
||||
"access_token": create_access_token(
|
||||
data={"username": form_data.username}, expires_delta=access_token_expires
|
||||
),
|
||||
"token_type": "bearer",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/login/test-token", tags=["login"], response_model=User)
|
||||
def route_test_token(current_user: UserInDB = Depends(get_current_user)):
|
||||
"""
|
||||
Test access token
|
||||
"""
|
||||
return current_user
|
||||
|
||||
|
||||
@router.post("/password-recovery/{username}", tags=["login"], response_model=Msg)
|
||||
def route_recover_password(username: str):
|
||||
"""
|
||||
Password Recovery
|
||||
"""
|
||||
bucket = get_default_bucket()
|
||||
user = get_user(bucket, username)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="The user with this username does not exist in the system.",
|
||||
)
|
||||
password_reset_token = generate_password_reset_token(username)
|
||||
send_reset_password_email(
|
||||
email_to=user.email, username=username, token=password_reset_token
|
||||
)
|
||||
return {"msg": "Password recovery email sent"}
|
||||
|
||||
|
||||
@router.post("/reset-password/", tags=["login"], response_model=Msg)
|
||||
def route_reset_password(token: str, new_password: str):
|
||||
"""
|
||||
Reset password
|
||||
"""
|
||||
username = verify_password_reset_token(token)
|
||||
if not username:
|
||||
raise HTTPException(status_code=400, detail="Invalid token")
|
||||
bucket = get_default_bucket()
|
||||
user = get_user(bucket, username)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="The user with this username does not exist in the system.",
|
||||
)
|
||||
elif not check_if_user_is_active(user):
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
user_in = UserInUpdate(name=username, password=new_password)
|
||||
user = update_user(bucket, user_in)
|
||||
return {"msg": "Password updated successfully"}
|
||||
@@ -0,0 +1,205 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Body, Depends
|
||||
from pydantic.types import EmailStr
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from app.core import config
|
||||
from app.core.jwt import get_current_user
|
||||
from app.crud.user import (
|
||||
check_if_user_is_active,
|
||||
check_if_user_is_superuser,
|
||||
get_user,
|
||||
get_users,
|
||||
search_users,
|
||||
update_user,
|
||||
upsert_user,
|
||||
)
|
||||
from app.db.database import get_default_bucket
|
||||
from app.models.user import User, UserInCreate, UserInDB, UserInUpdate
|
||||
from app.utils import send_new_account_email
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/users/", tags=["users"], response_model=List[User])
|
||||
def route_users_get(
|
||||
skip: int = 0, limit: int = 100, current_user: UserInDB = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Retrieve users
|
||||
"""
|
||||
if not check_if_user_is_active(current_user):
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
elif not check_if_user_is_superuser(current_user):
|
||||
raise HTTPException(
|
||||
status_code=400, detail="The user doesn't have enough privileges"
|
||||
)
|
||||
bucket = get_default_bucket()
|
||||
users = get_users(bucket, skip=skip, limit=limit)
|
||||
return users
|
||||
|
||||
|
||||
@router.get("/users/search/", tags=["users"], response_model=List[User])
|
||||
def route_search_users(
|
||||
q: str,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Search users, use Bleve Query String syntax: http://blevesearch.com/docs/Query-String-Query/
|
||||
|
||||
For typeahead sufix with `*`. For example, a query with: `email:johnd*` will match users with
|
||||
email `johndoe@example.com`, `johndid@example.net`, etc.
|
||||
"""
|
||||
if not check_if_user_is_active(current_user):
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
elif not check_if_user_is_superuser(current_user):
|
||||
raise HTTPException(
|
||||
status_code=400, detail="The user doesn't have enough privileges"
|
||||
)
|
||||
bucket = get_default_bucket()
|
||||
users = search_users(bucket=bucket, query_string=q, skip=skip, limit=limit)
|
||||
return users
|
||||
|
||||
|
||||
@router.post("/users/", tags=["users"], response_model=User)
|
||||
def route_users_post(
|
||||
*, user_in: UserInCreate, current_user: UserInDB = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Create new user
|
||||
"""
|
||||
if not check_if_user_is_active(current_user):
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
elif not check_if_user_is_superuser(current_user):
|
||||
raise HTTPException(
|
||||
status_code=400, detail="The user doesn't have enough privileges"
|
||||
)
|
||||
bucket = get_default_bucket()
|
||||
user = get_user(bucket, user_in.username)
|
||||
if user:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="The user with this username already exists in the system.",
|
||||
)
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
if config.EMAILS_ENABLED and user_in.email:
|
||||
send_new_account_email(
|
||||
email_to=user_in.email, username=user_in.username, password=user_in.password
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
@router.put("/users/me", tags=["users"], response_model=User)
|
||||
def route_users_me_put(
|
||||
*,
|
||||
password: str = None,
|
||||
full_name: str = None,
|
||||
email: EmailStr = None,
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Update own user
|
||||
"""
|
||||
if not check_if_user_is_active(current_user):
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
user_in = UserInUpdate(**current_user.dict())
|
||||
if password is not None:
|
||||
user_in.password = password
|
||||
if full_name is not None:
|
||||
user_in.full_name = full_name
|
||||
if email is not None:
|
||||
user_in.email = email
|
||||
bucket = get_default_bucket()
|
||||
user = update_user(bucket, user_in)
|
||||
return user
|
||||
|
||||
|
||||
@router.get("/users/me", tags=["users"], response_model=User)
|
||||
def route_users_me_get(current_user: UserInDB = Depends(get_current_user)):
|
||||
"""
|
||||
Get current user
|
||||
"""
|
||||
if not check_if_user_is_active(current_user):
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
return current_user
|
||||
|
||||
|
||||
@router.post("/users/open", tags=["users"], response_model=User)
|
||||
def route_users_post_open(
|
||||
*,
|
||||
username: str = Body(...),
|
||||
password: str = Body(...),
|
||||
email: EmailStr = Body(None),
|
||||
full_name: str = Body(None),
|
||||
):
|
||||
"""
|
||||
Create new user without the need to be logged in
|
||||
"""
|
||||
if not config.USERS_OPEN_REGISTRATION:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Open user resgistration is forbidden on this server",
|
||||
)
|
||||
bucket = get_default_bucket()
|
||||
user = get_user(bucket, username)
|
||||
if user:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="The user with this username already exists in the system",
|
||||
)
|
||||
user_in = UserInCreate(
|
||||
username=username, password=password, email=email, full_name=full_name
|
||||
)
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
return user
|
||||
|
||||
|
||||
@router.get("/users/{username}", tags=["users"], response_model=User)
|
||||
def route_users_id_get(
|
||||
username: str, current_user: UserInDB = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get a specific user by username (email)
|
||||
"""
|
||||
if not check_if_user_is_active(current_user):
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
bucket = get_default_bucket()
|
||||
user = get_user(bucket, username)
|
||||
if user == current_user:
|
||||
return user
|
||||
if not check_if_user_is_superuser(current_user):
|
||||
raise HTTPException(
|
||||
status_code=400, detail="The user doesn't have enough privileges"
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
@router.put("/users/{username}", tags=["users"], response_model=User)
|
||||
def route_users_put(
|
||||
*,
|
||||
username: str,
|
||||
user_in: UserInUpdate,
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Update a user
|
||||
"""
|
||||
if not check_if_user_is_active(current_user):
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
elif not check_if_user_is_superuser(current_user):
|
||||
raise HTTPException(
|
||||
status_code=400, detail="The user doesn't have enough privileges"
|
||||
)
|
||||
bucket = get_default_bucket()
|
||||
user = get_user(bucket, username)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="The user with this username does not exist in the system",
|
||||
)
|
||||
user = update_user(bucket, user_in)
|
||||
return user
|
||||
@@ -0,0 +1,36 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic.types import EmailStr
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from app.core.celery_app import celery_app
|
||||
from app.core.jwt import get_current_user
|
||||
from app.crud.user import check_if_user_is_superuser
|
||||
from app.models.msg import Msg
|
||||
from app.models.user import UserInDB
|
||||
from app.utils import send_test_email
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/test-celery/", tags=["utils"], response_model=Msg, status_code=201)
|
||||
def route_test_celery(msg: Msg, current_user: UserInDB = Depends(get_current_user)):
|
||||
"""
|
||||
Test Celery worker
|
||||
"""
|
||||
if not check_if_user_is_superuser(current_user):
|
||||
raise HTTPException(status_code=400, detail="Not a superuser")
|
||||
celery_app.send_task("app.worker.test_celery", args=[msg.msg])
|
||||
return {"msg": "Word received"}
|
||||
|
||||
|
||||
@router.post("/test-email/", tags=["utils"], response_model=Msg, status_code=201)
|
||||
def route_test_email(
|
||||
email_to: EmailStr, current_user: UserInDB = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Test emails
|
||||
"""
|
||||
if not check_if_user_is_superuser(current_user):
|
||||
raise HTTPException(status_code=400, detail="Not a superuser")
|
||||
send_test_email(email_to=email_to)
|
||||
return {"msg": "Test email sent"}
|
||||
@@ -0,0 +1,32 @@
|
||||
import logging
|
||||
|
||||
from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed
|
||||
|
||||
from app.db.external_session import db_session
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
max_tries = 60 * 5 # 5 minutes
|
||||
wait_seconds = 1
|
||||
|
||||
|
||||
@retry(
|
||||
stop=stop_after_attempt(max_tries),
|
||||
wait=wait_fixed(wait_seconds),
|
||||
before=before_log(logger, logging.INFO),
|
||||
after=after_log(logger, logging.WARN),
|
||||
)
|
||||
def init():
|
||||
# Try to create session to check if DB is awake
|
||||
db_session.execute("SELECT 1")
|
||||
|
||||
|
||||
def main():
|
||||
logger.info("Initializing service")
|
||||
init()
|
||||
logger.info("Service finished initializing")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,32 @@
|
||||
import logging
|
||||
|
||||
from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed
|
||||
|
||||
from app.db.external_session import db_session
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
max_tries = 60 * 5 # 5 minutes
|
||||
wait_seconds = 1
|
||||
|
||||
|
||||
@retry(
|
||||
stop=stop_after_attempt(max_tries),
|
||||
wait=wait_fixed(wait_seconds),
|
||||
before=before_log(logger, logging.INFO),
|
||||
after=after_log(logger, logging.WARN),
|
||||
)
|
||||
def init():
|
||||
# Try to create session to check if DB is awake
|
||||
db_session.execute("SELECT 1")
|
||||
|
||||
|
||||
def main():
|
||||
logger.info("Initializing service")
|
||||
init()
|
||||
logger.info("Service finished initializing")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,5 @@
|
||||
from celery import Celery
|
||||
|
||||
celery_app = Celery("worker", broker="amqp://guest@queue//")
|
||||
|
||||
celery_app.conf.task_routes = {"app.worker.test_celery": "main-queue"}
|
||||
78
{{cookiecutter.project_slug}}/backend/app/app/core/config.py
Normal file
78
{{cookiecutter.project_slug}}/backend/app/app/core/config.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import os
|
||||
|
||||
|
||||
def getenv_boolean(var_name, default_value=False):
|
||||
result = default_value
|
||||
env_value = os.getenv(var_name)
|
||||
if env_value is not None:
|
||||
result = env_value.upper() in ("TRUE", "1")
|
||||
return result
|
||||
|
||||
|
||||
API_V1_STR = "/api/v1"
|
||||
|
||||
SECRET_KEY = os.getenvb(b"SECRET_KEY")
|
||||
if not SECRET_KEY:
|
||||
SECRET_KEY = os.urandom(32)
|
||||
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 8 # 60 minutes * 24 hours * 8 days = 8 days
|
||||
|
||||
SERVER_NAME = os.getenv("SERVER_NAME")
|
||||
SERVER_HOST = os.getenv("SERVER_HOST")
|
||||
BACKEND_CORS_ORIGINS = os.getenv(
|
||||
"BACKEND_CORS_ORIGINS"
|
||||
) # a string of origins separated by commas, e.g: "http://localhost, http://localhost:4200, http://localhost:3000, http://localhost:8080, http://dev.couchbase-project.com, https://stag.couchbase-project.com, https://couchbase-project.com, http://local.dockertoolbox.tiangolo.com"
|
||||
PROJECT_NAME = os.getenv("PROJECT_NAME")
|
||||
SENTRY_DSN = os.getenv("SENTRY_DSN")
|
||||
|
||||
# Couchbase server settings
|
||||
COUCHBASE_MEMORY_QUOTA_MB = os.getenv("COUCHBASE_MEMORY_QUOTA_MB", "256")
|
||||
COUCHBASE_INDEX_MEMORY_QUOTA_MB = os.getenv("COUCHBASE_INDEX_MEMORY_QUOTA_MB" "256")
|
||||
COUCHBASE_FTS_MEMORY_QUOTA_MB = os.getenv("COUCHBASE_FTS_MEMORY_QUOTA_MB", "256")
|
||||
COUCHBASE_HOST = os.getenv("COUCHBASE_HOST", "couchbase")
|
||||
COUCHBASE_PORT = os.getenv("COUCHBASE_PORT", "8091")
|
||||
COUCHBASE_FULL_TEXT_PORT = os.getenv("COUCHBASE_FULL_TEXT_PORT", "8094")
|
||||
COUCHBASE_ENTERPRISE = getenv_boolean("COUCHBASE_ENTERPRISE")
|
||||
COUCHBASE_USER = os.getenv("COUCHBASE_USER", "Administrator")
|
||||
COUCHBASE_PASSWORD = os.getenv("COUCHBASE_PASSWORD", "password")
|
||||
COUCHBASE_BUCKET_NAME = os.getenv("COUCHBASE_BUCKET_NAME", "app")
|
||||
|
||||
COUCHBASE_SYNC_GATEWAY_HOST = os.getenv("COUCHBASE_SYNC_GATEWAY_HOST", "sync-gateway")
|
||||
COUCHBASE_SYNC_GATEWAY_PORT = os.getenv("COUCHBASE_SYNC_GATEWAY_PORT", "4985")
|
||||
COUCHBASE_SYNC_GATEWAY_USER = os.getenv("COUCHBASE_SYNC_GATEWAY_USER")
|
||||
COUCHBASE_SYNC_GATEWAY_PASSWORD = os.getenv("COUCHBASE_SYNC_GATEWAY_PASSWORD")
|
||||
COUCHBASE_SYNC_GATEWAY_DATABASE = os.getenv("COUCHBASE_SYNC_GATEWAY_DATABASE")
|
||||
|
||||
# Couchbase query timeouts
|
||||
COUCHBASE_DURABILITY_TIMEOUT_SECS = 60.0
|
||||
COUCHBASE_OPERATION_TIMEOUT_SECS = 30.0
|
||||
COUCHBASE_N1QL_TIMEOUT_SECS = 300.0
|
||||
|
||||
|
||||
# Couchbase Sync Gateway settings
|
||||
COUCHBASE_CORS_ORIGINS = os.getenv("COUCHBASE_CORS_ORIGINS")
|
||||
# a string of origins separated by commas, e.g: "http://localhost:5984, http://localhost, http://localhost:4200, http://localhost:3000, http://localhost:8080, http://dev.couchbase-project.com, https://stag.couchbase-project.com, https://db.stag.couchbase-project.com, https://couchbase-project.com, https://db.couchbase-project.com, http://local.dockertoolbox.tiangolo.com, http://local.dockertoolbox.tiangolo.com:5984"
|
||||
COUCHBASE_AUTH_TIMEOUT = ACCESS_TOKEN_EXPIRE_MINUTES * 60
|
||||
|
||||
COUCHBASE_FULL_TEXT_INDEX_DEFINITIONS_DIR = "/app/app/search_index_definitions/"
|
||||
|
||||
SMTP_TLS = getenv_boolean("SMTP_TLS", True)
|
||||
SMTP_PORT = None
|
||||
_SMTP_PORT = os.getenv("SMTP_PORT")
|
||||
if _SMTP_PORT is not None:
|
||||
SMTP_PORT = int(_SMTP_PORT)
|
||||
SMTP_HOST = os.getenv("SMTP_HOST")
|
||||
SMTP_USER = os.getenv("SMTP_USER")
|
||||
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD")
|
||||
EMAILS_FROM_EMAIL = os.getenv("EMAILS_FROM_EMAIL")
|
||||
EMAILS_FROM_NAME = PROJECT_NAME
|
||||
EMAIL_RESET_TOKEN_EXPIRE_HOURS = 48
|
||||
EMAIL_TEMPLATES_DIR = "/app/app/email-templates/build"
|
||||
EMAILS_ENABLED = SMTP_HOST and SMTP_PORT and EMAILS_FROM_EMAIL
|
||||
|
||||
ROLE_SUPERUSER = "superuser"
|
||||
|
||||
FIRST_SUPERUSER = os.getenv("FIRST_SUPERUSER")
|
||||
FIRST_SUPERUSER_PASSWORD = os.getenv("FIRST_SUPERUSER_PASSWORD")
|
||||
|
||||
USERS_OPEN_REGISTRATION = getenv_boolean("USERS_OPEN_REGISTRATION")
|
||||
44
{{cookiecutter.project_slug}}/backend/app/app/core/jwt.py
Normal file
44
{{cookiecutter.project_slug}}/backend/app/app/core/jwt.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import jwt
|
||||
from fastapi import Security
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jwt import PyJWTError
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
|
||||
from app.core.config import SECRET_KEY
|
||||
from app.crud.user import get_user
|
||||
from app.db.database import get_default_bucket
|
||||
from app.models.token import TokenPayload
|
||||
|
||||
ALGORITHM = "HS256"
|
||||
access_token_jwt_subject = "access"
|
||||
|
||||
reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v1/login/access-token")
|
||||
|
||||
|
||||
def get_current_user(token: str = Security(reusable_oauth2)):
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
token_data = TokenPayload(**payload)
|
||||
except PyJWTError:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
|
||||
)
|
||||
bucket = get_default_bucket()
|
||||
user = get_user(bucket, username=token_data.username)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return user
|
||||
|
||||
|
||||
def create_access_token(*, data: dict, expires_delta: timedelta = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||
to_encode.update({"exp": expire, "sub": access_token_jwt_subject})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
@@ -0,0 +1,11 @@
|
||||
from passlib.context import CryptContext
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def get_password_hash(password):
|
||||
return pwd_context.hash(password)
|
||||
91
{{cookiecutter.project_slug}}/backend/app/app/crud/user.py
Normal file
91
{{cookiecutter.project_slug}}/backend/app/app/crud/user.py
Normal file
@@ -0,0 +1,91 @@
|
||||
from app.core.security import get_password_hash
|
||||
from app.models.role import Role
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
def get_user(username, db_session):
|
||||
return db_session.query(User).filter(User.id == username).first()
|
||||
|
||||
|
||||
def check_if_user_is_active(user):
|
||||
return user.is_active
|
||||
|
||||
|
||||
def check_if_user_is_superuser(user):
|
||||
return user.is_superuser
|
||||
|
||||
|
||||
def check_if_username_is_active(username, db_session):
|
||||
user = get_user(username, db_session)
|
||||
return check_if_user_is_active(user)
|
||||
|
||||
|
||||
def get_role_by_name(name, db_session):
|
||||
role = db_session.query(Role).filter(Role.name == name).first()
|
||||
return role
|
||||
|
||||
|
||||
def get_role_by_id(role_id, db_session):
|
||||
role = db_session.query(Role).filter(Role.id == role_id).first()
|
||||
return role
|
||||
|
||||
|
||||
def create_role(name, db_session):
|
||||
role = Role(name=name)
|
||||
db_session.add(role)
|
||||
db_session.commit()
|
||||
return role
|
||||
|
||||
|
||||
def get_roles(db_session):
|
||||
return db_session.query(Role).all()
|
||||
|
||||
|
||||
def get_user_roles(user):
|
||||
return user.roles
|
||||
|
||||
|
||||
def get_user_by_username(username, db_session) -> User:
|
||||
user = db_session.query(User).filter(User.email == username).first() # type: User
|
||||
return user
|
||||
|
||||
|
||||
def get_user_by_id(user_id, db_session):
|
||||
user = db_session.query(User).filter(User.id == user_id).first() # type: User
|
||||
return user
|
||||
|
||||
|
||||
def get_user_hashed_password(user):
|
||||
return user.password
|
||||
|
||||
|
||||
def get_user_id(user):
|
||||
return user.id
|
||||
|
||||
|
||||
def get_users(db_session):
|
||||
return db_session.query(User).all()
|
||||
|
||||
|
||||
def create_user(
|
||||
db_session, username, password, first_name=None, last_name=None, is_superuser=False
|
||||
):
|
||||
user = User(
|
||||
email=username,
|
||||
password=get_password_hash(password),
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
is_superuser=is_superuser,
|
||||
)
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
db_session.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
def assign_role_to_user(role: Role, user: User, db_session):
|
||||
user.roles.append(role)
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
db_session.refresh(user)
|
||||
return user
|
||||
214
{{cookiecutter.project_slug}}/backend/app/app/crud/utils.py
Normal file
214
{{cookiecutter.project_slug}}/backend/app/app/crud/utils.py
Normal file
@@ -0,0 +1,214 @@
|
||||
import uuid
|
||||
from enum import Enum
|
||||
from typing import List, Sequence, Type, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic.fields import Field, Shape
|
||||
|
||||
from app.core.config import COUCHBASE_BUCKET_NAME
|
||||
from couchbase.bucket import Bucket
|
||||
from couchbase.fulltext import MatchAllQuery, QueryStringQuery
|
||||
from couchbase.n1ql import CONSISTENCY_REQUEST, N1QLQuery
|
||||
|
||||
|
||||
def generate_new_id():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def ensure_enums_to_strs(items: Union[Sequence[Union[Enum, str]], Type[Enum]]):
|
||||
str_items = []
|
||||
for item in items:
|
||||
if isinstance(item, Enum):
|
||||
str_items.append(str(item.value))
|
||||
else:
|
||||
str_items.append(str(item))
|
||||
return str_items
|
||||
|
||||
|
||||
def get_all_documents_by_type(bucket: Bucket, *, doc_type: str, skip=0, limit=100):
|
||||
query_str = f"SELECT *, META().id as id FROM {COUCHBASE_BUCKET_NAME} WHERE type = $type LIMIT $limit OFFSET $skip;"
|
||||
q = N1QLQuery(
|
||||
query_str, bucket=COUCHBASE_BUCKET_NAME, type=doc_type, limit=limit, skip=skip
|
||||
)
|
||||
q.consistency = CONSISTENCY_REQUEST
|
||||
result = bucket.n1ql_query(q)
|
||||
return result
|
||||
|
||||
|
||||
def get_documents_by_keys(
|
||||
bucket: Bucket, *, keys: List[str], doc_model=Type[BaseModel]
|
||||
):
|
||||
results = bucket.get_multi(keys, quiet=True)
|
||||
docs = []
|
||||
for result in results.values():
|
||||
doc = doc_model(**result.value)
|
||||
docs.append(doc)
|
||||
return docs
|
||||
|
||||
|
||||
def results_to_model(results_from_couchbase: list, *, doc_model: Type[BaseModel]):
|
||||
items = []
|
||||
for doc in results_from_couchbase:
|
||||
data = doc[COUCHBASE_BUCKET_NAME]
|
||||
doc = doc_model(**data)
|
||||
items.append(doc)
|
||||
return items
|
||||
|
||||
|
||||
def search_results_to_model(
|
||||
results_from_couchbase: list, *, doc_model: Type[BaseModel]
|
||||
):
|
||||
items = []
|
||||
for doc in results_from_couchbase:
|
||||
data = doc.get("fields")
|
||||
if not data:
|
||||
continue
|
||||
data_nones = {}
|
||||
for key, value in data.items():
|
||||
field: Field = doc_model.__fields__[key]
|
||||
if not value:
|
||||
value = None
|
||||
elif field.shape in {Shape.LIST, Shape.SET, Shape.TUPLE} and not isinstance(
|
||||
value, list
|
||||
):
|
||||
value = [value]
|
||||
data_nones[key] = value
|
||||
doc = doc_model(**data_nones)
|
||||
items.append(doc)
|
||||
return items
|
||||
|
||||
|
||||
def get_docs(
|
||||
bucket: Bucket, *, doc_type: str, doc_model=Type[BaseModel], skip=0, limit=100
|
||||
):
|
||||
doc_results = get_all_documents_by_type(
|
||||
bucket, doc_type=doc_type, skip=skip, limit=limit
|
||||
)
|
||||
return results_to_model(doc_results, doc_model=doc_model)
|
||||
|
||||
|
||||
def get_doc(bucket: Bucket, *, doc_id: str, doc_model: Type[BaseModel]):
|
||||
result = bucket.get(doc_id, quiet=True)
|
||||
if not result.value:
|
||||
return None
|
||||
model = doc_model(**result.value)
|
||||
return model
|
||||
|
||||
|
||||
def search_docs_get_doc_ids(
|
||||
bucket: Bucket,
|
||||
*,
|
||||
query_string: str,
|
||||
index_name: str,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
):
|
||||
query = QueryStringQuery(query_string)
|
||||
hits = bucket.search(index_name, query, skip=skip, limit=limit)
|
||||
doc_ids = []
|
||||
for hit in hits:
|
||||
doc_ids.append(hit["id"])
|
||||
return doc_ids
|
||||
|
||||
|
||||
def search_get_results(
|
||||
bucket: Bucket,
|
||||
*,
|
||||
query_string: str,
|
||||
index_name: str,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
):
|
||||
if query_string:
|
||||
query = QueryStringQuery(query_string)
|
||||
else:
|
||||
query = MatchAllQuery()
|
||||
hits = bucket.search(index_name, query, fields=["*"], skip=skip, limit=limit)
|
||||
docs = []
|
||||
for hit in hits:
|
||||
docs.append(hit)
|
||||
return docs
|
||||
|
||||
|
||||
def search_get_results_by_type(
|
||||
bucket: Bucket,
|
||||
*,
|
||||
query_string: str,
|
||||
index_name: str,
|
||||
doc_type: str,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
):
|
||||
type_filter = f"type:{doc_type}"
|
||||
if not query_string:
|
||||
query_string = type_filter
|
||||
if query_string and type_filter not in query_string:
|
||||
query_string += f" {type_filter}"
|
||||
query = QueryStringQuery(query_string)
|
||||
hits = bucket.search(index_name, query, fields=["*"], skip=skip, limit=limit)
|
||||
docs = []
|
||||
for hit in hits:
|
||||
docs.append(hit)
|
||||
return docs
|
||||
|
||||
|
||||
def search_docs(
|
||||
bucket: Bucket,
|
||||
*,
|
||||
query_string: str,
|
||||
index_name: str,
|
||||
doc_model: Type[BaseModel],
|
||||
skip=0,
|
||||
limit=100,
|
||||
):
|
||||
keys = search_docs_get_doc_ids(
|
||||
bucket=bucket,
|
||||
query_string=query_string,
|
||||
index_name=index_name,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
if not keys:
|
||||
return []
|
||||
doc_results = get_documents_by_keys(bucket=bucket, keys=keys, doc_model=doc_model)
|
||||
return doc_results
|
||||
|
||||
|
||||
def search_results(
|
||||
bucket: Bucket,
|
||||
*,
|
||||
query_string: str,
|
||||
index_name: str,
|
||||
doc_model: Type[BaseModel],
|
||||
skip=0,
|
||||
limit=100,
|
||||
):
|
||||
doc_results = search_get_results(
|
||||
bucket=bucket,
|
||||
query_string=query_string,
|
||||
index_name=index_name,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
return search_results_to_model(doc_results, doc_model=doc_model)
|
||||
|
||||
|
||||
def search_results_by_type(
|
||||
bucket: Bucket,
|
||||
*,
|
||||
query_string: str,
|
||||
index_name: str,
|
||||
doc_type: str,
|
||||
doc_model: Type[BaseModel],
|
||||
skip=0,
|
||||
limit=100,
|
||||
):
|
||||
doc_results = search_get_results_by_type(
|
||||
bucket=bucket,
|
||||
query_string=query_string,
|
||||
index_name=index_name,
|
||||
doc_type=doc_type,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
return search_results_to_model(doc_results, doc_model=doc_model)
|
||||
5
{{cookiecutter.project_slug}}/backend/app/app/db/base.py
Normal file
5
{{cookiecutter.project_slug}}/backend/app/app/db/base.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# Import all the models, so that Base has them before being
|
||||
# imported by Alembic
|
||||
from app.db.base_class import Base # noqa
|
||||
from app.models.role import Role # noqa
|
||||
from app.models.user import User # noqa
|
||||
@@ -0,0 +1,11 @@
|
||||
from sqlalchemy.ext.declarative import declarative_base, declared_attr
|
||||
|
||||
|
||||
class CustomBase(object):
|
||||
# Generate __tablename__ automatically
|
||||
@declared_attr
|
||||
def __tablename__(cls):
|
||||
return cls.__name__.lower()
|
||||
|
||||
|
||||
Base = declarative_base(cls=CustomBase)
|
||||
@@ -0,0 +1,8 @@
|
||||
from app.core import config
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||
|
||||
engine = create_engine(config.SQLALCHEMY_DATABASE_URI, convert_unicode=True)
|
||||
db_session = scoped_session(
|
||||
sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
)
|
||||
@@ -0,0 +1,106 @@
|
||||
import json
|
||||
from pathlib import Path, PurePath
|
||||
from typing import Any, Dict
|
||||
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
from app.core.config import (
|
||||
COUCHBASE_FULL_TEXT_INDEX_DEFINITIONS_DIR,
|
||||
COUCHBASE_PASSWORD,
|
||||
COUCHBASE_USER,
|
||||
)
|
||||
|
||||
|
||||
def get_index(
|
||||
index_name: str,
|
||||
*,
|
||||
username: str = COUCHBASE_USER,
|
||||
password: str = COUCHBASE_PASSWORD,
|
||||
host="couchbase",
|
||||
port="8094",
|
||||
):
|
||||
full_text_url = f"http://{host}:{port}"
|
||||
index_url = f"{full_text_url}/api/index/{index_name}"
|
||||
auth = HTTPBasicAuth(username, password)
|
||||
response = requests.get(index_url, auth=auth)
|
||||
if response.status_code == 400:
|
||||
content = response.json()
|
||||
error = content.get("error")
|
||||
if error == "rest_auth: preparePerms, err: index not found":
|
||||
return None
|
||||
raise ValueError(error)
|
||||
elif response.status_code == 200:
|
||||
content = response.json()
|
||||
assert (
|
||||
content.get("status") == "ok"
|
||||
), "Expected a status OK communicating with Full Text Search"
|
||||
index_def = content.get("indexDef")
|
||||
return index_def
|
||||
raise ValueError(response.text)
|
||||
|
||||
|
||||
def create_index(
|
||||
index_definition: Dict[str, Any],
|
||||
*,
|
||||
reset_uuids=True,
|
||||
username: str = COUCHBASE_USER,
|
||||
password: str = COUCHBASE_PASSWORD,
|
||||
host="couchbase",
|
||||
port="8094",
|
||||
):
|
||||
index_name = index_definition.get("name")
|
||||
assert index_name, "An index name is required as key in an index definition"
|
||||
if reset_uuids:
|
||||
index_definition.update({"uuid": "", "sourceUUID": ""})
|
||||
full_text_url = f"http://{host}:{port}"
|
||||
index_url = f"{full_text_url}/api/index/{index_name}"
|
||||
auth = HTTPBasicAuth(username, password)
|
||||
response = requests.put(index_url, auth=auth, json=index_definition)
|
||||
content = response.json()
|
||||
if response.status_code == 400:
|
||||
error = content.get("error")
|
||||
if (
|
||||
"cannot create index because an index with the same name already exists:"
|
||||
in error
|
||||
):
|
||||
raise ValueError(error)
|
||||
else:
|
||||
raise ValueError(error)
|
||||
elif response.status_code == 200:
|
||||
assert (
|
||||
content.get("status") == "ok"
|
||||
), "Expected a status OK communicating with Full Text Search"
|
||||
return True
|
||||
raise ValueError(response.text)
|
||||
|
||||
|
||||
def ensure_create_full_text_indexes(
|
||||
index_dir=COUCHBASE_FULL_TEXT_INDEX_DEFINITIONS_DIR,
|
||||
username: str = COUCHBASE_USER,
|
||||
password: str = COUCHBASE_PASSWORD,
|
||||
host="couchbase",
|
||||
port="8094",
|
||||
):
|
||||
file_path: PurePath
|
||||
for file_path in Path(index_dir).iterdir():
|
||||
if file_path.name.endswith(".json"):
|
||||
with open(file_path) as f:
|
||||
index_definition = json.load(f)
|
||||
name = index_definition.get("name")
|
||||
assert name, "A full text search index definition must have a name field"
|
||||
current_index = get_index(
|
||||
index_name=name,
|
||||
username=username,
|
||||
password=password,
|
||||
host=host,
|
||||
port=port,
|
||||
)
|
||||
if not current_index:
|
||||
assert create_index(
|
||||
index_definition=index_definition,
|
||||
username=username,
|
||||
password=password,
|
||||
host=host,
|
||||
port=port,
|
||||
), "Full Text Search index could not be created"
|
||||
29
{{cookiecutter.project_slug}}/backend/app/app/db/init_db.py
Normal file
29
{{cookiecutter.project_slug}}/backend/app/app/db/init_db.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from app.core import config
|
||||
from app.db.utils import (
|
||||
assign_role_to_user,
|
||||
create_role,
|
||||
create_user,
|
||||
get_role_by_name,
|
||||
get_user_by_username,
|
||||
)
|
||||
|
||||
|
||||
def init_db(db_session):
|
||||
# Tables should be created with Alembic migrations
|
||||
# But if you don't want to use migrations, create
|
||||
# the tables uncommenting the next line
|
||||
# Base.metadata.create_all(bind=engine)
|
||||
|
||||
role = get_role_by_name("default", db_session)
|
||||
if not role:
|
||||
role = create_role("default", db_session)
|
||||
|
||||
user = get_user_by_username(config.FIRST_SUPERUSER, db_session)
|
||||
if not user:
|
||||
user = create_user(
|
||||
db_session,
|
||||
config.FIRST_SUPERUSER,
|
||||
config.FIRST_SUPERUSER_PASSWORD,
|
||||
is_superuser=True,
|
||||
)
|
||||
assign_role_to_user(role, user, db_session)
|
||||
0
{{cookiecutter.project_slug}}/backend/app/app/db_models/__init__.py
Executable file
0
{{cookiecutter.project_slug}}/backend/app/app/db_models/__init__.py
Executable file
11
{{cookiecutter.project_slug}}/backend/app/app/db_models/base_relations.py
Executable file
11
{{cookiecutter.project_slug}}/backend/app/app/db_models/base_relations.py
Executable file
@@ -0,0 +1,11 @@
|
||||
# Import installed packages
|
||||
# Import app code
|
||||
from app.db.base_class import Base
|
||||
from sqlalchemy import Column, ForeignKey, Integer, Table
|
||||
|
||||
users_roles = Table(
|
||||
"users_roles",
|
||||
Base.metadata,
|
||||
Column("user_id", Integer, ForeignKey("user.id")),
|
||||
Column("role_id", Integer, ForeignKey("role.id")),
|
||||
)
|
||||
19
{{cookiecutter.project_slug}}/backend/app/app/db_models/role.py
Executable file
19
{{cookiecutter.project_slug}}/backend/app/app/db_models/role.py
Executable file
@@ -0,0 +1,19 @@
|
||||
# Import standard library packages
|
||||
from datetime import datetime
|
||||
|
||||
# Import app code
|
||||
from app.db.base_class import Base
|
||||
from app.models.base_relations import users_roles
|
||||
|
||||
# Import installed packages
|
||||
from sqlalchemy import Column, DateTime, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
|
||||
class Role(Base):
|
||||
# Own properties
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow(), index=True)
|
||||
name = Column(String, index=True)
|
||||
# Relationships
|
||||
users = relationship("User", secondary=users_roles, back_populates="roles")
|
||||
29
{{cookiecutter.project_slug}}/backend/app/app/db_models/user.py
Executable file
29
{{cookiecutter.project_slug}}/backend/app/app/db_models/user.py
Executable file
@@ -0,0 +1,29 @@
|
||||
# Import standard library packages
|
||||
from datetime import datetime
|
||||
|
||||
# Typings, for autocompletion (VS Code with Python plug-in)
|
||||
from typing import List # noqa
|
||||
|
||||
# Import app code
|
||||
from app.db.base_class import Base
|
||||
from app.models.base_relations import users_roles
|
||||
|
||||
# Import installed packages
|
||||
from sqlalchemy import Boolean, Column, DateTime, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
|
||||
class User(Base):
|
||||
# Own properties
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow(), index=True)
|
||||
first_name = Column(String, index=True)
|
||||
last_name = Column(String, index=True)
|
||||
email = Column(String, unique=True, index=True)
|
||||
password = Column(String)
|
||||
is_active = Column(Boolean(), default=True)
|
||||
is_superuser = Column(Boolean(), default=False)
|
||||
# Relationships
|
||||
roles = relationship(
|
||||
"Role", secondary=users_roles, back_populates="users"
|
||||
) # type: List[role.Role]
|
||||
@@ -0,0 +1,26 @@
|
||||
<!doctype html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><head><title></title><!--[if !mso]><!-- --><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]--><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style type="text/css">#outlook a { padding:0; }
|
||||
.ReadMsgBody { width:100%; }
|
||||
.ExternalClass { width:100%; }
|
||||
.ExternalClass * { line-height:100%; }
|
||||
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
|
||||
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
|
||||
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
|
||||
p { display:block;margin:13px 0; }</style><!--[if !mso]><!--><style type="text/css">@media only screen and (max-width:480px) {
|
||||
@-ms-viewport { width:320px; }
|
||||
@viewport { width:320px; }
|
||||
}</style><!--<![endif]--><!--[if mso]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]--><!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]--><!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css"><style type="text/css">@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);</style><!--<![endif]--><style type="text/css">@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 { width:100% !important; max-width: 100%; }
|
||||
}</style><style type="text/css"></style></head><body style="background-color:#ffffff;"><div style="background-color:#ffffff;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="Margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;vertical-align:top;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--><div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 4px #555555;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 4px #555555;font-size:1;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]--></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:helvetica;font-size:20px;line-height:1;text-align:left;color:#555555;">{{ project_name }} - New Account</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">You have a new account:</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">Username: {{ username }}</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">Password: {{ password }}</div></td></tr><tr><td align="center" vertical-align="middle" style="font-size:0px;padding:50px 0px;word-break:break-word;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"><tr><td align="center" bgcolor="#414141" role="presentation" style="border:none;border-radius:3px;cursor:auto;padding:10px 25px;background:#414141;" valign="middle"><a href="{{ link }}" style="background:#414141;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;Margin:0;text-decoration:none;text-transform:none;" target="_blank">Go to Dashboard</a></td></tr></table></td></tr><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 2px #555555;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #555555;font-size:1;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]--></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>
|
||||
@@ -0,0 +1,26 @@
|
||||
<!doctype html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><head><title></title><!--[if !mso]><!-- --><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]--><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style type="text/css">#outlook a { padding:0; }
|
||||
.ReadMsgBody { width:100%; }
|
||||
.ExternalClass { width:100%; }
|
||||
.ExternalClass * { line-height:100%; }
|
||||
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
|
||||
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
|
||||
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
|
||||
p { display:block;margin:13px 0; }</style><!--[if !mso]><!--><style type="text/css">@media only screen and (max-width:480px) {
|
||||
@-ms-viewport { width:320px; }
|
||||
@viewport { width:320px; }
|
||||
}</style><!--<![endif]--><!--[if mso]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]--><!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]--><!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css"><style type="text/css">@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);</style><!--<![endif]--><style type="text/css">@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 { width:100% !important; max-width: 100%; }
|
||||
}</style><style type="text/css"></style></head><body style="background-color:#ffffff;"><div style="background-color:#ffffff;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="Margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;vertical-align:top;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--><div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 4px #555555;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 4px #555555;font-size:1;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]--></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:helvetica;font-size:20px;line-height:1;text-align:left;color:#555555;">{{ project_name }} - Password Recovery</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">We received a request to recover the password for user {{ username }} with email {{ email }}</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">Reset your password by clicking the button below:</div></td></tr><tr><td align="center" vertical-align="middle" style="font-size:0px;padding:50px 0px;word-break:break-word;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"><tr><td align="center" bgcolor="#414141" role="presentation" style="border:none;border-radius:3px;cursor:auto;padding:10px 25px;background:#414141;" valign="middle"><a href="{{ link }}" style="background:#414141;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;Margin:0;text-decoration:none;text-transform:none;" target="_blank">Reset Password</a></td></tr></table></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">Or open the following link:</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;"><a href="{{ link }}">{{ link }}</a></div></td></tr><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 2px #555555;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #555555;font-size:1;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]--></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:14px;line-height:1;text-align:left;color:#555555;">The reset password link / button will expire in {{ valid_hours }} hours.</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:14px;line-height:1;text-align:left;color:#555555;">If you didn't request a password recovery you can disregard this email.</div></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>
|
||||
@@ -0,0 +1,25 @@
|
||||
<!doctype html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><head><title></title><!--[if !mso]><!-- --><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]--><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style type="text/css">#outlook a { padding:0; }
|
||||
.ReadMsgBody { width:100%; }
|
||||
.ExternalClass { width:100%; }
|
||||
.ExternalClass * { line-height:100%; }
|
||||
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
|
||||
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
|
||||
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
|
||||
p { display:block;margin:13px 0; }</style><!--[if !mso]><!--><style type="text/css">@media only screen and (max-width:480px) {
|
||||
@-ms-viewport { width:320px; }
|
||||
@viewport { width:320px; }
|
||||
}</style><!--<![endif]--><!--[if mso]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]--><!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]--><!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css"><style type="text/css">@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);</style><!--<![endif]--><style type="text/css">@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 { width:100% !important; max-width: 100%; }
|
||||
}</style><style type="text/css"></style></head><body style="background-color:#ffffff;"><div style="background-color:#ffffff;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="Margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;vertical-align:top;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--><div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 4px #555555;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 4px #555555;font-size:1;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]--></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:helvetica;font-size:20px;line-height:1;text-align:left;color:#555555;">{{ project_name }}</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">Test email for: {{ email }}</div></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>
|
||||
@@ -0,0 +1,15 @@
|
||||
<mjml>
|
||||
<mj-body background-color="#fff">
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-divider border-color="#555"></mj-divider>
|
||||
<mj-text font-size="20px" color="#555" font-family="helvetica">{{ project_name }} - New Account</mj-text>
|
||||
<mj-text font-size="16px" color="#555">You have a new account:</mj-text>
|
||||
<mj-text font-size="16px" color="#555">Username: {{ username }}</mj-text>
|
||||
<mj-text font-size="16px" color="#555">Password: {{ password }}</mj-text>
|
||||
<mj-button padding="50px 0px" href="{{ link }}">Go to Dashboard</mj-button>
|
||||
<mj-divider border-color="#555" border-width="2px" />
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -0,0 +1,19 @@
|
||||
<mjml>
|
||||
<mj-body background-color="#fff">
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-divider border-color="#555"></mj-divider>
|
||||
<mj-text font-size="20px" color="#555" font-family="helvetica">{{ project_name }} - Password Recovery</mj-text>
|
||||
<mj-text font-size="16px" color="#555">We received a request to recover the password for user {{ username }}
|
||||
with email {{ email }}</mj-text>
|
||||
<mj-text font-size="16px" color="#555">Reset your password by clicking the button below:</mj-text>
|
||||
<mj-button padding="50px 0px" href="{{ link }}">Reset Password</mj-button>
|
||||
<mj-text font-size="16px" color="#555">Or open the following link:</mj-text>
|
||||
<mj-text font-size="16px" color="#555"><a href="{{ link }}">{{ link }}</a></mj-text>
|
||||
<mj-divider border-color="#555" border-width="2px" />
|
||||
<mj-text font-size="14px" color="#555">The reset password link / button will expire in {{ valid_hours }} hours.</mj-text>
|
||||
<mj-text font-size="14px" color="#555">If you didn't request a password recovery you can disregard this email.</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -0,0 +1,11 @@
|
||||
<mjml>
|
||||
<mj-body background-color="#fff">
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-divider border-color="#555"></mj-divider>
|
||||
<mj-text font-size="20px" color="#555" font-family="helvetica">{{ project_name }}</mj-text>
|
||||
<mj-text font-size="16px" color="#555">Test email for: {{ email }}</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
26
{{cookiecutter.project_slug}}/backend/app/app/main.py
Normal file
26
{{cookiecutter.project_slug}}/backend/app/app/main.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.api.api_v1.api import api_router
|
||||
from app.core.config import API_V1_STR, BACKEND_CORS_ORIGINS, PROJECT_NAME
|
||||
|
||||
app = FastAPI(title=PROJECT_NAME, openapi_url="/api/v1/openapi.json")
|
||||
|
||||
# CORS
|
||||
origins = []
|
||||
|
||||
# Set all CORS enabled origins
|
||||
if BACKEND_CORS_ORIGINS:
|
||||
origins_raw = BACKEND_CORS_ORIGINS.split(",")
|
||||
for origin in origins_raw:
|
||||
use_origin = origin.strip()
|
||||
origins.append(use_origin)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
),
|
||||
|
||||
app.include_router(api_router, prefix=API_V1_STR)
|
||||
@@ -0,0 +1 @@
|
||||
USERPROFILE_DOC_TYPE = "userprofile"
|
||||
@@ -0,0 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Msg(BaseModel):
|
||||
msg: str
|
||||
14
{{cookiecutter.project_slug}}/backend/app/app/models/role.py
Normal file
14
{{cookiecutter.project_slug}}/backend/app/app/models/role.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.core.config import ROLE_SUPERUSER
|
||||
|
||||
|
||||
class RoleEnum(Enum):
|
||||
superuser = ROLE_SUPERUSER
|
||||
|
||||
|
||||
class Roles(BaseModel):
|
||||
roles: List[RoleEnum]
|
||||
@@ -0,0 +1,10 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
|
||||
class TokenPayload(BaseModel):
|
||||
username: str = None
|
||||
48
{{cookiecutter.project_slug}}/backend/app/app/models/user.py
Normal file
48
{{cookiecutter.project_slug}}/backend/app/app/models/user.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.models.config import USERPROFILE_DOC_TYPE
|
||||
from app.models.role import RoleEnum
|
||||
|
||||
|
||||
# Shared properties
|
||||
class UserBase(BaseModel):
|
||||
email: Optional[str] = None
|
||||
admin_roles: Optional[List[Union[str, RoleEnum]]] = None
|
||||
admin_channels: Optional[List[Union[str, RoleEnum]]] = None
|
||||
disabled: Optional[bool] = None
|
||||
|
||||
|
||||
class UserBaseInDB(UserBase):
|
||||
username: str
|
||||
full_name: Optional[str] = None
|
||||
|
||||
|
||||
# Properties to receive via API on creation
|
||||
class UserInCreate(UserBaseInDB):
|
||||
password: str
|
||||
admin_roles: List[Union[str, RoleEnum]] = []
|
||||
admin_channels: List[Union[str, RoleEnum]] = []
|
||||
disabled: bool = False
|
||||
|
||||
|
||||
# Properties to receive via API on update
|
||||
class UserInUpdate(UserBaseInDB):
|
||||
password: Optional[str] = None
|
||||
|
||||
|
||||
# Additional properties to return via API
|
||||
class User(UserBaseInDB):
|
||||
pass
|
||||
|
||||
|
||||
# Additional properties stored in DB
|
||||
class UserInDB(UserBaseInDB):
|
||||
type: str = USERPROFILE_DOC_TYPE
|
||||
hashed_password: str
|
||||
|
||||
|
||||
class UserSyncIn(UserBase):
|
||||
name: str
|
||||
password: Optional[str] = None
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "users",
|
||||
"type": "fulltext-alias",
|
||||
"params": {
|
||||
"targets": {
|
||||
"users_01": {}
|
||||
}
|
||||
},
|
||||
"sourceType": "nil",
|
||||
"sourceName": "",
|
||||
"sourceUUID": "",
|
||||
"sourceParams": null,
|
||||
"planParams": {},
|
||||
"uuid": ""
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
{
|
||||
"name": "users_01",
|
||||
"type": "fulltext-index",
|
||||
"params": {
|
||||
"doc_config": {
|
||||
"docid_prefix_delim": "",
|
||||
"docid_regexp": "",
|
||||
"mode": "type_field",
|
||||
"type_field": "type"
|
||||
},
|
||||
"mapping": {
|
||||
"analysis": {
|
||||
"analyzers": {
|
||||
"userprofile": {
|
||||
"token_filters": [
|
||||
"apostrophe",
|
||||
"to_lower"
|
||||
],
|
||||
"tokenizer": "unicode",
|
||||
"type": "custom"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default_analyzer": "standard",
|
||||
"default_datetime_parser": "dateTimeOptional",
|
||||
"default_field": "_all",
|
||||
"default_mapping": {
|
||||
"dynamic": true,
|
||||
"enabled": false
|
||||
},
|
||||
"default_type": "_default",
|
||||
"docvalues_dynamic": true,
|
||||
"index_dynamic": true,
|
||||
"store_dynamic": false,
|
||||
"type_field": "_type",
|
||||
"types": {
|
||||
"userprofile": {
|
||||
"dynamic": false,
|
||||
"enabled": true,
|
||||
"properties": {
|
||||
"type": {
|
||||
"enabled": true,
|
||||
"dynamic": false,
|
||||
"fields": [
|
||||
{
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"analyzer": "keyword",
|
||||
"store": false,
|
||||
"index": true,
|
||||
"include_term_vectors": false,
|
||||
"include_in_all": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"admin_channels": {
|
||||
"enabled": true,
|
||||
"dynamic": false,
|
||||
"fields": [
|
||||
{
|
||||
"analyzer": "keyword",
|
||||
"include_in_all": true,
|
||||
"include_term_vectors": true,
|
||||
"index": true,
|
||||
"name": "admin_channels",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"admin_roles": {
|
||||
"enabled": true,
|
||||
"dynamic": false,
|
||||
"fields": [
|
||||
{
|
||||
"analyzer": "keyword",
|
||||
"include_in_all": true,
|
||||
"include_term_vectors": true,
|
||||
"index": true,
|
||||
"name": "admin_roles",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"disabled": {
|
||||
"enabled": true,
|
||||
"dynamic": false,
|
||||
"fields": [
|
||||
{
|
||||
"include_in_all": true,
|
||||
"include_term_vectors": true,
|
||||
"index": true,
|
||||
"name": "disabled",
|
||||
"type": "boolean"
|
||||
}
|
||||
]
|
||||
},
|
||||
"email": {
|
||||
"enabled": true,
|
||||
"dynamic": false,
|
||||
"fields": [
|
||||
{
|
||||
"analyzer": "keyword",
|
||||
"include_in_all": true,
|
||||
"include_term_vectors": true,
|
||||
"index": true,
|
||||
"name": "email",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"full_name": {
|
||||
"enabled": true,
|
||||
"dynamic": false,
|
||||
"fields": [
|
||||
{
|
||||
"analyzer": "standard",
|
||||
"include_in_all": true,
|
||||
"include_term_vectors": true,
|
||||
"index": true,
|
||||
"name": "full_name",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"username": {
|
||||
"enabled": true,
|
||||
"dynamic": false,
|
||||
"fields": [
|
||||
{
|
||||
"analyzer": "keyword",
|
||||
"include_in_all": true,
|
||||
"include_term_vectors": true,
|
||||
"index": true,
|
||||
"name": "username",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"store": {
|
||||
"indexType": "scorch",
|
||||
"kvStoreName": ""
|
||||
}
|
||||
},
|
||||
"sourceType": "couchbase",
|
||||
"sourceName": "app",
|
||||
"sourceUUID": "",
|
||||
"sourceParams": {},
|
||||
"planParams": {
|
||||
"maxPartitionsPerPIndex": 171,
|
||||
"numReplicas": 0
|
||||
},
|
||||
"uuid": ""
|
||||
}
|
||||
1
{{cookiecutter.project_slug}}/backend/app/app/tests/.gitignore
vendored
Executable file
1
{{cookiecutter.project_slug}}/backend/app/app/tests/.gitignore
vendored
Executable file
@@ -0,0 +1 @@
|
||||
.cache
|
||||
@@ -0,0 +1,16 @@
|
||||
import requests
|
||||
|
||||
from app.core import config
|
||||
from app.tests.utils.utils import get_server_api
|
||||
|
||||
|
||||
def test_celery_worker_test(superuser_token_headers):
|
||||
server_api = get_server_api()
|
||||
data = {"msg": "test"}
|
||||
r = requests.post(
|
||||
f"{server_api}{config.API_V1_STR}/test-celery/",
|
||||
json=data,
|
||||
headers=superuser_token_headers,
|
||||
)
|
||||
response = r.json()
|
||||
assert response["msg"] == "Word received"
|
||||
@@ -0,0 +1,30 @@
|
||||
import requests
|
||||
|
||||
from app.core import config
|
||||
from app.tests.utils.utils import get_server_api
|
||||
|
||||
|
||||
def test_get_access_token():
|
||||
server_api = get_server_api()
|
||||
login_data = {
|
||||
"username": config.FIRST_SUPERUSER,
|
||||
"password": config.FIRST_SUPERUSER_PASSWORD,
|
||||
}
|
||||
r = requests.post(
|
||||
f"{server_api}{config.API_V1_STR}/login/access-token", data=login_data
|
||||
)
|
||||
tokens = r.json()
|
||||
assert r.status_code == 200
|
||||
assert "access_token" in tokens
|
||||
assert tokens["access_token"]
|
||||
|
||||
|
||||
def test_use_access_token(superuser_token_headers):
|
||||
server_api = get_server_api()
|
||||
r = requests.post(
|
||||
f"{server_api}{config.API_V1_STR}/login/test-token",
|
||||
headers=superuser_token_headers,
|
||||
)
|
||||
result = r.json()
|
||||
assert r.status_code == 200
|
||||
assert "username" in result
|
||||
@@ -0,0 +1,112 @@
|
||||
import requests
|
||||
|
||||
from app.core import config
|
||||
from app.crud.user import get_user, upsert_user
|
||||
from app.db.database import get_default_bucket
|
||||
from app.models.user import UserInCreate
|
||||
from app.tests.utils.user import user_authentication_headers
|
||||
from app.tests.utils.utils import get_server_api, random_lower_string
|
||||
|
||||
|
||||
def test_get_users_superuser_me(superuser_token_headers):
|
||||
server_api = get_server_api()
|
||||
r = requests.get(
|
||||
f"{server_api}{config.API_V1_STR}/users/me", headers=superuser_token_headers
|
||||
)
|
||||
current_user = r.json()
|
||||
assert current_user
|
||||
assert current_user["disabled"] is False
|
||||
assert "superuser" in current_user["admin_roles"]
|
||||
assert current_user["username"] == config.FIRST_SUPERUSER
|
||||
|
||||
|
||||
def test_create_user_new_email(superuser_token_headers):
|
||||
server_api = get_server_api()
|
||||
username = random_lower_string()
|
||||
password = random_lower_string()
|
||||
data = {"username": username, "password": password}
|
||||
r = requests.post(
|
||||
f"{server_api}{config.API_V1_STR}/users/",
|
||||
headers=superuser_token_headers,
|
||||
json=data,
|
||||
)
|
||||
assert 200 <= r.status_code < 300
|
||||
created_user = r.json()
|
||||
bucket = get_default_bucket()
|
||||
user = get_user(bucket, username)
|
||||
assert user.username == created_user["username"]
|
||||
|
||||
|
||||
def test_get_existing_user(superuser_token_headers):
|
||||
server_api = get_server_api()
|
||||
username = random_lower_string()
|
||||
password = random_lower_string()
|
||||
user_in = UserInCreate(username=username, email=username, password=password)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
r = requests.get(
|
||||
f"{server_api}{config.API_V1_STR}/users/{username}",
|
||||
headers=superuser_token_headers,
|
||||
)
|
||||
assert 200 <= r.status_code < 300
|
||||
api_user = r.json()
|
||||
user = get_user(bucket, username)
|
||||
assert user.username == api_user["username"]
|
||||
|
||||
|
||||
def test_create_user_existing_username(superuser_token_headers):
|
||||
server_api = get_server_api()
|
||||
username = random_lower_string()
|
||||
# username = email
|
||||
password = random_lower_string()
|
||||
user_in = UserInCreate(username=username, email=username, password=password)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
data = {"username": username, "password": password}
|
||||
r = requests.post(
|
||||
f"{server_api}{config.API_V1_STR}/users/",
|
||||
headers=superuser_token_headers,
|
||||
json=data,
|
||||
)
|
||||
created_user = r.json()
|
||||
assert r.status_code == 400
|
||||
assert "_id" not in created_user
|
||||
|
||||
|
||||
def test_create_user_by_normal_user():
|
||||
server_api = get_server_api()
|
||||
username = random_lower_string()
|
||||
password = random_lower_string()
|
||||
user_in = UserInCreate(username=username, email=username, password=password)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
user_token_headers = user_authentication_headers(server_api, username, password)
|
||||
data = {"username": username, "password": password}
|
||||
r = requests.post(
|
||||
f"{server_api}{config.API_V1_STR}/users/", headers=user_token_headers, json=data
|
||||
)
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_retrieve_users(superuser_token_headers):
|
||||
server_api = get_server_api()
|
||||
username = random_lower_string()
|
||||
password = random_lower_string()
|
||||
user_in = UserInCreate(username=username, email=username, password=password)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
|
||||
username2 = random_lower_string()
|
||||
password2 = random_lower_string()
|
||||
user_in2 = UserInCreate(username=username2, email=username2, password=password2)
|
||||
user2 = upsert_user(bucket, user_in, persist_to=1)
|
||||
|
||||
r = requests.get(
|
||||
f"{server_api}{config.API_V1_STR}/users/", headers=superuser_token_headers
|
||||
)
|
||||
all_users = r.json()
|
||||
|
||||
assert len(all_users) > 1
|
||||
for user in all_users:
|
||||
assert "username" in user
|
||||
assert "admin_roles" in user
|
||||
@@ -0,0 +1,13 @@
|
||||
import pytest
|
||||
|
||||
from app.tests.utils.utils import get_server_api, get_superuser_token_headers
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def server_api():
|
||||
return get_server_api()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def superuser_token_headers():
|
||||
return get_superuser_token_headers()
|
||||
@@ -0,0 +1,7 @@
|
||||
from app.crud.user import get_user_doc_id
|
||||
|
||||
|
||||
def test_get_user_id():
|
||||
username = "johndoe@example.com"
|
||||
user_id = get_user_doc_id(username)
|
||||
assert user_id == "userprofile::johndoe@example.com"
|
||||
@@ -0,0 +1,105 @@
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
||||
from app.crud.user import (
|
||||
authenticate_user,
|
||||
check_if_user_is_active,
|
||||
check_if_user_is_superuser,
|
||||
get_user,
|
||||
upsert_user,
|
||||
)
|
||||
from app.db.database import get_default_bucket
|
||||
from app.models.role import RoleEnum
|
||||
from app.models.user import UserInCreate
|
||||
from app.tests.utils.utils import random_lower_string
|
||||
|
||||
|
||||
def test_create_user():
|
||||
email = random_lower_string()
|
||||
password = random_lower_string()
|
||||
user_in = UserInCreate(username=email, email=email, password=password)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
assert hasattr(user, "username")
|
||||
assert user.username == email
|
||||
assert hasattr(user, "hashed_password")
|
||||
assert hasattr(user, "type")
|
||||
assert user.type == "userprofile"
|
||||
|
||||
|
||||
def test_authenticate_user():
|
||||
email = random_lower_string()
|
||||
password = random_lower_string()
|
||||
user_in = UserInCreate(username=email, email=email, password=password)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
authenticated_user = authenticate_user(bucket, email, password)
|
||||
assert authenticated_user
|
||||
assert user.username == authenticated_user.username
|
||||
|
||||
|
||||
def test_not_authenticate_user():
|
||||
email = random_lower_string()
|
||||
password = random_lower_string()
|
||||
bucket = get_default_bucket()
|
||||
user = authenticate_user(bucket, email, password)
|
||||
assert user is False
|
||||
|
||||
|
||||
def test_check_if_user_is_active():
|
||||
email = random_lower_string()
|
||||
password = random_lower_string()
|
||||
user_in = UserInCreate(username=email, email=email, password=password)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
is_active = check_if_user_is_active(user)
|
||||
assert is_active is True
|
||||
|
||||
|
||||
def test_check_if_user_is_active_inactive():
|
||||
email = random_lower_string()
|
||||
password = random_lower_string()
|
||||
user_in = UserInCreate(
|
||||
username=email, email=email, password=password, disabled=True
|
||||
)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
is_active = check_if_user_is_active(user)
|
||||
assert is_active is False
|
||||
|
||||
|
||||
def test_check_if_user_is_superuser():
|
||||
email = random_lower_string()
|
||||
password = random_lower_string()
|
||||
user_in = UserInCreate(
|
||||
username=email, email=email, password=password, admin_roles=[RoleEnum.superuser]
|
||||
)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
is_superuser = check_if_user_is_superuser(user)
|
||||
assert is_superuser is True
|
||||
|
||||
|
||||
def test_check_if_user_is_superuser_normal_user():
|
||||
username = random_lower_string()
|
||||
password = random_lower_string()
|
||||
user_in = UserInCreate(username=username, email=username, password=password)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
is_superuser = check_if_user_is_superuser(user)
|
||||
assert is_superuser is False
|
||||
|
||||
|
||||
def test_get_user():
|
||||
password = random_lower_string()
|
||||
username = random_lower_string()
|
||||
user_in = UserInCreate(
|
||||
username=username,
|
||||
email=username,
|
||||
password=password,
|
||||
admin_roles=[RoleEnum.superuser],
|
||||
)
|
||||
bucket = get_default_bucket()
|
||||
user = upsert_user(bucket, user_in, persist_to=1)
|
||||
user_2 = get_user(bucket, username)
|
||||
assert user.username == user_2.username
|
||||
assert jsonable_encoder(user) == jsonable_encoder(user_2)
|
||||
@@ -0,0 +1,13 @@
|
||||
import requests
|
||||
|
||||
from app.core import config
|
||||
|
||||
|
||||
def user_authentication_headers(server_api, email, password):
|
||||
data = {"username": email, "password": password}
|
||||
|
||||
r = requests.post(f"{server_api}{config.API_V1_STR}/login/access-token", data=data)
|
||||
response = r.json()
|
||||
auth_token = response["access_token"]
|
||||
headers = {"Authorization": f"Bearer {auth_token}"}
|
||||
return headers
|
||||
@@ -0,0 +1,31 @@
|
||||
import random
|
||||
import string
|
||||
|
||||
import requests
|
||||
|
||||
from app.core import config
|
||||
|
||||
|
||||
def random_lower_string():
|
||||
return "".join(random.choices(string.ascii_lowercase, k=32))
|
||||
|
||||
|
||||
def get_server_api():
|
||||
server_name = f"http://{config.SERVER_NAME}"
|
||||
return server_name
|
||||
|
||||
|
||||
def get_superuser_token_headers():
|
||||
server_api = get_server_api()
|
||||
login_data = {
|
||||
"username": config.FIRST_SUPERUSER,
|
||||
"password": config.FIRST_SUPERUSER_PASSWORD,
|
||||
}
|
||||
r = requests.post(
|
||||
f"{server_api}{config.API_V1_STR}/login/access-token", data=login_data
|
||||
)
|
||||
tokens = r.json()
|
||||
a_token = tokens["access_token"]
|
||||
headers = {"Authorization": f"Bearer {a_token}"}
|
||||
# superuser_token_headers = headers
|
||||
return headers
|
||||
@@ -0,0 +1,35 @@
|
||||
import logging
|
||||
|
||||
from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed
|
||||
|
||||
from app.db.external_session import db_session
|
||||
from app.tests.api.api_v1.token.test_token import test_get_access_token
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
max_tries = 60 * 5 # 5 minutes
|
||||
wait_seconds = 1
|
||||
|
||||
|
||||
@retry(
|
||||
stop=stop_after_attempt(max_tries),
|
||||
wait=wait_fixed(wait_seconds),
|
||||
before=before_log(logger, logging.INFO),
|
||||
after=after_log(logger, logging.WARN),
|
||||
)
|
||||
def init():
|
||||
# Try to create session to check if DB is awake
|
||||
db_session.execute("SELECT 1")
|
||||
# Wait for API to be awake, run one simple tests to authenticate
|
||||
test_get_access_token()
|
||||
|
||||
|
||||
def main():
|
||||
logger.info("Initializing service")
|
||||
init()
|
||||
logger.info("Service finished initializing")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
126
{{cookiecutter.project_slug}}/backend/app/app/utils.py
Normal file
126
{{cookiecutter.project_slug}}/backend/app/app/utils.py
Normal file
@@ -0,0 +1,126 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
import emails
|
||||
import jwt
|
||||
from emails.template import JinjaTemplate
|
||||
from jwt.exceptions import InvalidTokenError
|
||||
|
||||
from app.core.config import (
|
||||
EMAIL_RESET_TOKEN_EXPIRE_HOURS,
|
||||
EMAIL_TEMPLATES_DIR,
|
||||
EMAILS_ENABLED,
|
||||
EMAILS_FROM_EMAIL,
|
||||
EMAILS_FROM_NAME,
|
||||
PROJECT_NAME,
|
||||
SECRET_KEY,
|
||||
SERVER_HOST,
|
||||
SMTP_HOST,
|
||||
SMTP_PASSWORD,
|
||||
SMTP_PORT,
|
||||
SMTP_TLS,
|
||||
SMTP_USER,
|
||||
)
|
||||
|
||||
password_reset_jwt_subject = "preset"
|
||||
|
||||
|
||||
def send_email(email_to: str, subject_template="", html_template="", environment={}):
|
||||
assert EMAILS_ENABLED, "no provided configuration for email variables"
|
||||
message = emails.Message(
|
||||
subject=JinjaTemplate(subject_template),
|
||||
html=JinjaTemplate(html_template),
|
||||
mail_from=(EMAILS_FROM_NAME, EMAILS_FROM_EMAIL),
|
||||
)
|
||||
smtp_options = {"host": SMTP_HOST, "port": SMTP_PORT}
|
||||
if SMTP_TLS:
|
||||
smtp_options["tls"] = True
|
||||
if SMTP_USER:
|
||||
smtp_options["user"] = SMTP_USER
|
||||
if SMTP_PASSWORD:
|
||||
smtp_options["password"] = SMTP_PASSWORD
|
||||
response = message.send(to=email_to, render=environment, smtp=smtp_options)
|
||||
logging.info(f"send email result: {response}")
|
||||
|
||||
|
||||
def send_test_email(email_to: str):
|
||||
subject = f"{PROJECT_NAME} - Test email"
|
||||
with open(Path(EMAIL_TEMPLATES_DIR) / "test_email.html") as f:
|
||||
template_str = f.read()
|
||||
send_email(
|
||||
email_to=email_to,
|
||||
subject_template=subject,
|
||||
html_template=template_str,
|
||||
environment={"project_name": PROJECT_NAME, "email": email_to},
|
||||
)
|
||||
|
||||
|
||||
def send_reset_password_email(email_to: str, username: str, token: str):
|
||||
subject = f"{PROJECT_NAME} - Password recovery for user {username}"
|
||||
with open(Path(EMAIL_TEMPLATES_DIR) / "reset_password.html") as f:
|
||||
template_str = f.read()
|
||||
if hasattr(token, "decode"):
|
||||
use_token = token.decode()
|
||||
else:
|
||||
use_token = token
|
||||
link = f"{SERVER_HOST}/reset-password?token={use_token}"
|
||||
send_email(
|
||||
email_to=email_to,
|
||||
subject_template=subject,
|
||||
html_template=template_str,
|
||||
environment={
|
||||
"project_name": PROJECT_NAME,
|
||||
"username": username,
|
||||
"email": email_to,
|
||||
"valid_hours": EMAIL_RESET_TOKEN_EXPIRE_HOURS,
|
||||
"link": link,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def send_new_account_email(email_to: str, username: str, password: str):
|
||||
subject = f"{PROJECT_NAME} - New acccount for user {username}"
|
||||
with open(Path(EMAIL_TEMPLATES_DIR) / "new_account.html") as f:
|
||||
template_str = f.read()
|
||||
link = f"{SERVER_HOST}"
|
||||
send_email(
|
||||
email_to=email_to,
|
||||
subject_template=subject,
|
||||
html_template=template_str,
|
||||
environment={
|
||||
"project_name": PROJECT_NAME,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"email": email_to,
|
||||
"link": link,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def generate_password_reset_token(username):
|
||||
delta = timedelta(hours=EMAIL_RESET_TOKEN_EXPIRE_HOURS)
|
||||
now = datetime.utcnow()
|
||||
expires = now + delta
|
||||
exp = expires.timestamp()
|
||||
encoded_jwt = jwt.encode(
|
||||
{
|
||||
"exp": exp,
|
||||
"nbf": now,
|
||||
"sub": password_reset_jwt_subject,
|
||||
"username": username,
|
||||
},
|
||||
SECRET_KEY,
|
||||
algorithm="HS256",
|
||||
)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def verify_password_reset_token(token) -> Union[str, bool]:
|
||||
try:
|
||||
decoded_token = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
|
||||
assert decoded_token["sub"] == password_reset_jwt_subject
|
||||
return decoded_token["username"]
|
||||
except InvalidTokenError:
|
||||
return False
|
||||
19
{{cookiecutter.project_slug}}/backend/app/app/worker.py
Normal file
19
{{cookiecutter.project_slug}}/backend/app/app/worker.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Import standard library modules
|
||||
|
||||
|
||||
# Import installed packages
|
||||
from raven import Client
|
||||
|
||||
from app.core.celery_app import celery_app
|
||||
|
||||
# Import app code
|
||||
# Absolute imports for Hydrogen (Jupyter Kernel) compatibility
|
||||
from app.core.config import SENTRY_DSN
|
||||
|
||||
client_sentry = Client(SENTRY_DSN)
|
||||
|
||||
|
||||
@celery_app.task(acks_late=True)
|
||||
def test_celery(word: str):
|
||||
print("test task")
|
||||
return f"test task return {word}"
|
||||
@@ -0,0 +1,2 @@
|
||||
#! /usr/bin/env bash
|
||||
uvicorn app.main:app --host 0.0.0.0 --port 80 --debug
|
||||
27
{{cookiecutter.project_slug}}/backend/app/backend-start.sh
Normal file
27
{{cookiecutter.project_slug}}/backend/app/backend-start.sh
Normal file
@@ -0,0 +1,27 @@
|
||||
#! /usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
# Let the DB start
|
||||
python /app/app/backend_pre_start.py
|
||||
|
||||
|
||||
LOG_LEVEL=info
|
||||
# Uncomment to squeeze performance in exchange of logs
|
||||
# LOG_LEVEL=warning
|
||||
|
||||
# Get CPU cores
|
||||
CORES=$(nproc --all)
|
||||
# Read env var WORKERS_PER_CORE with default of 2
|
||||
WORKERS_PER_CORE_PERCENT=${WORKERS_PER_CORE_PERCENT:-200}
|
||||
# Compute DEFAULT_WEB_CONCURRENCY as CPU cores * workers per core
|
||||
DEFAULT_WEB_CONCURRENCY=$(( ($CORES * $WORKERS_PER_CORE_PERCENT) / 100 ))
|
||||
# Minimum default of workers is 1
|
||||
if [ "$DEFAULT_WEB_CONCURRENCY" -lt 1 ]; then
|
||||
DEFAULT_WEB_CONCURRENCY=1
|
||||
fi
|
||||
# Read WEB_CONCURRENCY env var, with default of computed value
|
||||
WEB_CONCURRENCY=${WEB_CONCURRENCY:-$DEFAULT_WEB_CONCURRENCY}
|
||||
echo "Using these many workers: $WEB_CONCURRENCY"
|
||||
|
||||
gunicorn -k uvicorn.workers.UvicornWorker --log-level $LOG_LEVEL app.main:app --bind 0.0.0.0:80
|
||||
4
{{cookiecutter.project_slug}}/backend/app/prestart.sh
Normal file
4
{{cookiecutter.project_slug}}/backend/app/prestart.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
#! /usr/bin/env bash
|
||||
|
||||
# Let the DB start
|
||||
python /app/app/backend_pre_start.py
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -x
|
||||
|
||||
autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place app --exclude=__init__.py
|
||||
isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --apply app
|
||||
black app
|
||||
6
{{cookiecutter.project_slug}}/backend/app/tests-start.sh
Normal file
6
{{cookiecutter.project_slug}}/backend/app/tests-start.sh
Normal file
@@ -0,0 +1,6 @@
|
||||
#! /usr/bin/env bash
|
||||
set -e
|
||||
|
||||
python /app/app/tests_pre_start.py
|
||||
|
||||
pytest /app/app/tests/
|
||||
@@ -0,0 +1,6 @@
|
||||
#! /usr/bin/env bash
|
||||
set -e
|
||||
|
||||
python /app/app/celeryworker_pre_start.py
|
||||
|
||||
celery worker -A app.worker -l info -Q main-queue -c 1
|
||||
Reference in New Issue
Block a user