Initial commit

This commit is contained in:
Mawoka
2022-02-27 21:23:57 +01:00
commit 1805d6fe98
46 changed files with 4453 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
__pycache__/
.idea/
node_modules/
.svelte-kit/
.env*
*.db
*.rdb
+12
View File
@@ -0,0 +1,12 @@
:8080 {
reverse_proxy /* localhost:3000
reverse_proxy /api* localhost:8000
reverse_proxy /rapidoc* localhost:8000
reverse_proxy /openapi.json localhost:8000
reverse_proxy /socket.io* localhost:8000
}
+22
View File
@@ -0,0 +1,22 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
fastapi = "*"
uvicorn = {extras = ["standard"], version = "*"}
python-socketio = "*"
ormar = "==0.10.24"
passlib = "*"
python-jose = "*"
alembic = "*"
email-validator = "*"
python-multipart = "*"
pydantic = "*"
redis = "==4.2.0rc1"
[dev-packages]
[requires]
python_version = "3.10"
Generated
+691
View File
@@ -0,0 +1,691 @@
{
"_meta": {
"hash": {
"sha256": "638d4341afa82b3e49972eebf51b7c451f25b3ec0bea06d0356eb237180654b6"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.10"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"aiosqlite": {
"hashes": [
"sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231",
"sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"
],
"markers": "python_version >= '3.6'",
"version": "==0.17.0"
},
"alembic": {
"hashes": [
"sha256:6c0c05e9768a896d804387e20b299880fe01bc56484246b0dffe8075d6d3d847",
"sha256:ad842f2c3ab5c5d4861232730779c05e33db4ba880a08b85eb505e87c01095bc"
],
"index": "pypi",
"version": "==1.7.6"
},
"anyio": {
"hashes": [
"sha256:a0aeffe2fb1fdf374a8e4b471444f0f3ac4fb9f5a5b542b48824475e0042a5a6",
"sha256:b5fa16c5ff93fa1046f2eeb5bbff2dad4d3514d6cda61d02816dba34fa8c3c2e"
],
"markers": "python_full_version >= '3.6.2'",
"version": "==3.5.0"
},
"asgiref": {
"hashes": [
"sha256:2f8abc20f7248433085eda803936d98992f1343ddb022065779f37c5da0181d0",
"sha256:88d59c13d634dcffe0510be048210188edd79aeccb6a6c9028cdad6f31d730a9"
],
"markers": "python_version >= '3.7'",
"version": "==3.5.0"
},
"async-timeout": {
"hashes": [
"sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15",
"sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"
],
"markers": "python_version >= '3.6'",
"version": "==4.0.2"
},
"bidict": {
"hashes": [
"sha256:3ac67daa353ecf853a1df9d3e924f005e729227a60a8dbada31a4c31aba7f654",
"sha256:42c84ffbe6f8de898af6073b4be9ea7ccedcd78d3474aa844c54e49d5a079f6f"
],
"markers": "python_version >= '3.6'",
"version": "==0.21.4"
},
"click": {
"hashes": [
"sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1",
"sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"
],
"markers": "python_version >= '3.6'",
"version": "==8.0.4"
},
"databases": {
"hashes": [
"sha256:04a3294d053bd8d9f4162fc4975ab11a3e9ad01ae37992adce84440725957fec",
"sha256:85a6b0dd92e4bc95205c08141baf1e192c8aedb2159ce03bee39bb4117cfed83"
],
"markers": "python_version >= '3.6'",
"version": "==0.5.4"
},
"deprecated": {
"hashes": [
"sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d",
"sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.2.13"
},
"dnspython": {
"hashes": [
"sha256:081649da27ced5e75709a1ee542136eaba9842a0fe4c03da4fb0a3d3ed1f3c44",
"sha256:e79351e032d0b606b98d38a4b0e6e2275b31a5b85c873e587cc11b73aca026d6"
],
"markers": "python_version >= '3.6' and python_version < '4.0'",
"version": "==2.2.0"
},
"ecdsa": {
"hashes": [
"sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676",
"sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.17.0"
},
"email-validator": {
"hashes": [
"sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b",
"sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7"
],
"index": "pypi",
"version": "==1.1.3"
},
"fastapi": {
"hashes": [
"sha256:b58a2c46df14f62ebe6f24a9439927539ba1959b9be55ba0e2f516a683e5b9d4",
"sha256:b8ec8400623ef0b2ff558ebe06753b349f8e3a5dd38afea650800f2644ddba34"
],
"index": "pypi",
"version": "==0.74.1"
},
"greenlet": {
"hashes": [
"sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3",
"sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711",
"sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd",
"sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073",
"sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708",
"sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67",
"sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23",
"sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1",
"sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08",
"sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd",
"sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2",
"sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa",
"sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8",
"sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40",
"sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab",
"sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6",
"sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc",
"sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b",
"sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e",
"sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963",
"sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3",
"sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d",
"sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d",
"sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe",
"sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28",
"sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3",
"sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e",
"sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c",
"sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d",
"sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0",
"sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497",
"sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee",
"sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713",
"sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58",
"sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a",
"sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06",
"sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88",
"sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965",
"sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f",
"sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4",
"sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5",
"sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c",
"sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a",
"sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1",
"sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43",
"sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627",
"sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b",
"sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168",
"sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d",
"sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5",
"sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478",
"sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf",
"sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce",
"sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c",
"sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"
],
"markers": "python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))",
"version": "==1.1.2"
},
"h11": {
"hashes": [
"sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06",
"sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"
],
"markers": "python_version >= '3.6'",
"version": "==0.13.0"
},
"httptools": {
"hashes": [
"sha256:04114db99605c9b56ea22a8ec4d7b1485b908128ed4f4a8f6438489c428da794",
"sha256:074afd8afdeec0fa6786cd4a1676e0c0be23dc9a017a86647efa6b695168104f",
"sha256:113816f9af7dcfc4aa71ebb5354d77365f666ecf96ac7ff2aa1d24b6bca44165",
"sha256:1a8f26327023fa1a947d36e60a0582149e182fbbc949c8a65ec8665754dbbe69",
"sha256:2119fa619a4c53311f594f25c0205d619350fcb32140ec5057f861952e9b2b4f",
"sha256:21e948034f70e47c8abfa2d5e6f1a5661f87a2cddc7bcc70f61579cc87897c70",
"sha256:32a10a5903b5bc0eb647d01cd1e95bec3bb614a9bf53f0af1e01360b2debdf81",
"sha256:3787c1f46e9722ef7f07ea5c76b0103037483d1b12e34a02c53ceca5afa4e09a",
"sha256:3f82eb106e1474c63dba36a176067e65b48385f4cecddf3616411aa5d1fbdfec",
"sha256:3f9b4856d46ba1f0c850f4e84b264a9a8b4460acb20e865ec00978ad9fbaa4cf",
"sha256:4137137de8976511a392e27bfdcf231bd926ac13d375e0414e927b08217d779e",
"sha256:4687dfc116a9f1eb22a7d797f0dc6f6e17190d406ca4e729634b38aa98044b17",
"sha256:47dba2345aaa01b87e4981e8756af441349340708d5b60712c98c55a4d28f4af",
"sha256:5a836bd85ae1fb4304f674808488dae403e136d274aa5bafd0e6ee456f11c371",
"sha256:6e676bc3bb911b11f3d7e2144b9a53600bf6b9b21e0e4437aa308e1eef094d97",
"sha256:72ee0e3fb9c6437ab3ae34e9abee67fcee6876f4f58504e3f613dd5882aafdb7",
"sha256:79717080dc3f8b1eeb7f820b9b81528acbc04be6041f323fdd97550da2062575",
"sha256:8ac842df4fc3952efa7820b277961ea55e068bbc54cb59a0820400de7ae358d8",
"sha256:9f475b642c48b1b78584bdd12a5143e2c512485664331eade9c29ef769a17598",
"sha256:b8ac7dee63af4346e02b1e6d32202e3b5b3706a9928bec6da6d7a5b066217422",
"sha256:c0ac2e0ce6733c55858932e7d37fcc7b67ba6bb23e9648593c55f663de031b93",
"sha256:c14576b737d9e6e4f2a86af04918dbe9b62f57ce8102a8695c9a382dbe405c7f",
"sha256:cdc3975db86c29817e6d13df14e037c931fc893a710fb71097777a4147090068",
"sha256:eda95634027200f4b2a6d499e7c2e7fa9b8ee57e045dfda26958ea0af27c070b"
],
"version": "==0.3.0"
},
"idna": {
"hashes": [
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
],
"markers": "python_version >= '3.5'",
"version": "==3.3"
},
"mako": {
"hashes": [
"sha256:4e9e345a41924a954251b95b4b28e14a301145b544901332e658907a7464b6b2",
"sha256:afaf8e515d075b22fad7d7b8b30e4a1c90624ff2f3733a06ec125f5a5f043a57"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.6"
},
"markupsafe": {
"hashes": [
"sha256:023af8c54fe63530545f70dd2a2a7eed18d07a9a77b94e8bf1e2ff7f252db9a3",
"sha256:09c86c9643cceb1d87ca08cdc30160d1b7ab49a8a21564868921959bd16441b8",
"sha256:142119fb14a1ef6d758912b25c4e803c3ff66920635c44078666fe7cc3f8f759",
"sha256:1d1fb9b2eec3c9714dd936860850300b51dbaa37404209c8d4cb66547884b7ed",
"sha256:204730fd5fe2fe3b1e9ccadb2bd18ba8712b111dcabce185af0b3b5285a7c989",
"sha256:24c3be29abb6b34052fd26fc7a8e0a49b1ee9d282e3665e8ad09a0a68faee5b3",
"sha256:290b02bab3c9e216da57c1d11d2ba73a9f73a614bbdcc027d299a60cdfabb11a",
"sha256:3028252424c72b2602a323f70fbf50aa80a5d3aa616ea6add4ba21ae9cc9da4c",
"sha256:30c653fde75a6e5eb814d2a0a89378f83d1d3f502ab710904ee585c38888816c",
"sha256:3cace1837bc84e63b3fd2dfce37f08f8c18aeb81ef5cf6bb9b51f625cb4e6cd8",
"sha256:4056f752015dfa9828dce3140dbadd543b555afb3252507348c493def166d454",
"sha256:454ffc1cbb75227d15667c09f164a0099159da0c1f3d2636aa648f12675491ad",
"sha256:598b65d74615c021423bd45c2bc5e9b59539c875a9bdb7e5f2a6b92dfcfc268d",
"sha256:599941da468f2cf22bf90a84f6e2a65524e87be2fce844f96f2dd9a6c9d1e635",
"sha256:5ddea4c352a488b5e1069069f2f501006b1a4362cb906bee9a193ef1245a7a61",
"sha256:62c0285e91414f5c8f621a17b69fc0088394ccdaa961ef469e833dbff64bd5ea",
"sha256:679cbb78914ab212c49c67ba2c7396dc599a8479de51b9a87b174700abd9ea49",
"sha256:6e104c0c2b4cd765b4e83909cde7ec61a1e313f8a75775897db321450e928cce",
"sha256:736895a020e31b428b3382a7887bfea96102c529530299f426bf2e636aacec9e",
"sha256:75bb36f134883fdbe13d8e63b8675f5f12b80bb6627f7714c7d6c5becf22719f",
"sha256:7d2f5d97fcbd004c03df8d8fe2b973fe2b14e7bfeb2cfa012eaa8759ce9a762f",
"sha256:80beaf63ddfbc64a0452b841d8036ca0611e049650e20afcb882f5d3c266d65f",
"sha256:84ad5e29bf8bab3ad70fd707d3c05524862bddc54dc040982b0dbcff36481de7",
"sha256:8da5924cb1f9064589767b0f3fc39d03e3d0fb5aa29e0cb21d43106519bd624a",
"sha256:961eb86e5be7d0973789f30ebcf6caab60b844203f4396ece27310295a6082c7",
"sha256:96de1932237abe0a13ba68b63e94113678c379dca45afa040a17b6e1ad7ed076",
"sha256:a0a0abef2ca47b33fb615b491ce31b055ef2430de52c5b3fb19a4042dbc5cadb",
"sha256:b2a5a856019d2833c56a3dcac1b80fe795c95f401818ea963594b345929dffa7",
"sha256:b8811d48078d1cf2a6863dafb896e68406c5f513048451cd2ded0473133473c7",
"sha256:c532d5ab79be0199fa2658e24a02fce8542df196e60665dd322409a03db6a52c",
"sha256:d3b64c65328cb4cd252c94f83e66e3d7acf8891e60ebf588d7b493a55a1dbf26",
"sha256:d4e702eea4a2903441f2735799d217f4ac1b55f7d8ad96ab7d4e25417cb0827c",
"sha256:d5653619b3eb5cbd35bfba3c12d575db2a74d15e0e1c08bf1db788069d410ce8",
"sha256:d66624f04de4af8bbf1c7f21cc06649c1c69a7f84109179add573ce35e46d448",
"sha256:e67ec74fada3841b8c5f4c4f197bea916025cb9aa3fe5abf7d52b655d042f956",
"sha256:e6f7f3f41faffaea6596da86ecc2389672fa949bd035251eab26dc6697451d05",
"sha256:f02cf7221d5cd915d7fa58ab64f7ee6dd0f6cddbb48683debf5d04ae9b1c2cc1",
"sha256:f0eddfcabd6936558ec020130f932d479930581171368fd728efcfb6ef0dd357",
"sha256:fabbe18087c3d33c5824cb145ffca52eccd053061df1d79d4b66dafa5ad2a5ea",
"sha256:fc3150f85e2dbcf99e65238c842d1cfe69d3e7649b19864c1cc043213d9cd730"
],
"markers": "python_version >= '3.7'",
"version": "==2.1.0"
},
"ormar": {
"hashes": [
"sha256:0ac7765bc14237cb4ed828c823cae3a4a9f5dea6daa402e0999c80b36662c410",
"sha256:908eba2cb7350c5ef0c8e7d9653d061f357e2c7706b78298bd446e0848000762"
],
"index": "pypi",
"version": "==0.10.24"
},
"packaging": {
"hashes": [
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
],
"markers": "python_version >= '3.6'",
"version": "==21.3"
},
"passlib": {
"hashes": [
"sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1",
"sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"
],
"index": "pypi",
"version": "==1.7.4"
},
"pyasn1": {
"hashes": [
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
"sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
"sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"
],
"version": "==0.4.8"
},
"pydantic": {
"hashes": [
"sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3",
"sha256:086254884d10d3ba16da0588604ffdc5aab3f7f09557b998373e885c690dd398",
"sha256:0b6037175234850ffd094ca77bf60fb54b08b5b22bc85865331dd3bda7a02fa1",
"sha256:0fe476769acaa7fcddd17cadd172b156b53546ec3614a4d880e5d29ea5fbce65",
"sha256:1d5278bd9f0eee04a44c712982343103bba63507480bfd2fc2790fa70cd64cf4",
"sha256:2cc6a4cb8a118ffec2ca5fcb47afbacb4f16d0ab8b7350ddea5e8ef7bcc53a16",
"sha256:2ee7e3209db1e468341ef41fe263eb655f67f5c5a76c924044314e139a1103a2",
"sha256:3011b975c973819883842c5ab925a4e4298dffccf7782c55ec3580ed17dc464c",
"sha256:3c3b035103bd4e2e4a28da9da7ef2fa47b00ee4a9cf4f1a735214c1bcd05e0f6",
"sha256:4c68c3bc88dbda2a6805e9a142ce84782d3930f8fdd9655430d8576315ad97ce",
"sha256:574936363cd4b9eed8acdd6b80d0143162f2eb654d96cb3a8ee91d3e64bf4cf9",
"sha256:5a79330f8571faf71bf93667d3ee054609816f10a259a109a0738dac983b23c3",
"sha256:5e48ef4a8b8c066c4a31409d91d7ca372a774d0212da2787c0d32f8045b1e034",
"sha256:6c5b77947b9e85a54848343928b597b4f74fc364b70926b3c4441ff52620640c",
"sha256:742645059757a56ecd886faf4ed2441b9c0cd406079c2b4bee51bcc3fbcd510a",
"sha256:7bdfdadb5994b44bd5579cfa7c9b0e1b0e540c952d56f627eb227851cda9db77",
"sha256:815ddebb2792efd4bba5488bc8fde09c29e8ca3227d27cf1c6990fc830fd292b",
"sha256:8b5ac0f1c83d31b324e57a273da59197c83d1bb18171e512908fe5dc7278a1d6",
"sha256:96f240bce182ca7fe045c76bcebfa0b0534a1bf402ed05914a6f1dadff91877f",
"sha256:a733965f1a2b4090a5238d40d983dcd78f3ecea221c7af1497b845a9709c1721",
"sha256:ab624700dc145aa809e6f3ec93fb8e7d0f99d9023b713f6a953637429b437d37",
"sha256:b2571db88c636d862b35090ccf92bf24004393f85c8870a37f42d9f23d13e032",
"sha256:bbbc94d0c94dd80b3340fc4f04fd4d701f4b038ebad72c39693c794fd3bc2d9d",
"sha256:c0727bda6e38144d464daec31dff936a82917f431d9c39c39c60a26567eae3ed",
"sha256:c556695b699f648c58373b542534308922c46a1cda06ea47bc9ca45ef5b39ae6",
"sha256:c86229333cabaaa8c51cf971496f10318c4734cf7b641f08af0a6fbf17ca3054",
"sha256:c8d7da6f1c1049eefb718d43d99ad73100c958a5367d30b9321b092771e96c25",
"sha256:c8e9dcf1ac499679aceedac7e7ca6d8641f0193c591a2d090282aaf8e9445a46",
"sha256:cb23bcc093697cdea2708baae4f9ba0e972960a835af22560f6ae4e7e47d33f5",
"sha256:d1e4c28f30e767fd07f2ddc6f74f41f034d1dd6bc526cd59e63a82fe8bb9ef4c",
"sha256:d9c9bdb3af48e242838f9f6e6127de9be7063aad17b32215ccc36a09c5cf1070",
"sha256:dee5ef83a76ac31ab0c78c10bd7d5437bfdb6358c95b91f1ba7ff7b76f9996a1",
"sha256:e0896200b6a40197405af18828da49f067c2fa1f821491bc8f5bde241ef3f7d7",
"sha256:f5a64b64ddf4c99fe201ac2724daada8595ada0d102ab96d019c1555c2d6441d",
"sha256:f947352c3434e8b937e3aa8f96f47bdfe6d92779e44bb3f41e4c213ba6a32145"
],
"index": "pypi",
"version": "==1.9.0"
},
"pyparsing": {
"hashes": [
"sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea",
"sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"
],
"markers": "python_version >= '3.6'",
"version": "==3.0.7"
},
"python-dotenv": {
"hashes": [
"sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3",
"sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f"
],
"version": "==0.19.2"
},
"python-engineio": {
"hashes": [
"sha256:6e1d26977ffefe3b7da1b5df7a8750aedc7686da8201cd90daf36693db122489",
"sha256:85986067cb9f7695347954d4e03491f7d45152c5428c07109a9707e04e8942cb"
],
"markers": "python_version >= '3.6'",
"version": "==4.3.1"
},
"python-jose": {
"hashes": [
"sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a",
"sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"
],
"index": "pypi",
"version": "==3.3.0"
},
"python-multipart": {
"hashes": [
"sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"
],
"index": "pypi",
"version": "==0.0.5"
},
"python-socketio": {
"hashes": [
"sha256:5a1b173a4c3471bed363c502a93b36a02e2a29847645c5557237b3359d49caf0",
"sha256:6213f7dfbb87e554472d11151f96bae9059854aa88e4a1e0fa6e97cca1c3fcf4"
],
"index": "pypi",
"version": "==5.5.2"
},
"pyyaml": {
"hashes": [
"sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293",
"sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b",
"sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57",
"sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b",
"sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4",
"sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07",
"sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba",
"sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9",
"sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287",
"sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513",
"sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0",
"sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0",
"sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92",
"sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f",
"sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2",
"sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc",
"sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c",
"sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86",
"sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4",
"sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c",
"sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34",
"sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b",
"sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c",
"sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb",
"sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737",
"sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3",
"sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d",
"sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53",
"sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78",
"sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803",
"sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a",
"sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174",
"sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"
],
"version": "==6.0"
},
"redis": {
"hashes": [
"sha256:95ff6381bc15acbb330d24896601fb8c50468003c0f30c9503afda2907225d08",
"sha256:f5c52d3bb91d1ce94f0abaf7a660de7d95ea02cccd0cd0930912c49285fced6f"
],
"index": "pypi",
"version": "==4.2.0rc1"
},
"rsa": {
"hashes": [
"sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17",
"sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"
],
"markers": "python_version >= '3.6' and python_version < '4.0'",
"version": "==4.8"
},
"six": {
"hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"sniffio": {
"hashes": [
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
],
"markers": "python_version >= '3.5'",
"version": "==1.2.0"
},
"sqlalchemy": {
"hashes": [
"sha256:0072f9887aabe66db23f818bbe950cfa1b6127c5cb769b00bcc07935b3adb0ad",
"sha256:027f356c727db24f3c75828c7feb426f87ce1241242d08958e454bd025810660",
"sha256:08cfd35eecaba79be930c9bfd2e1f0c67a7e1314355d83a378f9a512b1cf7587",
"sha256:0fc4eec2f46b40bdd42112b3be3fbbf88e194bcf02950fbb88bcdc1b32f07dc7",
"sha256:101d2e100ba9182c9039699588e0b2d833c54b3bad46c67c192159876c9f27ea",
"sha256:15b65887b6c324cad638c7671cb95985817b733242a7eb69edd7cdf6953be1e0",
"sha256:37b46bfc4af3dc226acb6fa28ecd2e1fd223433dc5e15a2bad62bf0a0cbb4e8b",
"sha256:56d9d62021946263d4478c9ca012fbd1805f10994cb615c88e7bfd1ae14604d8",
"sha256:5919e647e1d4805867ea556ed4967c68b4d8b266059fa35020dbaed8ffdd60f3",
"sha256:5a717c2e70fd1bb477161c4cc85258e41d978584fbe5522613618195f7e87d9b",
"sha256:5e9cd33459afa69c88fa648e803d1f1245e3caa60bfe8b80a9595e5edd3bda9c",
"sha256:621854dbb4d2413c759a5571564170de45ef37299df52e78e62b42e2880192e1",
"sha256:78abc507d17753ed434b6cc0c0693126279723d5656d9775bfcac966a99a899b",
"sha256:7dd0502cb091660ad0d89c5e95a29825f37cde2a5249957838e975871fbffaad",
"sha256:804e22d5b6165a4f3f019dd9c94bec5687de985a9c54286b93ded9f7846b8c82",
"sha256:878daecb6405e786b07f97e1c77a9cfbbbec17432e8a90c487967e32cfdecb33",
"sha256:886359f734b95ad1ef443b13bb4518bcade4db4f9553c9ce33d6d04ebda8d44e",
"sha256:9ce960a1dc60524136cf6f75621588e2508a117e04a6e3eedb0968bd13b8c824",
"sha256:ad618d687d26d4cbfa9c6fa6141d59e05bcdfc60cb6e1f1d3baa18d8c62fef5f",
"sha256:c5de7adfb91d351f44062b8dedf29f49d4af7cb765be65816e79223a4e31062b",
"sha256:ceac84dd9abbbe115e8be0c817bed85d9fa639b4d294e7817f9e61162d5f766c",
"sha256:da64423c05256f4ab8c0058b90202053b201cbe3a081f3a43eb590cd554395ab",
"sha256:dc27dcc6c72eb38be7f144e9c2c4372d35a3684d3a6dd43bd98c1238358ee17c",
"sha256:dd49d21d1f03c81fbec9080ecdc4486d5ddda67e7fbb75ebf48294465c022cdc",
"sha256:debaf09a823061f88a8dee04949814cf7e82fb394c5bca22c780cb03172ca23b",
"sha256:e027bdf0a4cf6bd0a3ad3b998643ea374d7991bd117b90bf9982e41ceb742941",
"sha256:e4ddd4f2e247128c58bb3dd4489922874afce157d2cff0b2295d67fcd0f22494",
"sha256:e5f6959466a42b6569774c257e55f9cd85200d5b0ba09f0f5d8b5845349c5822",
"sha256:e89347d3bd2ef873832b47e85f4bbd810a5e626c5e749d90a07638da100eb1c8",
"sha256:e9cc6d844e24c307c3272677982a9b33816aeb45e4977791c3bdd47637a8d810",
"sha256:eb8c993706e86178ce15a6b86a335a2064f52254b640e7f53365e716423d33f4",
"sha256:eeaebceb24b46e884c4ad3c04f37feb178b81f6ce720af19bfa2592ca32fdef7",
"sha256:f3909194751bb6cb7c5511dd18bcf77e6e3f0b31604ed4004dffa9461f71e737",
"sha256:f74d6c05d2d163464adbdfbc1ab85048cc15462ff7d134b8aed22bd521e1faa5",
"sha256:fa2bad14e1474ba649cfc969c1d2ec915dd3e79677f346bbfe08e93ef9020b39",
"sha256:fbc6e63e481fa323036f305ada96a3362e1d60dd2bfa026cac10c3553e6880e9"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==1.4.29"
},
"starlette": {
"hashes": [
"sha256:26a18cbda5e6b651c964c12c88b36d9898481cd428ed6e063f5f29c418f73050",
"sha256:57eab3cc975a28af62f6faec94d355a410634940f10b30d68d31cb5ec1b44ae8"
],
"markers": "python_version >= '3.6'",
"version": "==0.17.1"
},
"typing-extensions": {
"hashes": [
"sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42",
"sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"
],
"markers": "python_version >= '3.6'",
"version": "==4.1.1"
},
"uvicorn": {
"extras": [
"standard"
],
"hashes": [
"sha256:8adddf629b79857b48b999ae1b14d6c92c95d4d7840bd86461f09bee75f1653e",
"sha256:c04a9c069111489c324f427501b3840d306c6b91a77b00affc136a840a3f45f1"
],
"index": "pypi",
"version": "==0.17.5"
},
"uvloop": {
"hashes": [
"sha256:04ff57aa137230d8cc968f03481176041ae789308b4d5079118331ab01112450",
"sha256:089b4834fd299d82d83a25e3335372f12117a7d38525217c2258e9b9f4578897",
"sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861",
"sha256:30ba9dcbd0965f5c812b7c2112a1ddf60cf904c1c160f398e7eed3a6b82dcd9c",
"sha256:3a19828c4f15687675ea912cc28bbcb48e9bb907c801873bd1519b96b04fb805",
"sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d",
"sha256:647e481940379eebd314c00440314c81ea547aa636056f554d491e40503c8464",
"sha256:6ccd57ae8db17d677e9e06192e9c9ec4bd2066b77790f9aa7dede2cc4008ee8f",
"sha256:772206116b9b57cd625c8a88f2413df2fcfd0b496eb188b82a43bed7af2c2ec9",
"sha256:8e0d26fa5875d43ddbb0d9d79a447d2ace4180d9e3239788208527c4784f7cab",
"sha256:98d117332cc9e5ea8dfdc2b28b0a23f60370d02e1395f88f40d1effd2cb86c4f",
"sha256:b572256409f194521a9895aef274cea88731d14732343da3ecdb175228881638",
"sha256:bd53f7f5db562f37cd64a3af5012df8cac2c464c97e732ed556800129505bd64",
"sha256:bd8f42ea1ea8f4e84d265769089964ddda95eb2bb38b5cbe26712b0616c3edee",
"sha256:e814ac2c6f9daf4c36eb8e85266859f42174a4ff0d71b99405ed559257750382",
"sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228"
],
"version": "==0.16.0"
},
"watchgod": {
"hashes": [
"sha256:48140d62b0ebe9dd9cf8381337f06351e1f2e70b2203fa9c6eff4e572ca84f29",
"sha256:d6c1ea21df37847ac0537ca0d6c2f4cdf513562e95f77bb93abbcf05573407b7"
],
"version": "==0.7"
},
"websockets": {
"hashes": [
"sha256:038afef2a05893578d10dadbdbb5f112bd115c46347e1efe99f6a356ff062138",
"sha256:05f6e9757017270e7a92a2975e2ae88a9a582ffc4629086fd6039aa80e99cd86",
"sha256:0b66421f9f13d4df60cd48ab977ed2c2b6c9147ae1a33caf5a9f46294422fda1",
"sha256:0cd02f36d37e503aca88ab23cc0a1a0e92a263d37acf6331521eb38040dcf77b",
"sha256:0f73cb2526d6da268e86977b2c4b58f2195994e53070fe567d5487c6436047e6",
"sha256:117383d0a17a0dda349f7a8790763dde75c1508ff8e4d6e8328b898b7df48397",
"sha256:1c1f3b18c8162e3b09761d0c6a0305fd642934202541cc511ef972cb9463261e",
"sha256:1c9031e90ebfc486e9cdad532b94004ade3aa39a31d3c46c105bb0b579cd2490",
"sha256:2349fa81b6b959484bb2bda556ccb9eb70ba68987646a0f8a537a1a18319fb03",
"sha256:24b879ba7db12bb525d4e58089fcbe6a3df3ce4666523183654170e86d372cbe",
"sha256:2aa9b91347ecd0412683f28aabe27f6bad502d89bd363b76e0a3508b1596402e",
"sha256:56d48eebe9e39ce0d68701bce3b21df923aa05dcc00f9fd8300de1df31a7c07c",
"sha256:5a38a0175ae82e4a8c4bac29fc01b9ee26d7d5a614e5ee11e7813c68a7d938ce",
"sha256:5b04270b5613f245ec84bb2c6a482a9d009aefad37c0575f6cda8499125d5d5c",
"sha256:6193bbc1ee63aadeb9a4d81de0e19477401d150d506aee772d8380943f118186",
"sha256:669e54228a4d9457abafed27cbf0e2b9f401445c4dfefc12bf8e4db9751703b8",
"sha256:6a009eb551c46fd79737791c0c833fc0e5b56bcd1c3057498b262d660b92e9cd",
"sha256:71a4491cfe7a9f18ee57d41163cb6a8a3fa591e0f0564ca8b0ed86b2a30cced4",
"sha256:7b38a5c9112e3dbbe45540f7b60c5204f49b3cb501b40950d6ab34cd202ab1d0",
"sha256:7bb9d8a6beca478c7e9bdde0159bd810cc1006ad6a7cb460533bae39da692ca2",
"sha256:82bc33db6d8309dc27a3bee11f7da2288ad925fcbabc2a4bb78f7e9c56249baf",
"sha256:8351c3c86b08156337b0e4ece0e3c5ec3e01fcd14e8950996832a23c99416098",
"sha256:8beac786a388bb99a66c3be4ab0fb38273c0e3bc17f612a4e0a47c4fc8b9c045",
"sha256:97950c7c844ec6f8d292440953ae18b99e3a6a09885e09d20d5e7ecd9b914cf8",
"sha256:98f57b3120f8331cd7440dbe0e776474f5e3632fdaa474af1f6b754955a47d71",
"sha256:9ca2ca05a4c29179f06cf6727b45dba5d228da62623ec9df4184413d8aae6cb9",
"sha256:a03a25d95cc7400bd4d61a63460b5d85a7761c12075ee2f51de1ffe73aa593d3",
"sha256:a10c0c1ee02164246f90053273a42d72a3b2452a7e7486fdae781138cf7fbe2d",
"sha256:a72b92f96e5e540d5dda99ee3346e199ade8df63152fa3c737260da1730c411f",
"sha256:ac081aa0307f263d63c5ff0727935c736c8dad51ddf2dc9f5d0c4759842aefaa",
"sha256:b22bdc795e62e71118b63e14a08bacfa4f262fd2877de7e5b950f5ac16b0348f",
"sha256:b4059e2ccbe6587b6dc9a01db5fc49ead9a884faa4076eea96c5ec62cb32f42a",
"sha256:b7fe45ae43ac814beb8ca09d6995b56800676f2cfa8e23f42839dc69bba34a42",
"sha256:bef03a51f9657fb03d8da6ccd233fe96e04101a852f0ffd35f5b725b28221ff3",
"sha256:bffc65442dd35c473ca9790a3fa3ba06396102a950794f536783f4b8060af8dd",
"sha256:c21a67ab9a94bd53e10bba21912556027fea944648a09e6508415ad14e37c325",
"sha256:c67d9cacb3f6537ca21e9b224d4fd08481538e43bcac08b3d93181b0816def39",
"sha256:c6e56606842bb24e16e36ae7eb308d866b4249cf0be8f63b212f287eeb76b124",
"sha256:cb316b87cbe3c0791c2ad92a5a36bf6adc87c457654335810b25048c1daa6fd5",
"sha256:cef40a1b183dcf39d23b392e9dd1d9b07ab9c46aadf294fff1350fb79146e72b",
"sha256:cf931c33db9c87c53d009856045dd524e4a378445693382a920fa1e0eb77c36c",
"sha256:d4d110a84b63c5cfdd22485acc97b8b919aefeecd6300c0c9d551e055b9a88ea",
"sha256:d5396710f86a306cf52f87fd8ea594a0e894ba0cc5a36059eaca3a477dc332aa",
"sha256:f09f46b1ff6d09b01c7816c50bd1903cf7d02ebbdb63726132717c2fcda835d5",
"sha256:f14bd10e170abc01682a9f8b28b16e6f20acf6175945ef38db6ffe31b0c72c3f",
"sha256:f5c335dc0e7dc271ef36df3f439868b3c790775f345338c2f61a562f1074187b",
"sha256:f8296b8408ec6853b26771599990721a26403e62b9de7e50ac0a056772ac0b5e",
"sha256:fa35c5d1830d0fb7b810324e9eeab9aa92e8f273f11fdbdc0741dcded6d72b9f"
],
"version": "==10.2"
},
"wrapt": {
"hashes": [
"sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179",
"sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096",
"sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374",
"sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df",
"sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185",
"sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785",
"sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7",
"sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909",
"sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918",
"sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33",
"sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068",
"sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829",
"sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af",
"sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79",
"sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce",
"sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc",
"sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36",
"sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade",
"sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca",
"sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32",
"sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125",
"sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e",
"sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709",
"sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f",
"sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b",
"sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb",
"sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb",
"sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489",
"sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640",
"sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb",
"sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851",
"sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d",
"sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44",
"sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13",
"sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2",
"sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb",
"sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b",
"sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9",
"sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755",
"sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c",
"sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a",
"sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf",
"sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3",
"sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229",
"sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e",
"sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de",
"sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554",
"sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10",
"sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80",
"sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056",
"sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==1.13.3"
}
},
"develop": {}
}
+58
View File
@@ -0,0 +1,58 @@
# Socket.IO-Client-Docs
## Init
Event: `join_game`
body: `{game_id}` (obtained by `/api/v1/quiz/join`)
return: `None`
## Get Game Data
Event: `get_game_data`
body: `{game_id}`
return (example):
```json
{
"quiz_id": "26058ee8-6c6f-41e0-9d3d-70c82e38d9de",
"questions": [
{
"question": "Is ClassQuiz cool?",
"answers": [
{
"right": true,
"answer": "Yes"
},
{
"right": false,
"answer": "No"
}
]
},
{
"question": "Do you like open source?",
"answers": [
{
"right": true,
"answer": "Yes"
},
{
"right": false,
"answer": "No"
},
{
"right": false,
"answer": "Maybe"
}
]
}
],
"game_id": "d2923b0a-f02e-45b7-8876-268b473beb06",
"game_pin": "45823169",
"started": false
}
```
+102
View File
@@ -0,0 +1,102 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = alembic
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = asdas
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
+1
View File
@@ -0,0 +1 @@
Generic single-database configuration.
+84
View File
@@ -0,0 +1,84 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# 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
from classquiz.db import metadata
target_metadata = metadata
from classquiz.config import settings
url = settings.db_url
# 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.
"""
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
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),
url=url,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
+24
View 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,0 +1,28 @@
"""made some changes
Revision ID: 76507db71fde
Revises:
Create Date: 2022-02-25 18:38:08.687974
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '76507db71fde'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
+28
View File
@@ -0,0 +1,28 @@
from fastapi import FastAPI
from classquiz.socket_server import sio
from socketio import ASGIApp
from classquiz.routers import users, quiz
from classquiz.db import database
app = FastAPI()
app.state.database = database
@app.on_event("startup")
async def startup() -> None:
database_ = app.state.database
if not database_.is_connected:
await database_.connect()
@app.on_event("shutdown")
async def shutdown() -> None:
database_ = app.state.database
if database_.is_connected:
await database_.disconnect()
app.include_router(users.router, tags=["users"], prefix="/api/v1/users")
app.include_router(quiz.router, tags=["quiz"], prefix="/api/v1/quiz")
app.mount("/", ASGIApp(sio))
+147
View File
@@ -0,0 +1,147 @@
from classquiz.config import settings
from classquiz.db.models import *
from typing import Union
from jose import JWTError, jwt
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from passlib.context import CryptContext
from datetime import datetime, timedelta
from classquiz.cache import get_cache
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2
from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
from fastapi import Request
from fastapi.security.utils import get_authorization_scheme_param
from fastapi import HTTPException
from fastapi import status
from typing import Optional
from typing import Dict
from starlette.status import HTTP_401_UNAUTHORIZED
import pydantic
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = settings.access_token_expire_minutes
class OAuth2PasswordBearerWithCookie(OAuth2):
def __init__(
self,
tokenUrl: str,
scheme_name: Optional[str] = None,
scopes: Optional[Dict[str, str]] = None,
auto_error: bool = True,
):
if not scopes:
scopes = {}
flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes})
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
async def __call__(self, request: Request) -> Optional[str]:
authorization: str = request.cookies.get("access_token") # changed to accept access token from httpOnly Cookie
scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer":
if self.auto_error:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
else:
return None
return param
oauth2_scheme = OAuth2PasswordBearerWithCookie(tokenUrl="/api/v1/users/token/cookie")
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)
async def get_user_from_mail(email: str) -> Union[User, None]:
return await get_cache(criteria="email", content=email)
async def get_user_from_username(username: str) -> Union[User, None]:
return await get_cache(criteria="username", content=username)
async def get_user_from_id(id: str) -> Union[User, None]:
return await get_cache(criteria="id", content=id)
async def authenticate_user(email: str, password: str) -> Union[User, bool]:
user = await get_user_from_mail(email)
if not user:
return False
if not verify_password(password, user.password):
return False
return user
def create_access_token(data: dict, expires_delta: Optional[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})
encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=ALGORITHM)
return encoded_jwt
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, settings.secret_key, algorithms=[ALGORITHM])
email: str = payload.get("sub")
if email is None:
raise credentials_exception
token_data = TokenData(email=email)
except JWTError:
raise credentials_exception
user = await get_user_from_mail(email=token_data.email)
if user is None:
raise credentials_exception
return user
async def get_current_user_optional(token: str = Depends(oauth2_scheme)) -> User | None:
try:
payload = jwt.decode(token, settings.secret_key, algorithms=[ALGORITHM])
email: str = payload.get("sub")
if email is None:
return None
token_data = TokenData(email=email)
except JWTError:
return None
user = await get_user_from_mail(email=token_data.email)
if user is None:
return None
return user
async def check_token(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, settings.secret_key, algorithms=[ALGORITHM])
email: str = payload.get("sub")
if email is None:
raise credentials_exception
token_data = TokenData(email=email)
except JWTError:
raise credentials_exception
return token_data.email
+52
View File
@@ -0,0 +1,52 @@
from classquiz.config import redis, settings
from classquiz.db.models import User
import ormar
import uuid
from typing import Union
from json import loads, dumps
async def cache_account(criteria: str, content: str) -> Union[User, None]:
async def insert_into_redis(usermodel: User, key: str):
await redis.set(key, usermodel.json(), ex=settings.cache_expiry)
if criteria == "email":
try:
res = await User.objects.get(email=content)
except ormar.exceptions.NoMatch:
return None
await insert_into_redis(res, content)
return res
elif criteria == "username":
try:
res = await User.objects.get(username=content)
except ormar.exceptions.NoMatch:
return None
await insert_into_redis(res, content)
return res
elif criteria == "id":
try:
res = await User.objects.get(id=uuid.UUID(content))
except ormar.exceptions.NoMatch:
return None
await insert_into_redis(res, content)
return res
else:
return None
async def get_from_redis(key: str) -> Union[None, User]:
user = await redis.get(key)
if user is None:
return None
else:
return User.parse_obj(loads(user))
async def get_cache(criteria: str, content: str) -> User:
cache = await get_from_redis(content)
if cache is not None:
return cache
else:
return await cache_account(criteria, content)
+38
View File
@@ -0,0 +1,38 @@
from pydantic import BaseSettings, RedisDsn
import redis.asyncio as redis_lib
class Settings(BaseSettings):
"""
Settings class for the shop app.
"""
root_address: str = "http://127.0.0.1:8000"
redis: RedisDsn = "redis://localhost:6379/0?decode_responses=True"
skip_email_verification: bool = False
db_url: str = "sqlite:///classquiz.db"
mail_address: str
mail_password: str
mail_username: str
mail_server: str
mail_port: int
secret_key: str
minio_url: str = "127.0.0.1"
minio_access_key: str
minio_secret_key: str
minio_bucket: str
minio_secure: bool = True
access_token_expire_minutes: int = 30
cache_expiry: int = 86400
typesense_api_key: str
typesense_host: str = "localhost"
typesense_port: int = 8108
typesense_protocol: str = "http"
typesense_timeout: int = 2
class Config:
env_file = ".env"
env_file_encoding = 'utf-8'
settings = Settings()
redis: redis_lib.client.Redis = redis_lib.Redis().from_url(settings.redis)
+6
View File
@@ -0,0 +1,6 @@
import databases
import sqlalchemy
from classquiz.config import settings
database = databases.Database(settings.db_url)
metadata = sqlalchemy.MetaData()
+115
View File
@@ -0,0 +1,115 @@
import uuid
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, Json
from . import metadata, database
import ormar
class User(ormar.Model):
"""
The user model
"""
id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4())
email: str = ormar.String(unique=True, max_length=100)
username: str = ormar.String(unique=True, max_length=100)
password: str = ormar.String(unique=True, max_length=100)
verified: bool = ormar.Boolean(default=False)
verify_key: str = ormar.String(unique=True, max_length=100, nullable=True)
created_at: datetime = ormar.DateTime(default=datetime.now())
class Meta:
tablename = 'users'
metadata = metadata
database = database
class UserSession(ormar.Model):
"""
The user session model
"""
id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4())
user: uuid.UUID = ormar.ForeignKey(User)
session_key: str = ormar.String(unique=True, max_length=64)
created_at: datetime = ormar.DateTime(default=datetime.now())
class Meta:
tablename = 'user_sessions'
metadata = metadata
database = database
class QuizAnswer(BaseModel):
right: bool
answer: str
class QuizQuestion(BaseModel):
question: str
time: str # in Secs
answers: list[QuizAnswer]
class QuizInput(BaseModel):
public: bool = False
title: str
description: str
questions: list[QuizQuestion]
class Quiz(ormar.Model):
id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4())
public: bool = ormar.Boolean(default=False)
title: str = ormar.String(max_length=100)
description: str = ormar.String(max_length=300, nullable=True)
created_at: datetime = ormar.DateTime(default=datetime.now())
updated_at: datetime = ormar.DateTime(default=datetime.now())
user_id: uuid.UUID = ormar.UUID(foreign_key=User.id)
questions: Json[list[QuizQuestion]] = ormar.JSON(nullable=False)
class Meta:
tablename = 'quiz'
metadata = metadata
database = database
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
email: str | None = None
class PlayGame(BaseModel):
quiz_id: uuid.UUID | str
description: str
title: str
questions: list[QuizQuestion]
game_id: uuid.UUID
game_pin: str
started: bool = False
class GamePlayer(BaseModel):
username: str
sid: str
class GameAnser2(BaseModel):
username: str
right: bool
answer: str
class GameAnser1(BaseModel):
id: int
answers: list[GameAnser2]
class GameSession(BaseModel):
admin: str
game_id: str
players: list[GamePlayer | None]
answers: list[GameAnser1 | None]
+4
View File
@@ -0,0 +1,4 @@
async def send_mail(email: str):
pass
View File
+67
View File
@@ -0,0 +1,67 @@
import json
import os
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse
from classquiz.config import redis
import pydantic
from classquiz.auth import get_current_user, get_current_user_optional
from classquiz.db.models import Quiz, QuizInput, User, PlayGame
from random import randint
import uuid
router = APIRouter()
@router.post("/create")
async def create_quiz_lol(quiz_input: QuizInput, user: User = Depends(get_current_user)):
quiz = Quiz(**quiz_input.dict(), user_id=user.id)
return await quiz.save()
@router.get("/get/{quiz_id}")
async def get_quiz_from_id(quiz_id: str, user: User | None = Depends(get_current_user_optional)):
try:
quiz_id = uuid.UUID(quiz_id)
except ValueError:
raise HTTPException(status_code=400, detail="badly formed quiz id")
if user is None:
quiz = await Quiz.objects.get_or_none(id=quiz_id, public=True)
else:
quiz = await Quiz.objects.get_or_none(id=quiz_id, public=False, user_id=user.id)
if quiz is None:
return JSONResponse(status_code=404, content={"detail": "quiz not found"})
else:
return quiz
@router.post("/start/{quiz_id}")
async def start_quiz(quiz_id: str, user: User = Depends(get_current_user)):
try:
quiz_id = uuid.UUID(quiz_id)
except ValueError:
raise HTTPException(status_code=400, detail="badly formed quiz id")
quiz = await Quiz.objects.get_or_none(id=quiz_id, public=False, user_id=user.id)
if quiz is None:
return JSONResponse(status_code=404, content={"detail": "quiz not found"})
else:
game_pin = randint(10000000, 99999999)
game = PlayGame(quiz_id=quiz_id, game_pin=str(game_pin), questions=quiz.questions, game_id=uuid.uuid4(),
title=quiz.title, description=quiz.description)
await redis.set(f"game:{str(game.game_pin)}", (game.json()))
return {**quiz.dict(exclude={"id"}), **game.dict(exclude={"questions"})}
@router.get("/join/{game_pin}")
async def get_game_id(game_pin: str):
redis_res = (await redis.get(f"game:{game_pin}")).decode()
if redis_res is None:
raise HTTPException(status_code=404, detail="game not found")
else:
return json.loads(redis_res)["game_id"]
@router.get("/list")
async def get_quiz_list(user: User = Depends(get_current_user)):
return await Quiz.objects.filter(user_id=user.id).all()
+90
View File
@@ -0,0 +1,90 @@
import os
from fastapi import APIRouter, HTTPException, Response
from email_validator import validate_email, EmailNotValidError
from classquiz.auth import *
from fastapi.background import BackgroundTasks
from classquiz.emails import send_mail
from fastapi.responses import JSONResponse
from classquiz.db.models import *
router = APIRouter()
route_user = User.get_pydantic(
exclude={"id": ..., "verified": ..., "verify_key": ..., "created_at": ..., "usersessions": ...})
@router.post("/create", response_model=User,
response_model_include={"id": ..., "verified": ..., "email": ...})
async def create_user(user: route_user, background_task: BackgroundTasks) -> User | JSONResponse:
user = User(**user.dict())
try:
validate_email(user.email)
except EmailNotValidError as e:
raise HTTPException(status_code=400, detail=str(e))
user.verify_key = str(os.urandom(16).hex())
res = await User.objects.filter((User.email == user.email) | (User.username == user.username)).all()
if len(res) != 0:
raise HTTPException(status_code=400, detail="User already exists")
user.password = get_password_hash(user.password)
if len(user.username) == 32:
return JSONResponse({"details": "Username mustn't be 32 characters long"}, 400)
res = await user.save()
background_task.add_task(send_mail, email=user.email)
return user
@router.post("/token/cookie", response_model=Token)
async def login_for_cookie_access_token(request: Request, response: Response,
form_data: OAuth2PasswordRequestForm = Depends()):
user = await authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
)
session_key = os.urandom(32).hex()
# user_session = UserSession(user=user, session_key=session_key)
# print(user_session, "HALLO!!!!")
# await user_session.save()
access_token_expires = timedelta(minutes=settings.access_token_expire_minutes)
access_token = create_access_token(
data={"sub": user.email}, expires_delta=access_token_expires
)
rememberme_token = create_access_token(
data={"sub": user.email}, expires_delta=timedelta(days=360))
response.set_cookie(key="access_token", value=f"Bearer {access_token}",
httponly=True, samesite='strict')
response.set_cookie(key="expiry", value="", max_age=settings.access_token_expire_minutes * 60)
response.set_cookie(key="rememberme", value="")
response.set_cookie(key="rememberme_token", value=rememberme_token, httponly=True, samesite='strict')
return {"access_token": access_token, "token_type": "bearer"}
@router.get("/token/rememberme")
async def rememberme_token(request: Request, response: Response):
rememberme_cookie = request.cookies.get("rememberme_token")
if rememberme_cookie is None:
raise HTTPException(status_code=400, detail="No rememberme cookie")
payload = jwt.decode(rememberme_cookie, settings.secret_key, algorithms=[ALGORITHM])
access_token_expires = timedelta(minutes=settings.access_token_expire_minutes)
access_token = create_access_token(
data={"sub": payload}, expires_delta=access_token_expires
)
response.set_cookie(key="access_token", value=f"Bearer {access_token}",
httponly=True, samesite='strict')
response.set_cookie(key="expiry", value="", max_age=settings.access_token_expire_minutes * 60)
@router.get("/logout")
async def logout(request: Request, response: Response):
remember_token = request.cookies.get("rememberme_token")
if remember_token is not None:
UserSession.objects.filter(session_key=remember_token).delete()
response.delete_cookie("access_token")
response.delete_cookie("expiry")
response.delete_cookie("rememberme")
response.delete_cookie("rememberme_token")
+147
View File
@@ -0,0 +1,147 @@
import socketio
import json
from classquiz.config import redis
from classquiz.db.models import GameSession, GameAnser1, GameAnser2, PlayGame
from redis.commands.json.path import Path
sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*")
@sio.event
async def connect(sid, environ, lol):
print(sid, "connected")
# sio.enter_room(sid, lol)
# print(lol)
# print(environ["asgi.scope"]["headers"])
# print(environ)
# async with sio.session(sid) as session:
# session['username'] = "username"
@sio.event
async def join_game(sid, data):
print(sid, data, "JOIN_GAME")
redis_res = await redis.get(f"game:{data['game_pin']}")
if redis_res is None:
await sio.emit("game_not_found", room=sid)
else:
session = {'game_pin': data["game_pin"], "username": data["username"], "admin": False}
await sio.save_session(sid, session)
await sio.emit("joined_game", redis_res, room=sid)
redis_res = (await redis.get(f"game_session:{data['game_pin']}"))
redis_res = json.loads(redis_res)
print(redis_res)
redis_res["players"].append({"username": data["username"], "sid": sid})
await redis.set(f"game_session:{data['game_pin']}",
json.dumps({"admin": redis_res["admin"], "game_id": redis_res["game_id"],
"players": redis_res["players"], "answers": []}))
await sio.emit("player_joined", {"username": data["username"], "sid": sid}, room=redis_res["admin"])
sio.enter_room(sid, data["game_pin"]) # TODO: make more secure
@sio.event
async def start_game(sid, data):
print(sid, data, "START_GAME")
session = await sio.get_session(sid)
print(session)
if session["admin"]:
await sio.emit("start_game", room=session["game_pin"])
@sio.event
async def register_as_admin(sid, data):
game_pin = data["game_pin"]
game_id = data["game_id"]
if (await redis.get(f"game_session:{game_pin}")) is None:
await redis.set(f"game_session:{game_pin}", json.dumps({"admin": sid, "game_id": game_id, "players": []}))
await sio.emit("registered_as_admin", {"game_id": game_id, "game": await redis.get(f"game:{data['game_pin']}")},
room=sid)
async with sio.session(sid) as session:
session['game_pin'] = data["game_pin"]
session["admin"] = True
sio.enter_room(sid, data["game_pin"])
else:
await sio.emit("already_registered_as_admin", room=sid)
@sio.event
async def get_question_results(sid, data):
session = await sio.get_session(sid)
if session["admin"]:
redis_res = await redis.get(f"game_session:{session['game_pin']}:{data['question_number']}")
game_pin = session['game_pin']
await sio.emit("question_results", redis_res, room=game_pin)
@sio.event
async def set_question_number(sid, data):
session = await sio.get_session(sid)
if session["admin"]:
game_pin = session['game_pin']
await sio.emit("set_question_number", data, room=game_pin)
@sio.event
async def submit_answer(sid, data):
session = await sio.get_session(sid)
redis_res = await redis.get(f"game_session:{session['game_pin']}")
game_session = GameSession(**json.loads(redis_res))
game_data = PlayGame(**json.loads(await redis.get(f"game:{session['game_pin']}")))
print(game_session)
answer_right = False
for answer in game_data.questions[int(data["question_index"])].answers:
if answer.answer == data["answer"] and answer.right:
answer_right = True
break
answers = await redis.get(f"game_session:{session['game_pin']}:{data['question_index']}")
if answers is None:
await redis.set(f"game_session:{session['game_pin']}:{data['question_index']}",
json.dumps(
[{"username": session["username"], "answer": data["answer"], "right": answer_right}]))
else:
answers = json.loads(answers)
answers.append({session["username"]: data["answer"]})
await redis.set(f"game_session:{session['game_pin']}:{data['question_index']}", json.dumps(answers))
# await redis.set(f"game_data:{session['game_pin']}", json.dumps(data))
# @sio.event
# async def admin_game(sid, data):
# redis_res = await redis.get(f"game:{data['game_pin']}")
# if redis_res is None:
# await sio.emit("game_not_found", room=sid)
# else:
#
# await sio.emit("joined_game", data, room=data['game_pin'])
# @sio.event
# async def start_game(sid, data):
# game_pin = (await sio.get_session(sid))['game_pin']
# session = await sio.get_session(sid)
# if session['admin']:
# await sio.emit("start_game", data, room=game_pin)
@sio.event
async def get_game_data(sid, data):
game_pin = (await sio.get_session(sid))['game_pin']
game_data = await redis.get(f"game:{game_pin}")
if game_data is not None:
await sio.emit("game_data", json.loads(game_data), room=game_pin)
print(sid, data, "GET_GAME_DATA")
#
# @sio.event
# async def message(sid, data):
# game_pin = (await sio.get_session(sid))['game_pin']
# await sio.emit("message", data, room=game_pin)
#
#
# @sio.on('*')
# async def catch_all(event, sid, data):
# print(event, sid, data)
+39
View File
@@ -0,0 +1,39 @@
{
"public":false,
"title":"Some test question",
"description":"A description",
"questions":[
{
"question":"Is ClassQuiz cool?",
"time": 10,
"answers":[
{
"right":true,
"answer":"Yes"
},
{
"right":false,
"answer":"No"
}
]
},
{
"question":"Do you like open source?",
"time": 5,
"answers":[
{
"right":true,
"answer":"Yes"
},
{
"right":false,
"answer":"No"
},
{
"right":false,
"answer":"Maybe"
}
]
}
]
}
+20
View File
@@ -0,0 +1,20 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
plugins: ['svelte3', '@typescript-eslint'],
ignorePatterns: ['*.cjs'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: {
'svelte3/typescript': () => require('typescript')
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020
},
env: {
browser: true,
es2017: true,
node: true
}
};
+8
View File
@@ -0,0 +1,8 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+7
View File
@@ -0,0 +1,7 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"tabWidth": 4
}
+40
View File
@@ -0,0 +1,40 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm init svelte@next
# create a new project in my-app
npm init svelte@next my-app
```
> Note: the `@next` is temporary
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
+38
View File
@@ -0,0 +1,38 @@
{
"name": "frontend",
"version": "0.0.1",
"scripts": {
"dev": "svelte-kit dev",
"build": "svelte-kit build",
"package": "svelte-kit package",
"preview": "svelte-kit preview",
"check": "svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "next",
"@sveltejs/kit": "next",
"@types/luxon": "^2.0.9",
"@typescript-eslint/eslint-plugin": "^5.10.1",
"@typescript-eslint/parser": "^5.10.1",
"autoprefixer": "^10.4.2",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-svelte3": "^3.2.1",
"luxon": "^2.3.1",
"postcss": "^8.4.5",
"postcss-load-config": "^3.1.1",
"prettier": "^2.5.1",
"prettier-plugin-svelte": "^2.5.0",
"socket.io-client": "^4.4.1",
"svelte": "^3.44.0",
"svelte-check": "^2.2.6",
"svelte-preprocess": "^4.10.1",
"tailwindcss": "^3.0.12",
"tslib": "^2.3.1",
"typescript": "~4.5.4"
},
"type": "module"
}
+2140
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
const tailwindcss = require('tailwindcss');
const autoprefixer = require('autoprefixer');
const config = {
plugins: [
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
tailwindcss(),
//But others, like autoprefixer, need to run after,
autoprefixer
]
};
module.exports = config;
+4
View File
@@ -0,0 +1,4 @@
/* Write your global styles here, in PostCSS syntax */
@tailwind base;
@tailwind components;
@tailwind utilities;
+31
View File
@@ -0,0 +1,31 @@
/// <reference types="@sveltejs/kit" />
// See https://kit.svelte.dev/docs/types#the-app-namespace
// for information about these interfaces
declare namespace App {
// interface Locals {}
// interface Platform {}
// interface Session {}
// interface Stuff {}
}
export interface QuizData {
title: string;
description: string;
quiz_id: string;
questions: Question[];
game_id: string;
game_pin: string;
started: boolean;
}
export interface Question {
time: string;
question: string;
answers: Answer[];
}
export interface Answer {
right: boolean;
answer: string;
}
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="description" content="" />
<link rel="icon" href="%svelte.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%svelte.head%
</head>
<body>
<div>%svelte.body%</div>
</body>
</html>
+77
View File
@@ -0,0 +1,77 @@
<script lang='ts'>
import type { Question } from '../../app';
import { socket } from '$lib/socket';
export let question: Question;
export let question_index: string | number;
if (typeof question_index === 'string') {
question_index = parseInt(question_index);
} else {
throw new Error('question_index must be a string or number');
}
let timer_res = question.time;
let selected_answer: string;
// Stop the timer if the question is answered
let timer_interval;
const timer = (time: string) => {
let seconds = Number(time);
let timer_interval = setInterval(() => {
if (timer_res === '0') {
clearInterval(timer_interval);
return;
} else {
seconds--;
}
timer_res = seconds.toString();
}, 1000);
};
console.log(selected_answer)
timer(question.time);
const selectAnswer = (answer: string) => {
selected_answer = answer;
//timer_res = '0';
socket.emit('submit_answer', {
question_index: question_index,
answer: answer
});
};
</script>
<div class='flex flex-col justify-center w-screen h-1/6'>
<h1 class='text-6xl text-center'>
{question.question}
</h1>
<span class='text-center py-2 text-lg'>{timer_res}</span>
</div>
{#if timer_res !== "0"}
<div class='flex flex-wrap'>
{#each question.answers as answer}
<button class='w-1/2 text-3xl bg-amber-700 my-2 disabled:opacity-60 border border-white' disabled={selected_answer !== undefined}
on:click={() => selectAnswer(answer.answer)}>{answer.answer}</button>
{/each}
</div>
{:else}
<div class='flex flex-wrap'>
{#each question.answers as answer}
{#if answer.right}
<button class='w-1/2 text-3xl bg-green-600 border border-white' disabled
class:opacity-30={answer.answer !== selected_answer}
>{answer.answer}</button>
{:else }
<button class='w-1/2 text-3xl bg-red-500 border border-white' disabled
class:opacity-30={answer.answer !== selected_answer}
>{answer.answer}</button>
{/if}
{/each}
</div>
{/if}
+10
View File
@@ -0,0 +1,10 @@
<script lang="ts">
export let title: string;
export let description: string;
</script>
<div class='flex flex-col justify-center w-screen h-screen'>
<h1 class='text-7xl text-center'>{title}</h1>
<p class='text-3xl pt-8 text-center'>{description}</p>
</div>
+3
View File
@@ -0,0 +1,3 @@
import { io } from 'socket.io-client';
export const socket = io();
+1
View File
@@ -0,0 +1 @@
<script>import "../app.css";</script><slot></slot>
+119
View File
@@ -0,0 +1,119 @@
<script lang='ts'>
import type { QuizData } from '../app';
import { socket } from '$lib/socket';
let gameData = {
game_id: '4ced1410-e2bf-47e6-9bc7-c1f332642963',
game_pin: '81039240'
};
let success = false;
let players: Array<string> = [];
let errorMessage = '';
let game_started = false;
let quiz_data: QuizData;
let question_number: string = '0';
let question_results = null;
let shown_question_now: number;
const connect = () => {
socket.emit('register_as_admin', gameData);
};
socket.on('registered_as_admin', (data) => {
console.log('registered_as_admin', data['game']);
quiz_data = JSON.parse(data['game']);
console.log(quiz_data);
console.log(quiz_data.questions[0].question);
console.log(quiz_data.questions);
success = true;
});
socket.on('player_joined', (data) => {
players = [...players, data];
});
socket.on('already_registered_as_admin', (data) => {
errorMessage = 'There is already an admin registered for this game.';
});
let timer_res: string;
const set_question_number = (q_number: number) => {
question_results = null;
socket.emit('set_question_number', q_number.toString());
shown_question_now = q_number;
timer_res = quiz_data.questions[q_number].time;
timer(timer_res);
};
const get_question_results = () => {
socket.emit('get_question_results', {
game_id: gameData.game_id,
question_number: shown_question_now
});
};
socket.on('question_results', (data) => {
data = JSON.parse(data);
console.log(data);
question_results = data;
});
const timer = (time: string) => {
let seconds = Number(time);
let timer_interval = setInterval(() => {
if (timer_res === '0') {
clearInterval(timer_interval);
return;
} else {
seconds--;
}
timer_res = seconds.toString();
}, 1000);
};
</script>
{#if !success}
<input placeholder='game id' bind:value={gameData.game_id}>
<input placeholder='game pin' bind:value={gameData.game_pin}>
<button on:click={connect}>Connect!</button>
{#if errorMessage !== ""}
<p class='text-red-700'>{errorMessage}</p>
{/if}
{:else }
{#if !game_started}
<ul>
{#if players.length > 0}
{#each players as player}
<li><span>{player.username} </span>
<button>Kick</button>
</li>
{/each}
{/if}
</ul>
{#if players.length > 0}
<button on:click={() => {socket.emit('start_game', ""); game_started = true}}>Start Game</button>
{/if}
{:else }
<span>Time left: {timer_res}</span>
<br>
{#if timer_res === '0'}
<button on:click={get_question_results}>Get results</button>
<br>
{#if question_results !== null}
{question_results}
<br>
<ul>
{#each question_results as result}
<li>{result.username} - {result.answer} - {result.right}</li>
{/each}
</ul>
{/if}
{/if}
<br>
{#each quiz_data.questions as { question }, index}
<button on:click={() => {set_question_number(index)}}>{index}: {question}</button>
<br>
{/each}
{/if}
{/if}
+82
View File
@@ -0,0 +1,82 @@
<script lang='ts'>
import type { QuizData } from '../app';
import Title from '$lib/play/title.svelte';
import Question from '$lib/play/question.svelte';
import {socket} from '$lib/socket';
interface GameMeta {
started: boolean;
}
let gameId = '81039240';
let message = '';
let username = '';
let gameData: QuizData;
let gameMeta: GameMeta = {
started: false
};
let question_index = '';
let unique = {};
function restart() {
unique = {}; // every {} is unique, {} === {} evaluates to false
}
const connect = () => {
socket.emit('join_game', { game_pin: gameId, username: username });
};
const sendMessage = () => {
socket.emit('message', message);
message = '';
};
socket.on('joined_game', (data) => {
console.log('joined_game', data);
gameData = JSON.parse(data);
});
socket.on('game_not_found', (data) => {
console.log(data, 'game not found');
});
socket.on('join_game', (data) => {
console.log(data, 'join_game');
});
socket.on('message', (data) => {
console.log(data, 'message');
});
socket.on('start_game', (data) => {
gameMeta.started = true;
});
socket.on('set_question_number', (data) => {
restart();
question_index = data;
});
</script>
{#if !gameMeta.started}
<input bind:value={gameId} placeholder='GameID'>
<input bind:value={username} placeholder='Username'>
<button on:click={connect}>Connect</button>
<br>
<input bind:value={message} placeholder='message'>
<button on:click={sendMessage}>Send Message</button>
{:else}
{#if question_index === ""}
<Title bind:description={gameData.description} bind:title={gameData.title} />
{:else}
{#key unique}
<Question bind:question={gameData.questions[parseInt(question_index)]} bind:question_index/>
{/key}
{/if}
{/if}
+2
View File
@@ -0,0 +1,2 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
+9
View File
@@ -0,0 +1,9 @@
import { writable } from 'svelte/store';
import { io } from 'socket.io-client';
const messageStore = writable('');
// Connection opened
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

+19
View File
@@ -0,0 +1,19 @@
import adapter from '@sveltejs/adapter-auto';
import preprocess from 'svelte-preprocess';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: [
preprocess({
postcss: true
})
],
kit: {
adapter: adapter()
}
};
export default config;
+11
View File
@@ -0,0 +1,11 @@
const config = {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {}
},
plugins: []
};
module.exports = config;
+36
View File
@@ -0,0 +1,36 @@
{
"compilerOptions": {
"moduleResolution": "node",
"module": "es2020",
"lib": ["es2020", "DOM"],
"target": "es2020",
/**
svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript
to enforce using \`import type\` instead of \`import\` for Types.
*/
"importsNotUsedAsValues": "error",
/**
TypeScript doesn't know about import usages in the template because it only sees the
script of a Svelte file. Therefore preserve all value imports. Requires TS 4.5 or higher.
*/
"preserveValueImports": true,
"isolatedModules": true,
"resolveJsonModule": true,
/**
To have warnings/errors of the Svelte compiler at the correct position,
enable source maps by default.
*/
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"allowJs": true,
"checkJs": true,
"paths": {
"$lib": ["src/lib"],
"$lib/*": ["src/lib/*"]
}
},
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"]
}
+7
View File
@@ -0,0 +1,7 @@
from classquiz.config import settings
import sqlalchemy
from classquiz.db import metadata
engine = sqlalchemy.create_engine(settings.db_url)
metadata.create_all(engine)