├── .gitignore ├── LICENSE ├── Pipfile ├── backend ├── Dockerfile ├── Pipfile ├── Pipfile.lock ├── alembic.ini ├── constants.py ├── domain │ ├── __init__.py │ ├── core │ │ ├── __init__.py │ │ ├── base.py │ │ ├── engine.py │ │ └── session_local.py │ └── models │ │ ├── __init__.py │ │ ├── idea.py │ │ ├── ideas_tags.py │ │ └── tag.py ├── interfaces │ ├── __init__.py │ ├── api_models │ │ ├── __init__.py │ │ ├── idea.py │ │ └── tags.py │ ├── dependencies.py │ ├── ideas │ │ ├── __init__.py │ │ ├── banwords.py │ │ ├── get_ideas_order.py │ │ ├── like_approver.py │ │ └── router.py │ └── tags │ │ ├── __init__.py │ │ └── router.py ├── main.py └── migrations │ ├── README │ ├── env.py │ ├── script.py.mako │ └── versions │ ├── 1d890129779a_.py │ ├── 214eb54f18d9_.py │ ├── 8190ad8014a1_.py │ ├── 8632e11b69be_.py │ └── d7a8af7b9227_.py ├── docker-compose.yml └── frontend ├── .dockerignore ├── Dockerfile ├── apiClient.js ├── components ├── Idea.js ├── Idea.module.sass ├── IdeaOrder.js ├── IdeasCardList.js ├── Layout.js ├── PopularTagsAside.js ├── SideMenu.js └── TagLink.js ├── constants.js ├── hooks ├── useLikesCookie.js └── useTokenCookie.js ├── next.config.js ├── package.json ├── pages ├── _app.js ├── about.js ├── contact.js ├── index.js ├── privacy.js ├── random.js └── submit.js ├── public ├── favicons │ ├── android-icon-144x144.png │ ├── android-icon-192x192.png │ ├── android-icon-36x36.png │ ├── android-icon-48x48.png │ ├── android-icon-72x72.png │ ├── android-icon-96x96.png │ ├── apple-icon-114x114.png │ ├── apple-icon-120x120.png │ ├── apple-icon-144x144.png │ ├── apple-icon-152x152.png │ ├── apple-icon-180x180.png │ ├── apple-icon-57x57.png │ ├── apple-icon-60x60.png │ ├── apple-icon-72x72.png │ ├── apple-icon-76x76.png │ ├── apple-icon-precomposed.png │ ├── apple-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── manifest.json │ ├── ms-icon-144x144.png │ ├── ms-icon-150x150.png │ ├── ms-icon-310x310.png │ └── ms-icon-70x70.png └── img │ └── idea.svg ├── styles.sass └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .*_env 2 | frontend/node_modules 3 | frontend/.next 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | 8 | [dev-packages] 9 | 10 | [requires] 11 | python_version = "3.9" 12 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | RUN pip install pipenv 3 | 4 | EXPOSE 80 5 | 6 | COPY ./ /app 7 | WORKDIR /app 8 | 9 | ENV PYTHONPATH /app 10 | 11 | RUN pipenv install 12 | 13 | CMD pipenv run migrate && pipenv run prod 14 | -------------------------------------------------------------------------------- /backend/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | fastapi = "*" 10 | sqlalchemy = "*" 11 | psycopg2-binary = "*" 12 | alembic = "*" 13 | redis = "*" 14 | uvicorn = {extras = ["standard"], version = "*"} 15 | requests = "*" 16 | 17 | [requires] 18 | python_version = "3.10" 19 | 20 | [scripts] 21 | createmigrations = "pipenv run alembic revision --autogenerate" 22 | migrate = "pipenv run alembic upgrade head" 23 | dev = "pipenv run uvicorn main:app --reload --proxy-headers" 24 | prod = "pipenv run uvicorn main:app --proxy-headers --host 0.0.0.0 --port 80" 25 | db = "docker run --rm --name=what-to-code-db -e POSTGRES_DB=what_to_code -e POSTGRES_HOST_AUTH_METHOD=trust -e POSTGRES_USER=what_to_code -p 5432:5432 postgres" 26 | -------------------------------------------------------------------------------- /backend/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "3dcd99c1539b9b8ac04d3c45616bb1be74c55357817c2c5c36ccb14428ae89ad" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.10" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "alembic": { 20 | "hashes": [ 21 | "sha256:99bd884ca390466db5e27ffccff1d179ec5c05c965cfefc0607e69f9e411cb25", 22 | "sha256:b00892b53b3642d0b8dbedba234dbf1924b69be83a9a769d5a624b01094e304b" 23 | ], 24 | "index": "pypi", 25 | "markers": "python_version >= '3.8'", 26 | "version": "==1.14.0" 27 | }, 28 | "annotated-types": { 29 | "hashes": [ 30 | "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", 31 | "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" 32 | ], 33 | "markers": "python_version >= '3.8'", 34 | "version": "==0.7.0" 35 | }, 36 | "anyio": { 37 | "hashes": [ 38 | "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", 39 | "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d" 40 | ], 41 | "markers": "python_version >= '3.9'", 42 | "version": "==4.6.2.post1" 43 | }, 44 | "async-timeout": { 45 | "hashes": [ 46 | "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", 47 | "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3" 48 | ], 49 | "markers": "python_full_version < '3.11.3'", 50 | "version": "==5.0.1" 51 | }, 52 | "certifi": { 53 | "hashes": [ 54 | "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", 55 | "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" 56 | ], 57 | "markers": "python_version >= '3.6'", 58 | "version": "==2024.8.30" 59 | }, 60 | "charset-normalizer": { 61 | "hashes": [ 62 | "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", 63 | "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", 64 | "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", 65 | "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", 66 | "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", 67 | "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", 68 | "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", 69 | "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", 70 | "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", 71 | "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", 72 | "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", 73 | "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", 74 | "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", 75 | "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", 76 | "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", 77 | "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", 78 | "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", 79 | "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", 80 | "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", 81 | "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", 82 | "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", 83 | "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", 84 | "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", 85 | "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", 86 | "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", 87 | "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", 88 | "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", 89 | "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", 90 | "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", 91 | "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", 92 | "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", 93 | "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", 94 | "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", 95 | "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", 96 | "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", 97 | "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", 98 | "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", 99 | "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", 100 | "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", 101 | "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", 102 | "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", 103 | "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", 104 | "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", 105 | "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", 106 | "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", 107 | "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", 108 | "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", 109 | "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", 110 | "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", 111 | "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", 112 | "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", 113 | "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", 114 | "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", 115 | "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", 116 | "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", 117 | "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", 118 | "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", 119 | "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", 120 | "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", 121 | "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", 122 | "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", 123 | "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", 124 | "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", 125 | "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", 126 | "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", 127 | "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", 128 | "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", 129 | "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", 130 | "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", 131 | "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", 132 | "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", 133 | "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", 134 | "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", 135 | "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", 136 | "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", 137 | "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", 138 | "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", 139 | "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", 140 | "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", 141 | "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", 142 | "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", 143 | "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", 144 | "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", 145 | "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", 146 | "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", 147 | "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", 148 | "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", 149 | "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", 150 | "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", 151 | "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", 152 | "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", 153 | "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", 154 | "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", 155 | "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", 156 | "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", 157 | "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", 158 | "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", 159 | "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", 160 | "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", 161 | "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", 162 | "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", 163 | "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", 164 | "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", 165 | "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", 166 | "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" 167 | ], 168 | "markers": "python_full_version >= '3.7.0'", 169 | "version": "==3.4.0" 170 | }, 171 | "click": { 172 | "hashes": [ 173 | "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", 174 | "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" 175 | ], 176 | "markers": "python_version >= '3.7'", 177 | "version": "==8.1.7" 178 | }, 179 | "exceptiongroup": { 180 | "hashes": [ 181 | "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", 182 | "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" 183 | ], 184 | "markers": "python_version < '3.11'", 185 | "version": "==1.2.2" 186 | }, 187 | "fastapi": { 188 | "hashes": [ 189 | "sha256:0e7a4d0dc0d01c68df21887cce0945e72d3c48b9f4f79dfe7a7d53aa08fbb289", 190 | "sha256:596b95adbe1474da47049e802f9a65ab2ffa9c2b07e7efee70eb8a66c9f2f796" 191 | ], 192 | "index": "pypi", 193 | "markers": "python_version >= '3.8'", 194 | "version": "==0.115.5" 195 | }, 196 | "h11": { 197 | "hashes": [ 198 | "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", 199 | "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" 200 | ], 201 | "markers": "python_version >= '3.7'", 202 | "version": "==0.14.0" 203 | }, 204 | "httptools": { 205 | "hashes": [ 206 | "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", 207 | "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd", 208 | "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", 209 | "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", 210 | "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", 211 | "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", 212 | "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", 213 | "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da", 214 | "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0", 215 | "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", 216 | "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", 217 | "sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff", 218 | "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", 219 | "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", 220 | "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", 221 | "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4", 222 | "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", 223 | "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9", 224 | "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", 225 | "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", 226 | "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003", 227 | "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959", 228 | "sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc", 229 | "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076", 230 | "sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490", 231 | "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", 232 | "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6", 233 | "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c", 234 | "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50", 235 | "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547", 236 | "sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba", 237 | "sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440", 238 | "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", 239 | "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab", 240 | "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", 241 | "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1", 242 | "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", 243 | "sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f", 244 | "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", 245 | "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", 246 | "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", 247 | "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", 248 | "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43" 249 | ], 250 | "version": "==0.6.4" 251 | }, 252 | "idna": { 253 | "hashes": [ 254 | "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", 255 | "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" 256 | ], 257 | "markers": "python_version >= '3.6'", 258 | "version": "==3.10" 259 | }, 260 | "mako": { 261 | "hashes": [ 262 | "sha256:9ec3a1583713479fae654f83ed9fa8c9a4c16b7bb0daba0e6bbebff50c0d983d", 263 | "sha256:a91198468092a2f1a0de86ca92690fb0cfc43ca90ee17e15d93662b4c04b241a" 264 | ], 265 | "markers": "python_version >= '3.8'", 266 | "version": "==1.3.6" 267 | }, 268 | "markupsafe": { 269 | "hashes": [ 270 | "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", 271 | "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", 272 | "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", 273 | "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", 274 | "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", 275 | "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", 276 | "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", 277 | "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", 278 | "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", 279 | "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", 280 | "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", 281 | "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", 282 | "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", 283 | "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", 284 | "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", 285 | "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", 286 | "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", 287 | "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", 288 | "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", 289 | "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", 290 | "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", 291 | "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", 292 | "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", 293 | "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", 294 | "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", 295 | "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", 296 | "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", 297 | "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", 298 | "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", 299 | "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", 300 | "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", 301 | "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", 302 | "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", 303 | "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", 304 | "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", 305 | "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", 306 | "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", 307 | "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", 308 | "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", 309 | "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", 310 | "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", 311 | "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", 312 | "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", 313 | "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", 314 | "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", 315 | "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", 316 | "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", 317 | "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", 318 | "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", 319 | "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", 320 | "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", 321 | "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", 322 | "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", 323 | "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", 324 | "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", 325 | "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", 326 | "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", 327 | "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", 328 | "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", 329 | "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", 330 | "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" 331 | ], 332 | "markers": "python_version >= '3.9'", 333 | "version": "==3.0.2" 334 | }, 335 | "psycopg2-binary": { 336 | "hashes": [ 337 | "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff", 338 | "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5", 339 | "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f", 340 | "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", 341 | "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", 342 | "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c", 343 | "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c", 344 | "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341", 345 | "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", 346 | "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", 347 | "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", 348 | "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007", 349 | "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92", 350 | "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb", 351 | "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5", 352 | "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5", 353 | "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8", 354 | "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1", 355 | "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68", 356 | "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", 357 | "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1", 358 | "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53", 359 | "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", 360 | "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906", 361 | "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0", 362 | "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", 363 | "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a", 364 | "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b", 365 | "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44", 366 | "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648", 367 | "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7", 368 | "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", 369 | "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa", 370 | "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697", 371 | "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d", 372 | "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b", 373 | "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", 374 | "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4", 375 | "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287", 376 | "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e", 377 | "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", 378 | "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", 379 | "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30", 380 | "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3", 381 | "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", 382 | "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92", 383 | "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", 384 | "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c", 385 | "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8", 386 | "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", 387 | "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", 388 | "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864", 389 | "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc", 390 | "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", 391 | "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", 392 | "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", 393 | "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b", 394 | "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481", 395 | "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5", 396 | "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4", 397 | "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", 398 | "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392", 399 | "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4", 400 | "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", 401 | "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", 402 | "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", 403 | "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863" 404 | ], 405 | "index": "pypi", 406 | "markers": "python_version >= '3.8'", 407 | "version": "==2.9.10" 408 | }, 409 | "pydantic": { 410 | "hashes": [ 411 | "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f", 412 | "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12" 413 | ], 414 | "markers": "python_version >= '3.8'", 415 | "version": "==2.9.2" 416 | }, 417 | "pydantic-core": { 418 | "hashes": [ 419 | "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36", 420 | "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05", 421 | "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071", 422 | "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327", 423 | "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c", 424 | "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36", 425 | "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29", 426 | "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744", 427 | "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d", 428 | "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec", 429 | "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e", 430 | "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e", 431 | "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577", 432 | "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232", 433 | "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", 434 | "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6", 435 | "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368", 436 | "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480", 437 | "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2", 438 | "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2", 439 | "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6", 440 | "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", 441 | "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d", 442 | "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2", 443 | "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84", 444 | "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166", 445 | "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271", 446 | "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", 447 | "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb", 448 | "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13", 449 | "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323", 450 | "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556", 451 | "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665", 452 | "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef", 453 | "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb", 454 | "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119", 455 | "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126", 456 | "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510", 457 | "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b", 458 | "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87", 459 | "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f", 460 | "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc", 461 | "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8", 462 | "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21", 463 | "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f", 464 | "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6", 465 | "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658", 466 | "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b", 467 | "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3", 468 | "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb", 469 | "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59", 470 | "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24", 471 | "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9", 472 | "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3", 473 | "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd", 474 | "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753", 475 | "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55", 476 | "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad", 477 | "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a", 478 | "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605", 479 | "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e", 480 | "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b", 481 | "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433", 482 | "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8", 483 | "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07", 484 | "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728", 485 | "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0", 486 | "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327", 487 | "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555", 488 | "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64", 489 | "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6", 490 | "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea", 491 | "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b", 492 | "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df", 493 | "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e", 494 | "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd", 495 | "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068", 496 | "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3", 497 | "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040", 498 | "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12", 499 | "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916", 500 | "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", 501 | "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f", 502 | "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801", 503 | "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231", 504 | "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5", 505 | "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8", 506 | "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee", 507 | "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607" 508 | ], 509 | "markers": "python_version >= '3.8'", 510 | "version": "==2.23.4" 511 | }, 512 | "python-dotenv": { 513 | "hashes": [ 514 | "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", 515 | "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" 516 | ], 517 | "version": "==1.0.1" 518 | }, 519 | "pyyaml": { 520 | "hashes": [ 521 | "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", 522 | "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", 523 | "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", 524 | "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", 525 | "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", 526 | "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", 527 | "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", 528 | "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", 529 | "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", 530 | "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", 531 | "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", 532 | "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", 533 | "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", 534 | "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", 535 | "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", 536 | "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", 537 | "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", 538 | "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", 539 | "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", 540 | "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", 541 | "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", 542 | "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", 543 | "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", 544 | "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", 545 | "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", 546 | "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", 547 | "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", 548 | "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", 549 | "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", 550 | "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", 551 | "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", 552 | "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", 553 | "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", 554 | "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", 555 | "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", 556 | "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", 557 | "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", 558 | "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", 559 | "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", 560 | "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", 561 | "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", 562 | "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", 563 | "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", 564 | "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", 565 | "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", 566 | "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", 567 | "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", 568 | "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", 569 | "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", 570 | "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", 571 | "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", 572 | "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", 573 | "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" 574 | ], 575 | "version": "==6.0.2" 576 | }, 577 | "redis": { 578 | "hashes": [ 579 | "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0", 580 | "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897" 581 | ], 582 | "index": "pypi", 583 | "markers": "python_version >= '3.8'", 584 | "version": "==5.2.0" 585 | }, 586 | "requests": { 587 | "hashes": [ 588 | "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", 589 | "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" 590 | ], 591 | "index": "pypi", 592 | "markers": "python_version >= '3.8'", 593 | "version": "==2.32.3" 594 | }, 595 | "sniffio": { 596 | "hashes": [ 597 | "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", 598 | "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" 599 | ], 600 | "markers": "python_version >= '3.7'", 601 | "version": "==1.3.1" 602 | }, 603 | "sqlalchemy": { 604 | "hashes": [ 605 | "sha256:03e08af7a5f9386a43919eda9de33ffda16b44eb11f3b313e6822243770e9763", 606 | "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436", 607 | "sha256:07b441f7d03b9a66299ce7ccf3ef2900abc81c0db434f42a5694a37bd73870f2", 608 | "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588", 609 | "sha256:1e0d612a17581b6616ff03c8e3d5eff7452f34655c901f75d62bd86449d9750e", 610 | "sha256:23623166bfefe1487d81b698c423f8678e80df8b54614c2bf4b4cfcd7c711959", 611 | "sha256:2519f3a5d0517fc159afab1015e54bb81b4406c278749779be57a569d8d1bb0d", 612 | "sha256:28120ef39c92c2dd60f2721af9328479516844c6b550b077ca450c7d7dc68575", 613 | "sha256:37350015056a553e442ff672c2d20e6f4b6d0b2495691fa239d8aa18bb3bc908", 614 | "sha256:39769a115f730d683b0eb7b694db9789267bcd027326cccc3125e862eb03bfd8", 615 | "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8", 616 | "sha256:3d6718667da04294d7df1670d70eeddd414f313738d20a6f1d1f379e3139a545", 617 | "sha256:3dbb986bad3ed5ceaf090200eba750b5245150bd97d3e67343a3cfed06feecf7", 618 | "sha256:4557e1f11c5f653ebfdd924f3f9d5ebfc718283b0b9beebaa5dd6b77ec290971", 619 | "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855", 620 | "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c", 621 | "sha256:4f5e9cd989b45b73bd359f693b935364f7e1f79486e29015813c338450aa5a71", 622 | "sha256:50aae840ebbd6cdd41af1c14590e5741665e5272d2fee999306673a1bb1fdb4d", 623 | "sha256:59b1ee96617135f6e1d6f275bbe988f419c5178016f3d41d3c0abb0c819f75bb", 624 | "sha256:59b8f3adb3971929a3e660337f5dacc5942c2cdb760afcabb2614ffbda9f9f72", 625 | "sha256:66bffbad8d6271bb1cc2f9a4ea4f86f80fe5e2e3e501a5ae2a3dc6a76e604e6f", 626 | "sha256:69f93723edbca7342624d09f6704e7126b152eaed3cdbb634cb657a54332a3c5", 627 | "sha256:6a440293d802d3011028e14e4226da1434b373cbaf4a4bbb63f845761a708346", 628 | "sha256:72c28b84b174ce8af8504ca28ae9347d317f9dba3999e5981a3cd441f3712e24", 629 | "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e", 630 | "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5", 631 | "sha256:8318f4776c85abc3f40ab185e388bee7a6ea99e7fa3a30686580b209eaa35c08", 632 | "sha256:8958b10490125124463095bbdadda5aa22ec799f91958e410438ad6c97a7b793", 633 | "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88", 634 | "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686", 635 | "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b", 636 | "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2", 637 | "sha256:9fe53b404f24789b5ea9003fc25b9a3988feddebd7e7b369c8fac27ad6f52f28", 638 | "sha256:a4e46a888b54be23d03a89be510f24a7652fe6ff660787b96cd0e57a4ebcb46d", 639 | "sha256:a86bfab2ef46d63300c0f06936bd6e6c0105faa11d509083ba8f2f9d237fb5b5", 640 | "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a", 641 | "sha256:af148a33ff0349f53512a049c6406923e4e02bf2f26c5fb285f143faf4f0e46a", 642 | "sha256:b11d0cfdd2b095e7b0686cf5fabeb9c67fae5b06d265d8180715b8cfa86522e3", 643 | "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf", 644 | "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5", 645 | "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef", 646 | "sha256:b817d41d692bf286abc181f8af476c4fbef3fd05e798777492618378448ee689", 647 | "sha256:b81ee3d84803fd42d0b154cb6892ae57ea6b7c55d8359a02379965706c7efe6c", 648 | "sha256:be9812b766cad94a25bc63bec11f88c4ad3629a0cec1cd5d4ba48dc23860486b", 649 | "sha256:c245b1fbade9c35e5bd3b64270ab49ce990369018289ecfde3f9c318411aaa07", 650 | "sha256:c3f3631693003d8e585d4200730616b78fafd5a01ef8b698f6967da5c605b3fa", 651 | "sha256:c4ae3005ed83f5967f961fd091f2f8c5329161f69ce8480aa8168b2d7fe37f06", 652 | "sha256:c54a1e53a0c308a8e8a7dffb59097bff7facda27c70c286f005327f21b2bd6b1", 653 | "sha256:d0ddd9db6e59c44875211bc4c7953a9f6638b937b0a88ae6d09eb46cced54eff", 654 | "sha256:dc022184d3e5cacc9579e41805a681187650e170eb2fd70e28b86192a479dcaa", 655 | "sha256:e32092c47011d113dc01ab3e1d3ce9f006a47223b18422c5c0d150af13a00687", 656 | "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4", 657 | "sha256:f942a799516184c855e1a32fbc7b29d7e571b52612647866d4ec1c3242578fcb", 658 | "sha256:f9511d8dd4a6e9271d07d150fb2f81874a3c8c95e11ff9af3a2dfc35fe42ee44", 659 | "sha256:fd3a55deef00f689ce931d4d1b23fa9f04c880a48ee97af488fd215cf24e2a6c", 660 | "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e", 661 | "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53" 662 | ], 663 | "index": "pypi", 664 | "markers": "python_version >= '3.7'", 665 | "version": "==2.0.36" 666 | }, 667 | "starlette": { 668 | "hashes": [ 669 | "sha256:9834fd799d1a87fd346deb76158668cfa0b0d56f85caefe8268e2d97c3468b62", 670 | "sha256:fbc189474b4731cf30fcef52f18a8d070e3f3b46c6a04c97579e85e6ffca942d" 671 | ], 672 | "markers": "python_version >= '3.8'", 673 | "version": "==0.41.2" 674 | }, 675 | "typing-extensions": { 676 | "hashes": [ 677 | "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", 678 | "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" 679 | ], 680 | "markers": "python_version >= '3.8'", 681 | "version": "==4.12.2" 682 | }, 683 | "urllib3": { 684 | "hashes": [ 685 | "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", 686 | "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" 687 | ], 688 | "markers": "python_version >= '3.8'", 689 | "version": "==2.2.3" 690 | }, 691 | "uvicorn": { 692 | "extras": [ 693 | "standard" 694 | ], 695 | "hashes": [ 696 | "sha256:60b8f3a5ac027dcd31448f411ced12b5ef452c646f76f02f8cc3f25d8d26fd82", 697 | "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e" 698 | ], 699 | "markers": "python_version >= '3.8'", 700 | "version": "==0.32.0" 701 | }, 702 | "uvloop": { 703 | "hashes": [ 704 | "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", 705 | "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f", 706 | "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc", 707 | "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414", 708 | "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", 709 | "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d", 710 | "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd", 711 | "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff", 712 | "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", 713 | "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", 714 | "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", 715 | "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a", 716 | "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", 717 | "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2", 718 | "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0", 719 | "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", 720 | "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c", 721 | "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", 722 | "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", 723 | "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb", 724 | "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75", 725 | "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", 726 | "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", 727 | "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", 728 | "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", 729 | "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", 730 | "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206", 731 | "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", 732 | "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", 733 | "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b", 734 | "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", 735 | "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79", 736 | "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f", 737 | "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe", 738 | "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26", 739 | "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", 740 | "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2" 741 | ], 742 | "version": "==0.21.0" 743 | }, 744 | "watchfiles": { 745 | "hashes": [ 746 | "sha256:01550ccf1d0aed6ea375ef259706af76ad009ef5b0203a3a4cce0f6024f9b68a", 747 | "sha256:01def80eb62bd5db99a798d5e1f5f940ca0a05986dcfae21d833af7a46f7ee22", 748 | "sha256:07cdef0c84c03375f4e24642ef8d8178e533596b229d32d2bbd69e5128ede02a", 749 | "sha256:083dc77dbdeef09fa44bb0f4d1df571d2e12d8a8f985dccde71ac3ac9ac067a0", 750 | "sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827", 751 | "sha256:21ab23fdc1208086d99ad3f69c231ba265628014d4aed31d4e8746bd59e88cd1", 752 | "sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c", 753 | "sha256:2e28d91ef48eab0afb939fa446d8ebe77e2f7593f5f463fd2bb2b14132f95b6e", 754 | "sha256:2efec17819b0046dde35d13fb8ac7a3ad877af41ae4640f4109d9154ed30a188", 755 | "sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b", 756 | "sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5", 757 | "sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90", 758 | "sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef", 759 | "sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b", 760 | "sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15", 761 | "sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48", 762 | "sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e", 763 | "sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df", 764 | "sha256:449f43f49c8ddca87c6b3980c9284cab6bd1f5c9d9a2b00012adaaccd5e7decd", 765 | "sha256:4933a508d2f78099162da473841c652ad0de892719043d3f07cc83b33dfd9d91", 766 | "sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d", 767 | "sha256:49fb58bcaa343fedc6a9e91f90195b20ccb3135447dc9e4e2570c3a39565853e", 768 | "sha256:4a7fa2bc0efef3e209a8199fd111b8969fe9db9c711acc46636686331eda7dd4", 769 | "sha256:4abf4ad269856618f82dee296ac66b0cd1d71450fc3c98532d93798e73399b7a", 770 | "sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370", 771 | "sha256:4d28cea3c976499475f5b7a2fec6b3a36208656963c1a856d328aeae056fc5c1", 772 | "sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea", 773 | "sha256:54ca90a9ae6597ae6dc00e7ed0a040ef723f84ec517d3e7ce13e63e4bc82fa04", 774 | "sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896", 775 | "sha256:5c51749f3e4e269231510da426ce4a44beb98db2dce9097225c338f815b05d4f", 776 | "sha256:632676574429bee8c26be8af52af20e0c718cc7f5f67f3fb658c71928ccd4f7f", 777 | "sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43", 778 | "sha256:6bdcfa3cd6fdbdd1a068a52820f46a815401cbc2cb187dd006cb076675e7b735", 779 | "sha256:7138eff8baa883aeaa074359daabb8b6c1e73ffe69d5accdc907d62e50b1c0da", 780 | "sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a", 781 | "sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61", 782 | "sha256:78470906a6be5199524641f538bd2c56bb809cd4bf29a566a75051610bc982c3", 783 | "sha256:7ae3e208b31be8ce7f4c2c0034f33406dd24fbce3467f77223d10cd86778471c", 784 | "sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f", 785 | "sha256:82ae557a8c037c42a6ef26c494d0631cacca040934b101d001100ed93d43f361", 786 | "sha256:82b2509f08761f29a0fdad35f7e1638b8ab1adfa2666d41b794090361fb8b855", 787 | "sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327", 788 | "sha256:85d5f0c7771dcc7a26c7a27145059b6bb0ce06e4e751ed76cdf123d7039b60b5", 789 | "sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab", 790 | "sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633", 791 | "sha256:951088d12d339690a92cef2ec5d3cfd957692834c72ffd570ea76a6790222777", 792 | "sha256:95cf3b95ea665ab03f5a54765fa41abf0529dbaf372c3b83d91ad2cfa695779b", 793 | "sha256:96619302d4374de5e2345b2b622dc481257a99431277662c30f606f3e22f42be", 794 | "sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f", 795 | "sha256:9a60e2bf9dc6afe7f743e7c9b149d1fdd6dbf35153c78fe3a14ae1a9aee3d98b", 796 | "sha256:9f895d785eb6164678ff4bb5cc60c5996b3ee6df3edb28dcdeba86a13ea0465e", 797 | "sha256:a2a9891723a735d3e2540651184be6fd5b96880c08ffe1a98bae5017e65b544b", 798 | "sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366", 799 | "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823", 800 | "sha256:acbfa31e315a8f14fe33e3542cbcafc55703b8f5dcbb7c1eecd30f141df50db3", 801 | "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1", 802 | "sha256:b3ef2c69c655db63deb96b3c3e587084612f9b1fa983df5e0c3379d41307467f", 803 | "sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418", 804 | "sha256:b665caeeda58625c3946ad7308fbd88a086ee51ccb706307e5b1fa91556ac886", 805 | "sha256:b74fdffce9dfcf2dc296dec8743e5b0332d15df19ae464f0e249aa871fc1c571", 806 | "sha256:b995bfa6bf01a9e09b884077a6d37070464b529d8682d7691c2d3b540d357a0c", 807 | "sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94", 808 | "sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428", 809 | "sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234", 810 | "sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6", 811 | "sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968", 812 | "sha256:d337193bbf3e45171c8025e291530fb7548a93c45253897cd764a6a71c937ed9", 813 | "sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c", 814 | "sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e", 815 | "sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab", 816 | "sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec", 817 | "sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444", 818 | "sha256:e5171ef898299c657685306d8e1478a45e9303ddcd8ac5fed5bd52ad4ae0b69b", 819 | "sha256:e94e98c7cb94cfa6e071d401ea3342767f28eb5a06a58fafdc0d2a4974f4f35c", 820 | "sha256:ec39698c45b11d9694a1b635a70946a5bad066b593af863460a8e600f0dff1ca", 821 | "sha256:ed9aba6e01ff6f2e8285e5aa4154e2970068fe0fc0998c4380d0e6278222269b", 822 | "sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18", 823 | "sha256:ee82c98bed9d97cd2f53bdb035e619309a098ea53ce525833e26b93f673bc318", 824 | "sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07", 825 | "sha256:f7d9b87c4c55e3ea8881dfcbf6d61ea6775fffed1fedffaa60bd047d3c08c430", 826 | "sha256:f83df90191d67af5a831da3a33dd7628b02a95450e168785586ed51e6d28943c", 827 | "sha256:fca9433a45f18b7c779d2bae7beeec4f740d28b788b117a48368d95a3233ed83", 828 | "sha256:fd92bbaa2ecdb7864b7600dcdb6f2f1db6e0346ed425fbd01085be04c63f0b05" 829 | ], 830 | "version": "==0.24.0" 831 | }, 832 | "websockets": { 833 | "hashes": [ 834 | "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59", 835 | "sha256:01bb2d4f0a6d04538d3c5dfd27c0643269656c28045a53439cbf1c004f90897a", 836 | "sha256:034feb9f4286476f273b9a245fb15f02c34d9586a5bc936aff108c3ba1b21beb", 837 | "sha256:04a97aca96ca2acedf0d1f332c861c5a4486fdcba7bcef35873820f940c4231e", 838 | "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6", 839 | "sha256:1288369a6a84e81b90da5dbed48610cd7e5d60af62df9851ed1d1d23a9069f10", 840 | "sha256:14839f54786987ccd9d03ed7f334baec0f02272e7ec4f6e9d427ff584aeea8b4", 841 | "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0", 842 | "sha256:1f874ba705deea77bcf64a9da42c1f5fc2466d8f14daf410bc7d4ceae0a9fcb0", 843 | "sha256:205f672a6c2c671a86d33f6d47c9b35781a998728d2c7c2a3e1cf3333fcb62b7", 844 | "sha256:2177ee3901075167f01c5e335a6685e71b162a54a89a56001f1c3e9e3d2ad250", 845 | "sha256:219c8187b3ceeadbf2afcf0f25a4918d02da7b944d703b97d12fb01510869078", 846 | "sha256:25225cc79cfebc95ba1d24cd3ab86aaa35bcd315d12fa4358939bd55e9bd74a5", 847 | "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9", 848 | "sha256:368a05465f49c5949e27afd6fbe0a77ce53082185bbb2ac096a3a8afaf4de52e", 849 | "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b", 850 | "sha256:39450e6215f7d9f6f7bc2a6da21d79374729f5d052333da4d5825af8a97e6735", 851 | "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8", 852 | "sha256:3c3deac3748ec73ef24fc7be0b68220d14d47d6647d2f85b2771cb35ea847aa1", 853 | "sha256:3f14a96a0034a27f9d47fd9788913924c89612225878f8078bb9d55f859272b0", 854 | "sha256:3fc753451d471cff90b8f467a1fc0ae64031cf2d81b7b34e1811b7e2691bc4bc", 855 | "sha256:414ffe86f4d6f434a8c3b7913655a1a5383b617f9bf38720e7c0799fac3ab1c6", 856 | "sha256:449d77d636f8d9c17952628cc7e3b8faf6e92a17ec581ec0c0256300717e1512", 857 | "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4", 858 | "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e", 859 | "sha256:5a42d3ecbb2db5080fc578314439b1d79eef71d323dc661aa616fb492436af5d", 860 | "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3", 861 | "sha256:5ef440054124728cc49b01c33469de06755e5a7a4e83ef61934ad95fc327fbb0", 862 | "sha256:660c308dabd2b380807ab64b62985eaccf923a78ebc572bd485375b9ca2b7dc7", 863 | "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058", 864 | "sha256:6d24fc337fc055c9e83414c94e1ee0dee902a486d19d2a7f0929e49d7d604b09", 865 | "sha256:7048eb4415d46368ef29d32133134c513f507fff7d953c18c91104738a68c3b3", 866 | "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da", 867 | "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2", 868 | "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a", 869 | "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0", 870 | "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d", 871 | "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7", 872 | "sha256:8fda642151d5affdee8a430bd85496f2e2517be3a2b9d2484d633d5712b15c56", 873 | "sha256:90b5d9dfbb6d07a84ed3e696012610b6da074d97453bd01e0e30744b472c8179", 874 | "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f", 875 | "sha256:9481a6de29105d73cf4515f2bef8eb71e17ac184c19d0b9918a3701c6c9c4f23", 876 | "sha256:9607b9a442392e690a57909c362811184ea429585a71061cd5d3c2b98065c199", 877 | "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a", 878 | "sha256:a032855dc7db987dff813583d04f4950d14326665d7e714d584560b140ae6b8b", 879 | "sha256:a0adf84bc2e7c86e8a202537b4fd50e6f7f0e4a6b6bf64d7ccb96c4cd3330b29", 880 | "sha256:a35f704be14768cea9790d921c2c1cc4fc52700410b1c10948511039be824aac", 881 | "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58", 882 | "sha256:a4c805c6034206143fbabd2d259ec5e757f8b29d0a2f0bf3d2fe5d1f60147a4a", 883 | "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45", 884 | "sha256:ad2ab2547761d79926effe63de21479dfaf29834c50f98c4bf5b5480b5838434", 885 | "sha256:b1f3628a0510bd58968c0f60447e7a692933589b791a6b572fcef374053ca280", 886 | "sha256:b7e7ea2f782408c32d86b87a0d2c1fd8871b0399dd762364c731d86c86069a78", 887 | "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707", 888 | "sha256:bea45f19b7ca000380fbd4e02552be86343080120d074b87f25593ce1700ad58", 889 | "sha256:cc1fc87428c1d18b643479caa7b15db7d544652e5bf610513d4a3478dbe823d0", 890 | "sha256:cd7c11968bc3860d5c78577f0dbc535257ccec41750675d58d8dc66aa47fe52c", 891 | "sha256:ceada5be22fa5a5a4cdeec74e761c2ee7db287208f54c718f2df4b7e200b8d4a", 892 | "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9", 893 | "sha256:d9fd19ecc3a4d5ae82ddbfb30962cf6d874ff943e56e0c81f5169be2fda62979", 894 | "sha256:ddaa4a390af911da6f680be8be4ff5aaf31c4c834c1a9147bc21cbcbca2d4370", 895 | "sha256:df174ece723b228d3e8734a6f2a6febbd413ddec39b3dc592f5a4aa0aff28098", 896 | "sha256:e0744623852f1497d825a49a99bfbec9bea4f3f946df6eb9d8a2f0c37a2fec2e", 897 | "sha256:e5dc25a9dbd1a7f61eca4b7cb04e74ae4b963d658f9e4f9aad9cd00b688692c8", 898 | "sha256:e7591d6f440af7f73c4bd9404f3772bfee064e639d2b6cc8c94076e71b2471c1", 899 | "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05", 900 | "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed", 901 | "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6", 902 | "sha256:f95ba34d71e2fa0c5d225bde3b3bdb152e957150100e75c86bc7f3964c450d89" 903 | ], 904 | "version": "==14.1" 905 | } 906 | }, 907 | "develop": {} 908 | } 909 | -------------------------------------------------------------------------------- /backend/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = migrations 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # timezone to use when rendering the date 11 | # within the migration file as well as the filename. 12 | # string value is passed to dateutil.tz.gettz() 13 | # leave blank for localtime 14 | # timezone = 15 | 16 | # max length of characters to apply to the 17 | # "slug" field 18 | # truncate_slug_length = 40 19 | 20 | # set to 'true' to run the environment during 21 | # the 'revision' command, regardless of autogenerate 22 | # revision_environment = false 23 | 24 | # set to 'true' to allow .pyc and .pyo files without 25 | # a source .py file to be detected as revisions in the 26 | # versions/ directory 27 | # sourceless = false 28 | 29 | # version location specification; this defaults 30 | # to migrations/versions. When using multiple version 31 | # directories, initial revisions must be specified with --version-path 32 | # version_locations = %(here)s/bar %(here)s/bat migrations/versions 33 | 34 | # the output encoding used when revision files 35 | # are written from script.py.mako 36 | # output_encoding = utf-8 37 | 38 | sqlalchemy.url = driver://user:pass@localhost/dbname 39 | 40 | 41 | [post_write_hooks] 42 | # post_write_hooks defines scripts or Python functions that are run 43 | # on newly generated revision scripts. See the documentation for further 44 | # detail and examples 45 | 46 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 47 | # hooks=black 48 | # black.type=console_scripts 49 | # black.entrypoint=black 50 | # black.options=-l 79 51 | 52 | # Logging configuration 53 | [loggers] 54 | keys = root,sqlalchemy,alembic 55 | 56 | [handlers] 57 | keys = console 58 | 59 | [formatters] 60 | keys = generic 61 | 62 | [logger_root] 63 | level = WARN 64 | handlers = console 65 | qualname = 66 | 67 | [logger_sqlalchemy] 68 | level = WARN 69 | handlers = 70 | qualname = sqlalchemy.engine 71 | 72 | [logger_alembic] 73 | level = INFO 74 | handlers = 75 | qualname = alembic 76 | 77 | [handler_console] 78 | class = StreamHandler 79 | args = (sys.stderr,) 80 | level = NOTSET 81 | formatter = generic 82 | 83 | [formatter_generic] 84 | format = %(levelname)-5.5s [%(name)s] %(message)s 85 | datefmt = %H:%M:%S 86 | -------------------------------------------------------------------------------- /backend/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | RECAPTCHA_V3_PRIVATE_KEY = os.environ.get("RECAPTCHA_V3_PRIVATE_KEY") 4 | SQLALCHEMY_URL = os.environ.get('SQLALCHEMY_URL', 'postgresql://what_to_code@localhost:5432/what_to_code') 5 | REDIS_URL = os.environ.get('REDIS_URL', 'redis://localhost/0') 6 | -------------------------------------------------------------------------------- /backend/domain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bykof/what-to-code/5534879468b2495ed58e0fd5ee8ad1df9e22a42f/backend/domain/__init__.py -------------------------------------------------------------------------------- /backend/domain/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bykof/what-to-code/5534879468b2495ed58e0fd5ee8ad1df9e22a42f/backend/domain/core/__init__.py -------------------------------------------------------------------------------- /backend/domain/core/base.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.declarative import declarative_base 2 | 3 | Base = declarative_base() 4 | -------------------------------------------------------------------------------- /backend/domain/core/engine.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | 3 | from constants import SQLALCHEMY_URL 4 | 5 | engine = create_engine(SQLALCHEMY_URL) 6 | -------------------------------------------------------------------------------- /backend/domain/core/session_local.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import sessionmaker 2 | 3 | from domain.core.engine import engine 4 | 5 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 6 | -------------------------------------------------------------------------------- /backend/domain/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .tag import Tag 2 | from .idea import Idea 3 | -------------------------------------------------------------------------------- /backend/domain/models/idea.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy import String, Integer, Column, DateTime 4 | from sqlalchemy.orm import relationship 5 | 6 | from domain.core.base import Base 7 | from domain.models.ideas_tags import IdeasTagsTable 8 | 9 | 10 | class Idea(Base): 11 | __tablename__ = 'ideas' 12 | 13 | id = Column(Integer, primary_key=True) 14 | title = Column(String) 15 | created = Column(DateTime, default=datetime.utcnow) 16 | description = Column(String) 17 | likes = Column(Integer, default=1) 18 | tags = relationship( 19 | 'Tag', 20 | secondary=IdeasTagsTable, 21 | back_populates='ideas', 22 | cascade='all,delete', 23 | ) 24 | -------------------------------------------------------------------------------- /backend/domain/models/ideas_tags.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Table, Integer, String, ForeignKey 2 | 3 | from domain.core.base import Base 4 | 5 | IdeasTagsTable = Table( 6 | 'ideastags', 7 | Base.metadata, 8 | Column('tags_value', String, ForeignKey('tags.value', ondelete='CASCADE')), 9 | Column('ideas_id', Integer, ForeignKey('ideas.id', ondelete='CASCADE')) 10 | ) 11 | -------------------------------------------------------------------------------- /backend/domain/models/tag.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, String 2 | from sqlalchemy.orm import relationship 3 | 4 | from domain.core.base import Base 5 | from domain.models.ideas_tags import IdeasTagsTable 6 | 7 | 8 | class Tag(Base): 9 | __tablename__ = 'tags' 10 | 11 | value = Column(String, primary_key=True) 12 | ideas = relationship( 13 | "Idea", 14 | secondary=IdeasTagsTable, 15 | back_populates='tags', 16 | cascade='all,delete', 17 | ) 18 | -------------------------------------------------------------------------------- /backend/interfaces/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bykof/what-to-code/5534879468b2495ed58e0fd5ee8ad1df9e22a42f/backend/interfaces/__init__.py -------------------------------------------------------------------------------- /backend/interfaces/api_models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bykof/what-to-code/5534879468b2495ed58e0fd5ee8ad1df9e22a42f/backend/interfaces/api_models/__init__.py -------------------------------------------------------------------------------- /backend/interfaces/api_models/idea.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class IdeaTag(BaseModel): 7 | value: str 8 | 9 | class Config: 10 | orm_mode = True 11 | 12 | 13 | class IdeaBase(BaseModel): 14 | title: str 15 | description: str 16 | tags: List[IdeaTag] 17 | 18 | class Config: 19 | orm_mode = True 20 | 21 | 22 | class Idea(IdeaBase): 23 | id: int 24 | likes: int 25 | 26 | 27 | class CreateIdea(IdeaBase): 28 | captchaToken: str 29 | -------------------------------------------------------------------------------- /backend/interfaces/api_models/tags.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Tag(BaseModel): 5 | value: str 6 | -------------------------------------------------------------------------------- /backend/interfaces/dependencies.py: -------------------------------------------------------------------------------- 1 | from constants import REDIS_URL 2 | from domain.core.session_local import SessionLocal 3 | from redis import StrictRedis 4 | 5 | 6 | def get_db(): 7 | try: 8 | db = SessionLocal() 9 | yield db 10 | finally: 11 | db.close() 12 | 13 | 14 | def get_redis(): 15 | try: 16 | redis_client = StrictRedis.from_url(REDIS_URL) 17 | yield redis_client 18 | finally: 19 | redis_client.close() 20 | -------------------------------------------------------------------------------- /backend/interfaces/ideas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bykof/what-to-code/5534879468b2495ed58e0fd5ee8ad1df9e22a42f/backend/interfaces/ideas/__init__.py -------------------------------------------------------------------------------- /backend/interfaces/ideas/banwords.py: -------------------------------------------------------------------------------- 1 | BANNEDWORDS = [ 2 | "anal", 3 | "anus", 4 | "arse", 5 | "ass", 6 | "ballsack", 7 | "balls", 8 | "bastard", 9 | "bitch", 10 | "biatch", 11 | "bloody", 12 | "blowjob", 13 | "blow job", 14 | "bollock", 15 | "bollok", 16 | "boner", 17 | "boob", 18 | "bugger", 19 | "bum", 20 | "butt", 21 | "buttplug", 22 | "clitoris", 23 | "cock", 24 | "coon", 25 | "crap", 26 | "cunt", 27 | "damn", 28 | "dick", 29 | "dildo", 30 | "dyke", 31 | "fag", 32 | "feck", 33 | "fellate", 34 | "fellatio", 35 | "felching", 36 | "fuck", 37 | "f u c k", 38 | "fudgepacker", 39 | "fudge packer", 40 | "flange", 41 | "homo", 42 | "jerk", 43 | "jizz", 44 | "knobend", 45 | "knob end", 46 | "labia", 47 | "lmao", 48 | "lmfao", 49 | "muff", 50 | "nigger", 51 | "nigga", 52 | "omg", 53 | "penis", 54 | "piss", 55 | "poop", 56 | "prick", 57 | "pube", 58 | "pussy", 59 | "queer", 60 | "scrotum", 61 | "sex", 62 | "shit", 63 | "s hit", 64 | "sh1t", 65 | "slut", 66 | "smegma", 67 | "spunk", 68 | "slave" 69 | "tit", 70 | "tosser", 71 | "turd", 72 | "twat", 73 | "vagina", 74 | "wank", 75 | "whore", 76 | ] -------------------------------------------------------------------------------- /backend/interfaces/ideas/get_ideas_order.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | class GetIdeasOrder(enum.Enum): 5 | RECENT = 'RECENT' 6 | OLDEST = 'OLDEST' 7 | RISING = 'RISING' 8 | POPULAR = 'POPULAR' 9 | 10 | -------------------------------------------------------------------------------- /backend/interfaces/ideas/like_approver.py: -------------------------------------------------------------------------------- 1 | from redis import StrictRedis 2 | 3 | 4 | class LikeApprover: 5 | def __init__(self, redis_client: StrictRedis): 6 | self.redis_client = redis_client 7 | 8 | def key(self, token, idea_id): 9 | return f'{token}_{idea_id}' 10 | 11 | def allowed_to_like(self, token, idea_id) -> bool: 12 | if self.redis_client.get(self.key(token, idea_id)) is not None: 13 | return False 14 | 15 | self.redis_client.set(self.key(token, idea_id), 1) 16 | return True 17 | 18 | def remove_like(self, token, idea_id) -> bool: 19 | if self.redis_client.get(self.key(token, idea_id)) is not None: 20 | self.redis_client.delete(self.key(token, idea_id)) 21 | return True 22 | else: 23 | return False 24 | -------------------------------------------------------------------------------- /backend/interfaces/ideas/router.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import List, Union 3 | from constants import RECAPTCHA_V3_PRIVATE_KEY 4 | 5 | from fastapi import APIRouter, Depends, HTTPException, Header 6 | from redis import StrictRedis 7 | from sqlalchemy import desc 8 | from sqlalchemy.orm import Session 9 | from sqlalchemy.sql.elements import or_ 10 | from sqlalchemy.sql.functions import random 11 | import requests 12 | 13 | from domain.models import Idea, Tag 14 | from domain.models.ideas_tags import IdeasTagsTable 15 | from interfaces.api_models.idea import Idea as IdeaResponse, CreateIdea 16 | from interfaces.dependencies import get_db, get_redis 17 | from interfaces.ideas.get_ideas_order import GetIdeasOrder 18 | from interfaces.ideas.like_approver import LikeApprover 19 | from interfaces.ideas.banwords import BANNEDWORDS 20 | 21 | PAGE_SIZE: int = 15 22 | router = APIRouter() 23 | 24 | 25 | @router.get('', response_model=List[IdeaResponse]) 26 | async def get_ideas( 27 | page: int = 0, 28 | order: GetIdeasOrder = GetIdeasOrder.POPULAR, 29 | tag: str = '', 30 | search: str = '', 31 | db: Session = Depends(get_db), 32 | ): 33 | query = db.query(Idea) 34 | if tag != '': 35 | query = query.join(IdeasTagsTable).filter( 36 | IdeasTagsTable.columns.tags_value == tag.lower()) 37 | if page < 0: 38 | raise HTTPException( 39 | status_code=400, detail='Page could not be less than 1') 40 | if order == GetIdeasOrder.POPULAR: 41 | query = query.order_by(desc(Idea.likes)) 42 | if order == GetIdeasOrder.RISING: 43 | pass 44 | if order == GetIdeasOrder.OLDEST: 45 | query = query.order_by(Idea.created) 46 | if order == GetIdeasOrder.RECENT: 47 | query = query.order_by(desc(Idea.created)) 48 | 49 | if search != '': 50 | query = query.filter(or_(Idea.title.contains( 51 | search), Idea.description.contains(search))) 52 | 53 | return query.offset(page * PAGE_SIZE).limit(PAGE_SIZE).all() 54 | 55 | 56 | @router.get('/random', response_model=Union[IdeaResponse, None]) 57 | def random_idea(db: Session = Depends(get_db)): 58 | return db.query(Idea).order_by(random()).limit(1).one_or_none() 59 | 60 | 61 | @router.get('/{id}', response_model=IdeaResponse) 62 | async def get_idea(id: int, db: Session = Depends(get_db)): 63 | idea = db.query(Idea).filter(Idea.id == id).one_or_none() 64 | if idea is None: 65 | HTTPException(status_code=404, detail='Idea was not found') 66 | return idea 67 | 68 | 69 | @router.post('', response_model=IdeaResponse) 70 | async def create_idea(idea_request: CreateIdea, db: Session = Depends(get_db)): 71 | for word in BANNEDWORDS: 72 | lower_word = word.lower() 73 | if ( 74 | lower_word in idea_request.title.lower() or 75 | lower_word in idea_request.description.lower() or 76 | lower_word in ''.join(idea_request.tags) 77 | ): 78 | raise HTTPException( 79 | status_code=400, detail='Your submission contains a banned word') 80 | 81 | response = requests.post( 82 | "https://www.google.com/recaptcha/api/siteverify", 83 | { 84 | 'secret': RECAPTCHA_V3_PRIVATE_KEY, 85 | 'response': idea_request.captchaToken, 86 | }, 87 | ) 88 | 89 | if response.json()["success"] != True: 90 | raise HTTPException( 91 | status_code=400, 92 | detail='Please provide valid recaptcha token', 93 | ) 94 | 95 | if len(idea_request.title) > 100: 96 | raise HTTPException( 97 | status_code=400, 98 | detail='Max 100 characters for title', 99 | ) 100 | 101 | if not re.search(r'[\w]{3}', idea_request.title): 102 | raise HTTPException( 103 | status_code=400, detail='Please provide at least 3 characters') 104 | 105 | if len(idea_request.description) > 1000: 106 | raise HTTPException( 107 | status_code=400, 108 | detail='Max 10000 characters for description', 109 | ) 110 | 111 | idea = Idea(title=idea_request.title, description=idea_request.description) 112 | 113 | if len(idea_request.tags) > 5: 114 | raise HTTPException(status_code=400, detail='No more than 5 tags') 115 | 116 | for tag_request in idea_request.tags: 117 | tag = db.query(Tag).filter( 118 | Tag.value == tag_request.value).one_or_none() 119 | 120 | if tag is None: 121 | tag = Tag(value=tag_request.value) 122 | 123 | idea.tags.append(tag) 124 | 125 | db.add(idea) 126 | db.commit() 127 | return idea 128 | 129 | 130 | @router.post('/{id}/like') 131 | def like_idea( 132 | id: int, 133 | token: str = Header(None), 134 | db: Session = Depends(get_db), 135 | redis_client: StrictRedis = Depends(get_redis), 136 | ): 137 | idea = db.query(Idea).filter(Idea.id == id).one_or_none() 138 | if idea is None: 139 | raise HTTPException(status_code=404, detail='Idea was not found') 140 | like_approver = LikeApprover(redis_client) 141 | is_approved = like_approver.allowed_to_like(token, idea.id) 142 | 143 | if is_approved: 144 | idea.likes += 1 145 | db.commit() 146 | else: 147 | like_approver.remove_like(token, idea.id) 148 | idea.likes -= 1 149 | db.commit() 150 | -------------------------------------------------------------------------------- /backend/interfaces/tags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bykof/what-to-code/5534879468b2495ed58e0fd5ee8ad1df9e22a42f/backend/interfaces/tags/__init__.py -------------------------------------------------------------------------------- /backend/interfaces/tags/router.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from fastapi import APIRouter, Depends 3 | from sqlalchemy import func, desc 4 | from sqlalchemy.orm import Session 5 | 6 | from domain.models.tag import Tag 7 | from interfaces.api_models.idea import IdeaTag 8 | from domain.models.ideas_tags import IdeasTagsTable 9 | from interfaces.dependencies import get_db 10 | 11 | PAGE_SIZE: int = 10 12 | router = APIRouter() 13 | 14 | 15 | @router.get('/popular', response_model=List[IdeaTag]) 16 | def get_popular_tags(db: Session = Depends(get_db)): 17 | idea_tags = db.query( 18 | IdeasTagsTable.columns.tags_value, 19 | func.Count(IdeasTagsTable.columns.ideas_id), 20 | ).group_by(IdeasTagsTable.columns.tags_value).order_by( 21 | desc(func.Count(IdeasTagsTable.columns.ideas_id)) 22 | ).limit(6).all() 23 | return db.query(Tag).filter(Tag.value.in_([value for value, _ in idea_tags])).all() 24 | -------------------------------------------------------------------------------- /backend/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from starlette.middleware.cors import CORSMiddleware 3 | 4 | from interfaces.ideas.router import router as ideas_router 5 | from interfaces.tags.router import router as tags_router 6 | 7 | app = FastAPI() 8 | origins = [ 9 | "http://localhost", 10 | "http://localhost:3000", 11 | "http://127.0.0.1:3000", 12 | "http://server.bykovski.de:3000", 13 | "https://what-to-code.com", 14 | "https://www.what-to-code.com", 15 | ] 16 | app.add_middleware( 17 | CORSMiddleware, 18 | allow_origins=origins, 19 | allow_credentials=True, 20 | allow_methods=["*"], 21 | allow_headers=["*"], 22 | ) 23 | app.include_router(ideas_router, prefix='/ideas', tags=['Ideas']) 24 | app.include_router(tags_router, prefix='/tags', tags=['Tags']) 25 | -------------------------------------------------------------------------------- /backend/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /backend/migrations/env.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | from logging.config import fileConfig 5 | 6 | from alembic import context 7 | 8 | 9 | config = context.config 10 | 11 | # Interpret the config file for Python logging. 12 | # This line sets up loggers basically. 13 | fileConfig(config.config_file_name) 14 | 15 | sys.path.append(os.path.dirname(os.path.dirname(__file__))) 16 | 17 | from domain.core.engine import engine 18 | import domain.models 19 | from domain.core.base import Base 20 | 21 | target_metadata = Base.metadata 22 | 23 | 24 | # other values from the config, defined by the needs of env.py, 25 | # can be acquired: 26 | # my_important_option = config.get_main_option("my_important_option") 27 | # ... etc. 28 | 29 | 30 | def run_migrations_offline(): 31 | """Run migrations in 'offline' mode. 32 | 33 | This configures the context with just a URL 34 | and not an Engine, though an Engine is acceptable 35 | here as well. By skipping the Engine creation 36 | we don't even need a DBAPI to be available. 37 | 38 | Calls to context.execute() here emit the given string to the 39 | script output. 40 | 41 | """ 42 | url = config.get_main_option("sqlalchemy.url") 43 | context.configure( 44 | url=url, 45 | target_metadata=target_metadata, 46 | literal_binds=True, 47 | dialect_opts={"paramstyle": "named"}, 48 | ) 49 | 50 | with context.begin_transaction(): 51 | context.run_migrations() 52 | 53 | 54 | def run_migrations_online(): 55 | """Run migrations in 'online' mode. 56 | 57 | In this scenario we need to create an Engine 58 | and associate a connection with the context. 59 | 60 | """ 61 | with engine.connect() as connection: 62 | context.configure(connection=connection, target_metadata=target_metadata) 63 | 64 | with context.begin_transaction(): 65 | context.run_migrations() 66 | 67 | 68 | if context.is_offline_mode(): 69 | run_migrations_offline() 70 | else: 71 | run_migrations_online() 72 | -------------------------------------------------------------------------------- /backend/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /backend/migrations/versions/1d890129779a_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 1d890129779a 4 | Revises: 8632e11b69be 5 | Create Date: 2020-04-11 19:51:20.544616 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '1d890129779a' 14 | down_revision = '8632e11b69be' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('ideas', sa.Column('likes', sa.Integer(), nullable=True)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('ideas', 'likes') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /backend/migrations/versions/214eb54f18d9_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 214eb54f18d9 4 | Revises: d7a8af7b9227 5 | Create Date: 2020-04-12 10:43:46.605650 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '214eb54f18d9' 14 | down_revision = 'd7a8af7b9227' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.drop_constraint('ideastags_tags_value_fkey', 'ideastags', type_='foreignkey') 22 | op.drop_constraint('ideastags_ideas_id_fkey', 'ideastags', type_='foreignkey') 23 | op.create_foreign_key(None, 'ideastags', 'tags', ['tags_value'], ['value'], ondelete='CASCADE') 24 | op.create_foreign_key(None, 'ideastags', 'ideas', ['ideas_id'], ['id'], ondelete='CASCADE') 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | op.drop_constraint(None, 'ideastags', type_='foreignkey') 31 | op.drop_constraint(None, 'ideastags', type_='foreignkey') 32 | op.create_foreign_key('ideastags_ideas_id_fkey', 'ideastags', 'ideas', ['ideas_id'], ['id']) 33 | op.create_foreign_key('ideastags_tags_value_fkey', 'ideastags', 'tags', ['tags_value'], ['value']) 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /backend/migrations/versions/8190ad8014a1_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 8190ad8014a1 4 | Revises: 1d890129779a 5 | Create Date: 2020-04-11 20:14:37.267093 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '8190ad8014a1' 14 | down_revision = '1d890129779a' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | pass 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | pass 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /backend/migrations/versions/8632e11b69be_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 8632e11b69be 4 | Revises: 5 | Create Date: 2020-04-11 10:01:00.498435 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '8632e11b69be' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('ideas', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('title', sa.String(), nullable=True), 24 | sa.Column('description', sa.String(), nullable=True), 25 | sa.PrimaryKeyConstraint('id') 26 | ) 27 | op.create_table('tags', 28 | sa.Column('value', sa.String(), nullable=False), 29 | sa.PrimaryKeyConstraint('value') 30 | ) 31 | op.create_table('ideastags', 32 | sa.Column('tags_value', sa.String(), nullable=True), 33 | sa.Column('ideas_id', sa.Integer(), nullable=True), 34 | sa.ForeignKeyConstraint(['ideas_id'], ['ideas.id'], ), 35 | sa.ForeignKeyConstraint(['tags_value'], ['tags.value'], ) 36 | ) 37 | # ### end Alembic commands ### 38 | 39 | 40 | def downgrade(): 41 | # ### commands auto generated by Alembic - please adjust! ### 42 | op.drop_table('ideastags') 43 | op.drop_table('tags') 44 | op.drop_table('ideas') 45 | # ### end Alembic commands ### 46 | -------------------------------------------------------------------------------- /backend/migrations/versions/d7a8af7b9227_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: d7a8af7b9227 4 | Revises: 8190ad8014a1 5 | Create Date: 2020-04-11 20:24:04.235597 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'd7a8af7b9227' 14 | down_revision = '8190ad8014a1' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('ideas', sa.Column('created', sa.DateTime(), nullable=True)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('ideas', 'created') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | redis: 3 | image: redis:alpine 4 | restart: always 5 | env_file: 6 | - ./.what_to_code_env 7 | command: redis-server --requirepass $REDIS_PASSWORD 8 | ports: 9 | - "6380:6379" 10 | db: 11 | image: postgres:12.10-alpine 12 | restart: always 13 | ports: 14 | - "5433:5432" 15 | volumes: 16 | - db-data:/var/lib/postgresql/data 17 | env_file: 18 | - ./.db_env 19 | backend: 20 | build: ./backend 21 | restart: always 22 | ports: 23 | - "8001:80" 24 | env_file: 25 | - ./.what_to_code_env 26 | depends_on: 27 | - redis 28 | - db 29 | frontend: 30 | build: ./frontend 31 | restart: always 32 | ports: 33 | - "3000:3000" 34 | env_file: 35 | - ./.what_to_code_env 36 | depends_on: 37 | - redis 38 | - db 39 | volumes: 40 | db-data: 41 | -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 2 | 3 | # Setting working directory. All the path will be relative to WORKDIR 4 | WORKDIR /app 5 | 6 | # Installing dependencies 7 | COPY package*.json ./ 8 | RUN npm install 9 | 10 | # Copying source files 11 | COPY . . 12 | 13 | # Building app 14 | RUN npm run build 15 | 16 | # Running the app 17 | CMD ["npm", "start"] 18 | -------------------------------------------------------------------------------- /frontend/apiClient.js: -------------------------------------------------------------------------------- 1 | import Axios from "axios"; 2 | 3 | const API_URL = process.env.API_URL; 4 | 5 | export const createIdea = async ({captchaToken, title, description, tags}) => { 6 | return Axios.post(`${API_URL}/ideas`, { 7 | captchaToken, 8 | title, 9 | description, 10 | tags: tags.map((tag) => ({ 11 | value: tag, 12 | })), 13 | }); 14 | }; 15 | 16 | export const getIdeas = async (order, tag, page = 0) => { 17 | return Axios.get(`${API_URL}/ideas`, { 18 | params: { order, tag, page }, 19 | }); 20 | }; 21 | 22 | export const getIdea = async (id) => { 23 | return Axios.get(`${API_URL}/ideas/${id}`); 24 | }; 25 | 26 | export const getPopularTags = async () => { 27 | return Axios.get(`${API_URL}/tags/popular`); 28 | }; 29 | 30 | export const getRandomIdea = async () => { 31 | return Axios.get(`${API_URL}/ideas/random`); 32 | }; 33 | 34 | export const likeIdea = async (id, token) => { 35 | return Axios.post( 36 | `${API_URL}/ideas/${id}/like`, 37 | {}, 38 | { 39 | headers: { 40 | token: token, 41 | }, 42 | } 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /frontend/components/Idea.js: -------------------------------------------------------------------------------- 1 | import styles from "./Idea.module.sass"; 2 | import classNames from "classnames"; 3 | import { likeIdea } from "../apiClient"; 4 | import TagLink from "./TagLink"; 5 | import useTokenCookie from "../hooks/useTokenCookie"; 6 | import useLikesCookie from "../hooks/useLikesCookie"; 7 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 8 | import { faHeart as fasHeart } from "@fortawesome/free-solid-svg-icons"; 9 | import { faHeart as farHeart } from "@fortawesome/free-regular-svg-icons"; 10 | import Link from "next/link"; 11 | 12 | import '@fortawesome/fontawesome-svg-core/styles.css'; 13 | import { config } from '@fortawesome/fontawesome-svg-core'; 14 | config.autoAddCss = false; 15 | 16 | const Idea = ({ id, title, description, tags, likes, clickedLike }) => { 17 | const token = useTokenCookie(); 18 | const { isLiked, like } = useLikesCookie(); 19 | 20 | const clickLike = async (event) => { 21 | event.preventDefault(); 22 | let response = await likeIdea(id, token); 23 | like(id); 24 | clickedLike(); 25 | }; 26 | 27 | return ( 28 |
29 |
30 |

