diff --git a/.github/workflows/shared-build-deploy.yaml b/.github/workflows/shared-build-deploy.yaml index a1c53da2a..40e0949c1 100644 --- a/.github/workflows/shared-build-deploy.yaml +++ b/.github/workflows/shared-build-deploy.yaml @@ -52,7 +52,7 @@ jobs: strategy: fail-fast: true matrix: - product: [review, plan, alert] + product: [review, plan, alert, degree-plan] with: path: frontend/${{ matrix.product }} nodeVersion: 18.19.0 @@ -62,7 +62,7 @@ jobs: strategy: fail-fast: true matrix: - product: [{name: pcr, path: review}, {name: pcp, path: plan}, {name: pca, path: alert}] + product: [{name: pcr, path: review}, {name: pcp, path: plan}, {name: pca, path: alert}, {name: pdp, path: degree-plan}] with: # Inputs imageName: "${{ matrix.product.name }}-frontend" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 98f46f1b3..3c02bed68 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -15,6 +15,2361 @@ } ] }, - "default": {}, - "develop": {} + "default": { + "aiohttp": { + "hashes": [ + "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168", + "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb", + "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5", + "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f", + "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc", + "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c", + "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29", + "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4", + "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc", + "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc", + "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63", + "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e", + "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d", + "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a", + "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60", + "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38", + "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b", + "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2", + "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53", + "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5", + "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4", + "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96", + "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58", + "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa", + "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321", + "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae", + "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce", + "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8", + "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194", + "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c", + "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf", + "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d", + "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869", + "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b", + "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52", + "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528", + "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5", + "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1", + "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4", + "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8", + "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d", + "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7", + "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5", + "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54", + "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3", + "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5", + "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c", + "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29", + "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3", + "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747", + "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672", + "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5", + "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11", + "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca", + "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768", + "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6", + "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2", + "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533", + "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6", + "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266", + "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d", + "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec", + "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5", + "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1", + "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b", + "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679", + "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283", + "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb", + "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b", + "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3", + "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051", + "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511", + "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e", + "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d", + "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542", + "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f" + ], + "markers": "python_version >= '3.8'", + "version": "==3.9.3" + }, + "aiohttp-retry": { + "hashes": [ + "sha256:3aeeead8f6afe48272db93ced9440cf4eda8b6fd7ee2abb25357b7eb28525b45", + "sha256:9a8e637e31682ad36e1ff9f8bcba912fcfc7d7041722bc901a4b948da4d71ea9" + ], + "markers": "python_version >= '3.7'", + "version": "==2.8.3" + }, + "aiosignal": { + "hashes": [ + "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", + "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.1" + }, + "amqp": { + "hashes": [ + "sha256:827cb12fb0baa892aad844fd95258143bce4027fdac4fccddbc43330fd281637", + "sha256:a1ecff425ad063ad42a486c902807d1482311481c8ad95a72694b2975e75f7fd" + ], + "markers": "python_version >= '3.6'", + "version": "==5.2.0" + }, + "anyio": { + "hashes": [ + "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", + "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6" + ], + "markers": "python_version >= '3.8'", + "version": "==4.3.0" + }, + "arrow": { + "hashes": [ + "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80", + "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.0" + }, + "asgiref": { + "hashes": [ + "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", + "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590" + ], + "markers": "python_version >= '3.8'", + "version": "==3.8.1" + }, + "asttokens": { + "hashes": [ + "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24", + "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0" + ], + "version": "==2.4.1" + }, + "attrs": { + "hashes": [ + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2.0" + }, + "beautifulsoup4": { + "hashes": [ + "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", + "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed" + ], + "index": "pypi", + "markers": "python_full_version >= '3.6.0'", + "version": "==4.12.3" + }, + "billiard": { + "hashes": [ + "sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547", + "sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b" + ], + "version": "==3.6.4.0" + }, + "boto3": { + "hashes": [ + "sha256:530a4cea3d40a6bd2f15a368ea395beef1ea6dff4491823bc48bd20c7d4da655", + "sha256:8c598382e8fb61cfa8f75056197e9b509eb52039ebc291af3b1096241ba2542c" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.34.76" + }, + "botocore": { + "hashes": [ + "sha256:62e45e7374844ee39e86a96fe7f5e973eb5bf3469da028b4e3a8caba0909fb1f", + "sha256:68be44487a95132fccbc0b836fded4190dae30324f6bf822e1b6efd385ffdc83" + ], + "markers": "python_version >= '3.8'", + "version": "==1.34.76" + }, + "celery": { + "hashes": [ + "sha256:138420c020cd58d6707e6257b6beda91fd39af7afde5d36c6334d175302c0e14", + "sha256:fafbd82934d30f8a004f81e8f7a062e31413a23d444be8ee3326553915958c6d" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==5.2.7" + }, + "certifi": { + "hashes": [ + "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", + "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" + ], + "markers": "python_version >= '3.6'", + "version": "==2024.2.2" + }, + "cffi": { + "hashes": [ + "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc", + "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a", + "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", + "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", + "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520", + "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36", + "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743", + "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8", + "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed", + "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684", + "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", + "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324", + "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d", + "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235", + "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e", + "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088", + "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000", + "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7", + "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", + "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673", + "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c", + "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe", + "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2", + "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098", + "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8", + "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a", + "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0", + "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b", + "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896", + "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e", + "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9", + "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2", + "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b", + "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6", + "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", + "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f", + "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", + "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4", + "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", + "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", + "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", + "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872", + "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", + "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614", + "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1", + "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", + "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969", + "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b", + "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4", + "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", + "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", + "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" + ], + "markers": "platform_python_implementation != 'PyPy'", + "version": "==1.16.0" + }, + "channels": { + "hashes": [ + "sha256:0ce53507a7da7b148eaa454526e0e05f7da5e5d1c23440e4886cf146981d8420", + "sha256:2253334ac76f67cba68c2072273f7e0e67dbdac77eeb7e318f511d2f9a53c5e4" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==4.0.0" + }, + "channels-redis": { + "hashes": [ + "sha256:01c26c4d5d3a203f104bba9e5585c0305a70df390d21792386586068162027fd", + "sha256:2c5b944a39bd984b72aa8005a3ae11637bf29b5092adeb91c9aad4ab819a8ac4" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==4.2.0" + }, + "charset-normalizer": { + "hashes": [ + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" + }, + "click": { + "hashes": [ + "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1", + "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==8.0.4" + }, + "click-didyoumean": { + "hashes": [ + "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", + "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c" + ], + "markers": "python_full_version >= '3.6.2'", + "version": "==0.3.1" + }, + "click-plugins": { + "hashes": [ + "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b", + "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8" + ], + "version": "==1.1.1" + }, + "click-repl": { + "hashes": [ + "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", + "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812" + ], + "markers": "python_version >= '3.6'", + "version": "==0.3.0" + }, + "coreapi": { + "hashes": [ + "sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb", + "sha256:bf39d118d6d3e171f10df9ede5666f63ad80bba9a29a8ec17726a66cf52ee6f3" + ], + "index": "pypi", + "version": "==2.3.3" + }, + "coreschema": { + "hashes": [ + "sha256:5e6ef7bf38c1525d5e55a895934ab4273548629f16aed5c0a6caa74ebf45551f", + "sha256:9503506007d482ab0867ba14724b93c18a33b22b6d19fb419ef2d239dd4a1607" + ], + "version": "==0.0.4" + }, + "cryptography": { + "hashes": [ + "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee", + "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576", + "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d", + "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30", + "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413", + "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb", + "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da", + "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4", + "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd", + "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc", + "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8", + "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1", + "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc", + "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e", + "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8", + "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940", + "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400", + "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7", + "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16", + "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278", + "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74", + "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec", + "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1", + "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2", + "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c", + "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922", + "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a", + "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6", + "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1", + "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e", + "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac", + "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7" + ], + "markers": "python_version >= '3.7'", + "version": "==42.0.5" + }, + "ddt": { + "hashes": [ + "sha256:6adcfaf9785f0a36f9e73a89b91e412de9ef8649e289b750e3683bc79d5e2354", + "sha256:d215d6b083963013c4a19b1e4dcd6a96e80e43ab77519597a6acfcf2e9a3e04b" + ], + "index": "pypi", + "version": "==1.7.2" + }, + "decorator": { + "hashes": [ + "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", + "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186" + ], + "markers": "python_version >= '3.5'", + "version": "==5.1.1" + }, + "dj-database-url": { + "hashes": [ + "sha256:04bc34b248d4c21aaa13e4ab419ae6575ef5f10f3df735ce7da97722caa356e0", + "sha256:f2042cefe1086e539c9da39fad5ad7f61173bf79665e69bf7e4de55fa88b135f" + ], + "index": "pypi", + "version": "==2.1.0" + }, + "django": { + "hashes": [ + "sha256:56ab63a105e8bb06ee67381d7b65fe6774f057e41a8bab06c8020c8882d8ecd4", + "sha256:b5bb1d11b2518a5f91372a282f24662f58f66749666b0a286ab057029f728080" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==5.0.2" + }, + "django-auto-prefetching": { + "hashes": [ + "sha256:a61cfe95f0c8bd1212016d6242dd09278f47483d91fc65ab745921908f494e53", + "sha256:b095748adcf2f5c2358301044959e5cffd38dce28943b86f5a95778fd62de52c" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==0.2.12" + }, + "django-cors-headers": { + "hashes": [ + "sha256:0b1fd19297e37417fc9f835d39e45c8c642938ddba1acce0c1753d3edef04f36", + "sha256:0bf65ef45e606aff1994d35503e6b677c0b26cafff6506f8fd7187f3be840207" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==4.3.1" + }, + "django-extensions": { + "hashes": [ + "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a", + "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==3.2.3" + }, + "django-filter": { + "hashes": [ + "sha256:48e5fc1da3ccd6ca0d5f9bb550973518ce977a4edde9d2a8a154a7f4f0b9f96e", + "sha256:df2ee9857e18d38bed203c8745f62a803fa0f31688c9fe6f8e868120b1848e48" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==24.2" + }, + "django-labs-accounts": { + "hashes": [ + "sha256:1e9bce4b12af68f532adc912bf81aaa782ddf93eb888234c49d62843d04f02bb", + "sha256:3736835e2bcc9425bea139f94f590e56c8a3af052b11587aa43a32bb06b462d7" + ], + "index": "pypi", + "markers": "python_version >= '3.11' and python_version < '4.0'", + "version": "==0.9.5" + }, + "django-redis": { + "hashes": [ + "sha256:6a02abaa34b0fea8bf9b707d2c363ab6adc7409950b2db93602e6cb292818c42", + "sha256:ebc88df7da810732e2af9987f7f426c96204bf89319df4c6da6ca9a2942edd5b" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==5.4.0" + }, + "django-runtime-options": { + "hashes": [ + "sha256:85f76608b83bd4d2894c534569e0302449ce776974131a18b50ab07a9f983f5b", + "sha256:edb34dc27d5558cda19caf8650b93b2fec4753f9955a545bf75f2785bbf40e55" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==0.1.3" + }, + "djangorestframework": { + "hashes": [ + "sha256:3ccc0475bce968608cf30d07fb17d8e52d1d7fc8bfe779c905463200750cbca6", + "sha256:f88fad74183dfc7144b2756d0d2ac716ea5b4c7c9840995ac3bfd8ec034333c1" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==3.15.1" + }, + "docutils": { + "hashes": [ + "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6", + "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==0.20.1" + }, + "drf-nested-routers": { + "hashes": [ + "sha256:1407565abc7bada37c162c7e11bf214ae71625a17fdec6d9a47a17f4a3627d32", + "sha256:9a6813554020134a02e62f8c2934b2047717f7da06f8b801752c521e43735c63" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.93.5" + }, + "executing": { + "hashes": [ + "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147", + "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc" + ], + "markers": "python_version >= '3.5'", + "version": "==2.0.1" + }, + "frozenlist": { + "hashes": [ + "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7", + "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98", + "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad", + "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5", + "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae", + "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e", + "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a", + "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701", + "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d", + "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6", + "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6", + "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106", + "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75", + "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868", + "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a", + "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0", + "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1", + "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826", + "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec", + "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6", + "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950", + "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19", + "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0", + "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8", + "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a", + "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09", + "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86", + "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c", + "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5", + "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b", + "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b", + "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d", + "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0", + "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea", + "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776", + "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a", + "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897", + "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7", + "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09", + "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9", + "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe", + "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd", + "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742", + "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09", + "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0", + "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932", + "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1", + "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a", + "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49", + "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d", + "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7", + "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480", + "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89", + "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e", + "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b", + "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82", + "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb", + "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068", + "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8", + "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b", + "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb", + "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2", + "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11", + "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b", + "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc", + "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0", + "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497", + "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17", + "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0", + "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2", + "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439", + "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5", + "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac", + "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825", + "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887", + "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced", + "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74" + ], + "markers": "python_version >= '3.8'", + "version": "==1.4.1" + }, + "gunicorn": { + "hashes": [ + "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0", + "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033" + ], + "index": "pypi", + "markers": "python_version >= '3.5'", + "version": "==21.2.0" + }, + "h11": { + "hashes": [ + "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", + "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" + ], + "markers": "python_version >= '3.7'", + "version": "==0.14.0" + }, + "httptools": { + "hashes": [ + "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563", + "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142", + "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d", + "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b", + "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4", + "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb", + "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658", + "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084", + "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2", + "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97", + "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837", + "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3", + "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58", + "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da", + "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d", + "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90", + "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0", + "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1", + "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2", + "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e", + "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0", + "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf", + "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc", + "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3", + "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503", + "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a", + "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3", + "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949", + "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84", + "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb", + "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a", + "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f", + "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e", + "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81", + "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185", + "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.0'", + "version": "==0.6.1" + }, + "ics": { + "hashes": [ + "sha256:5fcf4d29ec6e7dfcb84120abd617bbba632eb77b097722b7df70e48dbcf26103", + "sha256:6743539bca10391635249b87d74fcd1094af20b82098bebf7c7521df91209f05", + "sha256:da352bdf8418619dc93611e6d251f3cefbb42664777f6e00b580a722098124b7" + ], + "index": "pypi", + "version": "==0.7.2" + }, + "idna": { + "hashes": [ + "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", + "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + ], + "markers": "python_version >= '3.5'", + "version": "==3.6" + }, + "importlib-metadata": { + "hashes": [ + "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570", + "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==7.1.0" + }, + "ipython": { + "hashes": [ + "sha256:07232af52a5ba146dc3372c7bf52a0f890a23edf38d77caef8d53f9cdc2584c1", + "sha256:7468edaf4f6de3e1b912e57f66c241e6fd3c7099f2ec2136e239e142e800274d" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==8.23.0" + }, + "itypes": { + "hashes": [ + "sha256:03da6872ca89d29aef62773672b2d408f490f80db48b23079a4b194c86dd04c6", + "sha256:af886f129dea4a2a1e3d36595a2d139589e4dd287f5cab0b40e799ee81570ff1" + ], + "version": "==1.2.0" + }, + "jedi": { + "hashes": [ + "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd", + "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0" + ], + "markers": "python_version >= '3.6'", + "version": "==0.19.1" + }, + "jinja2": { + "hashes": [ + "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", + "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.3" + }, + "jmespath": { + "hashes": [ + "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", + "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" + ], + "markers": "python_version >= '3.7'", + "version": "==1.0.1" + }, + "joblib": { + "hashes": [ + "sha256:92f865e621e17784e7955080b6d042489e3b8e294949cc44c6eac304f59772b1", + "sha256:ef4331c65f239985f3f2220ecc87db222f08fd22097a3dd5698f693875f8cbb9" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.2" + }, + "jsonref": { + "hashes": [ + "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", + "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.1.0" + }, + "jwcrypto": { + "hashes": [ + "sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789", + "sha256:771a87762a0c081ae6166958a954f80848820b2ab066937dc8b8379d65b1b039" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.6" + }, + "kombu": { + "hashes": [ + "sha256:49f1e62b12369045de2662f62cc584e7df83481a513db83b01f87b5b9785e378", + "sha256:f3da5b570a147a5da8280180aa80b03807283d63ea5081fcdb510d18242431d9" + ], + "markers": "python_version >= '3.8'", + "version": "==5.3.6" + }, + "lark-parser": { + "hashes": [ + "sha256:0eaf30cb5ba787fe404d73a7d6e61df97b21d5a63ac26c5008c78a494373c675", + "sha256:15967db1f1214013dca65b1180745047b9be457d73da224fcda3d9dd4e96a138" + ], + "index": "pypi", + "version": "==0.12.0" + }, + "markupsafe": { + "hashes": [ + "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", + "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", + "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", + "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", + "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", + "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", + "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", + "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", + "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", + "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", + "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", + "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", + "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", + "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", + "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", + "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", + "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", + "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", + "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", + "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", + "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", + "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", + "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", + "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", + "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", + "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", + "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", + "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", + "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", + "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", + "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", + "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", + "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", + "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", + "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", + "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", + "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", + "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", + "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", + "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", + "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", + "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", + "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", + "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", + "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", + "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", + "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", + "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", + "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", + "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", + "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", + "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", + "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", + "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", + "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", + "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", + "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", + "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", + "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", + "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.5" + }, + "matplotlib-inline": { + "hashes": [ + "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311", + "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304" + ], + "markers": "python_version >= '3.5'", + "version": "==0.1.6" + }, + "msgpack": { + "hashes": [ + "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982", + "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3", + "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40", + "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee", + "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693", + "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950", + "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151", + "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24", + "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305", + "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b", + "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c", + "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659", + "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d", + "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18", + "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746", + "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868", + "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2", + "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba", + "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228", + "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2", + "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273", + "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c", + "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653", + "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a", + "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596", + "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd", + "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8", + "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa", + "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85", + "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc", + "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836", + "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3", + "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58", + "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128", + "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db", + "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f", + "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77", + "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad", + "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13", + "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8", + "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b", + "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a", + "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543", + "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b", + "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce", + "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d", + "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a", + "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c", + "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f", + "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e", + "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011", + "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04", + "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480", + "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a", + "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d", + "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d" + ], + "markers": "python_version >= '3.8'", + "version": "==1.0.8" + }, + "multidict": { + "hashes": [ + "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556", + "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c", + "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29", + "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b", + "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8", + "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7", + "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd", + "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40", + "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6", + "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3", + "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c", + "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9", + "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5", + "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae", + "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442", + "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9", + "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc", + "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c", + "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea", + "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5", + "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50", + "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182", + "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453", + "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e", + "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600", + "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733", + "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda", + "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241", + "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461", + "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e", + "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e", + "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b", + "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e", + "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7", + "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386", + "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd", + "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9", + "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf", + "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee", + "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5", + "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a", + "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271", + "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54", + "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4", + "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496", + "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb", + "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319", + "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3", + "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f", + "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527", + "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed", + "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604", + "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef", + "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8", + "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5", + "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5", + "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626", + "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c", + "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d", + "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c", + "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc", + "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc", + "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b", + "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38", + "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450", + "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1", + "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f", + "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3", + "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755", + "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226", + "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a", + "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046", + "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf", + "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479", + "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e", + "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1", + "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a", + "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83", + "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929", + "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93", + "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a", + "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c", + "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44", + "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89", + "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba", + "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e", + "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da", + "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24", + "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423", + "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef" + ], + "markers": "python_version >= '3.7'", + "version": "==6.0.5" + }, + "nose": { + "hashes": [ + "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac", + "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a", + "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" + ], + "index": "pypi", + "version": "==1.3.7" + }, + "numpy": { + "hashes": [ + "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", + "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", + "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", + "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", + "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", + "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", + "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea", + "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c", + "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", + "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", + "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be", + "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", + "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", + "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", + "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", + "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd", + "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c", + "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", + "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0", + "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c", + "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", + "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", + "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", + "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6", + "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", + "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", + "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30", + "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", + "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", + "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", + "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", + "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", + "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764", + "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", + "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3", + "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==1.26.4" + }, + "oauthlib": { + "hashes": [ + "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", + "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918" + ], + "markers": "python_version >= '3.6'", + "version": "==3.2.2" + }, + "packaging": { + "hashes": [ + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + ], + "markers": "python_version >= '3.7'", + "version": "==24.0" + }, + "pandas": { + "hashes": [ + "sha256:04f6ec3baec203c13e3f8b139fb0f9f86cd8c0b94603ae3ae8ce9a422e9f5bee", + "sha256:06cf591dbaefb6da9de8472535b185cba556d0ce2e6ed28e21d919704fef1a9e", + "sha256:0ab90f87093c13f3e8fa45b48ba9f39181046e8f3317d3aadb2fffbb1b978572", + "sha256:0f573ab277252ed9aaf38240f3b54cfc90fff8e5cab70411ee1d03f5d51f3944", + "sha256:101d0eb9c5361aa0146f500773395a03839a5e6ecde4d4b6ced88b7e5a1a6403", + "sha256:11940e9e3056576ac3244baef2fedade891977bcc1cb7e5cc8f8cc7d603edc89", + "sha256:1ba21b1d5c0e43416218db63037dbe1a01fc101dc6e6024bcad08123e48004ab", + "sha256:4aa1d8707812a658debf03824016bf5ea0d516afdea29b7dc14cf687bc4d4ec6", + "sha256:4acf681325ee1c7f950d058b05a820441075b0dd9a2adf5c4835b9bc056bf4fb", + "sha256:53680dc9b2519cbf609c62db3ed7c0b499077c7fefda564e330286e619ff0dd9", + "sha256:739cc70eaf17d57608639e74d63387b0d8594ce02f69e7a0b046f117974b3019", + "sha256:76f27a809cda87e07f192f001d11adc2b930e93a2b0c4a236fde5429527423be", + "sha256:7d2ed41c319c9fb4fd454fe25372028dfa417aacb9790f68171b2e3f06eae8cd", + "sha256:88ecb5c01bb9ca927ebc4098136038519aa5d66b44671861ffab754cae75102c", + "sha256:8df8612be9cd1c7797c93e1c5df861b2ddda0b48b08f2c3eaa0702cf88fb5f88", + "sha256:94e714a1cca63e4f5939cdce5f29ba8d415d85166be3441165edd427dc9f6bc0", + "sha256:9bd8a40f47080825af4317d0340c656744f2bfdb6819f818e6ba3cd24c0e1397", + "sha256:9d1265545f579edf3f8f0cb6f89f234f5e44ba725a34d86535b1a1d38decbccc", + "sha256:a935a90a76c44fe170d01e90a3594beef9e9a6220021acfb26053d01426f7dc2", + "sha256:af5d3c00557d657c8773ef9ee702c61dd13b9d7426794c9dfeb1dc4a0bf0ebc7", + "sha256:c2ce852e1cf2509a69e98358e8458775f89599566ac3775e70419b98615f4b06", + "sha256:c38ce92cb22a4bea4e3929429aa1067a454dcc9c335799af93ba9be21b6beb51", + "sha256:c391f594aae2fd9f679d419e9a4d5ba4bce5bb13f6a989195656e7dc4b95c8f0", + "sha256:c70e00c2d894cb230e5c15e4b1e1e6b2b478e09cf27cc593a11ef955b9ecc81a", + "sha256:df0c37ebd19e11d089ceba66eba59a168242fc6b7155cba4ffffa6eccdfb8f16", + "sha256:e97fbb5387c69209f134893abc788a6486dbf2f9e511070ca05eed4b930b1b02", + "sha256:f02a3a6c83df4026e55b63c1f06476c9aa3ed6af3d89b4f04ea656ccdaaaa359", + "sha256:f821213d48f4ab353d20ebc24e4faf94ba40d76680642fb7ce2ea31a3ad94f9b", + "sha256:f9d3558d263073ed95e46f4650becff0c5e1ffe0fc3a015de3c79283dfbdb3df" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==2.2.1" + }, + "parso": { + "hashes": [ + "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0", + "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75" + ], + "markers": "python_version >= '3.6'", + "version": "==0.8.3" + }, + "pexpect": { + "hashes": [ + "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", + "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f" + ], + "markers": "sys_platform != 'win32' and sys_platform != 'emscripten'", + "version": "==4.9.0" + }, + "phonenumbers": { + "hashes": [ + "sha256:991f2619f0593b36b674c345af47944ec4bae526b353cf53d707e662087be63b", + "sha256:f2d653268ece55a4f3752d9cda4be6f7465f298e6d028d522aedda13cf057201" + ], + "index": "pypi", + "version": "==8.13.33" + }, + "prompt-toolkit": { + "hashes": [ + "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d", + "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.0.43" + }, + "psycopg2": { + "hashes": [ + "sha256:121081ea2e76729acfb0673ff33755e8703d45e926e416cb59bae3a86c6a4981", + "sha256:38a8dcc6856f569068b47de286b472b7c473ac7977243593a288ebce0dc89516", + "sha256:426f9f29bde126913a20a96ff8ce7d73fd8a216cfb323b1f04da402d452853c3", + "sha256:5e0d98cade4f0e0304d7d6f25bbfbc5bd186e07b38eac65379309c4ca3193efa", + "sha256:7e2dacf8b009a1c1e843b5213a87f7c544b2b042476ed7755be813eaf4e8347a", + "sha256:a7653d00b732afb6fc597e29c50ad28087dcb4fbfb28e86092277a559ae4e693", + "sha256:ade01303ccf7ae12c356a5e10911c9e1c51136003a9a1d92f7aa9d010fb98372", + "sha256:bac58c024c9922c23550af2a581998624d6e02350f4ae9c5f0bc642c633a2d5e", + "sha256:c92811b2d4c9b6ea0285942b2e7cac98a59e166d59c588fe5cfe1eda58e72d59", + "sha256:d1454bde93fb1e224166811694d600e746430c006fbb031ea06ecc2ea41bf156", + "sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024", + "sha256:de80739447af31525feddeb8effd640782cf5998e1a4e9192ebdf829717e3913", + "sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.9.9" + }, + "ptyprocess": { + "hashes": [ + "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", + "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" + ], + "version": "==0.7.0" + }, + "pure-eval": { + "hashes": [ + "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350", + "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3" + ], + "version": "==0.2.2" + }, + "pycparser": { + "hashes": [ + "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" + ], + "markers": "python_version >= '3.8'", + "version": "==2.22" + }, + "pygments": { + "hashes": [ + "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", + "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367" + ], + "markers": "python_version >= '3.7'", + "version": "==2.17.2" + }, + "pyjwt": { + "hashes": [ + "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de", + "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320" + ], + "markers": "python_version >= '3.7'", + "version": "==2.8.0" + }, + "python-dateutil": { + "hashes": [ + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0.post0" + }, + "python-dotenv": { + "hashes": [ + "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", + "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" + ], + "version": "==1.0.1" + }, + "pytz": { + "hashes": [ + "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", + "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319" + ], + "version": "==2024.1" + }, + "pyyaml": { + "hashes": [ + "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", + "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", + "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", + "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", + "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", + "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", + "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", + "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", + "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", + "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", + "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", + "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", + "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", + "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", + "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", + "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", + "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", + "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", + "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", + "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", + "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", + "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", + "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", + "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", + "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", + "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", + "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", + "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", + "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", + "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", + "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", + "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", + "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", + "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", + "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", + "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", + "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", + "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", + "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", + "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", + "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", + "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", + "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", + "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", + "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", + "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", + "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", + "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", + "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", + "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", + "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==6.0.1" + }, + "redis": { + "hashes": [ + "sha256:4973bae7444c0fbed64a06b87446f79361cb7e4ec1538c022d696ed7a5015580", + "sha256:5da9b8fe9e1254293756c16c008e8620b3d15fcc6dde6babde9541850e72a32d" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==5.0.3" + }, + "requests": { + "hashes": [ + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.31.0" + }, + "requests-oauthlib": { + "hashes": [ + "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5", + "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.3.1" + }, + "s3transfer": { + "hashes": [ + "sha256:5683916b4c724f799e600f41dd9e10a9ff19871bf87623cc8f491cb4f5fa0a19", + "sha256:ceb252b11bcf87080fb7850a224fb6e05c8a776bab8f2b64b7f25b969464839d" + ], + "markers": "python_version >= '3.8'", + "version": "==0.10.1" + }, + "scikit-learn": { + "hashes": [ + "sha256:0df87de9ce1c0140f2818beef310fb2e2afdc1e66fc9ad587965577f17733649", + "sha256:14e4c88436ac96bf69eb6d746ac76a574c314a23c6961b7d344b38877f20fee1", + "sha256:1754b0c2409d6ed5a3380512d0adcf182a01363c669033a2b55cca429ed86a81", + "sha256:1afed6951bc9d2053c6ee9a518a466cbc9b07c6a3f9d43bfe734192b6125d508", + "sha256:1d491ef66e37f4e812db7e6c8286520c2c3fc61b34bf5e59b67b4ce528de93af", + "sha256:234b6bda70fdcae9e4abbbe028582ce99c280458665a155eed0b820599377d25", + "sha256:2a3ee19211ded1a52ee37b0a7b373a8bfc66f95353af058a210b692bd4cda0dd", + "sha256:4310bff71aa98b45b46cd26fa641309deb73a5d1c0461d181587ad4f30ea3c36", + "sha256:4ba516fcdc73d60e7f48cbb0bccb9acbdb21807de3651531208aac73c758e3ab", + "sha256:6145dfd9605b0b50ae72cdf72b61a2acd87501369a763b0d73d004710ebb76b5", + "sha256:629e09f772ad42f657ca60a1a52342eef786218dd20cf1369a3b8d085e55ef8f", + "sha256:712c1c69c45b58ef21635360b3d0a680ff7d83ac95b6f9b82cf9294070cda710", + "sha256:78cd27b4669513b50db4f683ef41ea35b5dddc797bd2bbd990d49897fd1c8a46", + "sha256:93d3d496ff1965470f9977d05e5ec3376fb1e63b10e4fda5e39d23c2d8969a30", + "sha256:9f43dd527dabff5521af2786a2f8de5ba381e182ec7292663508901cf6ceaf6e", + "sha256:a1e289f33f613cefe6707dead50db31930530dc386b6ccff176c786335a7b01c", + "sha256:aa0029b78ef59af22cfbd833e8ace8526e4df90212db7ceccbea582ebb5d6794", + "sha256:c02e27d65b0c7dc32f2c5eb601aaf5530b7a02bfbe92438188624524878336f2", + "sha256:c540aaf44729ab5cd4bd5e394f2b375e65ceaea9cdd8c195788e70433d91bbc5", + "sha256:ce03506ccf5f96b7e9030fea7eb148999b254c44c10182ac55857bc9b5d4815f", + "sha256:d7cd3a77c32879311f2aa93466d3c288c955ef71d191503cf0677c3340ae8ae0" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==1.4.1.post1" + }, + "scipy": { + "hashes": [ + "sha256:05f1432ba070e90d42d7fd836462c50bf98bd08bed0aa616c359eed8a04e3922", + "sha256:09c74543c4fbeb67af6ce457f6a6a28e5d3739a87f62412e4a16e46f164f0ae5", + "sha256:0fbcf8abaf5aa2dc8d6400566c1a727aed338b5fe880cde64907596a89d576fa", + "sha256:109d391d720fcebf2fbe008621952b08e52907cf4c8c7efc7376822151820820", + "sha256:1d2f7bb14c178f8b13ebae93f67e42b0a6b0fc50eba1cd8021c9b6e08e8fb1cd", + "sha256:1e7626dfd91cdea5714f343ce1176b6c4745155d234f1033584154f60ef1ff42", + "sha256:22789b56a999265431c417d462e5b7f2b487e831ca7bef5edeb56efe4c93f86e", + "sha256:28e286bf9ac422d6beb559bc61312c348ca9b0f0dae0d7c5afde7f722d6ea13d", + "sha256:33fde20efc380bd23a78a4d26d59fc8704e9b5fd9b08841693eb46716ba13d86", + "sha256:45c08bec71d3546d606989ba6e7daa6f0992918171e2a6f7fbedfa7361c2de1e", + "sha256:4dca18c3ffee287ddd3bc8f1dabaf45f5305c5afc9f8ab9cbfab855e70b2df5c", + "sha256:5407708195cb38d70fd2d6bb04b1b9dd5c92297d86e9f9daae1576bd9e06f602", + "sha256:58569af537ea29d3f78e5abd18398459f195546bb3be23d16677fb26616cc11e", + "sha256:5e4a756355522eb60fcd61f8372ac2549073c8788f6114449b37e9e8104f15a5", + "sha256:6bf9fe63e7a4bf01d3645b13ff2aa6dea023d38993f42aaac81a18b1bda7a82a", + "sha256:8930ae3ea371d6b91c203b1032b9600d69c568e537b7988a3073dfe4d4774f21", + "sha256:9ff7dad5d24a8045d836671e082a490848e8639cabb3dbdacb29f943a678683d", + "sha256:a2f471de4d01200718b2b8927f7d76b5d9bde18047ea0fa8bd15c5ba3f26a1d6", + "sha256:ac38c4c92951ac0f729c4c48c9e13eb3675d9986cc0c83943784d7390d540c78", + "sha256:b2a3ff461ec4756b7e8e42e1c681077349a038f0686132d623fa404c0bee2551", + "sha256:b5acd8e1dbd8dbe38d0004b1497019b2dbbc3d70691e65d69615f8a7292865d7", + "sha256:b8434f6f3fa49f631fae84afee424e2483289dfc30a47755b4b4e6b07b2633a4", + "sha256:ba419578ab343a4e0a77c0ef82f088238a93eef141b2b8017e46149776dfad4d", + "sha256:d0de696f589681c2802f9090fff730c218f7c51ff49bf252b6a97ec4a5d19e8b", + "sha256:dcbb9ea49b0167de4167c40eeee6e167caeef11effb0670b554d10b1e693a8b9" + ], + "markers": "python_version >= '3.9'", + "version": "==1.13.0" + }, + "sentry-sdk": { + "hashes": [ + "sha256:eb65289da013ca92fad2694851ad2f086aa3825e808dc285bd7dcaf63602bb18", + "sha256:f7125a9235795811962d52ff796dc032cd1d0dd98b59beaced8380371cd9c13c" + ], + "index": "pypi", + "version": "==1.44.0" + }, + "shortener": { + "hashes": [ + "sha256:42f03bbbc2928b7db000ac92975b78b4c47fd938f258c4019a256652086a4a4d", + "sha256:b40d1309ae8cfea6132b8f10ff1d5e3e44675989f817305943cf97b492e09f72" + ], + "index": "pypi", + "markers": "python_version >= '3.5'", + "version": "==0.2.1" + }, + "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:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", + "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.1" + }, + "soupsieve": { + "hashes": [ + "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690", + "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7" + ], + "markers": "python_version >= '3.8'", + "version": "==2.5" + }, + "sqlparse": { + "hashes": [ + "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3", + "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c" + ], + "markers": "python_version >= '3.5'", + "version": "==0.4.4" + }, + "stack-data": { + "hashes": [ + "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", + "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695" + ], + "version": "==0.6.3" + }, + "tatsu": { + "hashes": [ + "sha256:2d085acce9c863c797891456721054489c9c36f858dfb6319de8971954dc6c73", + "sha256:a7a13bea264b749963695a3c42ef5631811330887882cb0de2c2ad6e2348f986" + ], + "markers": "python_version >= '3.11'", + "version": "==5.12.0" + }, + "tblib": { + "hashes": [ + "sha256:80a6c77e59b55e83911e1e607c649836a69c103963c5f28a46cbeef44acf8129", + "sha256:93622790a0a29e04f0346458face1e144dc4d32f493714c6c3dff82a4adb77e6" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==3.0.0" + }, + "threadpoolctl": { + "hashes": [ + "sha256:8f4c689a65b23e5ed825c8436a92b818aac005e0f3715f6a1664d7c7ee29d262", + "sha256:f11b491a03661d6dd7ef692dd422ab34185d982466c49c8f98c8f716b5c93196" + ], + "markers": "python_version >= '3.8'", + "version": "==3.4.0" + }, + "tqdm": { + "hashes": [ + "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9", + "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==4.66.2" + }, + "traitlets": { + "hashes": [ + "sha256:8cdd83c040dab7d1dee822678e5f5d100b514f7b72b01615b26fc5718916fdf9", + "sha256:fcdf85684a772ddeba87db2f398ce00b40ff550d1528c03c14dbf6a02003cd80" + ], + "markers": "python_version >= '3.8'", + "version": "==5.14.2" + }, + "twilio": { + "hashes": [ + "sha256:c13973415966e93b70651a58733fa76317df64621c74e0e4896613f10a368438", + "sha256:c21fd70d8518823831143999a38cc86c40dad45fab5a6b5ecfee85672e9e5cb2" + ], + "index": "pypi", + "markers": "python_full_version >= '3.7.0'", + "version": "==9.0.3" + }, + "types-python-dateutil": { + "hashes": [ + "sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202", + "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b" + ], + "markers": "python_version >= '3.8'", + "version": "==2.9.0.20240316" + }, + "typing-extensions": { + "hashes": [ + "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", + "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" + ], + "markers": "python_version < '3.12'", + "version": "==4.10.0" + }, + "tzdata": { + "hashes": [ + "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", + "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252" + ], + "markers": "python_version >= '2'", + "version": "==2024.1" + }, + "unidecode": { + "hashes": [ + "sha256:cfdb349d46ed3873ece4586b96aa75258726e2fa8ec21d6f00a591d98806c2f4", + "sha256:d130a61ce6696f8148a3bd8fe779c99adeb4b870584eeb9526584e9aa091fd39" + ], + "index": "pypi", + "markers": "python_version >= '3.5'", + "version": "==1.3.8" + }, + "uritemplate": { + "hashes": [ + "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0", + "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==4.1.1" + }, + "urllib3": { + "hashes": [ + "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", + "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" + ], + "markers": "python_version >= '3.8'", + "version": "==2.2.1" + }, + "uvicorn": { + "extras": [ + "standard" + ], + "hashes": [ + "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de", + "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0" + ], + "markers": "python_version >= '3.8'", + "version": "==0.29.0" + }, + "uvloop": { + "hashes": [ + "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd", + "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec", + "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b", + "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc", + "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797", + "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5", + "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2", + "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d", + "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be", + "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd", + "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12", + "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17", + "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef", + "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24", + "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428", + "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1", + "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849", + "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593", + "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd", + "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67", + "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6", + "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3", + "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd", + "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8", + "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7", + "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533", + "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957", + "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650", + "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e", + "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7", + "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256" + ], + "markers": "python_full_version >= '3.8.0' and sys_platform == 'linux'", + "version": "==0.19.0" + }, + "uwsgi": { + "hashes": [ + "sha256:77b6dd5cd633f4ae87ee393f7701f617736815499407376e78f3d16467523afe" + ], + "version": "==2.0.24" + }, + "vine": { + "hashes": [ + "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", + "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0" + ], + "markers": "python_version >= '3.6'", + "version": "==5.1.0" + }, + "watchfiles": { + "hashes": [ + "sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc", + "sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365", + "sha256:06247538e8253975bdb328e7683f8515ff5ff041f43be6c40bff62d989b7d0b0", + "sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e", + "sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124", + "sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c", + "sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317", + "sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094", + "sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7", + "sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235", + "sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c", + "sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c", + "sha256:1c9198c989f47898b2c22201756f73249de3748e0fc9de44adaf54a8b259cc0c", + "sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235", + "sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293", + "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa", + "sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef", + "sha256:3ad692bc7792be8c32918c699638b660c0de078a6cbe464c46e1340dadb94c19", + "sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8", + "sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d", + "sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915", + "sha256:40bca549fdc929b470dd1dbfcb47b3295cb46a6d2c90e50588b0a1b3bd98f429", + "sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097", + "sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe", + "sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0", + "sha256:4c48a10d17571d1275701e14a601e36959ffada3add8cdbc9e5061a6e3579a5d", + "sha256:4ea10a29aa5de67de02256a28d1bf53d21322295cb00bd2d57fcd19b850ebd99", + "sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1", + "sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a", + "sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895", + "sha256:59137c0c6826bd56c710d1d2bda81553b5e6b7c84d5a676747d80caf0409ad94", + "sha256:5a03651352fc20975ee2a707cd2d74a386cd303cc688f407296064ad1e6d1562", + "sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab", + "sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360", + "sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1", + "sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7", + "sha256:66fac0c238ab9a2e72d026b5fb91cb902c146202bbd29a9a1a44e8db7b710b6f", + "sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03", + "sha256:6c889025f59884423428c261f212e04d438de865beda0b1e1babab85ef4c0f01", + "sha256:6cb8fdc044909e2078c248986f2fc76f911f72b51ea4a4fbbf472e01d14faa58", + "sha256:6e9be3ef84e2bb9710f3f777accce25556f4a71e15d2b73223788d528fcc2052", + "sha256:7f762a1a85a12cc3484f77eee7be87b10f8c50b0b787bb02f4e357403cad0c0e", + "sha256:83a696da8922314ff2aec02987eefb03784f473281d740bf9170181829133765", + "sha256:853853cbf7bf9408b404754b92512ebe3e3a83587503d766d23e6bf83d092ee6", + "sha256:8ad3fe0a3567c2f0f629d800409cd528cb6251da12e81a1f765e5c5345fd0137", + "sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85", + "sha256:8d5f400326840934e3507701f9f7269247f7c026d1b6cfd49477d2be0933cfca", + "sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f", + "sha256:9a0aa47f94ea9a0b39dd30850b0adf2e1cd32a8b4f9c7aa443d852aacf9ca214", + "sha256:9b37a7ba223b2f26122c148bb8d09a9ff312afca998c48c725ff5a0a632145f7", + "sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7", + "sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3", + "sha256:9d353c4cfda586db2a176ce42c88f2fc31ec25e50212650c89fdd0f560ee507b", + "sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7", + "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6", + "sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994", + "sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9", + "sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec", + "sha256:b4a21f71885aa2744719459951819e7bf5a906a6448a6b2bbce8e9cc9f2c8128", + "sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c", + "sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2", + "sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078", + "sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3", + "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e", + "sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a", + "sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6", + "sha256:d5b1dc0e708fad9f92c296ab2f948af403bf201db8fb2eb4c8179db143732e49", + "sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b", + "sha256:d8f57c4461cd24fda22493109c45b3980863c58a25b8bec885ca8bea6b8d4b28", + "sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9", + "sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586", + "sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400", + "sha256:ec8c8900dc5c83650a63dd48c4d1d245343f904c4b64b48798c67a3767d7e165", + "sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303", + "sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d" + ], + "version": "==0.21.0" + }, + "wcwidth": { + "hashes": [ + "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", + "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5" + ], + "version": "==0.2.13" + }, + "websockets": { + "hashes": [ + "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b", + "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6", + "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df", + "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b", + "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205", + "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892", + "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53", + "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2", + "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed", + "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c", + "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd", + "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b", + "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931", + "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30", + "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370", + "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be", + "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec", + "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf", + "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62", + "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b", + "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402", + "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f", + "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123", + "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9", + "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603", + "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45", + "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558", + "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4", + "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438", + "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137", + "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480", + "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447", + "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8", + "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04", + "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c", + "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb", + "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967", + "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b", + "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d", + "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def", + "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c", + "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92", + "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2", + "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113", + "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b", + "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28", + "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7", + "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d", + "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f", + "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468", + "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8", + "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae", + "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611", + "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d", + "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9", + "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca", + "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f", + "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2", + "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077", + "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2", + "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6", + "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374", + "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc", + "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e", + "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53", + "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399", + "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547", + "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3", + "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870", + "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5", + "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8", + "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7" + ], + "version": "==12.0" + }, + "yarl": { + "hashes": [ + "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51", + "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce", + "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559", + "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0", + "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81", + "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc", + "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4", + "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c", + "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130", + "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136", + "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e", + "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec", + "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7", + "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1", + "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455", + "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099", + "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129", + "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10", + "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142", + "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98", + "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa", + "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7", + "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525", + "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c", + "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9", + "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c", + "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8", + "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b", + "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf", + "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23", + "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd", + "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27", + "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f", + "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece", + "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434", + "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec", + "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff", + "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78", + "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d", + "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863", + "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53", + "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31", + "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15", + "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5", + "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b", + "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57", + "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3", + "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1", + "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f", + "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad", + "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c", + "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7", + "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2", + "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b", + "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2", + "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b", + "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9", + "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be", + "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e", + "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984", + "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4", + "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074", + "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2", + "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392", + "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91", + "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541", + "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf", + "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572", + "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66", + "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575", + "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14", + "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5", + "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1", + "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e", + "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551", + "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17", + "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead", + "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0", + "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe", + "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234", + "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0", + "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7", + "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34", + "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42", + "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385", + "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78", + "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be", + "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958", + "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749", + "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec" + ], + "markers": "python_version >= '3.7'", + "version": "==1.9.4" + }, + "zipp": { + "hashes": [ + "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b", + "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715" + ], + "markers": "python_version >= '3.8'", + "version": "==3.18.1" + } + }, + "develop": { + "asgiref": { + "hashes": [ + "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", + "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590" + ], + "markers": "python_version >= '3.8'", + "version": "==3.8.1" + }, + "black": { + "hashes": [ + "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b", + "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176", + "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09", + "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a", + "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015", + "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79", + "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb", + "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20", + "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464", + "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968", + "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82", + "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21", + "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0", + "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265", + "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b", + "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a", + "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72", + "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce", + "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0", + "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a", + "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163", + "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad", + "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d" + ], + "index": "pypi", + "markers": "python_full_version >= '3.6.2'", + "version": "==22.3.0" + }, + "click": { + "hashes": [ + "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1", + "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==8.0.4" + }, + "coverage": { + "hashes": [ + "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c", + "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63", + "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7", + "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f", + "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8", + "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf", + "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0", + "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384", + "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76", + "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7", + "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d", + "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70", + "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f", + "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818", + "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b", + "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d", + "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec", + "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083", + "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2", + "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9", + "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd", + "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade", + "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e", + "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a", + "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227", + "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87", + "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c", + "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e", + "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c", + "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e", + "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd", + "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec", + "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562", + "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8", + "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677", + "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357", + "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c", + "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd", + "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49", + "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286", + "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1", + "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf", + "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51", + "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409", + "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384", + "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e", + "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978", + "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57", + "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e", + "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2", + "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48", + "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==7.4.4" + }, + "django": { + "hashes": [ + "sha256:56ab63a105e8bb06ee67381d7b65fe6774f057e41a8bab06c8020c8882d8ecd4", + "sha256:b5bb1d11b2518a5f91372a282f24662f58f66749666b0a286ab057029f728080" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==5.0.2" + }, + "django-debug-toolbar": { + "hashes": [ + "sha256:0b0dddee5ea29b9cb678593bc0d7a6d76b21d7799cb68e091a2148341a80f3c4", + "sha256:e09b7dcb8417b743234dfc57c95a7c1d1d87a88844abd13b4c5387f807b31bf6" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==4.3.0" + }, + "flake8": { + "hashes": [ + "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132", + "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.1'", + "version": "==7.0.0" + }, + "flake8-isort": { + "hashes": [ + "sha256:0fec4dc3a15aefbdbe4012e51d5531a2eb5fa8b981cdfbc882296a59b54ede12", + "sha256:c1f82f3cf06a80c13e1d09bfae460e9666255d5c780b859f19f8318d420370b3" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==6.1.1" + }, + "flake8-quotes": { + "hashes": [ + "sha256:aad8492fb710a2d3eabe68c5f86a1428de650c8484127e14c43d0504ba30276c" + ], + "index": "pypi", + "version": "==3.4.0" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "isort": { + "hashes": [ + "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", + "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.0'", + "version": "==5.13.2" + }, + "lxml": { + "hashes": [ + "sha256:04ab5415bf6c86e0518d57240a96c4d1fcfc3cb370bb2ac2a732b67f579e5a04", + "sha256:057cdc6b86ab732cf361f8b4d8af87cf195a1f6dc5b0ff3de2dced242c2015e0", + "sha256:058a1308914f20784c9f4674036527e7c04f7be6fb60f5d61353545aa7fcb739", + "sha256:08802f0c56ed150cc6885ae0788a321b73505d2263ee56dad84d200cab11c07a", + "sha256:0a15438253b34e6362b2dc41475e7f80de76320f335e70c5528b7148cac253a1", + "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218", + "sha256:0e7259016bc4345a31af861fdce942b77c99049d6c2107ca07dc2bba2435c1d9", + "sha256:0ed777c1e8c99b63037b91f9d73a6aad20fd035d77ac84afcc205225f8f41188", + "sha256:0f5d65c39f16717a47c36c756af0fb36144069c4718824b7533f803ecdf91138", + "sha256:0f8c09ed18ecb4ebf23e02b8e7a22a05d6411911e6fabef3a36e4f371f4f2585", + "sha256:11a04306fcba10cd9637e669fd73aa274c1c09ca64af79c041aa820ea992b637", + "sha256:1ae67b4e737cddc96c99461d2f75d218bdf7a0c3d3ad5604d1f5e7464a2f9ffe", + "sha256:1c5bb205e9212d0ebddf946bc07e73fa245c864a5f90f341d11ce7b0b854475d", + "sha256:1f7785f4f789fdb522729ae465adcaa099e2a3441519df750ebdccc481d961a1", + "sha256:200e63525948e325d6a13a76ba2911f927ad399ef64f57898cf7c74e69b71095", + "sha256:21c2e6b09565ba5b45ae161b438e033a86ad1736b8c838c766146eff8ceffff9", + "sha256:2213afee476546a7f37c7a9b4ad4d74b1e112a6fafffc9185d6d21f043128c81", + "sha256:27aa20d45c2e0b8cd05da6d4759649170e8dfc4f4e5ef33a34d06f2d79075d57", + "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536", + "sha256:2c9d147f754b1b0e723e6afb7ba1566ecb162fe4ea657f53d2139bbf894d050a", + "sha256:2ddfe41ddc81f29a4c44c8ce239eda5ade4e7fc305fb7311759dd6229a080052", + "sha256:31e9a882013c2f6bd2f2c974241bf4ba68c85eba943648ce88936d23209a2e01", + "sha256:3249cc2989d9090eeac5467e50e9ec2d40704fea9ab72f36b034ea34ee65ca98", + "sha256:3545039fa4779be2df51d6395e91a810f57122290864918b172d5dc7ca5bb433", + "sha256:394ed3924d7a01b5bd9a0d9d946136e1c2f7b3dc337196d99e61740ed4bc6fe1", + "sha256:3a6b45da02336895da82b9d472cd274b22dc27a5cea1d4b793874eead23dd14f", + "sha256:3a74c4f27167cb95c1d4af1c0b59e88b7f3e0182138db2501c353555f7ec57f4", + "sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b", + "sha256:3dbe858ee582cbb2c6294dc85f55b5f19c918c2597855e950f34b660f1a5ede6", + "sha256:3dc773b2861b37b41a6136e0b72a1a44689a9c4c101e0cddb6b854016acc0aa8", + "sha256:3e183c6e3298a2ed5af9d7a356ea823bccaab4ec2349dc9ed83999fd289d14d5", + "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306", + "sha256:417d14450f06d51f363e41cace6488519038f940676ce9664b34ebf5653433a5", + "sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f", + "sha256:491755202eb21a5e350dae00c6d9a17247769c64dcf62d8c788b5c135e179dc4", + "sha256:4951e4f7a5680a2db62f7f4ab2f84617674d36d2d76a729b9a8be4b59b3659be", + "sha256:52421b41ac99e9d91934e4d0d0fe7da9f02bfa7536bb4431b4c05c906c8c6919", + "sha256:530e7c04f72002d2f334d5257c8a51bf409db0316feee7c87e4385043be136af", + "sha256:533658f8fbf056b70e434dff7e7aa611bcacb33e01f75de7f821810e48d1bb66", + "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1", + "sha256:56c22432809085b3f3ae04e6e7bdd36883d7258fcd90e53ba7b2e463efc7a6af", + "sha256:58278b29cb89f3e43ff3e0c756abbd1518f3ee6adad9e35b51fb101c1c1daaec", + "sha256:588008b8497667f1ddca7c99f2f85ce8511f8f7871b4a06ceede68ab62dff64b", + "sha256:59565f10607c244bc4c05c0c5fa0c190c990996e0c719d05deec7030c2aa8289", + "sha256:59689a75ba8d7ffca577aefd017d08d659d86ad4585ccc73e43edbfc7476781a", + "sha256:5aea8212fb823e006b995c4dda533edcf98a893d941f173f6c9506126188860d", + "sha256:5c670c0406bdc845b474b680b9a5456c561c65cf366f8db5a60154088c92d102", + "sha256:5ca1e8188b26a819387b29c3895c47a5e618708fe6f787f3b1a471de2c4a94d9", + "sha256:5d077bc40a1fe984e1a9931e801e42959a1e6598edc8a3223b061d30fbd26bbc", + "sha256:5d5792e9b3fb8d16a19f46aa8208987cfeafe082363ee2745ea8b643d9cc5b45", + "sha256:5dd1537e7cc06efd81371f5d1a992bd5ab156b2b4f88834ca852de4a8ea523fa", + "sha256:5ea7b6766ac2dfe4bcac8b8595107665a18ef01f8c8343f00710b85096d1b53a", + "sha256:622020d4521e22fb371e15f580d153134bfb68d6a429d1342a25f051ec72df1c", + "sha256:627402ad8dea044dde2eccde4370560a2b750ef894c9578e1d4f8ffd54000461", + "sha256:644df54d729ef810dcd0f7732e50e5ad1bd0a135278ed8d6bcb06f33b6b6f708", + "sha256:64641a6068a16201366476731301441ce93457eb8452056f570133a6ceb15fca", + "sha256:64c2baa7774bc22dd4474248ba16fe1a7f611c13ac6123408694d4cc93d66dbd", + "sha256:6588c459c5627fefa30139be4d2e28a2c2a1d0d1c265aad2ba1935a7863a4913", + "sha256:66bc5eb8a323ed9894f8fa0ee6cb3e3fb2403d99aee635078fd19a8bc7a5a5da", + "sha256:68a2610dbe138fa8c5826b3f6d98a7cfc29707b850ddcc3e21910a6fe51f6ca0", + "sha256:6935bbf153f9a965f1e07c2649c0849d29832487c52bb4a5c5066031d8b44fd5", + "sha256:6992030d43b916407c9aa52e9673612ff39a575523c5f4cf72cdef75365709a5", + "sha256:6a014510830df1475176466b6087fc0c08b47a36714823e58d8b8d7709132a96", + "sha256:6ab833e4735a7e5533711a6ea2df26459b96f9eec36d23f74cafe03631647c41", + "sha256:6cc6ee342fb7fa2471bd9b6d6fdfc78925a697bf5c2bcd0a302e98b0d35bfad3", + "sha256:6cf58416653c5901e12624e4013708b6e11142956e7f35e7a83f1ab02f3fe456", + "sha256:70a9768e1b9d79edca17890175ba915654ee1725975d69ab64813dd785a2bd5c", + "sha256:70ac664a48aa64e5e635ae5566f5227f2ab7f66a3990d67566d9907edcbbf867", + "sha256:71e97313406ccf55d32cc98a533ee05c61e15d11b99215b237346171c179c0b0", + "sha256:7221d49259aa1e5a8f00d3d28b1e0b76031655ca74bb287123ef56c3db92f213", + "sha256:74b28c6334cca4dd704e8004cba1955af0b778cf449142e581e404bd211fb619", + "sha256:764b521b75701f60683500d8621841bec41a65eb739b8466000c6fdbc256c240", + "sha256:78bfa756eab503673991bdcf464917ef7845a964903d3302c5f68417ecdc948c", + "sha256:794f04eec78f1d0e35d9e0c36cbbb22e42d370dda1609fb03bcd7aeb458c6377", + "sha256:79bd05260359170f78b181b59ce871673ed01ba048deef4bf49a36ab3e72e80b", + "sha256:7a7efd5b6d3e30d81ec68ab8a88252d7c7c6f13aaa875009fe3097eb4e30b84c", + "sha256:7c17b64b0a6ef4e5affae6a3724010a7a66bda48a62cfe0674dabd46642e8b54", + "sha256:804f74efe22b6a227306dd890eecc4f8c59ff25ca35f1f14e7482bbce96ef10b", + "sha256:853e074d4931dbcba7480d4dcab23d5c56bd9607f92825ab80ee2bd916edea53", + "sha256:857500f88b17a6479202ff5fe5f580fc3404922cd02ab3716197adf1ef628029", + "sha256:865bad62df277c04beed9478fe665b9ef63eb28fe026d5dedcb89b537d2e2ea6", + "sha256:88e22fc0a6684337d25c994381ed8a1580a6f5ebebd5ad41f89f663ff4ec2885", + "sha256:8b9c07e7a45bb64e21df4b6aa623cb8ba214dfb47d2027d90eac197329bb5e94", + "sha256:8de8f9d6caa7f25b204fc861718815d41cbcf27ee8f028c89c882a0cf4ae4134", + "sha256:8e77c69d5892cb5ba71703c4057091e31ccf534bd7f129307a4d084d90d014b8", + "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9", + "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863", + "sha256:96323338e6c14e958d775700ec8a88346014a85e5de73ac7967db0367582049b", + "sha256:9676bfc686fa6a3fa10cd4ae6b76cae8be26eb5ec6811d2a325636c460da1806", + "sha256:9b0ff53900566bc6325ecde9181d89afadc59c5ffa39bddf084aaedfe3b06a11", + "sha256:9b9ec9c9978b708d488bec36b9e4c94d88fd12ccac3e62134a9d17ddba910ea9", + "sha256:9c6ad0fbf105f6bcc9300c00010a2ffa44ea6f555df1a2ad95c88f5656104817", + "sha256:9ca66b8e90daca431b7ca1408cae085d025326570e57749695d6a01454790e95", + "sha256:9e2addd2d1866fe112bc6f80117bcc6bc25191c5ed1bfbcf9f1386a884252ae8", + "sha256:a0af35bd8ebf84888373630f73f24e86bf016642fb8576fba49d3d6b560b7cbc", + "sha256:a2b44bec7adf3e9305ce6cbfa47a4395667e744097faed97abb4728748ba7d47", + "sha256:a2dfe7e2473f9b59496247aad6e23b405ddf2e12ef0765677b0081c02d6c2c0b", + "sha256:a55ee573116ba208932e2d1a037cc4b10d2c1cb264ced2184d00b18ce585b2c0", + "sha256:a7baf9ffc238e4bf401299f50e971a45bfcc10a785522541a6e3179c83eabf0a", + "sha256:a8d5c70e04aac1eda5c829a26d1f75c6e5286c74743133d9f742cda8e53b9c2f", + "sha256:a91481dbcddf1736c98a80b122afa0f7296eeb80b72344d7f45dc9f781551f56", + "sha256:ab31a88a651039a07a3ae327d68ebdd8bc589b16938c09ef3f32a4b809dc96ef", + "sha256:abc25c3cab9ec7fcd299b9bcb3b8d4a1231877e425c650fa1c7576c5107ab851", + "sha256:adfb84ca6b87e06bc6b146dc7da7623395db1e31621c4785ad0658c5028b37d7", + "sha256:afbbdb120d1e78d2ba8064a68058001b871154cc57787031b645c9142b937a62", + "sha256:afd5562927cdef7c4f5550374acbc117fd4ecc05b5007bdfa57cc5355864e0a4", + "sha256:b070bbe8d3f0f6147689bed981d19bbb33070225373338df755a46893528104a", + "sha256:b0b58fbfa1bf7367dde8a557994e3b1637294be6cf2169810375caf8571a085c", + "sha256:b560e3aa4b1d49e0e6c847d72665384db35b2f5d45f8e6a5c0072e0283430533", + "sha256:b6787b643356111dfd4032b5bffe26d2f8331556ecb79e15dacb9275da02866e", + "sha256:bcbf4af004f98793a95355980764b3d80d47117678118a44a80b721c9913436a", + "sha256:beb72935a941965c52990f3a32d7f07ce869fe21c6af8b34bf6a277b33a345d3", + "sha256:bf2e2458345d9bffb0d9ec16557d8858c9c88d2d11fed53998512504cd9df49b", + "sha256:c2d35a1d047efd68027817b32ab1586c1169e60ca02c65d428ae815b593e65d4", + "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0", + "sha256:c6f2c8372b98208ce609c9e1d707f6918cc118fea4e2c754c9f0812c04ca116d", + "sha256:c817d420c60a5183953c783b0547d9eb43b7b344a2c46f69513d5952a78cddf3", + "sha256:c8ba129e6d3b0136a0f50345b2cb3db53f6bda5dd8c7f5d83fbccba97fb5dcb5", + "sha256:c94e75445b00319c1fad60f3c98b09cd63fe1134a8a953dcd48989ef42318534", + "sha256:cc4691d60512798304acb9207987e7b2b7c44627ea88b9d77489bbe3e6cc3bd4", + "sha256:cc518cea79fd1e2f6c90baafa28906d4309d24f3a63e801d855e7424c5b34144", + "sha256:cd53553ddad4a9c2f1f022756ae64abe16da1feb497edf4d9f87f99ec7cf86bd", + "sha256:cf22b41fdae514ee2f1691b6c3cdeae666d8b7fa9434de445f12bbeee0cf48dd", + "sha256:d38c8f50ecf57f0463399569aa388b232cf1a2ffb8f0a9a5412d0db57e054860", + "sha256:d3be9b2076112e51b323bdf6d5a7f8a798de55fb8d95fcb64bd179460cdc0704", + "sha256:d4f2cc7060dc3646632d7f15fe68e2fa98f58e35dd5666cd525f3b35d3fed7f8", + "sha256:d7520db34088c96cc0e0a3ad51a4fd5b401f279ee112aa2b7f8f976d8582606d", + "sha256:d793bebb202a6000390a5390078e945bbb49855c29c7e4d56a85901326c3b5d9", + "sha256:da052e7962ea2d5e5ef5bc0355d55007407087392cf465b7ad84ce5f3e25fe0f", + "sha256:dae0ed02f6b075426accbf6b2863c3d0a7eacc1b41fb40f2251d931e50188dad", + "sha256:ddc678fb4c7e30cf830a2b5a8d869538bc55b28d6c68544d09c7d0d8f17694dc", + "sha256:df2e6f546c4df14bc81f9498bbc007fbb87669f1bb707c6138878c46b06f6510", + "sha256:e02c5175f63effbd7c5e590399c118d5db6183bbfe8e0d118bdb5c2d1b48d937", + "sha256:e196a4ff48310ba62e53a8e0f97ca2bca83cdd2fe2934d8b5cb0df0a841b193a", + "sha256:e233db59c8f76630c512ab4a4daf5a5986da5c3d5b44b8e9fc742f2a24dbd460", + "sha256:e32be23d538753a8adb6c85bd539f5fd3b15cb987404327c569dfc5fd8366e85", + "sha256:e3d30321949861404323c50aebeb1943461a67cd51d4200ab02babc58bd06a86", + "sha256:e89580a581bf478d8dcb97d9cd011d567768e8bc4095f8557b21c4d4c5fea7d0", + "sha256:e998e304036198b4f6914e6a1e2b6f925208a20e2042563d9734881150c6c246", + "sha256:ec42088248c596dbd61d4ae8a5b004f97a4d91a9fd286f632e42e60b706718d7", + "sha256:efa7b51824aa0ee957ccd5a741c73e6851de55f40d807f08069eb4c5a26b2baa", + "sha256:f0a1bc63a465b6d72569a9bba9f2ef0334c4e03958e043da1920299100bc7c08", + "sha256:f18a5a84e16886898e51ab4b1d43acb3083c39b14c8caeb3589aabff0ee0b270", + "sha256:f2a9efc53d5b714b8df2b4b3e992accf8ce5bbdfe544d74d5c6766c9e1146a3a", + "sha256:f3bbbc998d42f8e561f347e798b85513ba4da324c2b3f9b7969e9c45b10f6169", + "sha256:f42038016852ae51b4088b2862126535cc4fc85802bfe30dea3500fdfaf1864e", + "sha256:f443cdef978430887ed55112b491f670bba6462cea7a7742ff8f14b7abb98d75", + "sha256:f51969bac61441fd31f028d7b3b45962f3ecebf691a510495e5d2cd8c8092dbd", + "sha256:f8aca2e3a72f37bfc7b14ba96d4056244001ddcc18382bd0daa087fd2e68a354", + "sha256:f9737bf36262046213a28e789cc82d82c6ef19c85a0cf05e75c670a33342ac2c", + "sha256:fd6037392f2d57793ab98d9e26798f44b8b4da2f2464388588f48ac52c489ea1", + "sha256:feaa45c0eae424d3e90d78823f3828e7dc42a42f21ed420db98da2c4ecf0a2cb", + "sha256:ff097ae562e637409b429a7ac958a20aab237a0378c42dabaa1e3abf2f896e5f", + "sha256:ff46d772d5f6f73564979cd77a4fffe55c916a05f3cb70e7c9c0590059fb29ef" + ], + "markers": "python_version >= '3.6'", + "version": "==5.2.1" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "packaging": { + "hashes": [ + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + ], + "markers": "python_version >= '3.7'", + "version": "==24.0" + }, + "pathspec": { + "hashes": [ + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, + "platformdirs": { + "hashes": [ + "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", + "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" + ], + "markers": "python_version >= '3.8'", + "version": "==4.2.0" + }, + "pluggy": { + "hashes": [ + "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", + "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be" + ], + "markers": "python_version >= '3.8'", + "version": "==1.4.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", + "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" + ], + "markers": "python_version >= '3.8'", + "version": "==2.11.1" + }, + "pyflakes": { + "hashes": [ + "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", + "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a" + ], + "markers": "python_version >= '3.8'", + "version": "==3.2.0" + }, + "pytest": { + "hashes": [ + "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7", + "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==8.1.1" + }, + "pytest-django": { + "hashes": [ + "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90", + "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==4.8.0" + }, + "setuptools": { + "hashes": [ + "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e", + "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c" + ], + "markers": "python_version >= '3.8'", + "version": "==69.2.0" + }, + "sqlparse": { + "hashes": [ + "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3", + "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c" + ], + "markers": "python_version >= '3.5'", + "version": "==0.4.4" + }, + "unittest-xml-reporting": { + "hashes": [ + "sha256:edd8d3170b40c3a81b8cf910f46c6a304ae2847ec01036d02e9c0f9b85762d28", + "sha256:f3d7402e5b3ac72a5ee3149278339db1a8f932ee405f48bcb9c681372f2717d5" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==3.2.0" + } + } } diff --git a/backend/courses/management/commands/recompute_soft_state.py b/backend/courses/management/commands/recompute_soft_state.py index dec21c8b5..64ae2675d 100644 --- a/backend/courses/management/commands/recompute_soft_state.py +++ b/backend/courses/management/commands/recompute_soft_state.py @@ -99,7 +99,9 @@ def recompute_enrollment(): ) -# course credits = sum(section credis for all activities) +# course credits = sum(section credis for all activities for sections below 500) +# the < 500 heuristic comes from here: +# https://provider.www.upenn.edu/computing/da/dw/student/enrollment_section_type.e.html COURSE_CREDITS_RAW_SQL = dedent( """ WITH CourseCredits AS ( @@ -108,6 +110,7 @@ def recompute_enrollment(): INNER JOIN ( SELECT MAX(U1."credits") AS "activity_cus", U1."course_id" FROM "courses_section" U1 + WHERE U1."code" < '500' AND (U1."status" <> 'X' OR U1."status" <> '') GROUP BY U1."course_id", U1."activity" ) AS U2 ON U0."id" = U2."course_id" @@ -125,7 +128,6 @@ def recompute_enrollment(): def recompute_course_credits( model=Course, # so this function can be used in migrations (see django.db.migrations.RunPython) ): - with connection.cursor() as cursor: cursor.execute(COURSE_CREDITS_RAW_SQL) diff --git a/backend/courses/management/commands/recompute_topics.py b/backend/courses/management/commands/recompute_topics.py index 6cb508267..c56b3094c 100644 --- a/backend/courses/management/commands/recompute_topics.py +++ b/backend/courses/management/commands/recompute_topics.py @@ -1,9 +1,10 @@ from django.core.management.base import BaseCommand from django.db import transaction from django.db.models import Count, OuterRef, Subquery +from tqdm import tqdm from courses.models import Course, Topic -from courses.util import all_semesters +from courses.util import all_semesters, historical_semester_probability def garbage_collect_topics(): @@ -151,5 +152,28 @@ def handle(self, *args, **kwargs): assert ( min_semester in all_semesters() ), f"--min-semester={min_semester} is not a valid semester." - + semesters = sorted( + [sem for sem in all_semesters() if not min_semester or sem >= min_semester] + ) recompute_topics(min_semester, verbose=True, allow_null_parent_topic=bool(min_semester)) + recompute_historical_semester_probabilities(current_semester=semesters[-1], verbose=True) + + +def recompute_historical_semester_probabilities(current_semester, verbose=False): + """ + Recomputes the historical probabilities for all topics. + """ + if verbose: + print("Recomputing historical probabilities for all topics...") + topics = Topic.objects.all() + # Iterate over each Topic + for i, topic in tqdm(enumerate(topics), disable=not verbose, total=topics.count()): + # Calculate historical_year_probability for the current topic + ordered_courses = topic.courses.all().order_by("semester") + ordered_semester = [course.semester for course in ordered_courses] + historical_prob = historical_semester_probability(current_semester, ordered_semester) + # Update the historical_probabilities field for the current topic + topic.historical_probabilities_spring = historical_prob[0] + topic.historical_probabilities_summer = historical_prob[1] + topic.historical_probabilities_fall = historical_prob[2] + topic.save() diff --git a/backend/courses/migrations/0064_auto_20240225_1331.py b/backend/courses/migrations/0064_auto_20240225_1331.py new file mode 100644 index 000000000..ec711555b --- /dev/null +++ b/backend/courses/migrations/0064_auto_20240225_1331.py @@ -0,0 +1,66 @@ +# Generated by Django 3.2.23 on 2024-02-25 18:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("courses", "0063_auto_20231212_1750"), + ] + + operations = [ + migrations.AddField( + model_name="topic", + name="historical_probabilities_fall", + field=models.FloatField( + default=0, + help_text="\nThe historical probability of a student taking a course in this topic in the fall\nsemester, based on historical data. This field is recomputed nightly from the\n`parent_course` graph (in the recompute_soft_state cron job).\n", + ), + ), + migrations.AddField( + model_name="topic", + name="historical_probabilities_spring", + field=models.FloatField( + default=0, + help_text="\nThe historical probability of a student taking a course in this topic in the spring\nsemester, based on historical data. This field is recomputed nightly from the\n`parent_course` graph (in the recompute_soft_state cron job).\n", + ), + ), + migrations.AddField( + model_name="topic", + name="historical_probabilities_summer", + field=models.FloatField( + default=0, + help_text="\nThe historical probability of a student taking a course in this topic in the summer\nsemester, based on historical data. This field is recomputed nightly from the\n`parent_course` graph (in the recompute_soft_state cron job).\n", + ), + ), + migrations.AlterField( + model_name="section", + name="activity", + field=models.CharField( + choices=[ + ("", "Undefined"), + ("CLN", "Clinic"), + ("CRT", "Clinical Rotation"), + ("DAB", "Dissertation Abroad"), + ("DIS", "Dissertation"), + ("DPC", "Doctoral Program Exchange"), + ("FLD", "Field Work"), + ("HYB", "Hybrid"), + ("IND", "Independent Study"), + ("LAB", "Lab"), + ("LEC", "Lecture"), + ("MST", "Masters Thesis"), + ("ONL", "Online"), + ("PRC", "Practicum"), + ("REC", "Recitation"), + ("SEM", "Seminar"), + ("SRT", "Senior Thesis"), + ("STU", "Studio"), + ], + db_index=True, + help_text='The section activity, e.g. `LEC` for CIS-120-001 (2020A). Options and meanings:
"""Undefined"
"CLN""Clinic"
"CRT""Clinical Rotation"
"DAB""Dissertation Abroad"
"DIS""Dissertation"
"DPC""Doctoral Program Exchange"
"FLD""Field Work"
"HYB""Hybrid"
"IND""Independent Study"
"LAB""Lab"
"LEC""Lecture"
"MST""Masters Thesis"
"ONL""Online"
"PRC""Practicum"
"REC""Recitation"
"SEM""Seminar"
"SRT""Senior Thesis"
"STU""Studio"
', + max_length=50, + ), + ), + ] diff --git a/backend/courses/models.py b/backend/courses/models.py index 27a281d3d..32f79f1c6 100644 --- a/backend/courses/models.py +++ b/backend/courses/models.py @@ -377,6 +377,36 @@ class Topic(models.Model): ), ) + historical_probabilities_spring = models.FloatField( + default=0, + help_text=dedent( + """ + The historical probability of a student taking a course in this topic in the spring + semester, based on historical data. This field is recomputed nightly from the + `parent_course` graph (in the recompute_soft_state cron job). + """ + ), + ) + historical_probabilities_summer = models.FloatField( + default=0, + help_text=dedent( + """ + The historical probability of a student taking a course in this topic in the summer + semester, based on historical data. This field is recomputed nightly from the + `parent_course` graph (in the recompute_soft_state cron job). + """ + ), + ) + historical_probabilities_fall = models.FloatField( + default=0, + help_text=dedent( + """ + The historical probability of a student taking a course in this topic in the fall + semester, based on historical data. This field is recomputed nightly from the + `parent_course` graph (in the recompute_soft_state cron job). + """ + ), + ) branched_from = models.ForeignKey( "Topic", related_name="branched_to", diff --git a/backend/courses/serializers.py b/backend/courses/serializers.py index 744e916b0..d49f5e339 100644 --- a/backend/courses/serializers.py +++ b/backend/courses/serializers.py @@ -15,6 +15,7 @@ StatusUpdate, UserProfile, ) +from plan.management.commands.recommendcourses import cosine_similarity class MeetingSerializer(serializers.ModelSerializer): @@ -251,7 +252,23 @@ def get_num_sections(self, obj): return obj.sections.count() def get_recommendation_score(self, obj): - return 0 + user_vector = self.context.get("user_vector") + curr_course_vectors_dict = self.context.get("curr_course_vectors_dict") + + if user_vector is None or curr_course_vectors_dict is None: + # NOTE: there should be no case in which user_vector is None + # but curr_course_vectors_dict is not None. However, for + # stability in production, recommendation_score is None when + # either is None + return None + + course_vector = curr_course_vectors_dict.get(obj.full_code) + if course_vector is None: + # Fires when the curr_course_vectors_dict is defined (ie, the user is authenticated) + # but the course code is not in the model + return None + + return cosine_similarity(course_vector, user_vector) course_quality = serializers.DecimalField( max_digits=4, decimal_places=3, read_only=True, help_text=course_quality_help diff --git a/backend/courses/util.py b/backend/courses/util.py index 4e9c7a06f..77f06f612 100644 --- a/backend/courses/util.py +++ b/backend/courses/util.py @@ -721,3 +721,53 @@ def get_semesters(semesters: str = None) -> list[str]: if s not in possible_semesters: raise ValueError(f"Provided semester {s} was not found in the db.") return sorted(semesters) + + +def historical_semester_probability(current_semester: str, semesters: list[str]): + """ + :param current: The current semester represented in the 20XX(A|B|C) format. + :type current: str + :param courses: A list of Course objects sorted by date in ascending order. + :type courses: list + :returns: A list of 3 probabilities representing the likelihood of + taking a course in each semester. + :rtype: list + """ + PROB_DISTRIBUTION = [0.4, 0.3, 0.15, 0.1, 0.05] + + def normalize_and_round(prob, i): + """Modifies the probability distribution to account for the + fact that the last course was taken i years ago.""" + truncate = PROB_DISTRIBUTION[:i] + total = sum(truncate) + return list(map(lambda x: round(x / total, 3), truncate)) + + semester_probabilities = {"A": 0.0, "B": 0.0, "C": 0.0} + current_year = int(current_semester[:-1]) + semesters = [ + semester + for semester in semesters + if semester < str(current_year) and semester > str(current_year - 5) + ] + if not semesters: + return [0, 0, 0] + if current_year - int(semesters[0][:-1]) < 5: + # If the class hasn't been offered in the last 5 years, + # we make sure the resulting probabilities sum to 1 + modified_prob_distribution = normalize_and_round( + PROB_DISTRIBUTION, current_year - int(semesters[0][:-1]) + ) + else: + modified_prob_distribution = PROB_DISTRIBUTION + for historical_semester in semesters: + historical_year = int(historical_semester[:-1]) + sem_char = historical_semester[-1].upper() # A, B, C + semester_probabilities[sem_char] += modified_prob_distribution[ + current_year - historical_year - 1 + ] + return list( + map( + lambda x: min(round(x, 2), 1.00), + [semester_probabilities["A"], semester_probabilities["B"], semester_probabilities["C"]], + ) + ) diff --git a/backend/courses/views.py b/backend/courses/views.py index 321d7675b..20cc5053a 100644 --- a/backend/courses/views.py +++ b/backend/courses/views.py @@ -36,6 +36,7 @@ ) from courses.util import get_current_semester from PennCourses.docs_settings import PcxAutoSchema +from plan.management.commands.recommendcourses import retrieve_course_clusters, vectorize_user SEMESTER_PARAM_DESCRIPTION = ( @@ -66,8 +67,12 @@ def filter_by_semester(self, queryset): semester = self.get_semester() if semester != "all": queryset = queryset.filter(**{self.get_semester_field(): semester}) - else: - queryset = queryset.order_by("full_code", "-semester").distinct("full_code") + else: # Only used for Penn Degree Plan (as of 4/10/2024) + queryset = ( + queryset.exclude(credits=None) # heuristic: if the credits are empty, then ignore + .order_by("full_code", "-semester") + .distinct("full_code") + ) return queryset def get_queryset(self): @@ -209,6 +214,15 @@ def get_serializer_context(self): if self.request is None or not self.request.user or not self.request.user.is_authenticated: return context + + _, _, curr_course_vectors_dict, past_course_vectors_dict = retrieve_course_clusters() + user_vector, _ = vectorize_user( + self.request.user, curr_course_vectors_dict, past_course_vectors_dict + ) + context.update( + {"user_vector": user_vector, "curr_course_vectors_dict": curr_course_vectors_dict} + ) + return context filter_backends = [TypedCourseSearchBackend, CourseSearchFilterBackend] diff --git a/backend/degree/admin.py b/backend/degree/admin.py index 5f33388fe..cc978ad0f 100644 --- a/backend/degree/admin.py +++ b/backend/degree/admin.py @@ -1,7 +1,6 @@ -from django.conf.urls import url from django.contrib import admin from django.template.response import TemplateResponse -from django.urls import reverse +from django.urls import re_path, reverse from django.utils.html import format_html from degree.models import ( @@ -16,34 +15,25 @@ # Register your models here. -@admin.register(Rule) class RuleAdmin(admin.ModelAdmin): search_fields = ["title", "id"] list_display = ["title", "id", "parent"] list_select_related = ["parent"] -admin.site.register(DegreePlan) -admin.site.register(SatisfactionStatus) - - -@admin.register(PDPBetaUser) class PDPBetaUserAdmin(admin.ModelAdmin): search_fields = ("person__username", "person__id") autocomplete_fields = ("person",) -@admin.register(Fulfillment) class FulfillmentAdmin(admin.ModelAdmin): autocomplete_fields = ["rules"] -@admin.register(DoubleCountRestriction) class DoubleCountRestrictionAdmin(admin.ModelAdmin): autocomplete_fields = ["rule", "other_rule"] -@admin.register(Degree) class DegreeAdmin(admin.ModelAdmin): autocomplete_fields = ["rules"] list_display = ["program", "degree", "major", "concentration", "year", "view_degree_editor"] @@ -59,7 +49,7 @@ def get_urls(self): # get the default urls urls = super().get_urls() custom_urls = [ - url( + re_path( r"^degree-editor/$", self.admin_site.admin_view(self.degree_editor), name="degree-editor", @@ -70,3 +60,12 @@ def get_urls(self): def degree_editor(self, request): context = dict(self.admin_site.each_context(request)) return TemplateResponse(request, "degree-editor.html", context) + + +admin.site.register(Rule, RuleAdmin) +admin.site.register(DegreePlan) +admin.site.register(SatisfactionStatus) +admin.site.register(PDPBetaUser, PDPBetaUserAdmin) +admin.site.register(Fulfillment, FulfillmentAdmin) +admin.site.register(DoubleCountRestriction, DoubleCountRestrictionAdmin) +admin.site.register(Degree, DegreeAdmin) diff --git a/backend/degree/migrations/0001_initial.py b/backend/degree/migrations/0001_initial.py index 8cb9b198f..136cd1a8e 100644 --- a/backend/degree/migrations/0001_initial.py +++ b/backend/degree/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.23 on 2024-03-27 06:49 +# Generated by Django 5.0.2 on 2024-04-03 02:00 import django.db.models.deletion import django.utils.timezone @@ -11,7 +11,6 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("courses", "0061_merge_20231112_1524"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] @@ -21,7 +20,7 @@ class Migration(migrations.Migration): fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), @@ -33,6 +32,7 @@ class Migration(migrations.Migration): ("EU_BAS", "Engineering BAS"), ("AU_BA", "College BA"), ("WU_BS", "Wharton BS"), + ("NU_BSN", "Nursing BSN"), ], help_text="\nThe program code for this degree, e.g., EU_BSE\n", max_length=10, @@ -50,6 +50,14 @@ class Migration(migrations.Migration): help_text="\nThe major code for this degree, e.g., BIOL\n", max_length=4 ), ), + ( + "major_name", + models.CharField( + help_text="\nThe name of the major for this degree, e.g., Africana Studies\n", + max_length=128, + null=True, + ), + ), ( "concentration", models.CharField( @@ -58,12 +66,29 @@ class Migration(migrations.Migration): null=True, ), ), + ( + "concentration_name", + models.CharField( + help_text="\nThe name of the concentration for this degree, e.g., African American Studies\n", + max_length=128, + null=True, + ), + ), ( "year", models.IntegerField( help_text="\nThe effective year of this degree, e.g., 2023\n" ), ), + ( + "credits", + models.DecimalField( + decimal_places=2, + help_text="\nThe minimum number of CUs required for this degree.\n", + max_digits=4, + null=True, + ), + ), ], ), migrations.CreateModel( @@ -71,7 +96,7 @@ class Migration(migrations.Migration): fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), @@ -86,8 +111,9 @@ class Migration(migrations.Migration): ( "degrees", models.ManyToManyField( + blank=True, help_text="The degrees this degree plan is associated with.", - to="degree.Degree", + to="degree.degree", ), ), ( @@ -100,12 +126,60 @@ class Migration(migrations.Migration): ), ], ), + migrations.CreateModel( + name="DockedCourse", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "full_code", + models.CharField( + blank=True, + db_index=True, + help_text="The dash-joined department and code of the course, e.g., `CIS-120`", + max_length=16, + ), + ), + ( + "person", + models.ForeignKey( + help_text="The user the docked course belongs to.", + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="PDPBetaUser", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "person", + models.ForeignKey( + help_text="The user who has access to the PDP beta", + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), migrations.CreateModel( name="Rule", fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), @@ -151,67 +225,12 @@ class Migration(migrations.Migration): ), ], ), - migrations.CreateModel( - name="SatisfactionStatus", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), - ), - ( - "satisfied", - models.BooleanField(default=False, help_text="Whether the rule is satisfied"), - ), - ("last_updated", models.DateTimeField(auto_now=True)), - ("last_checked", models.DateTimeField(default=django.utils.timezone.now)), - ( - "degree_plan", - models.ForeignKey( - help_text="The degree plan that leads to the satisfaction of the rule", - on_delete=django.db.models.deletion.CASCADE, - related_name="satisfactions", - to="degree.degreeplan", - ), - ), - ( - "rule", - models.ForeignKey( - help_text="The rule that is satisfied", - on_delete=django.db.models.deletion.CASCADE, - related_name="+", - to="degree.rule", - ), - ), - ], - ), - migrations.CreateModel( - name="PDPBetaUser", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ( - "person", - models.ForeignKey( - help_text="The user who has access to the PDP beta", - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), - ], - ), migrations.CreateModel( name="Fulfillment", fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), @@ -241,23 +260,13 @@ class Migration(migrations.Migration): to="degree.degreeplan", ), ), - ( - "historical_course", - models.ForeignKey( - help_text="\nThe last offering of the course with the full code, or null if\nthere is no such historical course.\n", - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="+", - to="courses.course", - ), - ), ( "rules", models.ManyToManyField( blank=True, help_text="\nThe rules this course fulfills. Blank if this course does not apply\nto any rules.\n", - related_name="_degree_fulfillment_rules_+", - to="degree.Rule", + related_name="+", + to="degree.rule", ), ), ], @@ -267,7 +276,7 @@ class Migration(migrations.Migration): fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), @@ -306,70 +315,77 @@ class Migration(migrations.Migration): ), ], ), + migrations.AddField( + model_name="degree", + name="rules", + field=models.ManyToManyField( + blank=True, + help_text="\nThe rules for this degree. Blank if this degree has no rules.\n", + related_name="degrees", + to="degree.rule", + ), + ), migrations.CreateModel( - name="DockedCourse", + name="SatisfactionStatus", fields=[ ( "id", - models.BigAutoField( + models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), ( - "full_code", - models.CharField( - blank=True, - db_index=True, - help_text="The dash-joined department and code of the course, e.g., `CIS-120`", - max_length=16, + "satisfied", + models.BooleanField(default=False, help_text="Whether the rule is satisfied"), + ), + ("last_updated", models.DateTimeField(auto_now=True)), + ("last_checked", models.DateTimeField(default=django.utils.timezone.now)), + ( + "degree_plan", + models.ForeignKey( + help_text="The degree plan that leads to the satisfaction of the rule", + on_delete=django.db.models.deletion.CASCADE, + related_name="satisfactions", + to="degree.degreeplan", ), ), ( - "person", + "rule", models.ForeignKey( - help_text="The user the docked course belongs to.", + help_text="The rule that is satisfied", on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, + related_name="+", + to="degree.rule", ), ), ], ), - migrations.AddField( - model_name="degree", - name="rules", - field=models.ManyToManyField( - blank=True, - help_text="\nThe rules for this degree. Blank if this degree has no rules.\n", - related_name="degrees", - to="degree.Rule", - ), - ), migrations.AddConstraint( - model_name="satisfactionstatus", + model_name="degreeplan", constraint=models.UniqueConstraint( - fields=("degree_plan", "rule"), name="unique_satisfaction" + fields=("name", "person"), name="degreeplan_name_person" ), ), - migrations.AlterUniqueTogether( - name="fulfillment", - unique_together={("degree_plan", "full_code")}, - ), migrations.AddConstraint( model_name="dockedcourse", constraint=models.UniqueConstraint( fields=("person", "full_code"), name="unique docked course" ), ), + migrations.AlterUniqueTogether( + name="fulfillment", + unique_together={("degree_plan", "full_code")}, + ), migrations.AddConstraint( - model_name="degreeplan", + model_name="degree", constraint=models.UniqueConstraint( - fields=("name", "person"), name="degreeplan_name_person" + fields=("program", "degree", "major", "concentration", "year"), name="unique degree" ), ), migrations.AddConstraint( - model_name="degree", + model_name="satisfactionstatus", constraint=models.UniqueConstraint( - fields=("program", "degree", "major", "concentration", "year"), name="unique degree" + fields=("degree_plan", "rule"), name="unique_satisfaction" ), ), ] diff --git a/backend/degree/migrations/0008_auto_20240327_2029.py b/backend/degree/migrations/0008_auto_20240327_2029.py deleted file mode 100644 index 4c6c50a13..000000000 --- a/backend/degree/migrations/0008_auto_20240327_2029.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 3.2.23 on 2024-03-28 00:29 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("degree", "0007_alter_dockedcourse_full_code"), - ] - - operations = [ - migrations.AddField( - model_name="degree", - name="concentration_name", - field=models.CharField( - help_text="\nThe name of the concentration for this degree, e.g., Science Technology & Society\n", - max_length=128, - null=True, - ), - ), - migrations.AlterField( - model_name="degree", - name="program", - field=models.CharField( - choices=[ - ("EU_BSE", "Engineering BSE"), - ("EU_BAS", "Engineering BAS"), - ("AU_BA", "College BA"), - ("WU_BS", "Wharton BS"), - ("NU_BSN", "Nursing BSN"), - ], - help_text="\nThe program code for this degree, e.g., EU_BSE\n", - max_length=10, - ), - ), - ] diff --git a/backend/degree/migrations/0009_alter_degree_credits.py b/backend/degree/migrations/0009_alter_degree_credits.py deleted file mode 100644 index f2670d400..000000000 --- a/backend/degree/migrations/0009_alter_degree_credits.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.23 on 2024-03-28 00:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("degree", "0008_auto_20240327_2029"), - ] - - operations = [ - migrations.AlterField( - model_name="degree", - name="credits", - field=models.DecimalField( - decimal_places=2, - help_text="\nThe minimum number of CUs required for this degree.\n", - max_digits=4, - null=True, - ), - ), - ] diff --git a/backend/degree/migrations/0010_auto_20240327_2050.py b/backend/degree/migrations/0010_auto_20240327_2050.py deleted file mode 100644 index cf23fa7ba..000000000 --- a/backend/degree/migrations/0010_auto_20240327_2050.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 3.2.23 on 2024-03-28 00:50 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("degree", "0009_alter_degree_credits"), - ] - - operations = [ - migrations.AddField( - model_name="degree", - name="major_name", - field=models.CharField( - help_text="\nThe name of the major for this degree, e.g., Africana Studies\n", - max_length=128, - null=True, - ), - ), - migrations.AlterField( - model_name="degree", - name="concentration_name", - field=models.CharField( - help_text="\nThe name of the concentration for this degree, e.g., African American Studies\n", - max_length=128, - null=True, - ), - ), - ] diff --git a/backend/degree/serializers.py b/backend/degree/serializers.py index 17dacd113..e787552a8 100644 --- a/backend/degree/serializers.py +++ b/backend/degree/serializers.py @@ -1,6 +1,6 @@ from textwrap import dedent -from django.db.models import Q +from django.db.models import Q, Subquery from rest_framework import serializers from courses.models import Course @@ -69,11 +69,6 @@ class Meta: model = Rule fields = "__all__" - def to_representation(self, instance): - data = super(RuleSerializer, self).to_representation(instance) - data.q = "" - return data - # Allow recursive serialization of rules RuleSerializer._declared_fields["rules"] = RuleSerializer( @@ -148,13 +143,17 @@ def validate(self, data): for rule in rules: # NOTE: we don't do any validation if the course doesn't exist in DB. In future, # it may be better to prompt user for manual override - if ( - Course.objects.filter(full_code=full_code).exists() - and not Course.objects.filter(rule.get_q_object(), full_code=full_code).exists() - ): - raise serializers.ValidationError( - f"Course {full_code} does not satisfy rule {rule.id}" - ) + if Course.objects.filter(full_code=full_code).exists(): + satisfying_courses = Course.objects.filter(rule.get_q_object()) + if not ( + Course.objects.filter( + full_code=full_code, + topic_id__in=Subquery(satisfying_courses.values("topic_id")), + ).exists() + ): + raise serializers.ValidationError( + f"Course {full_code} does not satisfy rule {rule.id}" + ) # Check for double count restrictions double_count_restrictions = DoubleCountRestriction.objects.filter( @@ -189,7 +188,8 @@ class Meta: class DockedCourseSerializer(serializers.ModelSerializer): id = serializers.ReadOnlyField(help_text="The id of the docked course") + person = serializers.HiddenField(default=serializers.CurrentUserDefault()) class Meta: model = DockedCourse - fields = ["full_code", "id"] + fields = ["full_code", "id", "person"] diff --git a/backend/degree/static/pdp/degree-editor.js b/backend/degree/static/pdp/degree-editor.js index bac0ce983..fd8336a63 100644 --- a/backend/degree/static/pdp/degree-editor.js +++ b/backend/degree/static/pdp/degree-editor.js @@ -78,12 +78,10 @@ const LayoutFlow = () => { const [edges, setEdges, onEdgesChange] = useEdgesState([]); const onConnect = useCallback((params) => { - console.log("onConnect", params); if (params.source === params.target) return; const [sourceIsDegree, sourceId] = pkOfNodeId(params.source); const [targetIsDegree, targetId] = pkOfNodeId(params.target); if (sourceIsDegree || targetIsDegree) return; - console.log("HERE"); const redirect = `/admin/degree/doublecountrestriction/add/?rule=${sourceId}&other_rule=${targetId}`; window.location.href = redirect; }, []); diff --git a/backend/degree/utils/parse_degreeworks.py b/backend/degree/utils/parse_degreeworks.py index 8c9c7d16c..8126985f0 100644 --- a/backend/degree/utils/parse_degreeworks.py +++ b/backend/degree/utils/parse_degreeworks.py @@ -18,29 +18,30 @@ def parse_coursearray(courseArray) -> Q: case ("@", "@", end) | ("PSEUDO@", "@", end): assert end is None logging.info("ignoring @ course") - pass case discipline, "@", end: assert end is None course_q &= Q(department__code=discipline) + case "@", number, None: + assert "@" not in number + course_q &= Q(code=number) case discipline, number, None: - if number.isdigit(): + if "@" not in number: course_q &= Q(full_code=f"{discipline}-{number}") elif number[:-1].isdigit() and number[-1] == "@": course_q &= Q(full_code__startswith=f"{discipline}-{number[:-1]}") - else: - logging.warn(f"Non-integer course number: {number}") + case "@", number, end: + assert "@" not in number and "@" not in end + course_q &= Q( + code__gte=number.strip(), + code__lte=end.strip(), + ) case discipline, number, end: - if number.isdigit() and end.isdigit(): - course_q &= Q( - department__code=discipline, - code__gte=number.strip(), - code__lte=end.strip(), - ) - else: - logging.warn( - f"Non-integer course number or numberEnd: " - f"(number) {number} (numberEnd) {end}" - ) + assert "@" not in number and "@" not in end + course_q &= Q( + department__code=discipline, + code__gte=number.strip(), + code__lte=end.strip(), + ) connector = "AND" # the connector to the next element; and by default if "withArray" in course: diff --git a/backend/degree/views.py b/backend/degree/views.py index 5529ed6b8..8fd9d5697 100644 --- a/backend/degree/views.py +++ b/backend/degree/views.py @@ -49,7 +49,7 @@ class DegreePlanViewset(AutoPrefetchViewSetMixin, viewsets.ModelViewSet): List, retrieve, create, destroy, and update a DegreePlan. """ - # After beta: remove DegreeWaitlist + # After beta: remove InPDPBeta permission_classes = [IsAuthenticated & InPDPBeta] def get_queryset(self): @@ -130,7 +130,7 @@ class FulfillmentViewSet(viewsets.ModelViewSet): List, retrieve, create, destroy, and update a Fulfillment. """ - # After beta: remove DegreeWaitlist + # After beta: remove InPDPBeta permission_classes = [IsAuthenticated & InPDPBeta] serializer_class = FulfillmentSerializer http_method_names = ["get", "post", "head", "delete"] @@ -169,7 +169,7 @@ class DockedCourseViewset(viewsets.ModelViewSet): List, retrieve, create, destroy, and update docked courses """ - # After beta: remove DegreeWaitlist + # After beta: remove InPDPBeta permission_classes = [IsAuthenticated & InPDPBeta] serializer_class = DockedCourseSerializer http_method_names = ["get", "post", "head", "delete"] @@ -184,7 +184,6 @@ def create(self, request, *args, **kwargs): if request.data.get("full_code") is None: raise ValidationError({"full_code": "This field is required."}) self.kwargs["full_code"] = request.data["full_code"] - self.kwargs["person"] = self.request.user try: return self.partial_update(request, *args, **kwargs) except Http404: diff --git a/backend/tests/alert/test_alert.py b/backend/tests/alert/test_alert.py index 723a05b3e..0149f02c5 100644 --- a/backend/tests/alert/test_alert.py +++ b/backend/tests/alert/test_alert.py @@ -205,12 +205,10 @@ def send_alert_helper(self, mock_email, mock_text, mock_push_notification, push_ r in get_registrations_for_alerts("CIS-1600-001", TEST_SEMESTER, course_status="C") ) self.assertEquals( - 0, - len(get_registrations_for_alerts("CIS-1600-001", TEST_SEMESTER, course_status="X")), + 0, len(get_registrations_for_alerts("CIS-1600-001", TEST_SEMESTER, course_status="X")) ) self.assertEquals( - 0, - len(get_registrations_for_alerts("CIS-1600-001", TEST_SEMESTER, course_status="")), + 0, len(get_registrations_for_alerts("CIS-1600-001", TEST_SEMESTER, course_status="")) ) tasks.send_alert(self.r.id, False, sent_by="ADM") r = Registration.objects.get(id=self.r.id) @@ -388,12 +386,7 @@ def test_dont_resend_close_notification(self, mock_email, mock_text, mock_push_n ) def resend_alert_forced_helper( - self, - mock_email, - mock_text, - mock_push_notification, - push_notification, - close_notification, + self, mock_email, mock_text, mock_push_notification, push_notification, close_notification ): """ This helper checks that calling tasks.send_alert with the forced parameter as True @@ -1063,8 +1056,7 @@ def check_model_with_response_data(self, model, data): self.assertEqual(model.close_notification, data["close_notification"]) self.assertEqual(model.close_notification_sent, data["close_notification_sent"]) self.assertEqual( - model.close_notification_sent_at, - self.convert_date(data["close_notification_sent_at"]), + model.close_notification_sent_at, self.convert_date(data["close_notification_sent_at"]) ) self.assertEqual(model.original_created_at, self.convert_date(data["original_created_at"])) self.assertEqual(model.created_at, self.convert_date(data["created_at"])) @@ -2182,12 +2174,10 @@ def test_registration_list_multiple_candidates_same_section(self): self.assertEqual(3, len(set(item.get("id") for item in response.data))) self.assertEqual(1, len([item for item in response.data if item.get("id") == sixth_id])) self.assertEqual( - 1, - len([item for item in response.data if item.get("id") == ids["second_id"]]), + 1, len([item for item in response.data if item.get("id") == ids["second_id"]]) ) self.assertEqual( - 1, - len([item for item in response.data if item.get("id") == ids["fifth_id"]]), + 1, len([item for item in response.data if item.get("id") == ids["fifth_id"]]) ) def cancel_and_resub_helper(self, auto_resub, put, cancel_before_sim_webhook): @@ -2402,11 +2392,7 @@ def changeattrs_update_order_helper(self, put, update_field): self.client.put( reverse("registrations-detail", args=[first_id]), json.dumps( - { - "deleted": True, - "auto_resubscribe": True, - "close_notification": True, - } + {"deleted": True, "auto_resubscribe": True, "close_notification": True} ), content_type="application/json", ) @@ -2459,11 +2445,7 @@ def close_notification_creation_helper(self, push_notif): response = self.client.post( reverse("registrations-list"), json.dumps( - { - "section": "CIS-1600-001", - "auto_resubscribe": True, - "close_notification": True, - } + {"section": "CIS-1600-001", "auto_resubscribe": True, "close_notification": True} ), content_type="application/json", ) @@ -2482,11 +2464,7 @@ def close_notification_creation_helper(self, push_notif): r in get_registrations_for_alerts("CIS-1600-001", TEST_SEMESTER, course_status="C") ) self.simulate_alert( - self.cis1600, - 1, - close_notification=True, - should_send=False, - contact_infos=contact_infos, + self.cis1600, 1, close_notification=True, should_send=False, contact_infos=contact_infos ) r = Registration.objects.get(id=first_id) self.assertTrue( @@ -2505,11 +2483,7 @@ def close_notification_creation_helper(self, push_notif): ) contact_infos[0]["number"] = None self.simulate_alert( - self.cis1600, - 3, - close_notification=True, - should_send=True, - contact_infos=contact_infos, + self.cis1600, 3, close_notification=True, should_send=True, contact_infos=contact_infos ) r = Registration.objects.get(id=first_id) self.assertFalse( @@ -2533,11 +2507,7 @@ def test_close_notification_create_only_text(self): response = self.client.post( reverse("registrations-list"), json.dumps( - { - "section": "CIS-1600-001", - "auto_resubscribe": True, - "close_notification": True, - } + {"section": "CIS-1600-001", "auto_resubscribe": True, "close_notification": True} ), content_type="application/json", ) @@ -2562,11 +2532,7 @@ def close_notification_update_helper(self, put, auto_resub): self.client.post( reverse("registrations-list"), json.dumps( - { - "id": first_id, - "auto_resubscribe": auto_resub, - "close_notification": True, - } + {"id": first_id, "auto_resubscribe": auto_resub, "close_notification": True} ), content_type="application/json", ) @@ -2627,11 +2593,7 @@ def close_notification_cancel_helper(self, delete=False): self.assertTrue(first_reg.cancelled) self.simulate_alert( - self.cis1200, - 2, - close_notification=True, - should_send=False, - contact_infos=contact_infos, + self.cis1200, 2, close_notification=True, should_send=False, contact_infos=contact_infos ) def test_close_notification_cancel(self): @@ -2909,8 +2871,7 @@ def test_last_notification_sent_at(self): ob_lst = [ob for ob in response.data if ob.get("id") == ids[specific_ids + "_id"]] self.assertEquals(1, len(ob_lst)) self.assertEquals( - last_notification_sent_at_vals["fourth"], - ob_lst[0].get("last_notification_sent_at"), + last_notification_sent_at_vals["fourth"], ob_lst[0].get("last_notification_sent_at") ) for specific_ids in ["second", "fifth"]: ob_lst = [ob for ob in response.data if ob.get("id") == ids[specific_ids + "_id"]] diff --git a/backend/tests/courses/test_api.py b/backend/tests/courses/test_api.py index 543224819..e0309dddd 100644 --- a/backend/tests/courses/test_api.py +++ b/backend/tests/courses/test_api.py @@ -231,9 +231,7 @@ def test_auto_keyword_both(self): req = self.factory.get("/", {"type": "auto", "search": kw}) terms = self.search.get_search_fields(None, req) self.assertEqual( - ["^full_code", "title", "sections__instructors__name"], - terms, - f"search:{kw}", + ["^full_code", "title", "sections__instructors__name"], terms, f"search:{kw}" ) def test_auto_keyword_only(self): @@ -253,8 +251,7 @@ def setUp(self): def test_search_by_dept(self): response = self.client.get( - reverse("courses-search", args=["current"]), - {"search": "math", "type": "auto"}, + reverse("courses-search", args=["current"]), {"search": "math", "type": "auto"} ) self.assertEqual(200, response.status_code) self.assertEqual(len(response.data), 1) @@ -267,15 +264,13 @@ def test_search_by_instructor(self): searches = ["Tiffany", "Chang"] for search in searches: response = self.client.get( - reverse("courses-search", args=["current"]), - {"search": search, "type": "auto"}, + reverse("courses-search", args=["current"]), {"search": search, "type": "auto"} ) self.assertEqual(200, response.status_code) self.assertEqual(len(response.data), 1) course_codes = [d["id"] for d in response.data] self.assertTrue( - "CIS-120" in course_codes and "MATH-114" not in course_codes, - f"search:{search}", + "CIS-120" in course_codes and "MATH-114" not in course_codes, f"search:{search}" ) @@ -293,15 +288,12 @@ def setUp(self): self.user = User.objects.create_user(username=self.username, password=self.password) self.client = APIClient() - def test_recommendation_is_null_when_course_not_part_of_model_even_when_logged_in( - self, - ): + def test_recommendation_is_null_when_course_not_part_of_model_even_when_logged_in(self): self.client.login(username=self.username, password=self.password) self.course, self.section = create_mock_data("PSCI-437-001", TEST_SEMESTER) response = self.client.get( - reverse("courses-search", args=["current"]), - {"search": "PSCI-437", "type": "auto"}, + reverse("courses-search", args=["current"]), {"search": "PSCI-437", "type": "auto"} ) self.assertEqual(200, response.status_code) @@ -310,8 +302,7 @@ def test_recommendation_is_null_when_course_not_part_of_model_even_when_logged_i def test_recommendation_is_null_when_user_not_logged_in(self): response = self.client.get( - reverse("courses-search", args=["current"]), - {"search": "PSCI", "type": "auto"}, + reverse("courses-search", args=["current"]), {"search": "PSCI", "type": "auto"} ) self.assertEqual(200, response.status_code) @@ -346,8 +337,7 @@ def test_recommendation_is_number_when_user_is_logged_in(self, course_clusters_m ) response = self.client.get( - reverse("courses-search", args=["current"]), - {"search": "PSCI", "type": "auto"}, + reverse("courses-search", args=["current"]), {"search": "PSCI", "type": "auto"} ) self.assertEqual(200, response.status_code) @@ -612,8 +602,7 @@ def test_and_nonexistent(self): def test_and_or(self): response = self.client.get( - reverse("courses-search", args=[TEST_SEMESTER]), - {"attributes": "(EMCI*WUOM)|EMCI"}, + reverse("courses-search", args=[TEST_SEMESTER]), {"attributes": "(EMCI*WUOM)|EMCI"} ) self.assertEqual(response.status_code, 200) self.assertEqual({res["id"] for res in response.data}, {"CIS-120", "ECON-001"}) @@ -642,8 +631,7 @@ def test_and_not(self): def test_and_or_not(self): response = self.client.get( - reverse("courses-search", args=[TEST_SEMESTER]), - {"attributes": "(EMCI*WUOM)|~EMCI"}, + reverse("courses-search", args=[TEST_SEMESTER]), {"attributes": "(EMCI*WUOM)|~EMCI"} ) self.assertEqual(response.status_code, 200) self.assertEqual({res["id"] for res in response.data}, {"ECON-001", "MGMT-117", "ANTH-001"}) @@ -716,8 +704,7 @@ def test_unmatched_parens(self): ) self.assertEqual(response.status_code, 400) response = self.client.get( - reverse("courses-search", args=[TEST_SEMESTER]), - {"attributes": "(EMCI*(WUOM|LLLL)"}, + reverse("courses-search", args=[TEST_SEMESTER]), {"attributes": "(EMCI*(WUOM|LLLL)"} ) self.assertEqual(response.status_code, 400) @@ -749,8 +736,7 @@ def setUp(self): def test_sections_appear(self): response = self.client.get( - reverse("section-search", args=["current"]), - kwargs={"semester": TEST_SEMESTER}, + reverse("section-search", args=["current"]), kwargs={"semester": TEST_SEMESTER} ) course_codes = [d["section_id"] for d in response.data] self.assertTrue("CIS-120-001" in course_codes and "MATH-114-001" in course_codes) @@ -760,8 +746,7 @@ def test_section_without_(self): self.math1.activity = "" self.math1.save() response = self.client.get( - reverse("section-search", args=["current"]), - kwargs={"semester": TEST_SEMESTER}, + reverse("section-search", args=["current"]), kwargs={"semester": TEST_SEMESTER} ) self.assertEqual(1, len(response.data)) self.assertEqual("CIS-120-001", response.data[0]["section_id"]) diff --git a/backend/tests/courses/test_models.py b/backend/tests/courses/test_models.py index 18cfe1da1..db1c1edb3 100644 --- a/backend/tests/courses/test_models.py +++ b/backend/tests/courses/test_models.py @@ -264,16 +264,8 @@ def test_add_existing_class(self): set_crosslistings( self.clst, [ - { - "subject_code": "CLST", - "course_number": "027", - "is_primary_section": False, - }, - { - "subject_code": "ANCH", - "course_number": "027", - "is_primary_section": True, - }, + {"subject_code": "CLST", "course_number": "027", "is_primary_section": False}, + {"subject_code": "ANCH", "course_number": "027", "is_primary_section": True}, ], ) self.clst.save() @@ -284,16 +276,8 @@ def test_crosslisting_set(self): set_crosslistings( self.clst, [ - { - "subject_code": "CLST", - "course_number": "027", - "is_primary_section": False, - }, - { - "subject_code": "ANCH", - "course_number": "027", - "is_primary_section": True, - }, + {"subject_code": "CLST", "course_number": "027", "is_primary_section": False}, + {"subject_code": "ANCH", "course_number": "027", "is_primary_section": True}, ], ) set_crosslistings(self.anch, []) @@ -306,21 +290,9 @@ def test_crosslisting_newsection(self): set_crosslistings( self.anch, [ - { - "subject_code": "CLST", - "course_number": "027", - "is_primary_section": False, - }, - { - "subject_code": "ANCH", - "course_number": "027", - "is_primary_section": False, - }, - { - "subject_code": "HIST", - "course_number": "027", - "is_primary_section": True, - }, + {"subject_code": "CLST", "course_number": "027", "is_primary_section": False}, + {"subject_code": "ANCH", "course_number": "027", "is_primary_section": False}, + {"subject_code": "HIST", "course_number": "027", "is_primary_section": True}, ], ) self.anch.save() diff --git a/backend/tests/courses/test_opendata_import.py b/backend/tests/courses/test_opendata_import.py index 09be64832..8385a1623 100644 --- a/backend/tests/courses/test_opendata_import.py +++ b/backend/tests/courses/test_opendata_import.py @@ -38,10 +38,7 @@ def setUp(self): "MUSC-0050-001", TEST_SEMESTER ) self.AMTH = {"attribute_code": "AMTH", "attribute_desc": "MUSC M Tier Thre"} - self.NUFC = { - "attribute_code": "NUFC", - "attribute_desc": "NUR-ADMIN-FCH Department", - } + self.NUFC = {"attribute_code": "NUFC", "attribute_desc": "NUR-ADMIN-FCH Department"} def test_add_attribute(self): add_attributes(self.MUSC_0050, [self.AMTH]) @@ -68,8 +65,7 @@ def test_add_attribute_multiple_times(self): def test_add_attribute_with_no_school(self): add_attributes( - self.MUSC_0050, - [{"attribute_code": "ZPRS", "attribute_desc": "VIPER seminar"}], + self.MUSC_0050, [{"attribute_code": "ZPRS", "attribute_desc": "VIPER seminar"}] ) VPRS_obj = Attribute.objects.get(code="ZPRS") self.assertIsNone(VPRS_obj.school) diff --git a/backend/tests/courses/test_recompute_soft_state.py b/backend/tests/courses/test_recompute_soft_state.py index 1151248c2..82cee011b 100644 --- a/backend/tests/courses/test_recompute_soft_state.py +++ b/backend/tests/courses/test_recompute_soft_state.py @@ -203,6 +203,10 @@ def setUp(self): "CIS-1210-001", TEST_SEMESTER ) + # Implictly testing that we exclude sections with code > 500 + _, self.section5, _, _ = get_or_create_course_and_section("CIS-1210-500", TEST_SEMESTER) + self.section5.credits = 10.0 + def test_null_section_credits(self): self.assertIsNone(self.course3.credits) self.assertIsNone(self.section4.credits) @@ -267,3 +271,12 @@ def test_same_activity_null_credits(self): recompute_course_credits() self.course.refresh_from_db() self.assertEqual(self.course.credits, 2.00) + + def test_excludes_sections_with_status_besides_closed_and_open(self): + _, cancelled_section, _, _ = get_or_create_course_and_section("CIS-160-102", TEST_SEMESTER) + cancelled_section.credits = 10.0 + cancelled_section.status = "X" + recompute_course_credits() + + self.course2.refresh_from_db() + self.assertEqual(self.course2.credits, 1.50) diff --git a/backend/tests/degree/test_api.py b/backend/tests/degree/test_api.py index f5c9cea7d..12b48d54e 100644 --- a/backend/tests/degree/test_api.py +++ b/backend/tests/degree/test_api.py @@ -1,24 +1,39 @@ from django.db.models import Q +from django.db.models.signals import post_save from django.test import TestCase -from rest_framework.reverse import reverse +from django.urls import reverse +from options.models import Option from rest_framework.test import APIClient -from courses.models import User -from courses.serializers import CourseListSerializer -from courses.util import get_or_create_course_and_section +from alert.models import AddDropPeriod +from courses.models import Course, User +from courses.util import get_or_create_course_and_section, invalidate_current_semester_cache from degree.models import ( Degree, DegreePlan, DoubleCountRestriction, Fulfillment, + PDPBetaUser, Rule, SatisfactionStatus, ) +from degree.serializers import SimpleCourseSerializer +from tests.courses.util import fill_course_soft_state TEST_SEMESTER = "2023C" +def set_semester(): + post_save.disconnect( + receiver=invalidate_current_semester_cache, + sender=Option, + dispatch_uid="invalidate_current_semester_cache", + ) + Option(key="SEMESTER", value=TEST_SEMESTER, value_type="TXT").save() + AddDropPeriod(semester=TEST_SEMESTER).save() + + class DegreeViewsetTest(TestCase): def test_list_degrees(self): pass @@ -54,9 +69,11 @@ class FulfillmentViewsetTest(TestCase): def assertSerializedFulfillmentEquals(self, fulfillment: dict, expected: Fulfillment): self.assertEqual(len(fulfillment), 6) self.assertEqual(fulfillment["id"], expected.id) - self.assertEqual( - fulfillment["course"], CourseListSerializer(expected.historical_course).data - ) + + expected_course = SimpleCourseSerializer( + Course.with_reviews.get(full_code=expected.full_code) + ).data + self.assertDictEqual(fulfillment["course"], expected_course) self.assertEqual(fulfillment["rules"], [rule.id for rule in expected.rules.all()]) self.assertEqual(fulfillment["semester"], expected.semester) self.assertEqual(fulfillment["degree_plan"], expected.degree_plan.id) @@ -66,6 +83,13 @@ def setUp(self): self.user = User.objects.create_user( username="test", password="top_secret", email="test@example.com" ) + + set_semester() + + # register the user as a beta user + # TODO: remove after beta + PDPBetaUser.objects.create(person=self.user) + self.cis_1200, self.cis_1200_001, _, _ = get_or_create_course_and_section( "CIS-1200-001", TEST_SEMESTER, course_defaults={"credits": 1} ) @@ -75,6 +99,7 @@ def setUp(self): self.cis_1930, self.cis_1930_001, _, _ = get_or_create_course_and_section( "CIS-1920-001", TEST_SEMESTER, course_defaults={"credits": 1} ) + fill_course_soft_state() self.degree = Degree.objects.create(program="EU_BSE", degree="BSE", major="CIS", year=2023) self.parent_rule = Rule.objects.create() @@ -130,7 +155,6 @@ def test_create_fulfillment(self): fulfillment = Fulfillment.objects.get(full_code="CIS-1200") self.assertEqual(fulfillment.degree_plan, self.degree_plan) - self.assertEqual(fulfillment.historical_course, self.cis_1200) self.assertEqual(fulfillment.semester, TEST_SEMESTER) self.assertEqual(fulfillment.rules.count(), 1) self.assertEqual(fulfillment.rules.first(), self.rule1) @@ -177,7 +201,7 @@ def test_retrieve_fulfillment(self): response = self.client.get( reverse( "degreeplan-fulfillment-detail", - kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}, + kwargs={"degreeplan_pk": self.degree_plan.id, "full_code": a.full_code}, ) ) self.assertEqual(response.status_code, 200, response.json()) @@ -192,12 +216,12 @@ def test_update_fulfillment_replace_rule(self): a.save() a.rules.add(self.rule1) - response = self.client.patch( + response = self.client.post( reverse( - "degreeplan-fulfillment-detail", - kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}, + "degreeplan-fulfillment-list", + kwargs={"degreeplan_pk": self.degree_plan.id}, ), - {"rules": [self.rule3.id]}, + {"rules": [self.rule3.id], "full_code": a.full_code}, ) self.assertEqual(response.status_code, 200, response.json()) self.assertSerializedFulfillmentEquals(response.data, a) @@ -214,19 +238,19 @@ def test_update_semester(self): a.save() a.rules.add(self.rule1) - response = self.client.patch( + response = self.client.post( reverse( - "degreeplan-fulfillment-detail", - kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}, + "degreeplan-fulfillment-list", + kwargs={"degreeplan_pk": self.degree_plan.id}, ), - {"semester": "2022B"}, + {"semester": "2022B", "full_code": a.full_code}, ) self.assertEqual(response.status_code, 200, response.json()) a.refresh_from_db() self.assertSerializedFulfillmentEquals(response.data, a) self.assertEqual(a.semester, "2022B") - def test_update_fulfillment_full_code(self): + def test_trying_update_fulfillment_full_code_creates_fulfillment(self): a = Fulfillment( degree_plan=self.degree_plan, full_code="CIS-1200", @@ -235,17 +259,19 @@ def test_update_fulfillment_full_code(self): a.save() a.rules.add(self.rule3) - response = self.client.patch( + response = self.client.post( reverse( - "degreeplan-fulfillment-detail", - kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}, + "degreeplan-fulfillment-list", + kwargs={"degreeplan_pk": self.degree_plan.id}, ), {"full_code": "CIS-1910"}, ) - self.assertEqual(response.status_code, 200, response.json()) + self.assertEqual(response.status_code, 201, response.json()) + old_full_code = a.full_code + + # a doesn't update a.refresh_from_db() - self.assertSerializedFulfillmentEquals(response.data, a) - self.assertEqual(a.full_code, "CIS-1910") + self.assertEqual(a.full_code, old_full_code) def test_update_fulfillment_rule(self): a = Fulfillment( @@ -256,12 +282,12 @@ def test_update_fulfillment_rule(self): a.save() a.rules.add(self.rule1) - response = self.client.patch( + response = self.client.post( reverse( - "degreeplan-fulfillment-detail", - kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}, + "degreeplan-fulfillment-list", + kwargs={"degreeplan_pk": self.degree_plan.id}, ), - {"rules": [self.rule3.id, self.rule1.id]}, + {"rules": [self.rule3.id, self.rule1.id], "full_code": a.full_code}, ) self.assertEqual(response.status_code, 200, response.json()) a.refresh_from_db() @@ -278,12 +304,12 @@ def test_update_fulfillment_add_violated_rule(self): a.save() a.rules.add(self.rule1) - response = self.client.patch( + response = self.client.post( reverse( - "degreeplan-fulfillment-detail", - kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}, + "degreeplan-fulfillment-list", + kwargs={"degreeplan_pk": self.degree_plan.id}, ), - {"rules": [self.rule2.id, self.rule1.id]}, + {"rules": [self.rule2.id, self.rule1.id], "full_code": a.full_code}, ) self.assertEqual(response.status_code, 400, response.json()) self.assertEqual( @@ -291,28 +317,6 @@ def test_update_fulfillment_add_violated_rule(self): f"Course CIS-1200 does not satisfy rule {self.rule2.id}", ) - def test_update_fulfillment_full_code_violates_rule(self): - a = Fulfillment( - degree_plan=self.degree_plan, - full_code="CIS-1200", - semester=TEST_SEMESTER, - ) - a.save() - a.rules.add(self.rule1) - - response = self.client.patch( - reverse( - "degreeplan-fulfillment-detail", - kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}, - ), - {"full_code": "CIS-1910"}, - ) - self.assertEqual(response.status_code, 400, response.json()) - self.assertEqual( - response.data["non_field_errors"][0], - f"Course CIS-1910 does not satisfy rule {self.rule1.id}", - ) - def test_delete_fulfillment(self): a = Fulfillment( degree_plan=self.degree_plan, @@ -324,7 +328,7 @@ def test_delete_fulfillment(self): response = self.client.delete( reverse( "degreeplan-fulfillment-detail", - kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}, + kwargs={"degreeplan_pk": self.degree_plan.id, "full_code": a.full_code}, ) ) self.assertEqual(response.status_code, 204) @@ -335,8 +339,19 @@ def test_create_fulfillment_with_wrong_users_degreeplan(self): def test_create_fulfillment_with_nonexistant_degreeplan(self): pass - def test_create_fulfillment_with_rule_violation(self): - pass + def test_create_fulfillment_full_code_violates_rule(self): + response = self.client.post( + reverse( + "degreeplan-fulfillment-list", + kwargs={"degreeplan_pk": self.degree_plan.id}, + ), + {"full_code": "CIS-1910", "rules": [self.rule1.id]}, + ) + self.assertEqual(response.status_code, 400, response.json()) + self.assertEqual( + response.data["non_field_errors"][0], + f"Course CIS-1910 does not satisfy rule {self.rule1.id}", + ) def test_create_fulfillment_with_denormalized_course_code(self): pass diff --git a/backend/tests/degree/test_degreeworks_parser.py b/backend/tests/degree/test_degreeworks_parser.py index 024d0c35b..538b2eaca 100644 --- a/backend/tests/degree/test_degreeworks_parser.py +++ b/backend/tests/degree/test_degreeworks_parser.py @@ -202,7 +202,8 @@ def test_course_range(self): course_array = [ {"discipline": "BIBB", "number": "2000", "numberEnd": "2999"}, ] - expected = Q(department__code="BIBB", code__gte=2000, code__lte=2999) + expected = Q(department__code="BIBB", code__gte="2000", code__lte="2999") + print(expected, parse_degreeworks.parse_coursearray(course_array)) self.assertEqual(expected, parse_degreeworks.parse_coursearray(course_array)) def test_department(self): @@ -221,16 +222,16 @@ def test_empty_course(self): def test_non_int_course(self): course_array = [ - {"discipline": "CIS", "number": "not-a-number"}, + {"discipline": "CIS", "number": "4999A"}, ] - expected = Q() + expected = Q(full_code="CIS-4999A") self.assertEqual(expected, parse_degreeworks.parse_coursearray(course_array)) def test_non_int_course_range(self): course_array = [ - {"discipline": "CIS", "number": "not-a-number", "numberEnd": "also-not-a-number"}, + {"discipline": "CIS", "number": "4999A", "numberEnd": "4999B"}, ] - expected = Q() + expected = Q(department__code="CIS", code__gte="4999A", code__lte="4999B") self.assertEqual(expected, parse_degreeworks.parse_coursearray(course_array)) diff --git a/backend/tests/plan/test_api.py b/backend/tests/plan/test_api.py index 8451ff86b..e3e5d2664 100644 --- a/backend/tests/plan/test_api.py +++ b/backend/tests/plan/test_api.py @@ -75,8 +75,7 @@ def test_return_all_courses(self): def test_filter_for_req(self): response = self.client.get( - reverse("courses-search", args=["current"]), - {"pre_ngss_requirements": "REQ@SAS"}, + reverse("courses-search", args=["current"]), {"pre_ngss_requirements": "REQ@SAS"} ) self.assertEqual(200, response.status_code) self.assertEqual(1, len(response.data)) @@ -84,15 +83,12 @@ def test_filter_for_req(self): def test_filter_for_req_dif_sem(self): req2 = PreNGSSRequirement( - semester=("2019A" if TEST_SEMESTER == "2019C" else "2019C"), - code="REQ", - school="SAS", + semester=("2019A" if TEST_SEMESTER == "2019C" else "2019C"), code="REQ", school="SAS" ) req2.save() req2.courses.add(self.different_math) response = self.client.get( - reverse("courses-search", args=["current"]), - {"pre_ngss_requirements": "REQ@SAS"}, + reverse("courses-search", args=["current"]), {"pre_ngss_requirements": "REQ@SAS"} ) self.assertEqual(200, response.status_code) self.assertEqual(1, len(response.data)) @@ -288,9 +284,7 @@ def test_section_no_duplicates(self): self.assertEqual(200, response.status_code) self.assertEqual(2, len(response.data["sections"])) self.assertEqual( - 1.5, - response.data["sections"][1]["course_quality"], - response.data["sections"][1], + 1.5, response.data["sections"][1]["course_quality"], response.data["sections"][1] ) def test_filter_courses_by_review_included(self): @@ -439,8 +433,7 @@ def test_full_multi_meeting_match(self): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 4) self.assertEqual( - {res["id"] for res in response.data}, - {"CIS-120", "CIS-121", "CIS-160", "CIS-262"}, + {res["id"] for res in response.data}, {"CIS-120", "CIS-121", "CIS-160", "CIS-262"} ) @@ -633,37 +626,21 @@ def setUp(self): ) # time 11.0-12.0, days MWF _, self.cis_120_002 = create_mock_data( - code="CIS-120-002", - semester=TEST_SEMESTER, - start=1200, - end=1330, - meeting_days="TR", + code="CIS-120-002", semester=TEST_SEMESTER, start=1200, end=1330, meeting_days="TR" ) _, self.cis_160_001 = create_mock_data( - code="CIS-160-001", - semester=TEST_SEMESTER, - start=500, - end=630, - meeting_days="TR", + code="CIS-160-001", semester=TEST_SEMESTER, start=500, end=630, meeting_days="TR" ) _, self.cis_160_201 = create_mock_data( - code="CIS-160-201", - semester=TEST_SEMESTER, - start=1100, - end=1200, - meeting_days="M", + code="CIS-160-201", semester=TEST_SEMESTER, start=1100, end=1200, meeting_days="M" ) self.cis_160_201.activity = "REC" self.cis_160_201.save() _, self.cis_160_202 = create_mock_data( - code="CIS-160-202", - semester=TEST_SEMESTER, - start=1400, - end=1500, - meeting_days="W", + code="CIS-160-202", semester=TEST_SEMESTER, start=1400, end=1500, meeting_days="W" ) self.cis_160_202.activity = "REC" self.cis_160_202.save() @@ -704,8 +681,7 @@ def setUp(self): def test_all_match(self): response = self.client.get( - reverse("courses-search", args=[TEST_SEMESTER]), - {"time": "0-23.59", "days": "MTWRFSU"}, + reverse("courses-search", args=[TEST_SEMESTER]), {"time": "0-23.59", "days": "MTWRFSU"} ) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), len(self.all_codes)) @@ -722,8 +698,7 @@ def test_days_match_not_time(self): def test_time_matches_not_days(self): response = self.client.get( - reverse("courses-search", args=[TEST_SEMESTER]), - {"time": "1.00-", "days": "F"}, + reverse("courses-search", args=[TEST_SEMESTER]), {"time": "1.00-", "days": "F"} ) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 1) @@ -731,8 +706,7 @@ def test_time_matches_not_days(self): def test_days_time_partial_match(self): response = self.client.get( - reverse("courses-search", args=[TEST_SEMESTER]), - {"time": "12.0-15.0", "days": "TWR"}, + reverse("courses-search", args=[TEST_SEMESTER]), {"time": "12.0-15.0", "days": "TWR"} ) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 2) @@ -740,8 +714,7 @@ def test_days_time_partial_match(self): def test_multi_meeting_partial_match(self): response = self.client.get( - reverse("courses-search", args=[TEST_SEMESTER]), - {"time": "9.00-10.00", "days": "MTWR"}, + reverse("courses-search", args=[TEST_SEMESTER]), {"time": "9.00-10.00", "days": "MTWR"} ) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 1) @@ -749,8 +722,7 @@ def test_multi_meeting_partial_match(self): def test_multi_meeting_full_match(self): response = self.client.get( - reverse("courses-search", args=[TEST_SEMESTER]), - {"time": "9.00-14.30", "days": "MTWR"}, + reverse("courses-search", args=[TEST_SEMESTER]), {"time": "9.00-14.30", "days": "MTWR"} ) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 3) @@ -764,37 +736,21 @@ def setUp(self): ) # time 11.0-12.0, days MWF _, self.cis_120_002 = create_mock_data( - code="CIS-120-002", - semester=TEST_SEMESTER, - start=1200, - end=1330, - meeting_days="TR", + code="CIS-120-002", semester=TEST_SEMESTER, start=1200, end=1330, meeting_days="TR" ) _, self.cis_160_001 = create_mock_data( - code="CIS-160-001", - semester=TEST_SEMESTER, - start=500, - end=630, - meeting_days="TR", + code="CIS-160-001", semester=TEST_SEMESTER, start=500, end=630, meeting_days="TR" ) _, self.cis_160_201 = create_mock_data( - code="CIS-160-201", - semester=TEST_SEMESTER, - start=1100, - end=1200, - meeting_days="M", + code="CIS-160-201", semester=TEST_SEMESTER, start=1100, end=1200, meeting_days="M" ) self.cis_160_201.activity = "REC" self.cis_160_201.save() _, self.cis_160_202 = create_mock_data( - code="CIS-160-202", - semester=TEST_SEMESTER, - start=1400, - end=1500, - meeting_days="W", + code="CIS-160-202", semester=TEST_SEMESTER, start=1400, end=1500, meeting_days="W" ) self.cis_160_202.activity = "REC" self.cis_160_202.save() diff --git a/backend/tests/plan/test_schedule.py b/backend/tests/plan/test_schedule.py index 4d5640af4..2d3bd1a03 100644 --- a/backend/tests/plan/test_schedule.py +++ b/backend/tests/plan/test_schedule.py @@ -56,12 +56,7 @@ def check_serialized_section(self, serialized_section, section, reviews, conside self.assertEqual(section.semester, serialized_section.get("semester")) if consider_review_data: - fields = [ - "course_quality", - "instructor_quality", - "difficulty", - "work_required", - ] + fields = ["course_quality", "instructor_quality", "difficulty", "work_required"] for field in fields: expected = get_average_reviews(reviews, field) actual = serialized_section.get(field) diff --git a/backend/tests/review/test_api.py b/backend/tests/review/test_api.py index ec6668c3b..ac8b49734 100644 --- a/backend/tests/review/test_api.py +++ b/backend/tests/review/test_api.py @@ -111,9 +111,7 @@ def assertDictAlmostEquals(self, actual, expected, path=None, extra_error_str="" ) except (TypeError, ValueError): self.assertEquals( - actual, - expected, - "Dict path: " + "/".join(path) + "\n" + extra_error_str, + actual, expected, "Dict path: " + "/".join(path) + "\n" + extra_error_str ) def assertDictContainsAppx(self, entire, subdict, path=None, extra_error_str=""): @@ -171,10 +169,7 @@ def average_and_recent(a, r): def no_reviews_avg_recent(num_semesters, recent_semester): return { - "average_reviews": { - "rSemesterCount": num_semesters, - "rSemesterCalc": recent_semester, - }, + "average_reviews": {"rSemesterCount": num_semesters, "rSemesterCalc": recent_semester}, "recent_reviews": {"rSemesterCount": 0}, } @@ -212,13 +207,7 @@ def test_has_one(self): instructor, _ = Instructor.objects.get_or_create(name="Rajiv Gandhi") section.instructors.add(instructor) import_review( - section, - instructor, - None, - 10, - None, - {"instructor_quality": 4}, - lambda x, y=None: None, + section, instructor, None, 10, None, {"instructor_quality": 4}, lambda x, y=None: None ) fill_course_soft_state() self.assertTrue(Section.objects.get(id=section.id).has_reviews) @@ -228,22 +217,10 @@ def test_has_multiple(self): instructor, _ = Instructor.objects.get_or_create(name="Rajiv Gandhi") section.instructors.add(instructor) import_review( - section, - instructor, - None, - 10, - None, - {"instructor_quality": 4}, - lambda x, y: None, + section, instructor, None, 10, None, {"instructor_quality": 4}, lambda x, y: None ) import_review( - section, - instructor, - None, - 10, - None, - {"course_quality": 4}, - lambda x, y=None: None, + section, instructor, None, 10, None, {"course_quality": 4}, lambda x, y=None: None ) fill_course_soft_state() self.assertTrue(Section.objects.get(id=section.id).has_reviews) @@ -257,13 +234,6 @@ def setUp(self): self.client.force_login(User.objects.create_user(username="test")) create_review("CIS-120-001", TEST_SEMESTER, self.instructor_name, {"instructor_quality": 4}) self.instructor_pk = Instructor.objects.get(name=self.instructor_name).pk - create_review( - "CIS-120-002", - "2007C", - self.instructor_name, - {"instructor_quality": 0}, - responses=0, - ) create_review( "CIS-120-001", "2007C", @@ -294,19 +264,10 @@ def test_instructor(self): self.instructor_pk, {**average_and_recent(4, 4), "courses": {"CIS-120": {**average_and_recent(4, 4)}}}, ) - self.assertRequestContainsAppx( - "instructor-reviews", - self.instructor_nores_pk, - { - "courses": {"CIS-120": {**no_reviews_avg_recent(1, "2007C")}}, - }, - ) def test_department(self): self.assertRequestContainsAppx( - "department-reviews", - "CIS", - {"courses": {"CIS-120": average_and_recent(4, 4)}}, + "department-reviews", "CIS", {"courses": {"CIS-120": average_and_recent(4, 4)}} ) def test_history(self): @@ -363,13 +324,6 @@ def setUp(self): self.client.force_login(User.objects.create_user(username="test")) create_review("CIS-120-001", TEST_SEMESTER, self.instructor_name, {"instructor_quality": 4}) create_review("CIS-120-001", "2012A", self.instructor_name, {"instructor_quality": 2}) - create_review( - "CIS-120-002", - "2007C", - self.instructor_name, - {"instructor_quality": 0}, - responses=0, - ) create_review( "CIS-120-001", "2007C", @@ -398,17 +352,12 @@ def test_instructor(self): self.assertRequestContainsAppx( "instructor-reviews", Instructor.objects.get(name=self.instructor_name).pk, - { - **average_and_recent(3, 4), - "courses": {"CIS-120": average_and_recent(3, 4)}, - }, + {**average_and_recent(3, 4), "courses": {"CIS-120": average_and_recent(3, 4)}}, ) def test_department(self): self.assertRequestContainsAppx( - "department-reviews", - "CIS", - {"courses": {"CIS-120": average_and_recent(3, 4)}}, + "department-reviews", "CIS", {"courses": {"CIS-120": average_and_recent(3, 4)}} ) def test_history(self): @@ -425,18 +374,8 @@ def setUp(self): self.instructor_name = "Instructor One" self.client = APIClient() self.client.force_login(User.objects.create_user(username="test")) - create_review( - "CIS-120-001", - TEST_SEMESTER, - self.instructor_name, - {"instructor_quality": 4}, - ) - create_review( - "CIS-120-002", - TEST_SEMESTER, - self.instructor_name, - {"instructor_quality": 2}, - ) + create_review("CIS-120-001", TEST_SEMESTER, self.instructor_name, {"instructor_quality": 4}) + create_review("CIS-120-002", TEST_SEMESTER, self.instructor_name, {"instructor_quality": 2}) def test_course(self): self.assertRequestContainsAppx( @@ -467,9 +406,7 @@ def test_instructor(self): def test_department(self): self.assertRequestContainsAppx( - "department-reviews", - "CIS", - {"courses": {"CIS-120": average_and_recent(3, 3)}}, + "department-reviews", "CIS", {"courses": {"CIS-120": average_and_recent(3, 3)}} ) def test_history(self): @@ -488,12 +425,7 @@ def setUp(self): self.instructor_name = "Instructor One" self.client = APIClient() self.client.force_login(User.objects.create_user(username="test")) - create_review( - "CIS-120-001", - TEST_SEMESTER, - self.instructor_name, - {"instructor_quality": 4}, - ) + create_review("CIS-120-001", TEST_SEMESTER, self.instructor_name, {"instructor_quality": 4}) create_review("CIS-120-001", "2012A", self.instructor_name, {"instructor_quality": 2}) create_review( "CIS-120-002", @@ -548,13 +480,6 @@ def setUp(self): self.client.force_login(User.objects.create_user(username="test")) create_review("CIS-120-001", TEST_SEMESTER, self.instructor_name, {"instructor_quality": 4}) create_review("CIS-120-001", TEST_SEMESTER, "Instructor Two", {"instructor_quality": 2}) - create_review( - "CIS-120-002", - "2007C", - self.instructor_name, - {"instructor_quality": 0}, - responses=0, - ) create_review( "CIS-120-001", "2007C", @@ -584,19 +509,13 @@ def test_instructor(self): self.assertRequestContainsAppx( "instructor-reviews", self.instructor1.pk, - { - **average_and_recent(4, 4), - "courses": {"CIS-120": average_and_recent(4, 4)}, - }, + {**average_and_recent(4, 4), "courses": {"CIS-120": average_and_recent(4, 4)}}, ) self.assertRequestContainsAppx( "instructor-reviews", self.instructor2.pk, - { - **average_and_recent(2, 2), - "courses": {"CIS-120": average_and_recent(2, 2)}, - }, + {**average_and_recent(2, 2), "courses": {"CIS-120": average_and_recent(2, 2)}}, ) @@ -608,13 +527,6 @@ def setUp(self): self.client.force_login(User.objects.create_user(username="test")) create_review("CIS-120-001", TEST_SEMESTER, self.instructor_name, {"instructor_quality": 4}) create_review("CIS-120-002", TEST_SEMESTER, "Instructor Two", {"instructor_quality": 2}) - create_review( - "CIS-120-002", - "2007C", - self.instructor_name, - {"instructor_quality": 0}, - responses=0, - ) create_review( "CIS-120-001", "2007C", @@ -642,19 +554,13 @@ def test_instructor(self): self.assertRequestContainsAppx( "instructor-reviews", self.instructor1.pk, - { - **average_and_recent(4, 4), - "courses": {"CIS-120": average_and_recent(4, 4)}, - }, + {**average_and_recent(4, 4), "courses": {"CIS-120": average_and_recent(4, 4)}}, ) self.assertRequestContainsAppx( "instructor-reviews", self.instructor2.pk, - { - **average_and_recent(2, 2), - "courses": {"CIS-120": average_and_recent(2, 2)}, - }, + {**average_and_recent(2, 2), "courses": {"CIS-120": average_and_recent(2, 2)}}, ) @@ -669,13 +575,6 @@ def setUp(self): self.client.force_login(User.objects.create_user(username="test")) create_review("CIS-120-001", TEST_SEMESTER, self.instructor_name, {"instructor_quality": 4}) create_review("CIS-120-001", "2017A", "Instructor Two", {"instructor_quality": 2}) - create_review( - "CIS-120-002", - "2007C", - self.instructor_name, - {"instructor_quality": 0}, - responses=0, - ) create_review( "CIS-120-001", "2007C", @@ -901,21 +800,18 @@ def test_instructor(self): def test_department(self): self.assertEqual( - 404, - self.client.get(reverse("department-reviews", args=["BLAH"])).status_code, + 404, self.client.get(reverse("department-reviews", args=["BLAH"])).status_code ) def test_history(self): self.assertEqual( - 404, - self.client.get(reverse("course-history", args=["BLAH", 123])).status_code, + 404, self.client.get(reverse("course-history", args=["BLAH", 123])).status_code ) def test_no_reviews(self): get_or_create_course_and_section("CIS-120-001", TEST_SEMESTER) self.assertEqual( - 404, - self.client.get(reverse("course-reviews", args=["CIS-120"])).status_code, + 404, self.client.get(reverse("course-reviews", args=["CIS-120"])).status_code ) @@ -931,14 +827,12 @@ def test_instructor(self): def test_department(self): self.assertEqual( - 403, - self.client.get(reverse("department-reviews", args=["BLAH"])).status_code, + 403, self.client.get(reverse("department-reviews", args=["BLAH"])).status_code ) def test_history(self): self.assertEqual( - 403, - self.client.get(reverse("course-history", args=["BLAH", 0])).status_code, + 403, self.client.get(reverse("course-history", args=["BLAH", 0])).status_code ) @@ -948,12 +842,7 @@ def setUp(self): self.instructor_name = "Instructor One" self.client = APIClient() self.client.force_login(User.objects.create_user(username="test")) - create_review( - "CIS-120-001", - TEST_SEMESTER, - self.instructor_name, - {"instructor_quality": 4}, - ) + create_review("CIS-120-001", TEST_SEMESTER, self.instructor_name, {"instructor_quality": 4}) self.instructor_pk = Instructor.objects.get(name=self.instructor_name).pk rec_instructor = Instructor(name="Recitation Instructor") diff --git a/backend/tests/review/test_import.py b/backend/tests/review/test_import.py index aabae192e..a99fb48d1 100644 --- a/backend/tests/review/test_import.py +++ b/backend/tests/review/test_import.py @@ -215,41 +215,21 @@ def test_instructor_name_user_pennid_exist(self): class DescriptionImportTestCase(TestCase): def test_one_paragraph(self): get_or_create_course("CIS", "120", TEST_SEMESTER) - rows = [ - { - "COURSE_ID": "CIS120", - "PARAGRAPH_NUMBER": "1", - "COURSE_DESCRIPTION": "Hello", - } - ] + rows = [{"COURSE_ID": "CIS120", "PARAGRAPH_NUMBER": "1", "COURSE_DESCRIPTION": "Hello"}] import_description_rows(len(rows), iter(rows), show_progress_bar=False) self.assertEqual(1, Course.objects.count()) self.assertEqual("Hello", Course.objects.get().description) def test_no_course(self): - rows = [ - { - "COURSE_ID": "CIS120", - "PARAGRAPH_NUMBER": "1", - "COURSE_DESCRIPTION": "Hello", - } - ] + rows = [{"COURSE_ID": "CIS120", "PARAGRAPH_NUMBER": "1", "COURSE_DESCRIPTION": "Hello"}] import_description_rows(len(rows), iter(rows), show_progress_bar=False) self.assertEqual(0, Course.objects.count()) def test_two_paragraphs(self): get_or_create_course("CIS", "120", TEST_SEMESTER) rows = [ - { - "COURSE_ID": "CIS120", - "PARAGRAPH_NUMBER": "2", - "COURSE_DESCRIPTION": "world!", - }, - { - "COURSE_ID": "CIS120", - "PARAGRAPH_NUMBER": "1", - "COURSE_DESCRIPTION": "Hello", - }, + {"COURSE_ID": "CIS120", "PARAGRAPH_NUMBER": "2", "COURSE_DESCRIPTION": "world!"}, + {"COURSE_ID": "CIS120", "PARAGRAPH_NUMBER": "1", "COURSE_DESCRIPTION": "Hello"}, ] import_description_rows(len(rows), iter(rows), show_progress_bar=False) self.assertEqual(1, Course.objects.count()) @@ -259,13 +239,7 @@ def test_two_semesters(self): get_or_create_course("CIS", "120", TEST_SEMESTER) get_or_create_course("CIS", "120", "3008A") - rows = [ - { - "COURSE_ID": "CIS120", - "PARAGRAPH_NUMBER": "1", - "COURSE_DESCRIPTION": "Hello", - } - ] + rows = [{"COURSE_ID": "CIS120", "PARAGRAPH_NUMBER": "1", "COURSE_DESCRIPTION": "Hello"}] import_description_rows(len(rows), iter(rows), show_progress_bar=False) self.assertEqual(2, Course.objects.count()) c1 = Course.objects.get(semester=TEST_SEMESTER) @@ -280,13 +254,7 @@ def test_section_with_existing_description(self): c.description = "TILL 3005" c.save() - rows = [ - { - "COURSE_ID": "CIS120", - "PARAGRAPH_NUMBER": "1", - "COURSE_DESCRIPTION": "Hello", - } - ] + rows = [{"COURSE_ID": "CIS120", "PARAGRAPH_NUMBER": "1", "COURSE_DESCRIPTION": "Hello"}] import_description_rows(len(rows), iter(rows), show_progress_bar=False) self.assertEqual(3, Course.objects.count()) c1 = Course.objects.get(semester=TEST_SEMESTER) @@ -300,16 +268,8 @@ def test_two_courses(self): get_or_create_course("CIS", "120", TEST_SEMESTER) get_or_create_course("CIS", "121", TEST_SEMESTER) rows = [ - { - "COURSE_ID": "CIS120", - "PARAGRAPH_NUMBER": "1", - "COURSE_DESCRIPTION": "World", - }, - { - "COURSE_ID": "CIS121", - "PARAGRAPH_NUMBER": "1", - "COURSE_DESCRIPTION": "Hello", - }, + {"COURSE_ID": "CIS120", "PARAGRAPH_NUMBER": "1", "COURSE_DESCRIPTION": "World"}, + {"COURSE_ID": "CIS121", "PARAGRAPH_NUMBER": "1", "COURSE_DESCRIPTION": "Hello"}, ] import_description_rows(len(rows), iter(rows), show_progress_bar=False) c120 = Course.objects.get(code="120") diff --git a/backend/tests/review/test_mergeinstructors.py b/backend/tests/review/test_mergeinstructors.py index f6d00e93e..f2baadba5 100644 --- a/backend/tests/review/test_mergeinstructors.py +++ b/backend/tests/review/test_mergeinstructors.py @@ -27,8 +27,7 @@ def setUp(self): def test_batch_duplicates(self): dupes = batch_duplicates( - Instructor.objects.all().annotate(name_lower=Lower("name")), - lambda x: x.name_lower, + Instructor.objects.all().annotate(name_lower=Lower("name")), lambda x: x.name_lower ) self.assertEqual(1, len(dupes)) self.assertEqual("a", dupes[0].pop().name.lower()) diff --git a/backend/tests/review/test_stats.py b/backend/tests/review/test_stats.py index 3d73b8eb3..f337d4c62 100644 --- a/backend/tests/review/test_stats.py +++ b/backend/tests/review/test_stats.py @@ -116,10 +116,7 @@ def setUpTestData(cls): set_semester() cls.instructor_name = "Instructor One" create_review( - "ESE-120-001", - TEST_SEMESTER, - cls.instructor_name, - {"instructor_quality": 3.5}, + "ESE-120-001", TEST_SEMESTER, cls.instructor_name, {"instructor_quality": 3.5} ) create_review("ESE-120-001", "2020C", cls.instructor_name, {"instructor_quality": 2}) cls.ESE_120_001_TEST_SEMESTER_id = Section.objects.get( @@ -165,11 +162,9 @@ def setUpTestData(cls): old_status = "O" new_status = "C" start, end, duration = get_start_end_duration(cls.old_adp) - for date in [ - start - 3 * duration / 5, - start - 2 * duration / 5, - start - duration / 5, - ] + [start + i * duration / 4 for i in range(1, 4)]: + for date in [start - 3 * duration / 5, start - 2 * duration / 5, start - duration / 5] + [ + start + i * duration / 4 for i in range(1, 4) + ]: # C[.25]O[.5]C[.75]O record_update( Section.objects.get(id=cls.ESE_120_001_2020C_id), @@ -188,11 +183,7 @@ def setUpTestData(cls): to_date = get_to_date_func(cls.adp) # O[.2]C[.4]O[.6]C[.8]O[.81]C[.82]O registration_list_TS = [ - { - "created_at": to_date(0.1), - "cancelled_at": to_date(0.19), - "cancelled": True, - }, + {"created_at": to_date(0.1), "cancelled_at": to_date(0.19), "cancelled": True}, { "created_at": to_date(0.15), "notification_sent_at": to_date(0.4), @@ -338,10 +329,7 @@ def test_course(self): self.assertRequestContainsAppx( "course-reviews", "ESE-120", - { - **reviews_subdict, - "instructors": {Instructor.objects.get().pk: reviews_subdict}, - }, + {**reviews_subdict, "instructors": {Instructor.objects.get().pk: reviews_subdict}}, ) self.assertRequestContainsAppx( "course-plots", @@ -467,10 +455,7 @@ def setUpTestData(cls): set_semester() cls.instructor_name = "Instructor One" create_review( - "ESE-120-001", - TEST_SEMESTER, - cls.instructor_name, - {"instructor_quality": 3.5}, + "ESE-120-001", TEST_SEMESTER, cls.instructor_name, {"instructor_quality": 3.5} ) cls.ESE_120_001_id = Section.objects.get(full_code="ESE-120-001").id cls.instructor_quality = 3.5 @@ -482,11 +467,9 @@ def setUpTestData(cls): old_status = "C" new_status = "O" percent_open_plot = [(0, 1)] - for date in [ - start - 3 * duration / 7, - start - 2 * duration / 7, - start - duration / 7, - ] + [start + i * duration / 7 for i in range(1, 7)]: + for date in [start - 3 * duration / 7, start - 2 * duration / 7, start - duration / 7] + [ + start + i * duration / 7 for i in range(1, 7) + ]: # O[1/7]C[2/7]O[3/7]C[4/7]O[5/7]C[6/7]O percent_thru = cls.adp.get_percent_through_add_drop(date) record_update( @@ -508,21 +491,13 @@ def setUpTestData(cls): set_registrations( cls.ESE_120_001_id, [ - { - "created_at": to_date(0.25), - "cancelled_at": to_date(0.26), - "cancelled": True, - }, + {"created_at": to_date(0.25), "cancelled_at": to_date(0.26), "cancelled": True}, { "created_at": to_date(0.5), "notification_sent_at": to_date(4 / 7), "notification_sent": True, }, - { - "created_at": to_date(0.75), - "deleted_at": to_date(5.9 / 7), - "deleted": True, - }, + {"created_at": to_date(0.75), "deleted_at": to_date(5.9 / 7), "deleted": True}, ], ) @@ -603,10 +578,7 @@ def test_course(self): self.assertRequestContainsAppx( "course-reviews", "ESE-120", - { - **reviews_subdict, - "instructors": {Instructor.objects.get().pk: reviews_subdict}, - }, + {**reviews_subdict, "instructors": {Instructor.objects.get().pk: reviews_subdict}}, ) def test_check_offered_in(self): @@ -719,16 +691,10 @@ def setUpTestData(cls): cls.instructor_1_name = "Instructor One" cls.instructor_2_name = "Instructor Two" create_review( - "ESE-120-001", - TEST_SEMESTER, - cls.instructor_1_name, - {"instructor_quality": 3.5}, + "ESE-120-001", TEST_SEMESTER, cls.instructor_1_name, {"instructor_quality": 3.5} ) create_review( - "ESE-120-001", - TEST_SEMESTER, - cls.instructor_2_name, - {"instructor_quality": 3.5}, + "ESE-120-001", TEST_SEMESTER, cls.instructor_2_name, {"instructor_quality": 3.5} ) cls.ESE_120_001_id = Section.objects.get(full_code="ESE-120-001").id cls.instructor_quality = 3.5 @@ -740,11 +706,9 @@ def setUpTestData(cls): old_status = "C" new_status = "O" percent_open_plot = [(0, 1)] - for date in [ - start - 3 * duration / 7, - start - 2 * duration / 7, - start - duration / 7, - ] + [start + i * duration / 7 for i in range(1, 7)]: + for date in [start - 3 * duration / 7, start - 2 * duration / 7, start - duration / 7] + [ + start + i * duration / 7 for i in range(1, 7) + ]: # O[1/7]C[2/7]O[3/7]C[4/7]O[5/7]C[6/7]O percent_thru = cls.adp.get_percent_through_add_drop(date) record_update( @@ -765,21 +729,13 @@ def setUpTestData(cls): set_registrations( cls.ESE_120_001_id, [ - { - "created_at": to_date(0.25), - "cancelled_at": to_date(0.26), - "cancelled": True, - }, + {"created_at": to_date(0.25), "cancelled_at": to_date(0.26), "cancelled": True}, { "created_at": to_date(0.5), "notification_sent_at": to_date(4 / 7), "notification_sent": True, }, - { - "created_at": to_date(0.75), - "deleted_at": to_date(5.9 / 7), - "deleted": True, - }, + {"created_at": to_date(0.75), "deleted_at": to_date(5.9 / 7), "deleted": True}, ], ) cls.percent_open = (duration * 4 / 7).total_seconds() / duration.total_seconds() diff --git a/backend/tests/review/test_topics.py b/backend/tests/review/test_topics.py index 99d74b159..db655b3fb 100644 --- a/backend/tests/review/test_topics.py +++ b/backend/tests/review/test_topics.py @@ -26,12 +26,7 @@ def setUp(self): self.instructor_name = "Instructor One" self.client = APIClient() self.client.force_login(User.objects.create_user(username="test")) - create_review( - "CIS-471-001", - TEST_SEMESTER, - self.instructor_name, - {"instructor_quality": 4}, - ) + create_review("CIS-471-001", TEST_SEMESTER, self.instructor_name, {"instructor_quality": 4}) create_review("CIS-371-001", "2012A", self.instructor_name, {"instructor_quality": 2}) create_review( "CIS-471-001", @@ -111,8 +106,7 @@ def test_instructor(self): def test_instructor_no_old_codes(self): res = self.client.get( reverse( - "instructor-reviews", - args=[Instructor.objects.get(name=self.instructor_name).pk], + "instructor-reviews", args=[Instructor.objects.get(name=self.instructor_name).pk] ) ) self.assertEqual(200, res.status_code) @@ -251,8 +245,7 @@ def test_instructor(self): def test_instructor_no_old_codes(self): res = self.client.get( reverse( - "instructor-reviews", - args=[Instructor.objects.get(name=self.instructor_name).pk], + "instructor-reviews", args=[Instructor.objects.get(name=self.instructor_name).pk] ) ) self.assertEqual(200, res.status_code) @@ -424,8 +417,7 @@ def test_instructor(self): def test_instructor_no_old_codes(self): res = self.client.get( reverse( - "instructor-reviews", - args=[Instructor.objects.get(name=self.instructor_name).pk], + "instructor-reviews", args=[Instructor.objects.get(name=self.instructor_name).pk] ) ) self.assertEqual(200, res.status_code) @@ -527,12 +519,7 @@ def setUp(self): self.instructor_name = "Instructor One" self.client = APIClient() self.client.force_login(User.objects.create_user(username="test")) - create_review( - "CIS-471-001", - TEST_SEMESTER, - self.instructor_name, - {"instructor_quality": 4}, - ) + create_review("CIS-471-001", TEST_SEMESTER, self.instructor_name, {"instructor_quality": 4}) create_review("CIS-471-001", "2017A", "Instructor Two", {"instructor_quality": 2}) create_review("CIS-371-900", "2012A", self.instructor_name, {"instructor_quality": 2}) @@ -682,16 +669,10 @@ def setUp(self): self.client = APIClient() self.client.force_login(User.objects.create_user(username="test")) create_review( - "ARTH-2220-001", - TEST_SEMESTER, - self.instructor_name, - {"instructor_quality": 4}, + "ARTH-2220-001", TEST_SEMESTER, self.instructor_name, {"instructor_quality": 4} ) create_review( - "NELC-2055-001", - TEST_SEMESTER, - self.instructor_name, - {"instructor_quality": 3}, + "NELC-2055-001", TEST_SEMESTER, self.instructor_name, {"instructor_quality": 3} ) create_review("ARTH-222-001", "2012A", self.instructor_name, {"instructor_quality": 2}) topic_2220 = Topic.objects.get(most_recent__full_code="ARTH-2220") diff --git a/frontend/alert/components/Toast.tsx b/frontend/alert/components/Toast.tsx index 7ebe3b029..6a6a7b36b 100644 --- a/frontend/alert/components/Toast.tsx +++ b/frontend/alert/components/Toast.tsx @@ -121,6 +121,7 @@ const Toast = ({ onClose, children, type }: PropsWithChildren) => { {children} diff --git a/frontend/alert/components/managealert/ManageAlertUI.tsx b/frontend/alert/components/managealert/ManageAlertUI.tsx index 1b7530677..602fba0e5 100644 --- a/frontend/alert/components/managealert/ManageAlertUI.tsx +++ b/frontend/alert/components/managealert/ManageAlertUI.tsx @@ -103,7 +103,7 @@ export const ManageAlert = ({ clearTimeout(searchTimeout); } setSearchTimeout( - setTimeout(() => { + window.setTimeout(() => { setFilter({ search: searchText }); }, 100) ); diff --git a/frontend/alert/package.json b/frontend/alert/package.json index eae948bb2..fa5378944 100644 --- a/frontend/alert/package.json +++ b/frontend/alert/package.json @@ -27,7 +27,7 @@ "react-tooltip": "^4.2.21", "styled-components": "^6.1.8", "swr": "^1.0.1", - "typescript": "^3.9.5" + "typescript": "^4.3.5" }, "scripts": { "dev": "NODE_OPTIONS=--openssl-legacy-provider node server.js", diff --git a/frontend/alert/tsconfig.json b/frontend/alert/tsconfig.json index 5870a7b74..1cd08e882 100644 --- a/frontend/alert/tsconfig.json +++ b/frontend/alert/tsconfig.json @@ -17,7 +17,8 @@ "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve" + "jsx": "preserve", + "incremental": true }, "exclude": [ "node_modules" diff --git a/frontend/degree-plan/.eslintrc.json b/frontend/degree-plan/.eslintrc.json index bffb357a7..b381e5aad 100644 --- a/frontend/degree-plan/.eslintrc.json +++ b/frontend/degree-plan/.eslintrc.json @@ -1,3 +1,55 @@ { - "extends": "next/core-web-vitals" -} + "parser": "@typescript-eslint/parser", + "plugins": [ + "prettier" + ], + "extends": [ + "airbnb", + "react-app", + "prettier", + "prettier/react" + ], + "env": { + "browser": true + }, + "rules": { + "prettier/prettier": "error", + "quotes": [ + "error", + "double", + "avoid-escape" + ], + "no-unused-vars": [ + "error", + { + "args": "none" + } + ], + "import/prefer-default-export": 0, + "react/jsx-filename-extension": 0, + "react/prop-types": 0, + "jsx-a11y/click-events-have-key-events": 0, + "jsx-a11y/interactive-supports-focus": 0, + "react/require-default-props": 0, + "react/jsx-boolean-value": 0, + "import/extensions": 0, + "no-bitwise": "off", + "no-await-in-loop": "warn", + "semi": [ + "warn", + "always" + ] + }, + "settings": { + "import/resolver": { + "node": { + "extensions": [ + ".js", + ".jsx", + ".ts", + ".tsx" + ] + } + } + } +} \ No newline at end of file diff --git a/frontend/degree-plan/.gitignore b/frontend/degree-plan/.gitignore deleted file mode 100644 index 9bdabed24..000000000 --- a/frontend/degree-plan/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage -/data - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/frontend/degree-plan/.lintstagedrc.yml b/frontend/degree-plan/.lintstagedrc.yml new file mode 100644 index 000000000..943f10780 --- /dev/null +++ b/frontend/degree-plan/.lintstagedrc.yml @@ -0,0 +1,3 @@ +'*.{js, css, ts, tsx}': + - prettier --write + - eslint --fix diff --git a/frontend/degree-plan/.yarnrc.yml b/frontend/degree-plan/.yarnrc.yml deleted file mode 100644 index 3186f3f07..000000000 --- a/frontend/degree-plan/.yarnrc.yml +++ /dev/null @@ -1 +0,0 @@ -nodeLinker: node-modules diff --git a/frontend/degree-plan/Dockerfile b/frontend/degree-plan/Dockerfile index 8cc9ddc32..860f9c663 100644 --- a/frontend/degree-plan/Dockerfile +++ b/frontend/degree-plan/Dockerfile @@ -1,4 +1,6 @@ -FROM node:10-buster as build-deps +FROM node:18-bookworm + +LABEL maintainer="Penn Labs" WORKDIR /app/ @@ -9,20 +11,18 @@ COPY degree-plan/package.json /app/degree-plan/ COPY shared-components/package.json /app/shared-components/ # Install dependencies -RUN yarn workspace penndegreeplan install --production --frozen-lockfile +RUN yarn workspace degree-plan install --production --frozen-lockfile RUN yarn workspace pcx-shared-components install --production --frozen-lockfile # Copy project COPY degree-plan/ /app/degree-plan COPY shared-components/ /app/shared-components +# Disable next telemetry +ENV NEXT_TELEMETRY_DISABLED=1 + # Build project WORKDIR /app/degree-plan RUN yarn build -FROM nginx:1.12 - -LABEL maintainer="Penn Labs" - -COPY degree-plan/nginx.conf /etc/nginx/conf.d/default.conf -COPY --from=build-deps /app/degree-plan/build/ /usr/share/nginx/html +CMD ["yarn", "start"] \ No newline at end of file diff --git a/frontend/degree-plan/README.md b/frontend/degree-plan/README.md deleted file mode 100644 index 1f99eac53..000000000 --- a/frontend/degree-plan/README.md +++ /dev/null @@ -1,48 +0,0 @@ -## Penn Degree Plan - -The app has finished the transition from React with Redux to next.js with no Redux. This was done to provide the possibility for server-side rendering and a more light-weight frontend framework. Here's what it looks like: - - - -To run the development server: -``` -yarn install - -npm run dev -# or -yarn dev -``` - -Currently, the following features are implemented and ready for testing: - -Degree requirements -- expand/collapse major to see/hide requirements -- switch to edit mode by clicking edit button at top right corner of requirement panel -- rearrange degrees in edit mode - -Courses planned -- animation showing stats for all courses planned in a semester, including course quality, instructor quality, and difficulty -- drag a course from requirement panel and drop it in a semester in planning panel -- drag courses across semesters in planning panel -- delete a course from a semester by hovering over it and clicking the delete button that shows up - -Degree plans -- see all degree plans by expanding the top tab bar in planning panel -- switch to a degree plan by clicking on its tab -- create a new degree plan and give it a name from input box -- current plan is switched to the new plan upon creation - -TODO: - -Degree requirements -- integrate types with backend -- style adjustment - -Courses planned -- divider to seperate current and past semesters -- display more statistics - -Degree plans -- check with Aagam about data models, create temp data to implement types on the client side -- should be able to switch between degree plans freely -- style fix for dnd components (crop margin) diff --git a/frontend/degree-plan/components/Course/Course.tsx b/frontend/degree-plan/components/Course/Course.tsx index 33da3c5c8..c0c82637f 100644 --- a/frontend/degree-plan/components/Course/Course.tsx +++ b/frontend/degree-plan/components/Course/Course.tsx @@ -6,6 +6,7 @@ import { ReviewPanelTrigger } from "../Infobox/ReviewPanel"; import { Draggable } from "../common/DnD"; import Skeleton from "react-loading-skeleton" import 'react-loading-skeleton/dist/skeleton.css' +import { TRANSFER_CREDIT_SEMESTER_KEY } from "@/constants"; const COURSE_BORDER_RADIUS = "9px"; @@ -103,10 +104,9 @@ const IconBadge = styled.div` ` - const SemesterIcon = ({semester}:{semester: string | null}) => { - if (!semester) return; - const year = semester.substring(2,4); + if (!semester) return
; + const year = semester === TRANSFER_CREDIT_SEMESTER_KEY ? "AP" : semester.substring(2,4); const sem = semester.substring(4); return ( diff --git a/frontend/degree-plan/components/Course/CourseDetailHeader.tsx b/frontend/degree-plan/components/Course/CourseDetailHeader.tsx deleted file mode 100644 index 0c6812048..000000000 --- a/frontend/degree-plan/components/Course/CourseDetailHeader.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from "react"; -import Icon from '@mdi/react'; -import { mdiCart, mdiWindowClose, mdiWindowClosed } from '@mdi/js'; - -const addRemoveButton = { width: 50 }; - -const DetailHeader = ({course}:any) => { - - return ( - <> -
-

- {course.id} -

-
- -
-
-

{course.title}

- - ); -} - -export default DetailHeader; \ No newline at end of file diff --git a/frontend/degree-plan/components/Course/CourseDetailRatings.tsx b/frontend/degree-plan/components/Course/CourseDetailRatings.tsx deleted file mode 100644 index c5077f59e..000000000 --- a/frontend/degree-plan/components/Course/CourseDetailRatings.tsx +++ /dev/null @@ -1,22 +0,0 @@ -const DetailRatings = ({course} : any) => { - return ( - <> -
-
- { `Quality: ${course.course_quality || 'Not available'}`} -
-
- {`Difficulty: ${course.difficulty || 'Not available'}`} -
-
- {`Instructor Quality: ${course.instructor_quality || 'Not available'}`} -
-
- {`Work Required: ${course.work_required || 'Not available'}`} -
-
- - ) -} - -export default DetailRatings; \ No newline at end of file diff --git a/frontend/degree-plan/components/Course/DraggableCourseTitle.tsx b/frontend/degree-plan/components/Course/DraggableCourseTitle.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/degree-plan/components/Dock/Dock.tsx b/frontend/degree-plan/components/Dock/Dock.tsx index 8b1e100b0..14f88b651 100644 --- a/frontend/degree-plan/components/Dock/Dock.tsx +++ b/frontend/degree-plan/components/Dock/Dock.tsx @@ -9,6 +9,8 @@ import { SearchPanelContext } from '../Search/SearchPanel'; import { useSWRCrud } from '@/hooks/swrcrud'; import useSWR, { useSWRConfig } from 'swr'; import { DarkBlueBackgroundSkeleton } from "../FourYearPlan/PanelCommon"; +// TODO: Move shared components to typescript +// @ts-ignore import AccountIndicator from "pcx-shared-components/src/accounts/AccountIndicator"; import _ from 'lodash'; import CoursePlanned from '../FourYearPlan/CourseInPlan'; @@ -37,11 +39,14 @@ const DockContainer = styled.div<{$isDroppable:boolean, $isOver: boolean}>` const SearchIconContainer = styled.div` padding: .25rem 2rem; padding-left: 0; - border-color: var(--primary-color-extra-dark); + border-color: var(--primary-color-xx-dark); + color: var(--primary-color-extra-dark); border-width: 0; border-right-width: 2px; border-style: solid; flex-shrink: 0; + display: flex; + gap: 1rem; ` const DockedCoursesWrapper = styled.div` @@ -109,14 +114,14 @@ interface DockProps { login: (u: User) => void; logout: () => void; user: User | null; - activeDegreeplanId: DegreePlan["id"]; + activeDegreeplanId: DegreePlan["id"] | null; } const Dock = ({ user, login, logout, activeDegreeplanId }: DockProps) => { // const [courseAdded, setCourseAdded] = React.useState(false); const { searchPanelOpen, setSearchPanelOpen, setSearchRuleQuery, setSearchRuleId } = useContext(SearchPanelContext) const { createOrUpdate, remove } = useSWRCrud(`/api/degree/docked`, { idKey: 'full_code' }); - const {data: dockedCourses = [], isLoading} = useSWR(user ? `/api/degree/docked` : null); + const { data: dockedCourses = [], isLoading } = useSWR(user ? `/api/degree/docked` : null); // Returns a boolean that indiates whether this is the first render const useIsMount = () => { @@ -127,8 +132,6 @@ const Dock = ({ user, login, logout, activeDegreeplanId }: DockProps) => { return isMountRef.current; }; - const isMount = useIsMount(); - const [{ isOver, canDrop }, drop] = useDrop(() => ({ accept: [ItemTypes.COURSE_IN_PLAN, ItemTypes.COURSE_IN_REQ], drop: (course: DnDCourse) => { @@ -174,8 +177,11 @@ const Dock = ({ user, login, logout, activeDegreeplanId }: DockProps) => { setSearchPanelOpen(!searchPanelOpen); }}> - + +
+ Add Course +
{isLoading ? diff --git a/frontend/degree-plan/components/Footer.tsx b/frontend/degree-plan/components/Footer.tsx new file mode 100644 index 000000000..f60d03f89 --- /dev/null +++ b/frontend/degree-plan/components/Footer.tsx @@ -0,0 +1,35 @@ +import styled from "@emotion/styled"; + +const Wrapper = styled.div` + color: #999999; + font-size: 0.8rem; + text-align: center; + bottom: 15px; + width: 100%; + padding-bottom: 1rem; + line-height: 1.5; +`; + +const Link = styled.a` + color: rgb(50, 115, 220); +` + +const Footer = () => ( + + Made with{" "} + + + {" "} + by{" "} + + Penn Labs + + . + Have feedback about Penn Degree Plan? Let us know {" "} + {// contact@penncourses.org + } + here! + +); + +export default Footer; \ No newline at end of file diff --git a/frontend/degree-plan/components/FourYearPlan/CoursesPlanned.tsx b/frontend/degree-plan/components/FourYearPlan/CoursesPlanned.tsx index f080a76c4..253a1c648 100644 --- a/frontend/degree-plan/components/FourYearPlan/CoursesPlanned.tsx +++ b/frontend/degree-plan/components/FourYearPlan/CoursesPlanned.tsx @@ -24,17 +24,16 @@ interface CoursesPlannedProps { fulfillments: Fulfillment[]; removeCourse: (course: Course["id"]) => void; semester: Course["id"], - className: string; - isLoading: boolean; + className?: string; + isLoading?: boolean; } -const CoursesPlanned = ({fulfillments, removeCourse, className, semester, isLoading}: CoursesPlannedProps) => { +const CoursesPlanned = ({fulfillments, removeCourse, className, semester, isLoading = false}: CoursesPlannedProps) => { return ( {fulfillments.map(fulfillment => )} - {/* */} ) } diff --git a/frontend/degree-plan/components/FourYearPlan/DegreeModal.tsx b/frontend/degree-plan/components/FourYearPlan/DegreeModal.tsx index bb727880f..8c10a05c4 100644 --- a/frontend/degree-plan/components/FourYearPlan/DegreeModal.tsx +++ b/frontend/degree-plan/components/FourYearPlan/DegreeModal.tsx @@ -1,5 +1,6 @@ import styled from "@emotion/styled"; import type { + Degree, DegreeListing, DegreePlan, Fulfillment, @@ -119,6 +120,15 @@ const DegreeAddInterior = styled.div` padding: 1.2rem 2rem; `; +/** Create label for major listings */ +export const createMajorLabel = (degree: DegreeListing) => { + const concentration = + degree.concentration && degree.concentration !== "NONE" + ? ` - ${degree.concentration_name}` + : ""; + return `${degree.major_name}${concentration} (${degree.year})`; +}; + interface RemoveDegreeProps { degreeplanId: number; degreeId: number; @@ -130,7 +140,7 @@ interface RemoveSemesterProps { interface ModalInteriorProps { modalKey: ModalKey; - modalObject: DegreePlan | null | RemoveSemesterProps | RemoveDegreeProps; + modalObject: DegreePlan | null | RemoveSemesterProps | RemoveDegreeProps | Degree; setActiveDegreeplan: (arg0: DegreePlan | null) => void; close: () => void; modalRef: React.RefObject; @@ -168,15 +178,6 @@ const ModalInterior = ({ const { data: degrees, isLoading: isLoadingDegrees } = useSWR(`/api/degree/degrees`); - /** Create label for major listings */ - const createMajorLabel = (degree: DegreeListing) => { - const concentration = - degree.concentration && degree.concentration !== "NONE" - ? ` - ${degree.concentration_name}` - : ""; - return `${degree.major_name}${concentration}`; - }; - const getMajorOptions = React.useCallback(() => { /** Filter major options based on selected schools/degrees */ const majorOptions = @@ -244,7 +245,7 @@ const ModalInterior = ({ {/* */} { - updateDegreeplan({ name }, modalObject.id); + updateDegreeplan({ name }, (modalObject as DegreePlan).id); close(); }} > @@ -264,7 +265,8 @@ const ModalInterior = ({ { - delete_degreeplan(modalObject.id); + // TODO: these are not great type casts + delete_degreeplan((modalObject as DegreePlan).id); close(); }} > @@ -281,7 +283,7 @@ const ModalInterior = ({ setMajor(selectedOption)} + onChange={(selectedOption) => setMajor(selectedOption || undefined)} styles={{ menuPortal: base => ({ ...base, zIndex: 999 }) }} menuPortalTarget={modalRef.current} isClearable @@ -299,7 +301,7 @@ const ModalInterior = ({ placeholder={ isLoadingDegrees ? "loading programs..." - : "Major - Concentration" + : "Major - Concentration (Starting Year)" } isLoading={isLoadingDegrees} /> @@ -308,7 +310,7 @@ const ModalInterior = ({ { if (!major?.value.id) return; - add_degree(modalObject.id, major?.value.id); + add_degree((modalObject as Degree).id, major?.value.id); close(); }} > @@ -329,7 +331,7 @@ const ModalInterior = ({ { - remove_degree(modalObject.degreeplanId, modalObject.degreeId); + remove_degree((modalObject as RemoveDegreeProps).degreeplanId, (modalObject as RemoveDegreeProps).degreeId); close(); }} > @@ -348,7 +350,7 @@ const ModalInterior = ({ { - modalObject.helper(); + (modalObject as RemoveSemesterProps).helper(); close(); }} > @@ -364,7 +366,7 @@ interface DegreeModalProps { setModalKey: (arg0: ModalKey) => void; modalKey: ModalKey; modalObject: DegreePlan | null; - setActiveDegreeplan: (arg0: DegreePlan) => void; + setActiveDegreeplan: (arg0: DegreePlan | null) => void; } const DegreeModal = ({ setModalKey, @@ -377,6 +379,8 @@ const DegreeModal = ({ close={() => setModalKey(null)} modalKey={modalKey} > + {/* + // @ts-ignore */} = (props) => ( +export const DarkBlueBackgroundSkeleton: React.FC<{ width?: string; }> = (props) => ( @@ -44,6 +41,7 @@ export const PanelHeader = styled.div` justify-content: space-between; align-items: center; background-color: var(--primary-color); + color: var(--primary-color-ultra-dark); padding: 0.5rem 1rem; flex-grow: 0; font-weight: 300; diff --git a/frontend/degree-plan/components/FourYearPlan/PlanPanel.tsx b/frontend/degree-plan/components/FourYearPlan/PlanPanel.tsx index cd228e68c..826c2171b 100644 --- a/frontend/degree-plan/components/FourYearPlan/PlanPanel.tsx +++ b/frontend/degree-plan/components/FourYearPlan/PlanPanel.tsx @@ -7,6 +7,7 @@ import { useSWRCrud } from '@/hooks/swrcrud'; import { EditButton } from './EditButton'; import { PanelTopBarButton, PanelTopBarIcon } from "./PanelCommon"; import { PanelContainer, PanelHeader, PanelTopBarIconList, PanelBody } from "./PanelCommon"; +import { ModalKey } from "./DegreeModal"; const ShowStatsText = styled.div` min-width: 6rem; @@ -24,8 +25,8 @@ const ShowStatsButton = ({ showStats, setShowStats }: { showStats: boolean, setS ); interface PlanPanelProps { - setModalKey: (arg0: string) => void; - modalKey: string; + setModalKey: (arg0: ModalKey) => void; + modalKey: string | null; setModalObject: (arg0: DegreePlan | null) => void; setActiveDegreeplan: (arg0: DegreePlan | null) => void; activeDegreeplan: DegreePlan | null; @@ -55,10 +56,10 @@ const PlanPanel = ({ item.name} allItems={degreeplans || []} - selectItem={(id: DegreePlan["id"]) => setActiveDegreeplan(degreeplans?.filter(d => d.id === id)[0])} + selectItem={(id: DegreePlan["id"]) => setActiveDegreeplan(degreeplans?.filter(d => d.id === id)[0] || null)} mutators={{ copy: (item: DegreePlan) => { (copyDegreeplan({...item, name: `${item.name} (copy)`}, item.id) as Promise) @@ -86,7 +87,7 @@ const PlanPanel = ({ {/** map to semesters */} ( + +) +export default SatisfiedCheck; \ No newline at end of file diff --git a/frontend/degree-plan/components/FourYearPlan/ScoreRow.tsx b/frontend/degree-plan/components/FourYearPlan/ScoreRow.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/degree-plan/components/FourYearPlan/SelectListDropdown.tsx b/frontend/degree-plan/components/FourYearPlan/SelectListDropdown.tsx index 63c239469..837c62e21 100644 --- a/frontend/degree-plan/components/FourYearPlan/SelectListDropdown.tsx +++ b/frontend/degree-plan/components/FourYearPlan/SelectListDropdown.tsx @@ -236,7 +236,7 @@ const ScheduleDropdownHeader = styled.div` ` const SelectedName = styled.span` - font-weight: 500; + font-weight: 700; min-width: 5rem; font-size: 1.25rem; ` @@ -328,7 +328,7 @@ const SelectListDropdown = ({ /> ); })} - +