{title}

31 |

{description}

32 |

33 | {tags.map((tag) => ( 34 | 35 | #{tag.value} 36 | 37 | ))} 38 |

39 |
40 | 56 |
57 | ); 58 | }; 59 | 60 | export default Idea; 61 | -------------------------------------------------------------------------------- /frontend/components/Idea.module.sass: -------------------------------------------------------------------------------- 1 | .idea 2 | -webkit-border-radius: 10px 3 | -moz-border-radius: 10px 4 | border-radius: 10px 5 | margin-bottom: 5rem 6 | 7 | .cardFooterItem 8 | background-color: #f4e04d 9 | -webkit-border-bottom-right-radius: 10px 10 | -webkit-border-bottom-left-radius: 10px 11 | -moz-border-radius-bottomright: 10px 12 | -moz-border-radius-bottomleft: 10px 13 | border-bottom-right-radius: 10px 14 | border-bottom-left-radius: 10px 15 | 16 | &:hover 17 | color: #8db1ab 18 | -------------------------------------------------------------------------------- /frontend/components/IdeaOrder.js: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | 3 | export const RECENT = 'RECENT'; 4 | export const OLDEST = 'OLDEST'; 5 | export const RISING = 'RISING'; 6 | export const POPULAR = 'POPULAR'; 7 | export const ORDERS = [RECENT, OLDEST, RISING, POPULAR]; 8 | 9 | const IdeaOrder = ({ currentOrder, onOrderClick }) => { 10 | return ( 11 |
12 |

13 | 22 |

23 |

24 | 33 |

34 |

35 | 44 |

45 |

46 | 55 |

56 |
57 | ); 58 | }; 59 | 60 | export default IdeaOrder; 61 | -------------------------------------------------------------------------------- /frontend/components/IdeasCardList.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import InfiniteScroll from 'react-infinite-scroller'; 3 | import { getIdeas, getIdea } from '../apiClient'; 4 | import Idea from './Idea'; 5 | 6 | const IdeasCardList = ({ orderType, filterTag }) => { 7 | const [hasMore, setHasMore] = useState(false); 8 | const [ideas, setIdeas] = useState([]); 9 | const [isLoading, setIsLoading] = useState(); 10 | const [error, setError] = useState(null); 11 | const setErrorDefault = () => setError(null); 12 | const retrieveIdeas = async (page = 0) => { 13 | try { 14 | setIsLoading(true); 15 | const response = await getIdeas(orderType, filterTag, page); 16 | if (response.data.length >= 15) { 17 | setHasMore(true); 18 | } 19 | setIdeas(response.data); 20 | } catch (error) { 21 | setError(error); 22 | } finally { 23 | setIsLoading(false); 24 | setErrorDefault(); 25 | } 26 | }; 27 | const retrieveIdea = async (id) => { 28 | let response = await getIdea(id); 29 | let index = ideas.findIndex((idea) => idea.id === id); 30 | if (index < 0) return; 31 | let newIdeas = [...ideas]; 32 | newIdeas[index] = response.data; 33 | setIdeas(newIdeas); 34 | }; 35 | 36 | const loadMore = (page) => { 37 | const loadMoreIdeas = async () => { 38 | const response = await getIdeas(orderType, filterTag, page); 39 | if (response.data.length < 15) { 40 | setHasMore(false); 41 | } 42 | setIdeas([...ideas, ...response.data]); 43 | }; 44 | loadMoreIdeas(); 45 | }; 46 | 47 | useEffect(() => { 48 | retrieveIdeas(); 49 | }, [orderType, filterTag]); 50 | 51 | if (isLoading) { 52 | return

Loading...

; 53 | } 54 | 55 | if (error) { 56 | return

There was an unexpected error...

; 57 | } 58 | 59 | return ( 60 | Loading more ...} 68 | > 69 | {ideas.map((idea) => ( 70 | { 78 | retrieveIdea(idea.id); 79 | }} 80 | /> 81 | ))} 82 | 83 | ); 84 | }; 85 | 86 | export default IdeasCardList; 87 | -------------------------------------------------------------------------------- /frontend/components/Layout.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import Head from "next/head"; 3 | import Link from "next/link"; 4 | import classNames from "classnames"; 5 | import useTokenCookie from "../hooks/useTokenCookie"; 6 | 7 | const Layout = ({ children }) => { 8 | const [isActive, setIsActive] = useState(false); 9 | useTokenCookie(); 10 | return ( 11 | <> 12 | 13 | What to Code 14 | 18 | 22 | 28 | 34 | 40 | 46 | 52 | 58 | 64 | 70 | 76 | 83 | 90 | 97 | 104 | 105 | 110 | 115 | 116 | 121 | 122 |
123 | 178 |
179 |
180 |
{children}
181 |
182 | 183 | ); 184 | }; 185 | 186 | export default Layout; 187 | -------------------------------------------------------------------------------- /frontend/components/PopularTagsAside.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { getPopularTags } from "../apiClient"; 3 | import { withRouter } from "next/router"; 4 | import classNames from "classnames"; 5 | import TagLink from "./TagLink"; 6 | 7 | const PopularTagsAside = ({ router }) => { 8 | const [popularTags, setPopularTags] = useState([]); 9 | const [isLoading, setIsLoading] = useState(false); 10 | const [error, setError] = useState(null); 11 | const setErrorToDefault = () => setError(null); 12 | const retrievePopularTags = async () => { 13 | try { 14 | setIsLoading(true); 15 | let response = await getPopularTags(); 16 | setPopularTags(response.data); 17 | } catch (error) { 18 | setError(error); 19 | } finally { 20 | setIsLoading(false); 21 | setErrorToDefault(); 22 | } 23 | }; 24 | 25 | useEffect(() => { 26 | retrievePopularTags(); 27 | }, []); 28 | 29 | let content = null; 30 | 31 | if (isLoading) { 32 | content =

Loading...

; 33 | } else if (error) { 34 | content =

There was an error...

; 35 | } else { 36 | content = ( 37 | 52 | ); 53 | } 54 | 55 | return ( 56 | 60 | ); 61 | }; 62 | 63 | export default withRouter(PopularTagsAside); 64 | -------------------------------------------------------------------------------- /frontend/components/SideMenu.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const SideMenu = () => { 4 | return ( 5 | 19 | ); 20 | }; 21 | 22 | export default SideMenu; 23 | -------------------------------------------------------------------------------- /frontend/components/TagLink.js: -------------------------------------------------------------------------------- 1 | import { withRouter } from "next/router"; 2 | import Link from "next/link"; 3 | 4 | const TagLink = ({ tag, router, children }) => { 5 | let query = { ...router.query }; 6 | if (tag === undefined && query.tag !== undefined) { 7 | delete query.tag; 8 | } else { 9 | query.tag = tag; 10 | } 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | }; 17 | 18 | export default withRouter(TagLink); 19 | -------------------------------------------------------------------------------- /frontend/constants.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bykof/what-to-code/5534879468b2495ed58e0fd5ee8ad1df9e22a42f/frontend/constants.js -------------------------------------------------------------------------------- /frontend/hooks/useLikesCookie.js: -------------------------------------------------------------------------------- 1 | import { useCookies } from "react-cookie"; 2 | 3 | export default () => { 4 | const [cookies, setCookie] = useCookies(["likes"]); 5 | 6 | const inTenYears = new Date(); 7 | inTenYears.setFullYear(inTenYears.getFullYear() + 10); 8 | 9 | const savedLikes = cookies.likes ? cookies.likes : {}; 10 | 11 | const updateCookie = () => { 12 | setCookie("likes", savedLikes, { 13 | expires: inTenYears, 14 | }); 15 | }; 16 | 17 | if (!cookies.likes) { 18 | updateCookie(); 19 | } 20 | 21 | const isLiked = (id) => { 22 | return (id in savedLikes); 23 | }; 24 | 25 | const like = (id) => { 26 | if (isLiked(id)) { 27 | delete savedLikes[id]; 28 | } else { 29 | savedLikes[id] = true; 30 | } 31 | updateCookie(); 32 | } 33 | 34 | return { 35 | isLiked, like 36 | }; 37 | }; -------------------------------------------------------------------------------- /frontend/hooks/useTokenCookie.js: -------------------------------------------------------------------------------- 1 | import { useCookies } from 'react-cookie'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | 4 | const useTokenCookie = () => { 5 | const [cookies, setCookie] = useCookies(['token']); 6 | if (!cookies.token) { 7 | const inTenYears = new Date(); 8 | inTenYears.setFullYear(inTenYears.getFullYear() + 10); 9 | console.log('set cookie'); 10 | setCookie('token', uuidv4(), { 11 | expires: inTenYears, 12 | }); 13 | } 14 | return cookies.token; 15 | }; 16 | 17 | export default useTokenCookie; 18 | -------------------------------------------------------------------------------- /frontend/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | API_URL: process.env.API_URL || 'http://127.0.0.1:8000', 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "what-to-code", 3 | "version": "1.0.1", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "next dev", 8 | "start": "next start", 9 | "build": "next build" 10 | }, 11 | "dependencies": { 12 | "@fortawesome/fontawesome-svg-core": "^6.6.0", 13 | "@fortawesome/free-brands-svg-icons": "^6.6.0", 14 | "@fortawesome/free-regular-svg-icons": "^6.6.0", 15 | "@fortawesome/free-solid-svg-icons": "^6.6.0", 16 | "@fortawesome/react-fontawesome": "^0.2.2", 17 | "axios": "^1.7.7", 18 | "bulma": "^1.0.2", 19 | "classnames": "^2.5.1", 20 | "eslint-config-next": "^15.0.3", 21 | "next": "^15.0.3", 22 | "react": "^18.3.1", 23 | "react-cookie": "^7.2.2", 24 | "react-dom": "^18.3.1", 25 | "react-google-recaptcha-v3": "^1.10.1", 26 | "react-infinite-scroller": "^1.2.6", 27 | "sass": "^1.81.0", 28 | "uuid": "^11.0.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/pages/_app.js: -------------------------------------------------------------------------------- 1 | import "../styles.sass"; 2 | import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3"; 3 | 4 | export default ({ Component, pageProps }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /frontend/pages/about.js: -------------------------------------------------------------------------------- 1 | import Layout from "../components/Layout"; 2 | import SideMenu from "../components/SideMenu"; 3 | import Link from "next/link"; 4 | 5 | const About = () => { 6 | return ( 7 | 8 |
9 |
10 | 11 |
12 |
13 |

About

14 |
15 |

16 | What To Code was created during the corona pandemic.
17 |
18 | It helps developers to gather ideas for their next coding project. 19 |

20 |
21 |

22 | The software is completely{" "} 23 | 24 | open source 25 | {" "} 26 | and the hosting costs are paid by the maintainer. 27 |
28 | There will be no ads, no registration, and{" "} 29 | no tracking in the future.
30 | I promise 🤞! 31 |

32 |
33 |

34 | Michael Bykovski ( 35 | http://bykovski.de) is the creator 36 | of this project. Feel free to contact him via the{" "} 37 | 38 | Contact Page 39 | 40 | . 41 |

42 |
43 |

44 | This page is built with{" "} 45 | 46 | Next.js 47 | {" "} 48 | and FastAPI and ❤️. 49 |

50 |
51 |
52 |
53 |
54 | ); 55 | }; 56 | 57 | export default About; 58 | -------------------------------------------------------------------------------- /frontend/pages/contact.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Layout from "../components/Layout"; 3 | import SideMenu from '../components/SideMenu'; 4 | 5 | const Contact = () => { 6 | return ( 7 | 8 |
9 |
10 | 11 |
12 |
13 |

Contact

14 |
15 |

16 | Email: bykof@me.com 17 |

18 |
19 |

20 | Please make a GitHub issue for feature requests: 21 |
22 | 23 | https://github.com/bykof/what-to-code 24 | 25 | . 26 |

27 |
28 |
29 |
30 |
31 | ); 32 | }; 33 | 34 | export default Contact; 35 | -------------------------------------------------------------------------------- /frontend/pages/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Router, { useRouter } from "next/router"; 3 | import Layout from "../components/Layout"; 4 | import IdeaOrder, { POPULAR } from "../components/IdeaOrder"; 5 | 6 | import IdeasCardList from "../components/IdeasCardList"; 7 | import PopularTagsAside from "../components/PopularTagsAside"; 8 | import TagLink from "../components/TagLink"; 9 | import SideMenu from "../components/SideMenu"; 10 | 11 | import "@fortawesome/fontawesome-svg-core/styles.css"; 12 | import { config } from "@fortawesome/fontawesome-svg-core"; 13 | config.autoAddCss = false; 14 | 15 | const Index = ({ order }) => { 16 | const router = useRouter(); 17 | const [orderType, setOrderType] = useState(order || POPULAR); 18 | const setOrder = (orderType) => { 19 | Router.replace({ 20 | pathname: "/", 21 | query: { ...router.query, order: orderType }, 22 | }); 23 | setOrderType(orderType); 24 | }; 25 | 26 | return ( 27 | 28 |
29 |
30 | 31 |
32 | 33 |
34 |
35 | 36 |
37 | {router.query.tag ? ( 38 |
39 | 40 | #{router.query.tag} 41 | remove filter 42 | 43 |
44 | ) : null} 45 | 46 |
47 |
48 |
49 |
50 | ); 51 | }; 52 | 53 | export async function getServerSideProps(context) { 54 | return { 55 | props: { 56 | order: context.query.order || null, 57 | }, 58 | }; 59 | } 60 | 61 | export default Index; 62 | -------------------------------------------------------------------------------- /frontend/pages/privacy.js: -------------------------------------------------------------------------------- 1 | import Layout from "../components/Layout"; 2 | import SideMenu from "../components/SideMenu"; 3 | import Link from "next/link"; 4 | 5 | const About = () => { 6 | return ( 7 | 8 |
9 |
10 | 11 |
12 |
13 |

Privacy Policy for What To Code

14 |
15 |

16 | At What To Code, accessible from what-to-code.com, one of our main 17 | priorities is the privacy of our visitors. This Privacy Policy 18 | document contains types of information that is collected and 19 | recorded by What To Code and how we use it. 20 |

21 |
22 |

23 | If you have additional questions or require more information about 24 | our Privacy Policy, do not hesitate to contact us. 25 |

26 |
27 |

28 | This Privacy Policy applies only to our online activities and is 29 | valid for visitors to our website with regards to the information 30 | that they shared and/or collect in What To Code. This policy is not 31 | applicable to any information collected offline or via channels 32 | other than this website. 33 |

34 |
35 |

Consent

36 |
37 |

38 | By using our website, you hereby consent to our Privacy Policy and 39 | agree to its terms. 40 |

41 |
42 |

Information we collect

43 |
44 |

45 | The personal information that you are asked to provide, and the 46 | reasons why you are asked to provide it, will be made clear to you 47 | at the point we ask you to provide your personal information. 48 |

49 |
50 |

51 | If you contact us directly, we may receive additional information 52 | about you such as your name, email address, phone number, the 53 | contents of the message and/or attachments you may send us, and any 54 | other information you may choose to provide. 55 |

56 |
57 |

How we use your information

58 |
59 |

60 | We use the information we collect in various ways, including to: 61 |

62 |
63 |
    64 |
  • Provide, operate, and maintain our webste
  • 65 |
  • Improve, personalize, and expand our webste
  • 66 |
  • Store anonymous information about likes
  • 67 |
68 |
69 |

Log Files

70 |
71 |

72 | What To Code follows a standard procedure of using log files. These 73 | files log visitors when they visit websites. All hosting companies 74 | do this and a part of hosting services' analytics. The information 75 | collected by log files include date and time stamp, 76 | referring/exit pages, and possibly the number of clicks. These are 77 | not linked to any information that is personally identifiable. The 78 | purpose of the information is for analyzing trends, administering 79 | the site, tracking users' movement on the website, and gathering 80 | demographic information. 81 |

82 |
83 |

Cookies and Web Beacons

84 |
85 |

86 | Like any other website, What To Code uses 'cookies'. These cookies 87 | are used to generate a uuid for every user, so that likes can be 88 | counted properly on the webpage for every user. 89 |

90 |
91 |

92 | For more general information on cookies, please read{" "} 93 | 94 | "What Are Cookies" 95 | 96 | . 97 |

98 |
99 |

Advertising Partners Privacy Policies

100 |
101 |

102 | What To Code has not any advertising partners. 103 |

104 |
105 |

106 | Note that What To Code has no access to or control over these 107 | cookies that are used by third-party advertisers. 108 |

109 |
110 |

Third Party Privacy Policies

111 |
112 |

113 | What To Code's Privacy Policy does not apply to other advertisers or 114 | websites. Thus, we are advising you to consult the respective 115 | Privacy Policies of these third-party ad servers for more detailed 116 | information. It may include their practices and instructions about 117 | how to opt-out of certain options.{" "} 118 |

119 |
120 |

121 | You can choose to disable cookies through your individual browser 122 | options. To know more detailed information about cookie management 123 | with specific web browsers, it can be found at the browsers' 124 | respective websites. 125 |

126 |
127 |

CCPA Privacy Rights (Do Not Sell My Personal Information)

128 |
129 |

130 | Under the CCPA, among other rights, California consumers have the 131 | right to: 132 |

133 |
134 |

135 | Request that a business that collects a consumer's personal data 136 | disclose the categories and specific pieces of personal data that a 137 | business has collected about consumers. 138 |

139 |
140 |

141 | Request that a business delete any personal data about the consumer 142 | that a business has collected. 143 |

144 |
145 |

146 | Request that a business that sells a consumer's personal data, not 147 | sell the consumer's personal data. 148 |

149 |
150 |

151 | If you make a request, we have one month to respond to you. If you 152 | would like to exercise any of these rights, please contact us. 153 |

154 |
155 |

GDPR Data Protection Rights

156 |
157 |

158 | We would like to make sure you are fully aware of all of your data 159 | protection rights. Every user is entitled to the following: 160 |

161 |
162 |

163 | The right to access – You have the right to request copies of your 164 | personal data. We may charge you a small fee for this service. 165 |

166 |
167 |

168 | The right to rectification – You have the right to request that we 169 | correct any information you believe is inaccurate. You also have the 170 | right to request that we complete the information you believe is 171 | incomplete. 172 |

173 |
174 |

175 | The right to erasure – You have the right to request that we erase 176 | your personal data, under certain conditions. 177 |

178 |
179 |

180 | The right to restrict processing – You have the right to request 181 | that we restrict the processing of your personal data, under certain 182 | conditions. 183 |

184 |
185 |

186 | The right to object to processing – You have the right to object to 187 | our processing of your personal data, under certain conditions. 188 |

189 |
190 |

191 | The right to data portability – You have the right to request that 192 | we transfer the data that we have collected to another organization, 193 | or directly to you, under certain conditions. 194 |

195 |
196 |

197 | If you make a request, we have one month to respond to you. If you 198 | would like to exercise any of these rights, please contact us. 199 |

200 |
201 |

Children's Information

202 |
203 |

204 | Another part of our priority is adding protection for children while 205 | using the internet. We encourage parents and guardians to observe, 206 | participate in, and/or monitor and guide their online activity. 207 |

208 |
209 |

210 | What To Code does not knowingly collect any Personal Identifiable 211 | Information from children under the age of 13. If you think that 212 | your child provided this kind of information on our website, we 213 | strongly encourage you to contact us immediately and we will do our 214 | best efforts to promptly remove such information from our records. 215 |

216 |
217 |
218 |
219 |
220 | ); 221 | }; 222 | 223 | export default About; 224 | -------------------------------------------------------------------------------- /frontend/pages/random.js: -------------------------------------------------------------------------------- 1 | import Layout from "../components/Layout"; 2 | import { useState, useEffect } from "react"; 3 | import Idea from "../components/Idea"; 4 | import { getRandomIdea, getIdea } from "../apiClient"; 5 | import SideMenu from '../components/SideMenu'; 6 | 7 | export default () => { 8 | const [idea, setIdea] = useState(); 9 | const retrieveRandomIdea = async () => { 10 | try { 11 | const response = await getRandomIdea(); 12 | setIdea(response.data); 13 | } catch (error) { 14 | console.error(error); 15 | } 16 | }; 17 | const retrieveIdea = async (id) => { 18 | let response = await getIdea(id); 19 | setIdea(response.data); 20 | }; 21 | 22 | useEffect(() => { 23 | retrieveRandomIdea().catch(console.error); 24 | }, []); 25 | 26 | let content =
Loading...
; 27 | if (idea) { 28 | content = ( 29 | { 36 | retrieveIdea(idea.id); 37 | }} 38 | /> 39 | ); 40 | } 41 | 42 | return ( 43 | 44 |
45 |
46 | 47 |
48 |
{content}
49 |
50 |
51 |
52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /frontend/pages/submit.js: -------------------------------------------------------------------------------- 1 | import Router from "next/router"; 2 | import Layout from "../components/Layout"; 3 | import { useState, useEffect, useRef, useCallback } from "react"; 4 | import { createIdea } from "../apiClient"; 5 | import { RECENT } from "../components/IdeaOrder"; 6 | import { GoogleReCaptcha } from "react-google-recaptcha-v3"; 7 | 8 | export default () => { 9 | const [token, setToken] = useState(); 10 | const [error, setError] = useState(null); 11 | const [isCreating, setIsCreating] = useState(false); 12 | const [tagsInputError, setTagsInputError] = useState(null); 13 | const [titleInput, setTitleInput] = useState(""); 14 | const [titleInputError, setTitleInputError] = useState(null); 15 | const [descriptionInput, setDescriptionInput] = useState(""); 16 | const [tagInput, setTagInput] = useState(""); 17 | const [tags, setTags] = useState([]); 18 | const tagInputElement = useRef(null); 19 | const [refreshReCaptcha, setRefreshReCaptcha] = useState(false); 20 | 21 | const onVerify = useCallback((token) => { 22 | setToken(token); 23 | }, []); 24 | 25 | const removeTag = (key) => { 26 | const index = tags.findIndex((tag) => tag === key); 27 | console.log(index); 28 | if (index < 0) { 29 | return; 30 | } 31 | const copy = [...tags]; 32 | copy.splice(index, 1); 33 | setTags(copy); 34 | setTagsInputError(null); 35 | }; 36 | 37 | const addTag = (value) => { 38 | setTags([...new Set([...tags, value])]); 39 | setTagInput(""); 40 | }; 41 | 42 | const onTagInput = (event) => { 43 | const value = event.target.value; 44 | const regex = /([\w]+)[\W]+/gm; 45 | const regexMatches = value.matchAll(regex); 46 | const matches = []; 47 | 48 | for (const regexMatch of regexMatches) { 49 | matches.push(regexMatch[1]); 50 | } 51 | 52 | if (tags.length >= 6) { 53 | setTagsInputError("Only six tags are allowed!"); 54 | return; 55 | } 56 | setTagsInputError(null); 57 | 58 | if (matches.length !== 0) { 59 | addTag(matches[0]); 60 | } else { 61 | setTagInput(value); 62 | } 63 | }; 64 | 65 | const onTagKeyPress = (event) => { 66 | if (event.key === "Enter") { 67 | event.preventDefault(); 68 | if (tags.length >= 6) { 69 | setTagsInputError("Only six tags are allowed!"); 70 | return; 71 | } 72 | setTagsInputError(null); 73 | addTag(tagInputElement.current.value); 74 | } 75 | }; 76 | 77 | const onSubmit = async (event) => { 78 | console.log(event); 79 | 80 | event.preventDefault(); 81 | 82 | if (titleInput === "") { 83 | setTitleInputError("Please fill out this field."); 84 | return; 85 | } 86 | 87 | setIsCreating(true); 88 | setError(null); 89 | try { 90 | let response = await createIdea({ 91 | captchaToken: token, 92 | title: titleInput, 93 | description: descriptionInput, 94 | tags, 95 | }); 96 | Router.push({ pathname: "/", query: { order: RECENT } }); 97 | } catch (error) { 98 | console.error(error); 99 | setError("There was an error, please check the input fields!"); 100 | } finally { 101 | setIsCreating(false); 102 | } 103 | 104 | setRefreshReCaptcha(r => !r); 105 | }; 106 | 107 | useEffect(() => { 108 | if (titleInput !== "") { 109 | setTitleInputError(null); 110 | } 111 | }, [titleInput]); 112 | 113 | let renderedError; 114 | if (error) { 115 | renderedError = ( 116 |
117 | 123 | {error} 124 |
125 | ); 126 | } 127 | 128 | return ( 129 | 130 |
131 |
132 |
133 |
134 | 135 | Tell the world What to Code! 136 | 137 |
138 | {renderedError} 139 |
140 | 143 |
144 | setTitleInput(event.target.value)} 146 | value={titleInput} 147 | maxLength="100" 148 | id="title" 149 | className="input" 150 | type="text" 151 | placeholder="A small step for a human but a big step for mankind" 152 | /> 153 |
154 |
155 | {titleInputError ? ( 156 |

{titleInputError}

157 | ) : null} 158 |
159 | 162 |
163 |