├── .github └── workflows │ └── release.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── app.css ├── app.py ├── app.spec ├── docs ├── cli-screenshot.png ├── getting_started.md ├── getting_started_with_docker.md └── releases-page-screenshot.png ├── exercises ├── library_management_system.md └── scooter_rental_service.md.draft ├── exercises_test_suites ├── docker_compose_library_management_system.yml ├── library_management_system │ ├── Dockerfile │ ├── conftest.py │ ├── test_borrowing.py │ ├── test_borrowing_history.py │ ├── test_item_management.py │ ├── test_promotions.py │ ├── test_returning.py │ └── test_user_management.py └── scooter_rental_service │ ├── Dockerfile │ ├── conftest.py │ ├── test_reservations_and_rides.py │ ├── test_scooter_management.py │ └── test_user_management.py ├── exercises_utils.py ├── project_generators └── base_project_generator.py └── tests └── project_generators └── test_base_project_generator.py /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, windows-latest, macos-latest] 14 | 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v2 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: '3.12' 23 | 24 | - name: Install pipenv 25 | run: pip install pipenv 26 | 27 | - name: Install dependencies 28 | run: pipenv install --deploy --ignore-pipfile 29 | 30 | - name: Build executable 31 | run: | 32 | pipenv run pyinstaller app.spec 33 | env: 34 | DISPLAY: ":99.0" 35 | 36 | - name: Archive artifacts 37 | uses: actions/upload-artifact@v2 38 | with: 39 | name: executable-${{ matrix.os }} 40 | path: dist/* 41 | 42 | release: 43 | needs: build 44 | runs-on: ubuntu-latest 45 | steps: 46 | - name: Checkout repository 47 | uses: actions/checkout@v2 48 | 49 | - name: Download artifacts (Ubuntu) 50 | uses: actions/download-artifact@v2 51 | with: 52 | name: executable-ubuntu-latest 53 | path: dist/ 54 | 55 | - name: Download artifacts (Windows) 56 | uses: actions/download-artifact@v2 57 | with: 58 | name: executable-windows-latest 59 | path: dist/ 60 | 61 | - name: Download artifacts (macOS) 62 | uses: actions/download-artifact@v2 63 | with: 64 | name: executable-macos-latest 65 | path: dist/ 66 | 67 | - name: Create a unique tag 68 | id: create_tag 69 | run: echo "RELEASE_TAG=sissues-$(date +%Y%m%d%H%M)" >> $GITHUB_ENV 70 | 71 | - name: Create Release 72 | uses: softprops/action-gh-release@v2 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.PAT }} 75 | with: 76 | tag_name: ${{ env.RELEASE_TAG }} 77 | name: Release ${{ env.RELEASE_TAG }} 78 | draft: false 79 | prerelease: false 80 | files: dist/* 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | #*.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | .idea/ 163 | 164 | solutions/ 165 | 166 | my_solutions/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Thank you for considering contributing to Skill Issues Killer! We welcome contributions of all kinds, whether you're adding a new project, improving existing features, or fixing bugs. Below are the guidelines to help you get started. 4 | 5 | ## Adding a New Project 6 | 7 | To contribute a new project, please ensure your addition includes the following: 8 | 9 | 1. **Project Specification** 10 | - Create a detailed specification of the project in a markdown file. 11 | - The specification should include examples and clearly explain the requirements and objectives of the project. 12 | 13 | 2. **Comprehensive Test Suite** 14 | - Provide a comprehensive test suite to validate the project. 15 | - The test suite should be programming language agnostic (tests at the API level). 16 | - Include a Dockerfile to run the test suite. 17 | 18 | 3. **Docker Compose File** 19 | - Provide a docker-compose file. 20 | - This file should instrument the user's API implementation with the test suite, ensuring that the tests are executed correctly. 21 | 22 | 23 | ## Contributing Features, Fixes, or Improvements 24 | 25 | If you want to contribute by adding new features, fixing issues, or making other improvements, please follow these steps: 26 | 27 | 1. **Fork the Repository** 28 | - Fork the repository to your own GitHub account. 29 | 30 | 2. **Make Your Changes** 31 | - Make the necessary changes in your forked repository. 32 | 33 | 3. **Submit a Pull Request (PR)** 34 | - Create a PR to suggest your changes. 35 | - Ensure your PR is well-documented, explaining the changes and why they are necessary. 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 sissues 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | textualize = "*" 8 | textual = "*" 9 | requests = "*" 10 | flask = "*" 11 | pyinstaller = "*" 12 | macholib = "*" 13 | 14 | 15 | [dev-packages] 16 | textual-dev = "*" 17 | pytest = "*" 18 | 19 | [requires] 20 | python_version = "3.12" 21 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "72fe13896c0f016160db158aa0fdbfbd2abd97013644488f302fbc4cd3e89d6f" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.12" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "altgraph": { 20 | "hashes": [ 21 | "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406", 22 | "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff" 23 | ], 24 | "version": "==0.17.4" 25 | }, 26 | "blinker": { 27 | "hashes": [ 28 | "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01", 29 | "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83" 30 | ], 31 | "markers": "python_version >= '3.8'", 32 | "version": "==1.8.2" 33 | }, 34 | "certifi": { 35 | "hashes": [ 36 | "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", 37 | "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" 38 | ], 39 | "markers": "python_version >= '3.6'", 40 | "version": "==2024.7.4" 41 | }, 42 | "charset-normalizer": { 43 | "hashes": [ 44 | "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", 45 | "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", 46 | "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", 47 | "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", 48 | "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", 49 | "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", 50 | "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", 51 | "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", 52 | "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", 53 | "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", 54 | "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", 55 | "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", 56 | "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", 57 | "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", 58 | "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", 59 | "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", 60 | "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", 61 | "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", 62 | "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", 63 | "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", 64 | "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", 65 | "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", 66 | "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", 67 | "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", 68 | "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", 69 | "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", 70 | "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", 71 | "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", 72 | "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", 73 | "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", 74 | "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", 75 | "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", 76 | "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", 77 | "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", 78 | "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", 79 | "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", 80 | "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", 81 | "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", 82 | "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", 83 | "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", 84 | "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", 85 | "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", 86 | "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", 87 | "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", 88 | "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", 89 | "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", 90 | "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", 91 | "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", 92 | "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", 93 | "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", 94 | "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", 95 | "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", 96 | "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", 97 | "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", 98 | "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", 99 | "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", 100 | "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", 101 | "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", 102 | "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", 103 | "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", 104 | "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", 105 | "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", 106 | "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", 107 | "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", 108 | "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", 109 | "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", 110 | "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", 111 | "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", 112 | "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", 113 | "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", 114 | "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", 115 | "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", 116 | "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", 117 | "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", 118 | "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", 119 | "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", 120 | "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", 121 | "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", 122 | "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", 123 | "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", 124 | "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", 125 | "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", 126 | "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", 127 | "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", 128 | "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", 129 | "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", 130 | "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", 131 | "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", 132 | "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", 133 | "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" 134 | ], 135 | "markers": "python_full_version >= '3.7.0'", 136 | "version": "==3.3.2" 137 | }, 138 | "click": { 139 | "hashes": [ 140 | "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", 141 | "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" 142 | ], 143 | "markers": "python_version >= '3.7'", 144 | "version": "==8.1.7" 145 | }, 146 | "colorama": { 147 | "hashes": [ 148 | "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", 149 | "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" 150 | ], 151 | "markers": "platform_system == 'Windows'", 152 | "version": "==0.4.6" 153 | }, 154 | "flask": { 155 | "hashes": [ 156 | "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3", 157 | "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842" 158 | ], 159 | "index": "pypi", 160 | "version": "==3.0.3" 161 | }, 162 | "idna": { 163 | "hashes": [ 164 | "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", 165 | "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" 166 | ], 167 | "markers": "python_version >= '3.5'", 168 | "version": "==3.7" 169 | }, 170 | "importlib-metadata": { 171 | "hashes": [ 172 | "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f", 173 | "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812" 174 | ], 175 | "markers": "python_version < '3.10'", 176 | "version": "==8.0.0" 177 | }, 178 | "itsdangerous": { 179 | "hashes": [ 180 | "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", 181 | "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173" 182 | ], 183 | "markers": "python_version >= '3.8'", 184 | "version": "==2.2.0" 185 | }, 186 | "jinja2": { 187 | "hashes": [ 188 | "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", 189 | "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" 190 | ], 191 | "markers": "python_version >= '3.7'", 192 | "version": "==3.1.4" 193 | }, 194 | "linkify-it-py": { 195 | "hashes": [ 196 | "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", 197 | "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79" 198 | ], 199 | "version": "==2.0.3" 200 | }, 201 | "macholib": { 202 | "hashes": [ 203 | "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30", 204 | "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c" 205 | ], 206 | "index": "pypi", 207 | "version": "==1.16.3" 208 | }, 209 | "markdown-it-py": { 210 | "extras": [ 211 | "linkify", 212 | "plugins" 213 | ], 214 | "hashes": [ 215 | "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", 216 | "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" 217 | ], 218 | "markers": "python_version >= '3.8'", 219 | "version": "==3.0.0" 220 | }, 221 | "markupsafe": { 222 | "hashes": [ 223 | "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", 224 | "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", 225 | "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", 226 | "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", 227 | "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", 228 | "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", 229 | "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", 230 | "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", 231 | "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", 232 | "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", 233 | "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", 234 | "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", 235 | "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", 236 | "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", 237 | "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", 238 | "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", 239 | "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", 240 | "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", 241 | "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", 242 | "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", 243 | "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", 244 | "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", 245 | "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", 246 | "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", 247 | "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", 248 | "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", 249 | "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", 250 | "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", 251 | "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", 252 | "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", 253 | "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", 254 | "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", 255 | "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", 256 | "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", 257 | "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", 258 | "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", 259 | "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", 260 | "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", 261 | "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", 262 | "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", 263 | "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", 264 | "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", 265 | "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", 266 | "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", 267 | "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", 268 | "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", 269 | "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", 270 | "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", 271 | "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", 272 | "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", 273 | "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", 274 | "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", 275 | "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", 276 | "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", 277 | "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", 278 | "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", 279 | "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", 280 | "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", 281 | "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", 282 | "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" 283 | ], 284 | "markers": "python_version >= '3.7'", 285 | "version": "==2.1.5" 286 | }, 287 | "mdit-py-plugins": { 288 | "hashes": [ 289 | "sha256:1020dfe4e6bfc2c79fb49ae4e3f5b297f5ccd20f010187acc52af2921e27dc6a", 290 | "sha256:834b8ac23d1cd60cec703646ffd22ae97b7955a6d596eb1d304be1e251ae499c" 291 | ], 292 | "version": "==0.4.1" 293 | }, 294 | "mdurl": { 295 | "hashes": [ 296 | "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", 297 | "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" 298 | ], 299 | "markers": "python_version >= '3.7'", 300 | "version": "==0.1.2" 301 | }, 302 | "packaging": { 303 | "hashes": [ 304 | "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", 305 | "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" 306 | ], 307 | "markers": "python_version >= '3.8'", 308 | "version": "==24.1" 309 | }, 310 | "pefile": { 311 | "hashes": [ 312 | "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc", 313 | "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6" 314 | ], 315 | "markers": "sys_platform == 'win32'", 316 | "version": "==2023.2.7" 317 | }, 318 | "pygments": { 319 | "hashes": [ 320 | "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", 321 | "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a" 322 | ], 323 | "markers": "python_version >= '3.8'", 324 | "version": "==2.18.0" 325 | }, 326 | "pyinstaller": { 327 | "hashes": [ 328 | "sha256:2bf4de17a1c63c0b797b38e13bfb4d03b5ee7c0a68e28b915a7eaacf6b76087f", 329 | "sha256:43709c70b1da8441a730327a8ed362bfcfdc3d42c1bf89f3e2b0a163cc4e7d33", 330 | "sha256:4e3e50743c091a06e6d01c59bdd6d03967b453ee5384a9e790759be4129db4a4", 331 | "sha256:5ced2e83acf222b936ea94abc5a5cc96588705654b39138af8fb321d9cf2b954", 332 | "sha256:7bf0c13c5a8560c89540746ae742f4f4b82290e95a6b478374d9f34959fe25d6", 333 | "sha256:a0f378f64ad0655d11ade9fde7877e7573fd3d5066231608ce7dfa9040faecdd", 334 | "sha256:b041be2fe78da47a269604d62c940d68c62f9a3913bdf64af4123f7689d47099", 335 | "sha256:da994aba14c5686db88796684de265a8665733b4df09b939f7ebdf097d18df72", 336 | "sha256:f15c1ef11ed5ceb32447dfbdab687017d6adbef7fc32aa359d584369bfe56eda", 337 | "sha256:f18a3d551834ef8fb7830d48d4cc1527004d0e6b51ded7181e78374ad6111846", 338 | "sha256:f2fc568de3d6d2a176716a3fc9f20da06d351e8bea5ddd10ecb5659fce3a05b0", 339 | "sha256:f4a75c552facc2e2a370f1e422b971b5e5cdb4058ff38cea0235aa21fc0b378f" 340 | ], 341 | "index": "pypi", 342 | "version": "==6.9.0" 343 | }, 344 | "pyinstaller-hooks-contrib": { 345 | "hashes": [ 346 | "sha256:8bf0775771fbaf96bcd2f4dfd6f7ae6c1dd1b1efe254c7e50477b3c08e7841d8", 347 | "sha256:fd5f37dcf99bece184e40642af88be16a9b89613ecb958a8bd1136634fc9fac5" 348 | ], 349 | "markers": "python_version >= '3.7'", 350 | "version": "==2024.7" 351 | }, 352 | "pywin32-ctypes": { 353 | "hashes": [ 354 | "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60", 355 | "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7" 356 | ], 357 | "markers": "sys_platform == 'win32'", 358 | "version": "==0.2.2" 359 | }, 360 | "requests": { 361 | "hashes": [ 362 | "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", 363 | "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" 364 | ], 365 | "index": "pypi", 366 | "version": "==2.32.3" 367 | }, 368 | "rich": { 369 | "hashes": [ 370 | "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", 371 | "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432" 372 | ], 373 | "markers": "python_full_version >= '3.7.0'", 374 | "version": "==13.7.1" 375 | }, 376 | "setuptools": { 377 | "hashes": [ 378 | "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5", 379 | "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc" 380 | ], 381 | "markers": "python_version >= '3.8'", 382 | "version": "==70.3.0" 383 | }, 384 | "textual": { 385 | "hashes": [ 386 | "sha256:14174ce8d49016a85aa6c0669d0881b5419e98cf46d429f263314295409ed262", 387 | "sha256:a9886eb96bd6391b8795244d2b8fe592204556c42264ea7513a1211584e17366" 388 | ], 389 | "index": "pypi", 390 | "version": "==0.72.0" 391 | }, 392 | "textualize": { 393 | "hashes": [ 394 | "sha256:cb06c655af7fa9b42e86c8ab4d6203493777179f8e3fe7cbf1cb703c0bffd2c2" 395 | ], 396 | "index": "pypi", 397 | "version": "==0.1" 398 | }, 399 | "typing-extensions": { 400 | "hashes": [ 401 | "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", 402 | "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" 403 | ], 404 | "markers": "python_version >= '3.8'", 405 | "version": "==4.12.2" 406 | }, 407 | "uc-micro-py": { 408 | "hashes": [ 409 | "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", 410 | "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5" 411 | ], 412 | "markers": "python_version >= '3.7'", 413 | "version": "==1.0.3" 414 | }, 415 | "urllib3": { 416 | "hashes": [ 417 | "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", 418 | "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" 419 | ], 420 | "markers": "python_version >= '3.8'", 421 | "version": "==2.2.2" 422 | }, 423 | "werkzeug": { 424 | "hashes": [ 425 | "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18", 426 | "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8" 427 | ], 428 | "markers": "python_version >= '3.8'", 429 | "version": "==3.0.3" 430 | }, 431 | "zipp": { 432 | "hashes": [ 433 | "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19", 434 | "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c" 435 | ], 436 | "markers": "python_version >= '3.8'", 437 | "version": "==3.19.2" 438 | } 439 | }, 440 | "develop": { 441 | "aiohttp": { 442 | "hashes": [ 443 | "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8", 444 | "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c", 445 | "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475", 446 | "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed", 447 | "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf", 448 | "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372", 449 | "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81", 450 | "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f", 451 | "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1", 452 | "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd", 453 | "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a", 454 | "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb", 455 | "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46", 456 | "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de", 457 | "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78", 458 | "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c", 459 | "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771", 460 | "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb", 461 | "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430", 462 | "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233", 463 | "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156", 464 | "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9", 465 | "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59", 466 | "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888", 467 | "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c", 468 | "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c", 469 | "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da", 470 | "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424", 471 | "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2", 472 | "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb", 473 | "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8", 474 | "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a", 475 | "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10", 476 | "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0", 477 | "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09", 478 | "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031", 479 | "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4", 480 | "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3", 481 | "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa", 482 | "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a", 483 | "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe", 484 | "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a", 485 | "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2", 486 | "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1", 487 | "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323", 488 | "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b", 489 | "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b", 490 | "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106", 491 | "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac", 492 | "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6", 493 | "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832", 494 | "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75", 495 | "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6", 496 | "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d", 497 | "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72", 498 | "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db", 499 | "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a", 500 | "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da", 501 | "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678", 502 | "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b", 503 | "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24", 504 | "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed", 505 | "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f", 506 | "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e", 507 | "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58", 508 | "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a", 509 | "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342", 510 | "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558", 511 | "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2", 512 | "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551", 513 | "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595", 514 | "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee", 515 | "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11", 516 | "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d", 517 | "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7", 518 | "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f" 519 | ], 520 | "markers": "python_version >= '3.8'", 521 | "version": "==3.9.5" 522 | }, 523 | "aiosignal": { 524 | "hashes": [ 525 | "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", 526 | "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17" 527 | ], 528 | "markers": "python_version >= '3.7'", 529 | "version": "==1.3.1" 530 | }, 531 | "async-timeout": { 532 | "hashes": [ 533 | "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", 534 | "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" 535 | ], 536 | "markers": "python_version < '3.11'", 537 | "version": "==4.0.3" 538 | }, 539 | "attrs": { 540 | "hashes": [ 541 | "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", 542 | "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" 543 | ], 544 | "markers": "python_version >= '3.7'", 545 | "version": "==23.2.0" 546 | }, 547 | "click": { 548 | "hashes": [ 549 | "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", 550 | "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" 551 | ], 552 | "markers": "python_version >= '3.7'", 553 | "version": "==8.1.7" 554 | }, 555 | "colorama": { 556 | "hashes": [ 557 | "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", 558 | "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" 559 | ], 560 | "markers": "platform_system == 'Windows'", 561 | "version": "==0.4.6" 562 | }, 563 | "exceptiongroup": { 564 | "hashes": [ 565 | "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", 566 | "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" 567 | ], 568 | "markers": "python_version < '3.11'", 569 | "version": "==1.2.2" 570 | }, 571 | "frozenlist": { 572 | "hashes": [ 573 | "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7", 574 | "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98", 575 | "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad", 576 | "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5", 577 | "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae", 578 | "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e", 579 | "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a", 580 | "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701", 581 | "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d", 582 | "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6", 583 | "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6", 584 | "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106", 585 | "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75", 586 | "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868", 587 | "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a", 588 | "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0", 589 | "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1", 590 | "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826", 591 | "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec", 592 | "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6", 593 | "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950", 594 | "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19", 595 | "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0", 596 | "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8", 597 | "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a", 598 | "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09", 599 | "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86", 600 | "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c", 601 | "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5", 602 | "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b", 603 | "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b", 604 | "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d", 605 | "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0", 606 | "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea", 607 | "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776", 608 | "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a", 609 | "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897", 610 | "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7", 611 | "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09", 612 | "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9", 613 | "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe", 614 | "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd", 615 | "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742", 616 | "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09", 617 | "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0", 618 | "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932", 619 | "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1", 620 | "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a", 621 | "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49", 622 | "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d", 623 | "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7", 624 | "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480", 625 | "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89", 626 | "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e", 627 | "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b", 628 | "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82", 629 | "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb", 630 | "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068", 631 | "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8", 632 | "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b", 633 | "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb", 634 | "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2", 635 | "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11", 636 | "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b", 637 | "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc", 638 | "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0", 639 | "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497", 640 | "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17", 641 | "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0", 642 | "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2", 643 | "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439", 644 | "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5", 645 | "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac", 646 | "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825", 647 | "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887", 648 | "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced", 649 | "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74" 650 | ], 651 | "markers": "python_version >= '3.8'", 652 | "version": "==1.4.1" 653 | }, 654 | "idna": { 655 | "hashes": [ 656 | "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", 657 | "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" 658 | ], 659 | "markers": "python_version >= '3.5'", 660 | "version": "==3.7" 661 | }, 662 | "iniconfig": { 663 | "hashes": [ 664 | "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", 665 | "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" 666 | ], 667 | "markers": "python_version >= '3.7'", 668 | "version": "==2.0.0" 669 | }, 670 | "linkify-it-py": { 671 | "hashes": [ 672 | "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", 673 | "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79" 674 | ], 675 | "version": "==2.0.3" 676 | }, 677 | "markdown-it-py": { 678 | "extras": [ 679 | "linkify", 680 | "plugins" 681 | ], 682 | "hashes": [ 683 | "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", 684 | "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" 685 | ], 686 | "markers": "python_version >= '3.8'", 687 | "version": "==3.0.0" 688 | }, 689 | "mdit-py-plugins": { 690 | "hashes": [ 691 | "sha256:1020dfe4e6bfc2c79fb49ae4e3f5b297f5ccd20f010187acc52af2921e27dc6a", 692 | "sha256:834b8ac23d1cd60cec703646ffd22ae97b7955a6d596eb1d304be1e251ae499c" 693 | ], 694 | "version": "==0.4.1" 695 | }, 696 | "mdurl": { 697 | "hashes": [ 698 | "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", 699 | "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" 700 | ], 701 | "markers": "python_version >= '3.7'", 702 | "version": "==0.1.2" 703 | }, 704 | "msgpack": { 705 | "hashes": [ 706 | "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982", 707 | "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3", 708 | "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40", 709 | "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee", 710 | "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693", 711 | "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950", 712 | "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151", 713 | "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24", 714 | "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305", 715 | "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b", 716 | "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c", 717 | "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659", 718 | "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d", 719 | "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18", 720 | "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746", 721 | "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868", 722 | "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2", 723 | "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba", 724 | "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228", 725 | "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2", 726 | "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273", 727 | "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c", 728 | "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653", 729 | "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a", 730 | "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596", 731 | "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd", 732 | "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8", 733 | "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa", 734 | "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85", 735 | "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc", 736 | "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836", 737 | "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3", 738 | "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58", 739 | "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128", 740 | "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db", 741 | "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f", 742 | "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77", 743 | "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad", 744 | "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13", 745 | "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8", 746 | "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b", 747 | "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a", 748 | "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543", 749 | "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b", 750 | "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce", 751 | "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d", 752 | "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a", 753 | "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c", 754 | "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f", 755 | "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e", 756 | "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011", 757 | "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04", 758 | "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480", 759 | "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a", 760 | "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d", 761 | "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d" 762 | ], 763 | "markers": "python_version >= '3.8'", 764 | "version": "==1.0.8" 765 | }, 766 | "multidict": { 767 | "hashes": [ 768 | "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556", 769 | "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c", 770 | "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29", 771 | "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b", 772 | "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8", 773 | "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7", 774 | "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd", 775 | "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40", 776 | "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6", 777 | "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3", 778 | "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c", 779 | "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9", 780 | "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5", 781 | "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae", 782 | "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442", 783 | "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9", 784 | "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc", 785 | "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c", 786 | "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea", 787 | "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5", 788 | "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50", 789 | "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182", 790 | "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453", 791 | "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e", 792 | "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600", 793 | "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733", 794 | "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda", 795 | "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241", 796 | "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461", 797 | "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e", 798 | "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e", 799 | "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b", 800 | "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e", 801 | "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7", 802 | "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386", 803 | "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd", 804 | "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9", 805 | "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf", 806 | "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee", 807 | "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5", 808 | "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a", 809 | "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271", 810 | "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54", 811 | "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4", 812 | "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496", 813 | "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb", 814 | "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319", 815 | "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3", 816 | "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f", 817 | "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527", 818 | "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed", 819 | "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604", 820 | "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef", 821 | "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8", 822 | "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5", 823 | "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5", 824 | "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626", 825 | "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c", 826 | "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d", 827 | "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c", 828 | "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc", 829 | "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc", 830 | "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b", 831 | "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38", 832 | "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450", 833 | "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1", 834 | "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f", 835 | "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3", 836 | "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755", 837 | "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226", 838 | "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a", 839 | "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046", 840 | "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf", 841 | "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479", 842 | "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e", 843 | "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1", 844 | "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a", 845 | "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83", 846 | "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929", 847 | "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93", 848 | "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a", 849 | "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c", 850 | "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44", 851 | "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89", 852 | "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba", 853 | "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e", 854 | "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da", 855 | "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24", 856 | "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423", 857 | "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef" 858 | ], 859 | "markers": "python_version >= '3.7'", 860 | "version": "==6.0.5" 861 | }, 862 | "packaging": { 863 | "hashes": [ 864 | "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", 865 | "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" 866 | ], 867 | "markers": "python_version >= '3.8'", 868 | "version": "==24.1" 869 | }, 870 | "pluggy": { 871 | "hashes": [ 872 | "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", 873 | "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" 874 | ], 875 | "markers": "python_version >= '3.8'", 876 | "version": "==1.5.0" 877 | }, 878 | "pygments": { 879 | "hashes": [ 880 | "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", 881 | "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a" 882 | ], 883 | "markers": "python_version >= '3.8'", 884 | "version": "==2.18.0" 885 | }, 886 | "pytest": { 887 | "hashes": [ 888 | "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343", 889 | "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977" 890 | ], 891 | "index": "pypi", 892 | "version": "==8.2.2" 893 | }, 894 | "rich": { 895 | "hashes": [ 896 | "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", 897 | "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432" 898 | ], 899 | "markers": "python_full_version >= '3.7.0'", 900 | "version": "==13.7.1" 901 | }, 902 | "textual": { 903 | "hashes": [ 904 | "sha256:14174ce8d49016a85aa6c0669d0881b5419e98cf46d429f263314295409ed262", 905 | "sha256:a9886eb96bd6391b8795244d2b8fe592204556c42264ea7513a1211584e17366" 906 | ], 907 | "index": "pypi", 908 | "version": "==0.72.0" 909 | }, 910 | "textual-dev": { 911 | "hashes": [ 912 | "sha256:bb37dd769ae6b67e1422aa97f6d6ef952e0a6d2aafe08327449e8bdd70474776", 913 | "sha256:e0366ab6f42c128d7daa37a7c418e61fe7aa83731983da990808e4bf2de922a1" 914 | ], 915 | "index": "pypi", 916 | "version": "==1.5.1" 917 | }, 918 | "tomli": { 919 | "hashes": [ 920 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 921 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 922 | ], 923 | "markers": "python_version < '3.11'", 924 | "version": "==2.0.1" 925 | }, 926 | "typing-extensions": { 927 | "hashes": [ 928 | "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", 929 | "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" 930 | ], 931 | "markers": "python_version >= '3.8'", 932 | "version": "==4.12.2" 933 | }, 934 | "uc-micro-py": { 935 | "hashes": [ 936 | "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", 937 | "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5" 938 | ], 939 | "markers": "python_version >= '3.7'", 940 | "version": "==1.0.3" 941 | }, 942 | "yarl": { 943 | "hashes": [ 944 | "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51", 945 | "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce", 946 | "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559", 947 | "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0", 948 | "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81", 949 | "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc", 950 | "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4", 951 | "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c", 952 | "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130", 953 | "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136", 954 | "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e", 955 | "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec", 956 | "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7", 957 | "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1", 958 | "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455", 959 | "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099", 960 | "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129", 961 | "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10", 962 | "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142", 963 | "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98", 964 | "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa", 965 | "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7", 966 | "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525", 967 | "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c", 968 | "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9", 969 | "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c", 970 | "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8", 971 | "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b", 972 | "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf", 973 | "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23", 974 | "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd", 975 | "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27", 976 | "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f", 977 | "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece", 978 | "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434", 979 | "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec", 980 | "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff", 981 | "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78", 982 | "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d", 983 | "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863", 984 | "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53", 985 | "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31", 986 | "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15", 987 | "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5", 988 | "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b", 989 | "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57", 990 | "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3", 991 | "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1", 992 | "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f", 993 | "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad", 994 | "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c", 995 | "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7", 996 | "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2", 997 | "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b", 998 | "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2", 999 | "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b", 1000 | "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9", 1001 | "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be", 1002 | "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e", 1003 | "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984", 1004 | "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4", 1005 | "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074", 1006 | "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2", 1007 | "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392", 1008 | "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91", 1009 | "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541", 1010 | "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf", 1011 | "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572", 1012 | "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66", 1013 | "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575", 1014 | "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14", 1015 | "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5", 1016 | "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1", 1017 | "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e", 1018 | "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551", 1019 | "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17", 1020 | "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead", 1021 | "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0", 1022 | "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe", 1023 | "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234", 1024 | "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0", 1025 | "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7", 1026 | "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34", 1027 | "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42", 1028 | "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385", 1029 | "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78", 1030 | "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be", 1031 | "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958", 1032 | "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749", 1033 | "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec" 1034 | ], 1035 | "markers": "python_version >= '3.7'", 1036 | "version": "==1.9.4" 1037 | } 1038 | } 1039 | } 1040 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Skill Issues Killer Projects Plaform 2 | 3 | Welcome to our projects platform! This platform is designed to help you build and test API projects through a series of guided exercises. This README will guide you through the installation process and provide links to additional guides to help you get started. 4 | 5 | ## Installation 6 | 7 | To get started, you need to install the platform on your machine. Follow these steps: 8 | 9 | 1. **Go to the Repository Releases on GitHub**: Navigate to the [releases page](https://github.com/sissues/cli/releases) on our GitHub repository. 10 | 2. **Download the Correct Asset**: Find the latest release and download the asset appropriate for your operating system. Here's a picture to help you locate the releases: 11 | 12 | ![GitHub Releases](./docs/releases-page-screenshot.png) 13 | 14 | **_For Mac / Linux_** - Download the first asset named `sissues`. 15 | 16 | **_For Windows_** - Download the second asset named `sissues.exe` 17 | 18 | 3. **Run the platform**: Click on the downloaded asset, and it should open up the following CLI 19 | 20 | ![](./docs/cli-screenshot.png) 21 | 22 | ## Getting Started 23 | 24 | Once you have installed the platform, you can start working on your API projects. Here are some guides to help you get started: 25 | 1. [Getting Started with Your API Project](https://github.com/sissues/cli/blob/main/docs/getting_started.md) 26 | 2. [Getting Started with Docker for API Projects](https://github.com/sissues/cli/blob/main/docs/getting_started_with_docker.md) 27 | 28 | ## Additional Resources 29 | 30 | - **Community Support**: If you have any questions or need help, don't hesitate to reach out to by [opening an issue](https://github.com/sissues/cli/issues). 31 | - **Documentation**: Refer to the platform's documentation for detailed information on features and usage. 32 | 33 | ## Final Tips 34 | 35 | - **Stay Positive**: Learning new skills can be challenging, but stay positive and persistent. You can do this! 36 | - **Practice Makes Perfect**: The more you practice, the better you'll become. Keep working on the exercises and refining your skills. 37 | - **Use Resources**: Make use of the resources available to you, including documentation, forums, and community support. 38 | 39 | We’re excited to see what you create with our platform. Happy coding! 40 | -------------------------------------------------------------------------------- /app.css: -------------------------------------------------------------------------------- 1 | /* Style for the Markdown viewer TUI */ 2 | 3 | .container { 4 | height: 100%; 5 | } 6 | 7 | #menu { 8 | width: 25%; 9 | padding: 1; 10 | border-right: solid #333; 11 | background: #222; 12 | } 13 | 14 | #content { 15 | width: 75%; 16 | padding: 1; 17 | } 18 | 19 | #markdown_viewer { 20 | padding: 1; 21 | height: 60%; 22 | } 23 | 24 | #test_output { 25 | height: 40%; 26 | padding: 1; 27 | border-top: solid #333; 28 | margin-top: 1; 29 | } 30 | 31 | /* Style buttons */ 32 | 33 | .view-button, .test-button { 34 | background: #444; 35 | border: solid #666; 36 | color: #fff; 37 | padding: 1; 38 | margin: 1 0; /* Adjusted margin */ 39 | text-align: center; 40 | } 41 | 42 | .view-button:hover, .test-button:hover { 43 | background: #555; 44 | } 45 | 46 | .menu_widget { 47 | margin: 1 1; /* top, right, bottom, left margins */ 48 | } 49 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | from textual import on 4 | from textual.app import App, ComposeResult 5 | from textual.containers import Container, Horizontal 6 | from textual.widgets import Button, Footer, MarkdownViewer, Header, TextArea, Select, Label 7 | 8 | from exercises_utils import EXERCISES_DIR, ExercisesUtils 9 | from project_generators.base_project_generator import BaseProjectGenerator 10 | 11 | 12 | class MarkdownApp(App): 13 | 14 | TITLE = "Skill Issues Killer" 15 | SUB_TITLE = "practice real-world projects" 16 | CSS_PATH = "app.css" 17 | BINDINGS = [ 18 | ("t", "toggle_table_of_contents", "Toggle TOC"), 19 | ("h", "show_help", "Show Help"), 20 | ("q", "quit", "Quit") 21 | ] 22 | 23 | help_text = """Pick an exercise, and generate a project template by click 'Start Project' 24 | Once you are done implementing the exercise requirements, click on 'Run Tests'.\n 25 | If all the tests passed, congrats!\n 26 | If not, keep at it, one test at a time :)\n 27 | 28 | [b][i]Got stuck?[/b][/i] \nRefer to the project's README.md at https://github.com/sissues/cli/blob/main/README.MD,\nor open an issue at https://github.com/sissues/cli/issues 29 | """ 30 | 31 | def __init__(self): 32 | super().__init__() 33 | self.files_view_names = ExercisesUtils.generate_view_names_map() 34 | self.current_markdown_path = None 35 | 36 | def compose(self) -> ComposeResult: 37 | yield Header() 38 | yield Horizontal( 39 | Container( 40 | id="menu", classes="menu"), 41 | Container( 42 | MarkdownViewer(id="markdown_viewer"), 43 | Label("Tests Output"), 44 | TextArea(id="test_output", read_only=True), 45 | id="content", 46 | classes="content", 47 | ), 48 | ) 49 | yield Footer() 50 | 51 | def on_mount(self) -> None: 52 | """Initial setup when the app starts.""" 53 | self.show_menu() 54 | self.notify(title="Hello World!", message=self.help_text, timeout=30) 55 | 56 | def show_menu(self) -> None: 57 | menu = self.query_one("#menu") 58 | menu.remove_children() 59 | 60 | exercises = list(EXERCISES_DIR.glob("*.md")) 61 | exercise_names = [(self.files_view_names[exercise.stem], exercise.stem) for exercise in exercises] 62 | select_file_widget = Select(options=exercise_names, allow_blank=False, prompt="Select Exercise", id="exercise_select", classes="menu_widget", tooltip="Select an exercise to preview") 63 | 64 | menu.mount(select_file_widget) 65 | menu.mount(Button("Start Project", id="start", variant="warning", classes="menu_widget")) 66 | menu.mount(Button("Run Tests", id="test", variant="success", classes="menu_widget")) 67 | 68 | async def on_button_pressed(self, event: Button.Pressed) -> None: 69 | button_id = event.button.id 70 | select_widget = self.query_one("#exercise_select", Select) 71 | exercise_name = select_widget.value 72 | 73 | if button_id == "test": 74 | self.run_tests(exercise_name) 75 | 76 | elif button_id == 'start': 77 | self.start_project(exercise_name) 78 | 79 | def start_project(self, exercise_name: str) -> None: 80 | test_output = self.query_one("#test_output", TextArea) 81 | project_path = BaseProjectGenerator().generate(exercise_name) 82 | test_output.notify(f'Creating project structure for project {exercise_name}') 83 | ExercisesUtils.open_file_in_explorer(project_path) 84 | 85 | def run_tests(self, exercise_name: str) -> None: 86 | test_output = self.query_one("#test_output", TextArea) 87 | docker_compose_file_name = ExercisesUtils.get_resource_path(f"exercises_test_suites/docker_compose_{exercise_name}.yml") 88 | command = f"docker-compose -f {docker_compose_file_name} up --build" 89 | try: 90 | test_output.notify("Tests execution started, might take a few seconds", timeout=3) 91 | result = subprocess.run(command, shell=True, capture_output=True, text=True, check=True) 92 | test_output.insert(result.stdout) 93 | except subprocess.CalledProcessError as e: 94 | test_output.insert(f"An error occurred: {e.stderr}") 95 | 96 | test_output.notify("Tests execution done", timeout=3) 97 | 98 | def action_toggle_table_of_contents(self) -> None: 99 | """Toggle the display of the table of contents.""" 100 | markdown_viewer = self.query_one("#markdown_viewer", MarkdownViewer) 101 | markdown_viewer.show_table_of_contents = not markdown_viewer.show_table_of_contents 102 | 103 | def action_show_help(self): 104 | self.notify(title="Hello World!", message=self.help_text, timeout=30) 105 | 106 | @on(Select.Changed) 107 | async def view_select(self, event: Select.Changed): 108 | selected_exercise = EXERCISES_DIR / f"{event.value}.md" 109 | self.current_markdown_path = selected_exercise 110 | markdown_viewer = self.query_one("#markdown_viewer", MarkdownViewer) 111 | await markdown_viewer.go(selected_exercise) 112 | self.query_one("#content").display = True 113 | 114 | 115 | if __name__ == "__main__": 116 | MarkdownApp().run() 117 | -------------------------------------------------------------------------------- /app.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | a = Analysis( 5 | ['app.py'], 6 | pathex=[], 7 | binaries=[], 8 | datas=[('app.css', '.'), 9 | ('exercises', 'exercises'), 10 | ('exercises_test_suites', 'exercises_test_suites'),], 11 | hiddenimports=[ 12 | 'textual', 13 | 'textual.widgets', 14 | 'textual.widgets._markdown_viewer', 15 | ], 16 | hookspath=[], 17 | hooksconfig={}, 18 | runtime_hooks=[], 19 | excludes=[], 20 | noarchive=False, 21 | optimize=0, 22 | ) 23 | pyz = PYZ(a.pure) 24 | 25 | exe = EXE( 26 | pyz, 27 | a.scripts, 28 | a.binaries, 29 | a.datas, 30 | [], 31 | name='sissues', 32 | debug=False, 33 | bootloader_ignore_signals=False, 34 | strip=False, 35 | upx=True, 36 | upx_exclude=[], 37 | runtime_tmpdir=None, 38 | console=True, 39 | disable_windowed_traceback=False, 40 | argv_emulation=False, 41 | target_arch=None, 42 | codesign_identity=None, 43 | entitlements_file=None, 44 | ) 45 | -------------------------------------------------------------------------------- /docs/cli-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sissues/cli/386f74e88cd895e2a3aa99b338a3456211b46420/docs/cli-screenshot.png -------------------------------------------------------------------------------- /docs/getting_started.md: -------------------------------------------------------------------------------- 1 | ## Getting Started with Your API Project 2 | 3 | Welcome to our API exercise platform! We're excited to have you here and ready to guide you through getting started with your project. This guide will help you understand the platform, set up your project, and work towards a working solution. Don't worry—you're in good hands! 4 | 5 | ### Step 1: Understanding the Platform 6 | 7 | Our platform is designed to help you build API projects through a series of exercises. Each exercise will simulate real-world scenarios, helping you develop practical skills. Here’s what you need to know: 8 | 9 | - **Exercises**: These are mini-projects with multiple steps. Each step is designed to build on the previous one, gradually increasing in complexity. 10 | - **Automatic Test Suites**: Each exercise comes with a set of automated tests. These tests will check your code and ensure it meets the requirements. 11 | - **Manual Code Reviews**: For a fee, you can get your code reviewed manually. This is optional but can provide valuable feedback. 12 | 13 | ### Step 2: Setting Up Your Environment 14 | 15 | Before you start coding, you need to set up your development environment. Follow these steps: 16 | 17 | 1. Refer to the [Getting started with docker guide](https://github.com/sissues/cli/blob/main/docs/getting_start_with_docker.md) to learn how to install Docker, and tweak the dockerfile template to your programming language and API framework. 18 | 19 | ### Step 3: Starting Your Project 20 | 21 | 1. **Pick an Exercise**: Browse the available exercises. Choose one that interests you and click on it to view the details. 22 | 2. **Initialize the Project**: Click the "Start Project" button. This will initialize the project template for you, setting up everything you need to get started. 23 | 3. **Review the Dockerfile Template**: The initialized project will include a Dockerfile template. For guidance on how to modify it, refer to our [Getting started with docker guide](https://github.com/sissues/cli/blob/main/docs/getting_start_with_docker.md). 24 | 25 | ### Step 4: Writing Your Code 26 | 27 | With your project set up, you can start writing your code. Here are some tips to help you get started: 28 | 29 | 1. **Follow the Instructions**: Each exercise comes with detailed instructions. Follow them carefully to ensure you meet the requirements. 30 | 2. **Test Frequently**: Use the "Run Tests" button to run the provided tests frequently. This will help you catch issues early and ensure your code meets the requirements. 31 | 3. **Ask for Help**: If you get stuck, don't hesitate to ask questions by [opening an issue](https://github.com/sissues/cli/issues). We're here to help! 32 | 33 | ### Step 5: Running Tests 34 | 35 | Running tests is crucial to ensure your code works as expected. Here’s how you can run the tests on our platform: 36 | 37 | 1. **Click "Run Tests"**: Once you’ve written some code, and modified your Dockerfile correctly, click the "Run Tests" button. The platform will automatically build your Docker image and run the tests for you. 38 | 2. **Check the Results**: Review the test results and fix any issues. Repeat this process until all tests pass. 39 | 40 | ### Final Tips 41 | 42 | - **Stay Positive**: Learning new skills can be challenging, but stay positive and persistent. You can do this! 43 | - **Practice Makes Perfect**: The more you practice, the better you'll become. Keep working on the exercises and refining your skills. 44 | - **Use Resources**: Make use of the resources available to you, including documentation, forums, and community support. 45 | 46 | Remember, you're not alone on this journey. We're here to support you every step of the way. Happy coding! 47 | -------------------------------------------------------------------------------- /docs/getting_started_with_docker.md: -------------------------------------------------------------------------------- 1 | ## Getting Started with Docker for API Projects 2 | 3 | Welcome to our API exercise platform! We're here to help you get started with Docker, understand the provided Dockerfile template, and modify it to suit your chosen language and framework. Don't worry—setting up a working Dockerfile is easier than it seems! 4 | 5 | ### Step 1: Installing Docker 6 | 7 | Before you can use Docker, you need to install it on your machine. Follow these simple instructions for your operating system: 8 | 9 | **For Windows:** 10 | 11 | 1. Download Docker Desktop from [Docker's official website](https://www.docker.com/products/docker-desktop). 12 | 2. Run the installer and follow the on-screen instructions. 13 | 3. After installation, Docker Desktop should start automatically. If not, open it from the Start menu. 14 | 15 | **For macOS:** 16 | 17 | 1. Download Docker Desktop from [Docker's official website](https://www.docker.com/products/docker-desktop). 18 | 2. Open the downloaded `.dmg` file and drag Docker to your Applications folder. 19 | 3. Open Docker from your Applications folder. 20 | 21 | **For Linux:** 22 | 23 | 1. Follow the instructions on the [Docker installation page](https://docs.docker.com/engine/install/#server). 24 | 2. Make sure to follow any additional steps for your specific distribution. 25 | 26 | ### Step 2: Understanding the Dockerfile Template 27 | 28 | Here's the Dockerfile template you will be working with: 29 | 30 | ```dockerfile 31 | # Use an official runtime as a parent image 32 | FROM 33 | 34 | # Set the working directory in the container 35 | WORKDIR /{project_name} 36 | 37 | # Install any dependencies 38 | RUN 39 | 40 | # Copy the current directory contents into the container at /app 41 | COPY src/ . 42 | 43 | # Make port 5000 available to the world outside this container 44 | EXPOSE 5000 45 | 46 | # Run the application 47 | CMD [ "" ] 48 | ``` 49 | 50 | ### Step 3: Modifying the Dockerfile for Your Project 51 | You can choose any programming language and API framework for your project. The key is to write the Dockerfile correctly, and we believe you can do it! Here are some examples for popular languages and frameworks to help you get started. Feel free to Google for more examples specific to your setup. 52 | 53 | 54 | **Example: Python (Flask)** 55 | 56 | ```dockerfile 57 | # Use an official Python runtime as a parent image 58 | FROM python:3.8-slim 59 | 60 | # Set the working directory in the container 61 | # The correct dir here will be injected automatically whenever you click 'Start Project' 62 | WORKDIR /library_management_system 63 | 64 | # Install any dependencies 65 | RUN pip install flask 66 | 67 | # Copy the current directory contents into the container at /app 68 | COPY src/ . 69 | 70 | # Make port 5000 available to the world outside this container 71 | EXPOSE 5000 72 | 73 | # Run the application 74 | CMD [ "python", "app.py" ] 75 | ``` 76 | 77 | **Example: Node.js (Express)** 78 | 79 | ```dockerfile 80 | # Use an official Node.js runtime as a parent image 81 | FROM node:14 82 | 83 | # Set the working directory in the container 84 | # The correct dir here will be injected automatically whenever you click 'Start Project' 85 | WORKDIR /library_management_system 86 | 87 | # Install any dependencies 88 | COPY package*.json ./ 89 | RUN npm install 90 | 91 | # Copy the current directory contents into the container at /app 92 | COPY src/ . 93 | 94 | # Make port 5000 available to the world outside this container 95 | EXPOSE 5000 96 | 97 | # Run the application 98 | CMD [ "node", "app.js" ] 99 | ``` 100 | 101 | **Example: Java (Spring Boot)** 102 | 103 | ```dockerfile 104 | # Use an official OpenJDK runtime as a parent image 105 | FROM openjdk:11 106 | 107 | # Set the working directory in the container 108 | # The correct dir here will be injected automatically whenever you click 'Start Project' 109 | WORKDIR /library_management_system 110 | 111 | # Install any dependencies (Maven in this case) 112 | RUN apt-get update && apt-get install -y maven 113 | 114 | # Copy the current directory contents into the container at /app 115 | COPY src/ . 116 | 117 | # Build the application 118 | RUN mvn clean package 119 | 120 | # Make port 5000 available to the world outside this container 121 | EXPOSE 5000 122 | 123 | # Run the application 124 | CMD [ "java", "-jar", "target/myapp.jar" ] 125 | ``` 126 | 127 | 128 | ## Step 4: Customizing for Your Project 129 | * Choose the base image: Look for an official image that suits your programming language and framework. You can find these on Docker Hub. 130 | * Set the working directory: This is already set for you at /project_name, so no changes needed here! 131 | * Install dependencies: Use the appropriate command to install your project's dependencies. 132 | * Copy your project files: Adjust the COPY command if your project structure differs. 133 | * Expose the correct port: Ensure the port you expose matches the port your application runs on. 134 | * Run your application: Modify the CMD to start your application correctly. 135 | 136 | ## Final Tips 137 | * Google is your friend: Search for Dockerfile examples specific to your language and framework. 138 | * Look at official documentation: Many frameworks provide Dockerfile examples and best practices. 139 | * Ask for help: If you get stuck, don't hesitate to ask questions in our community or look for solutions on forums like Stack Overflow. 140 | 141 | Remember, you can do this! Setting up a Dockerfile is a great skill to have, and with a bit of practice, it will become second nature. Happy coding! 142 | -------------------------------------------------------------------------------- /docs/releases-page-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sissues/cli/386f74e88cd895e2a3aa99b338a3456211b46420/docs/releases-page-screenshot.png -------------------------------------------------------------------------------- /exercises/library_management_system.md: -------------------------------------------------------------------------------- 1 | # Library Management System 2 | 3 | ## Overview 4 | 5 | In this project, you will build a backend system to manage a library's inventory of items, including books, magazines, DVDs, and music CDs. The system will handle borrowing and returning items, managing user memberships, and tracking user borrowing history. The project will be developed in steps to gradually add complexity and functionality. 6 | 7 | You should focus on adhering to Object-Oriented Programming (OOP) principles and modularizing your application, so that extending it is easier. 8 | 9 | ## Step 1: Basic Borrowing and Returning Functionality 10 | 11 | ### User & Item Management 12 | 13 | Before users can borrow or return items, they need to be added to the library system. 14 | 15 | 1. **Add a User** 16 | - Endpoint: `POST /users` 17 | - Request body should include: 18 | - `name` (string, required) 19 | - `email` (string, required) 20 | - `membership_type` (string, required; one of "student", "basic", "premium") 21 | - The system should generate a unique UUID for each user and return it in the response. 22 | - Example Request: 23 | 24 | ```json 25 | { 26 | "name": "John Doe", 27 | "email": "john.doe@example.com", 28 | "membership_type": "student" 29 | } 30 | ``` 31 | 32 | - Example Response: 33 | 34 | ```json 35 | { 36 | "user_id": "123e4567-e89b-12d3-a456-426614174000", 37 | "name": "John Doe", 38 | "email": "john.doe@example.com", 39 | "membership_type": "student" 40 | } 41 | ``` 42 | 43 | 2. Add an Item 44 | - Endpoint: `POST /items` 45 | - Request body should include: 46 | - `name` (string, required) 47 | - `type`(string, required; one of “book”, “dvd”, “magazine”) 48 | - The system should generate a unique UUID for each item and return it in the response. 49 | - Example Request: 50 | 51 | ```json 52 | { 53 | "name": "Madagascar", 54 | "type": "dvd", 55 | } 56 | ``` 57 | 58 | - Example Response: 59 | 60 | ```json 61 | { 62 | "item_id": "123e4567-e89b-12d3-a456-426614174020", 63 | "name": "Madagascar", 64 | "type": "dvd" 65 | } 66 | ``` 67 | 68 | 69 | > **Important Note:** you will need to store both users and items somehow. for the sake of the exercise, it doesn’t have to be a “real” database like sqlite, postgres, mongo, etc… 70 | To keep it simple you can store the users and items in a runtime object, or write the data into a file - whatever you are more comfortable with. 71 | > 72 | 73 | ### Library Items 74 | 75 | The library has the following items that can be borrowed: 76 | 77 | - **Books**: Borrow for up to 3 months. 78 | - **Magazines**: Borrow for up to 1 month. 79 | - **DVDs**: Borrow for up to 1 month. 80 | 81 | ### Membership Types 82 | 83 | There are three membership types in the library: 84 | 85 | - **Student** 86 | - Can hold up to 5 books, 5 magazines, and 2 DVDs at a time. 87 | - **Basic Membership** 88 | - Can hold up to 3 books, 3 magazines, and 2 DVDs at a time. 89 | - **Premium Membership** 90 | - Can hold up to 10 books, 5 magazines, and 5 DVDs at a time. 91 | 92 | ### Borrowing and Returning Rules 93 | 94 | - If a user tries to borrow a new item but already holds their limit per subscription type, the borrowing will fail (400 status code is expected to be returned). 95 | 96 | ### API Requirements 97 | 98 | 1. **Borrow an Item** 99 | - Endpoint: `POST /items/{id}/borrow` 100 | - Only authenticated users can borrow items. 101 | - Check the user’s membership type and current borrowed items before allowing borrowing. 102 | - Update the item’s status to "borrowed" and record the borrowing date. 103 | - Example Request: 104 | 105 | ```json 106 | { 107 | "user_id": "123e4567-e89b-12d3-a456-426614174000", 108 | "item_type": "book" 109 | } 110 | ``` 111 | 112 | 2. **Return an Item** 113 | - Endpoint: `POST /items/{id}/return` 114 | - Only authenticated users can return items. 115 | - Update the item’s status to "available" and record the return date. 116 | - Example Request: 117 | 118 | ```json 119 | { 120 | "user_id": "123e4567-e89b-12d3-a456-426614174000" 121 | } 122 | ``` 123 | 124 | 3. **User Borrowing History** 125 | - Endpoint: `GET /users/{id}/history` 126 | - Return a list of items borrowed by the user, including borrow and return dates. 127 | - Example Response: 128 | 129 | ```json 130 | [ 131 | { 132 | "item_id": 1, 133 | "item_type": "book", 134 | "borrowed_date": "2023-01-01", 135 | "returned_date": "2023-02-01" 136 | }, 137 | { 138 | "item_id": 2, 139 | "item_type": "dvd", 140 | "borrowed_date": "2023-03-01", 141 | "returned_date": "2023-03-15" 142 | } 143 | ] 144 | ``` 145 | 146 | 147 | ## Step 2: Adding Music CDs 148 | 149 | ### New Library Item 150 | 151 | The library now has music CDs that can be borrowed for up to 2 months. 152 | 153 | ### Membership Type Limits for CDs 154 | 155 | - **Student** 156 | - Can hold up to 2 CDs at a time. 157 | - **Basic Membership** 158 | - Can hold up to 2 CDs at a time. 159 | - **Premium Membership** 160 | - Can hold up to 4 CDs at a time. 161 | 162 | ### API Extensions 163 | 164 | 1. **Borrow a CD** 165 | - Update the borrowing endpoint to handle CDs. 166 | - Example Request: 167 | 168 | ```json 169 | { 170 | "user_id": "123e4567-e89b-12d3-a456-426614174000", 171 | "item_type": "cd" 172 | } 173 | ``` 174 | 175 | 2. **Return a CD** 176 | - Update the returning endpoint to handle CDs. 177 | - Example Request: 178 | 179 | ```json 180 | { 181 | "user_id": "123e4567-e89b-12d3-a456-426614174000", 182 | "item_type": "cd" 183 | } 184 | ``` 185 | 186 | 187 | ## Step 3: Promotion System 188 | 189 | ### Promotion Rules 190 | 191 | - Any user who has borrowed 15 or more books in the last year is eligible to hold double the number of books allowed by their membership type. 192 | - This applies to all membership types. 193 | 194 | ### API Extensions 195 | 196 | 1. **Check Promotion Eligibility** 197 | - Before allowing a user to borrow a book, check their borrowing history for the past year. 198 | - If they have borrowed 15 or more books, update their borrowing limit for books. 199 | - Example Logic: 200 | 201 | ```json 202 | { 203 | "user_id": "123e4567-e89b-12d3-a456-426614174000", 204 | "eligible_for_promotion": true, 205 | "new_book_limit": 20 206 | } 207 | ``` 208 | 209 | 210 | ### Implementation Details 211 | 212 | 1. **Modify Borrowing Logic** 213 | - Adjust the borrowing logic to consider promotion eligibility for books. 214 | - Ensure that the new book limit is applied correctly for eligible users. 215 | 216 | ## Conclusion 217 | 218 | By following these instructions, you will create a comprehensive Library Inventory Management System. Focus on adhering to OOP principles and modularizing your application to simulate real-world development practices. This project will help you develop practical backend skills and understand how to build and extend a real-world application. -------------------------------------------------------------------------------- /exercises/scooter_rental_service.md.draft: -------------------------------------------------------------------------------- 1 | # Scooter Rental Service 2 | 3 | ### Overview 4 | 5 | In this project, you will build a backend system to manage a scooter rental service. The system will handle scooter reservations, starting and ending rides, user management, and ride history tracking. The project will be developed in steps to gradually add complexity and functionality. 6 | 7 | ## Step 1: Basic Reservation and Ride Functionality 8 | 9 | ### User Management 10 | 11 | Before users can reserve or start a ride, they need to be added to the scooter rental system. 12 | 13 | 1. **Add a User** 14 | - Endpoint: `POST /users` 15 | - Request body should include: 16 | - `name` (string, required) 17 | - `email` (string, required) 18 | - `membership_type` (string, required; one of "basic", "premium") 19 | - The system should generate a unique UUID for each user and return it in the response. 20 | - Example Request: 21 | 22 | ```json 23 | { 24 | "name": "John Doe", 25 | "email": "john.doe@example.com", 26 | "membership_type": "basic" 27 | } 28 | ``` 29 | 30 | - Example Response: 31 | 32 | ```json 33 | { 34 | "user_id": "123e4567-e89b-12d3-a456-426614174000", 35 | "name": "John Doe", 36 | "email": "john.doe@example.com", 37 | "membership_type": "basic" 38 | } 39 | ``` 40 | 41 | 42 | ### Scooter Management 43 | 44 | 1. **Add a Scooter** 45 | - Endpoint: `POST /scooters` 46 | - Request body should include: 47 | - `location` (string, required) 48 | - `status` (string, required; one of "available", "reserved", "in_use", "maintenance") 49 | - The system should generate a unique ID for each scooter and return it in the response. 50 | - Example Request: 51 | 52 | ```json 53 | { 54 | "location": "Downtown", 55 | "status": "available" 56 | } 57 | ``` 58 | 59 | - Example Response: 60 | 61 | ```json 62 | { 63 | "scooter_id": "S123456", 64 | "location": "Downtown", 65 | "status": "available" 66 | } 67 | ``` 68 | 69 | 70 | ### Reservation and Ride Rules 71 | 72 | - Users can reserve an available scooter. 73 | - Users can start a ride on a reserved scooter. 74 | - Users can end a ride and make the scooter available again. 75 | 76 | ### API Requirements 77 | 78 | 1. **Reserve a Scooter** 79 | - Endpoint: `POST /scooters/{id}/reserve` 80 | - Only authenticated users can reserve scooters. 81 | - Update the scooter’s status to "reserved" and record the reservation time. 82 | - Example Request: 83 | 84 | ```json 85 | { 86 | "user_id": "123e4567-e89b-12d3-a456-426614174000" 87 | } 88 | ``` 89 | 90 | 2. **Start a Ride** 91 | - Endpoint: `POST /scooters/{id}/start` 92 | - Only authenticated users can start a ride on a reserved scooter. 93 | - Update the scooter’s status to "in_use" and record the start time. 94 | - Example Request: 95 | 96 | ```json 97 | { 98 | "user_id": "123e4567-e89b-12d3-a456-426614174000" 99 | } 100 | ``` 101 | 102 | 3. **End a Ride** 103 | - Endpoint: `POST /scooters/{id}/end` 104 | - Only authenticated users can end a ride. 105 | - Update the scooter’s status to "available", record the end time, and calculate the ride duration. 106 | - Example Request: 107 | 108 | ```json 109 | { 110 | "user_id": "123e4567-e89b-12d3-a456-426614174000" 111 | } 112 | ``` 113 | 114 | 4. **User Ride History** 115 | - Endpoint: `GET /users/{id}/history` 116 | - Return a list of rides taken by the user, including start and end times and duration. 117 | - Example Response: 118 | 119 | ```json 120 | [ 121 | { 122 | "scooter_id": "123e4567-e89b-12d3-a456-426614174001", 123 | "start_time": "2023-01-01T10:00:00Z", 124 | "end_time": "2023-01-01T10:30:00Z", 125 | "duration": 30 126 | }, 127 | { 128 | "scooter_id": "123e4567-e89b-12d3-a456-426614174002", 129 | "start_time": "2023-02-01T11:00:00Z", 130 | "end_time": "2023-02-01T11:45:00Z", 131 | "duration": 45 132 | } 133 | ] 134 | ``` 135 | 136 | 137 | ## Step 2: Advanced Features 138 | 139 | ### Membership Benefits 140 | 141 | 1. **Membership Levels** 142 | - **Basic Membership** 143 | - Pay-per-ride pricing. 144 | - **Premium Membership** 145 | - Monthly subscription with unlimited rides up to 60 minutes each. 146 | - Additional charges apply for rides longer than 60 minutes. 147 | 2. **Pricing System** 148 | - Implement a pricing system based on membership type and ride duration. 149 | - Calculate the cost of each ride and update the user’s account accordingly. 150 | 151 | ### API Extensions 152 | 153 | 1. **Calculate Ride Cost** 154 | - Endpoint: `GET /scooters/{id}/cost` 155 | - Calculate the cost of a ride based on the membership type and ride duration. 156 | - Example Response: 157 | 158 | ```json 159 | { 160 | "ride_cost": 5.00 161 | } 162 | ``` 163 | 164 | 2. **Payment Integration** 165 | - Integrate with a payment gateway to handle ride payments. 166 | - Update user’s account balance and transaction history. 167 | 168 | ## Step 3: Promotion and Reward System 169 | 170 | ### Promotion Rules 171 | 172 | 1. **Ride Rewards** 173 | - Implement a reward system where users earn points for each ride. 174 | - Points can be redeemed for discounts or free rides. 175 | 176 | ### API Extensions 177 | 178 | 1. **Reward Points** 179 | - Endpoint: `GET /users/{id}/rewards` 180 | - Return the user’s current reward points and reward history. 181 | - Example Response: 182 | 183 | ```json 184 | { 185 | "current_points": 120, 186 | "reward_history": [ 187 | { 188 | "ride_id": "123e4567-e89b-12d3-a456-426614174003", 189 | "points_earned": 10, 190 | "date": "2023-01-01" 191 | }, 192 | { 193 | "ride_id": "123e4567-e89b-12d3-a456-426614174004", 194 | "points_earned": 15, 195 | "date": "2023-02-01" 196 | } 197 | ] 198 | } 199 | ``` 200 | 201 | 2. **Redeem Rewards** 202 | - Endpoint: `POST /users/{id}/rewards/redeem` 203 | - Redeem points for discounts or free rides. 204 | - Update the user’s reward points and transaction history. 205 | - Example Request: 206 | 207 | ```json 208 | { 209 | "points_to_redeem": 50 210 | } 211 | ``` 212 | 213 | 214 | ## Conclusion 215 | 216 | By following these instructions, you will create a comprehensive Scooter Rental Service system. Focus on adhering to OOP principles and modularizing your application to simulate real-world development practices. This project will help you develop practical backend skills and understand how to build and extend a real-world application with advanced features and integrations. -------------------------------------------------------------------------------- /exercises_test_suites/docker_compose_library_management_system.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | api: 5 | build: 6 | context: ../my_solutions/library_management_system 7 | dockerfile: Dockerfile 8 | ports: 9 | - "5001:5000" 10 | 11 | test: 12 | build: 13 | context: ./library_management_system 14 | dockerfile: Dockerfile 15 | volumes: 16 | - ./library_management_system:/library_management_system 17 | depends_on: 18 | - api 19 | command: ["pytest", "."] -------------------------------------------------------------------------------- /exercises_test_suites/library_management_system/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Python image from the Docker Hub 2 | FROM python:3.8 3 | 4 | # Install pip and pytest 5 | RUN apt-get update && apt-get install -y python3-pip 6 | RUN pip install pytest requests 7 | 8 | # Set the working directory 9 | WORKDIR /library_management_system -------------------------------------------------------------------------------- /exercises_test_suites/library_management_system/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import requests 3 | 4 | BASE_URL = "http://api:5000" 5 | 6 | 7 | @pytest.fixture(scope="session") 8 | def base_url(): 9 | return BASE_URL 10 | 11 | 12 | @pytest.fixture(scope="module") 13 | def create_user(base_url): 14 | def _create_user(name, email, membership_type): 15 | response = requests.post(f"{base_url}/users", json={ 16 | "name": name, 17 | "email": email, 18 | "membership_type": membership_type 19 | }) 20 | return response.json() 21 | return _create_user 22 | 23 | 24 | @pytest.fixture(scope="module") 25 | def create_item(base_url): 26 | def _create_item(name, type): 27 | response = requests.post(f"{base_url}/items", json={ 28 | "name": name, 29 | "type": type 30 | }) 31 | return response.json() 32 | return _create_item 33 | -------------------------------------------------------------------------------- /exercises_test_suites/library_management_system/test_borrowing.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | def test_borrow_item(base_url, create_user, create_item): 5 | user = create_user("John Doe", "john.doe@example.com", "student") 6 | item = create_item("Harry Potter", "book") 7 | 8 | response = requests.post(f"{base_url}/items/{item['item_id']}/borrow", json={ 9 | "user_id": user["user_id"], 10 | "item_type": "book" 11 | }) 12 | assert response.status_code == 200, f"Expected status code 200 but got {response.status_code}" 13 | data = response.json() 14 | assert data["status"] == "borrowed", f"Expected item status 'borrowed' but got {data['status']}" 15 | assert "borrowed_date" in data, "Response JSON does not contain 'borrowed_date'" 16 | 17 | 18 | def test_borrow_item_exceeds_limit(base_url, create_user, create_item): 19 | user = create_user("Student User", "student@example.com", "student") 20 | for i in range(5): 21 | item = create_item(f"Book {i}", "book") 22 | requests.post(f"{base_url}/items/{item['item_id']}/borrow", json={ 23 | "user_id": user["user_id"], 24 | "item_type": "book" 25 | }) 26 | 27 | # Try to borrow one more book, should fail 28 | extra_item = create_item("Extra Book", "book") 29 | response = requests.post(f"{base_url}/items/{extra_item['item_id']}/borrow", json={ 30 | "user_id": user["user_id"], 31 | "item_type": "book" 32 | }) 33 | assert response.status_code == 400, f"Expected status code 400 for exceeding borrow limit but got {response.status_code}" 34 | 35 | 36 | def test_borrow_nonexistent_item(base_url, create_user): 37 | user = create_user("Test User", "test.user@example.com", "basic") 38 | response = requests.post(f"{base_url}/items/nonexistent/borrow", json={ 39 | "user_id": user["user_id"], 40 | "item_type": "book" 41 | }) 42 | assert response.status_code == 404, f"Expected status code 404 for nonexistent item but got {response.status_code}" 43 | 44 | 45 | def test_borrow_item_already_borrowed(base_url, create_user, create_item): 46 | user = create_user("User One", "user.one@example.com", "basic") 47 | item = create_item("Already Borrowed Book", "book") 48 | 49 | # Borrow the item once 50 | requests.post(f"{base_url}/items/{item['item_id']}/borrow", json={ 51 | "user_id": user["user_id"], 52 | "item_type": "book" 53 | }) 54 | 55 | # Try to borrow the same item again 56 | response = requests.post(f"{base_url}/items/{item['item_id']}/borrow", json={ 57 | "user_id": user["user_id"], 58 | "item_type": "book" 59 | }) 60 | assert response.status_code == 400, f"Expected status code 400 for already borrowed item but got {response.status_code}" 61 | 62 | 63 | def test_borrow_item_invalid_user(base_url, create_item): 64 | item = create_item("Invalid User Borrow", "book") 65 | response = requests.post(f"{base_url}/items/{item['item_id']}/borrow", json={ 66 | "user_id": "invalid_user_id", 67 | "item_type": "book" 68 | }) 69 | assert response.status_code == 404, f"Expected status code 404 for invalid user but got {response.status_code}" 70 | -------------------------------------------------------------------------------- /exercises_test_suites/library_management_system/test_borrowing_history.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | def test_borrowing_history(base_url, create_user, create_item): 5 | user = create_user("Alex Smith", "alex.smith@example.com", "premium") 6 | item = create_item("1984", "book") 7 | 8 | requests.post(f"{base_url}/items/{item['item_id']}/borrow", json={ 9 | "user_id": user["user_id"], 10 | "item_type": "book" 11 | }) 12 | 13 | requests.post(f"{base_url}/items/{item['item_id']}/return", json={ 14 | "user_id": user["user_id"] 15 | }) 16 | 17 | response = requests.get(f"{base_url}/users/{user['user_id']}/history") 18 | assert response.status_code == 200, f"Expected status code 200 but got {response.status_code}" 19 | data = response.json() 20 | assert len(data) > 0, "Expected at least one borrowing history record" 21 | assert "borrowed_date" in data[0], "Borrowing history record does not contain 'borrowed_date'" 22 | assert "returned_date" in data[0], "Borrowing history record does not contain 'returned_date'" 23 | 24 | 25 | def test_borrowing_history_no_history(base_url, create_user): 26 | user = create_user("History Test User", "history.test.user@example.com", "basic") 27 | 28 | response = requests.get(f"{base_url}/users/{user['user_id']}/history") 29 | assert response.status_code == 200, f"Expected status code 200 but got {response.status_code}" 30 | data = response.json() 31 | assert len(data) == 0, "Expected no borrowing history but found some records" 32 | 33 | 34 | def test_borrowing_history_invalid_user(base_url): 35 | response = requests.get(f"{base_url}/users/invalid_user_id/history") 36 | assert response.status_code == 404, f"Expected status code 404 for invalid user but got {response.status_code}" 37 | -------------------------------------------------------------------------------- /exercises_test_suites/library_management_system/test_item_management.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | def test_add_item(base_url): 5 | response = requests.post(f"{base_url}/items", json={ 6 | "name": "Madagascar", 7 | "type": "dvd" 8 | }) 9 | assert response.status_code == 200, f"Expected status code 200 but got {response.status_code}" 10 | data = response.json() 11 | assert "item_id" in data, "Response JSON does not contain 'item_id'" 12 | assert data["name"] == "Madagascar", f"Expected name 'Madagascar' but got {data['name']}" 13 | assert data["type"] == "dvd", f"Expected type 'dvd' but got {data['type']}" 14 | 15 | 16 | def test_add_item_invalid_type(base_url): 17 | response = requests.post(f"{base_url}/items", json={ 18 | "name": "Unknown Item", 19 | "type": "unknown_type" 20 | }) 21 | assert response.status_code == 400, f"Expected status code 400 for invalid item type but got {response.status_code}" 22 | 23 | 24 | def test_add_item_duplicate_name(base_url, create_item): 25 | item = create_item("Harry Potter", "book") 26 | response = requests.post(f"{base_url}/items", json={ 27 | "name": "Harry Potter", 28 | "type": "book" 29 | }) 30 | assert response.status_code == 400, f"Expected status code 400 for duplicate item name but got {response.status_code}" 31 | 32 | 33 | def test_add_item_missing_fields(base_url): 34 | response = requests.post(f"{base_url}/items", json={ 35 | "name": "Incomplete Item" 36 | # Missing type 37 | }) 38 | assert response.status_code == 400, f"Expected status code 400 for missing fields but got {response.status_code}" 39 | assert 'type' in response.json().get('errors', {}), "Expected error message for missing type" 40 | -------------------------------------------------------------------------------- /exercises_test_suites/library_management_system/test_promotions.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | def test_promotion_eligibility(base_url, create_user, create_item): 5 | user = create_user("Chris Lee", "chris.lee@example.com", "student") 6 | 7 | for _ in range(15): 8 | item = create_item("Book {}".format(_), "book") 9 | requests.post(f"{base_url}/items/{item['item_id']}/borrow", json={ 10 | "user_id": user["user_id"], 11 | "item_type": "book" 12 | }) 13 | requests.post(f"{base_url}/items/{item['item_id']}/return", json={ 14 | "user_id": user["user_id"] 15 | }) 16 | 17 | response = requests.get(f"{base_url}/users/{user['user_id']}/history") 18 | assert response.status_code == 200, f"Expected status code 200 but got {response.status_code}" 19 | data = response.json() 20 | assert len(data) >= 15, "Expected at least 15 borrowing records to be eligible for promotion" 21 | 22 | # Simulate checking the eligibility for promotion 23 | eligible = len([entry for entry in data if entry["item_type"] == "book"]) >= 15 24 | assert eligible is True, "User should be eligible for promotion but was not" 25 | 26 | 27 | def test_promotion_ineligibility(base_url, create_user, create_item): 28 | user = create_user("Promotion Test User", "promotion.test.user@example.com", "basic") 29 | 30 | for _ in range(10): 31 | item = create_item("Book {}".format(_), "book") 32 | requests.post(f"{base_url}/items/{item['item_id']}/borrow", json={ 33 | "user_id": user["user_id"], 34 | "item_type": "book" 35 | }) 36 | requests.post(f"{base_url}/items/{item['item_id']}/return", json={ 37 | "user_id": user["user_id"] 38 | }) 39 | 40 | response = requests.get(f"{base_url}/users/{user['user_id']}/history") 41 | assert response.status_code == 200, f"Expected status code 200 but got {response.status_code}" 42 | data = response.json() 43 | assert len(data) < 15, "Expected fewer than 15 borrowing records, but found more" 44 | 45 | # Simulate checking the eligibility for promotion 46 | eligible = len([entry for entry in data if entry["item_type"] == "book"]) >= 15 47 | assert eligible is False, "User should not be eligible for promotion but was" 48 | 49 | 50 | def test_promotion_invalid_user(base_url): 51 | response = requests.get(f"{base_url}/users/invalid_user_id/promotion") 52 | assert response.status_code == 404, f"Expected status code 404 for invalid user but got {response.status_code}" 53 | -------------------------------------------------------------------------------- /exercises_test_suites/library_management_system/test_returning.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | def test_return_item(base_url, create_user, create_item): 5 | user = create_user("Jane Doe", "jane.doe@example.com", "basic") 6 | item = create_item("The Hobbit", "book") 7 | 8 | requests.post(f"{base_url}/items/{item['item_id']}/borrow", json={ 9 | "user_id": user["user_id"], 10 | "item_type": "book" 11 | }) 12 | 13 | response = requests.post(f"{base_url}/items/{item['item_id']}/return", json={ 14 | "user_id": user["user_id"] 15 | }) 16 | assert response.status_code == 200, f"Expected status code 200 but got {response.status_code}" 17 | data = response.json() 18 | assert data["status"] == "available", f"Expected item status 'available' but got {data['status']}" 19 | assert "return_date" in data, "Response JSON does not contain 'return_date'" 20 | 21 | 22 | def test_return_item_not_borrowed(base_url, create_user, create_item): 23 | user = create_user("Test User", "test.user@example.com", "basic") 24 | item = create_item("Unused Book", "book") 25 | 26 | response = requests.post(f"{base_url}/items/{item['item_id']}/return", json={ 27 | "user_id": user["user_id"] 28 | }) 29 | assert response.status_code == 400, f"Expected status code 400 for returning not borrowed item but got {response.status_code}" 30 | 31 | 32 | def test_return_item_invalid_user(base_url, create_item): 33 | item = create_item("Invalid User Return", "book") 34 | 35 | response = requests.post(f"{base_url}/items/{item['item_id']}/return", json={ 36 | "user_id": "invalid_user_id" 37 | }) 38 | assert response.status_code == 404, f"Expected status code 404 for invalid user but got {response.status_code}" 39 | -------------------------------------------------------------------------------- /exercises_test_suites/library_management_system/test_user_management.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | def test_add_user(base_url): 5 | response = requests.post(f"{base_url}/users", json={ 6 | "name": "John Doe", 7 | "email": "john.doe@example.com", 8 | "membership_type": "student" 9 | }) 10 | assert response.status_code == 200, f"Expected status code 200 but got {response.status_code}" 11 | data = response.json() 12 | assert "user_id" in data, "Response JSON does not contain 'user_id'" 13 | assert data["name"] == "John Doe", f"Expected name 'John Doe' but got {data['name']}" 14 | assert data["email"] == "john.doe@example.com", f"Expected email 'john.doe@example.com' but got {data['email']}" 15 | assert data["membership_type"] == "student", f"Expected membership type 'student' but got {data['membership_type']}" 16 | 17 | 18 | def test_add_user_invalid_membership(base_url): 19 | response = requests.post(f"{base_url}/users", json={ 20 | "name": "Jane Doe", 21 | "email": "jane.doe@example.com", 22 | "membership_type": "invalid_type" 23 | }) 24 | assert response.status_code == 400, f"Expected status code 400 for invalid membership type but got {response.status_code}" 25 | 26 | 27 | def test_add_user_duplicate_email(base_url, create_user): 28 | user = create_user("Alice", "alice@example.com", "basic") 29 | response = requests.post(f"{base_url}/users", json={ 30 | "name": "Alice Clone", 31 | "email": "alice@example.com", 32 | "membership_type": "basic" 33 | }) 34 | assert response.status_code == 400, f"Expected status code 400 for duplicate email but got {response.status_code}" 35 | 36 | 37 | def test_add_user_missing_fields(base_url): 38 | response = requests.post(f"{base_url}/users", json={ 39 | "name": "Incomplete User" 40 | # Missing email and membership_type 41 | }) 42 | assert response.status_code == 400, f"Expected status code 400 for missing fields but got {response.status_code}" 43 | assert 'email' in response.json().get('errors', {}), "Expected error message for missing email" 44 | assert 'membership_type' in response.json().get('errors', {}), "Expected error message for missing membership_type" 45 | -------------------------------------------------------------------------------- /exercises_test_suites/scooter_rental_service/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Python image from the Docker Hub 2 | FROM python:3.8 3 | 4 | # Install pip and pytest 5 | RUN apt-get update && apt-get install -y python3-pip 6 | RUN pip install pytest requests 7 | 8 | # Set the working directory 9 | WORKDIR /scooter_rental_service -------------------------------------------------------------------------------- /exercises_test_suites/scooter_rental_service/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import requests 3 | 4 | BASE_URL = "http://localhost:5000" # Replace with the actual base URL of your API 5 | 6 | @pytest.fixture(scope="module") 7 | def base_url(): 8 | return BASE_URL 9 | 10 | @pytest.fixture 11 | def create_user(base_url): 12 | def _create_user(name="John Doe", email="john.doe@example.com", membership_type="basic"): 13 | payload = { 14 | "name": name, 15 | "email": email, 16 | "membership_type": membership_type 17 | } 18 | response = requests.post(f"{base_url}/users", json=payload) 19 | return response.json()["user_id"] 20 | return _create_user 21 | 22 | @pytest.fixture 23 | def create_scooter(base_url): 24 | def _create_scooter(location="Downtown", status="available"): 25 | payload = { 26 | "location": location, 27 | "status": status 28 | } 29 | response = requests.post(f"{base_url}/scooters", json=payload) 30 | return response.json()["scooter_id"] 31 | return _create_scooter 32 | -------------------------------------------------------------------------------- /exercises_test_suites/scooter_rental_service/test_reservations_and_rides.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | def test_reserve_scooter(create_user, create_scooter, base_url): 5 | user_id = create_user() 6 | scooter_id = create_scooter() 7 | 8 | response = requests.post(f"{base_url}/scooters/{scooter_id}/reserve", json={"user_id": user_id}) 9 | assert response.status_code == 200, f"Expected status code 200 but got {response.status_code}" 10 | data = response.json() 11 | assert data["status"] == "reserved", f"Expected status 'reserved' but got {data['status']}" 12 | 13 | 14 | def test_start_ride(create_user, create_scooter, base_url): 15 | user_id = create_user() 16 | scooter_id = create_scooter() 17 | 18 | # Reserve the scooter first 19 | requests.post(f"{base_url}/scooters/{scooter_id}/reserve", json={"user_id": user_id}) 20 | 21 | response = requests.post(f"{base_url}/scooters/{scooter_id}/start", json={"user_id": user_id}) 22 | assert response.status_code == 200, f"Expected status code 200 but got {response.status_code}" 23 | data = response.json() 24 | assert data["status"] == "in_use", f"Expected status 'in_use' but got {data['status']}" 25 | 26 | 27 | def test_end_ride(create_user, create_scooter, base_url): 28 | user_id = create_user() 29 | scooter_id = create_scooter() 30 | 31 | # Reserve and start the scooter first 32 | requests.post(f"{base_url}/scooters/{scooter_id}/reserve", json={"user_id": user_id}) 33 | requests.post(f"{base_url}/scooters/{scooter_id}/start", json={"user_id": user_id}) 34 | 35 | response = requests.post(f"{base_url}/scooters/{scooter_id}/end", json={"user_id": user_id}) 36 | assert response.status_code == 200, f"Expected status code 200 but got {response.status_code}" 37 | data = response.json() 38 | assert data["status"] == "available", f"Expected status 'available' but got {data['status']}" 39 | assert "duration" in data, "Response JSON does not contain 'duration'" 40 | 41 | 42 | def test_user_ride_history(create_user, create_scooter, base_url): 43 | user_id = create_user() 44 | scooter_id = create_scooter() 45 | 46 | # Reserve, start, and end the scooter ride first 47 | requests.post(f"{base_url}/scooters/{scooter_id}/reserve", json={"user_id": user_id}) 48 | requests.post(f"{base_url}/scooters/{scooter_id}/start", json={"user_id": user_id}) 49 | requests.post(f"{base_url}/scooters/{scooter_id}/end", json={"user_id": user_id}) 50 | 51 | response = requests.get(f"{base_url}/users/{user_id}/history") 52 | assert response.status_code == 200, f"Expected status code 200 but got {response.status_code}" 53 | data = response.json() 54 | assert isinstance(data, list), "Response JSON is not a list" 55 | assert len(data) > 0, "Expected at least one ride in the history" 56 | assert "start_time" in data[0], "Response JSON does not contain 'start_time'" 57 | assert "end_time" in data[0], "Response JSON does not contain 'end_time'" 58 | assert "duration" in data[0], "Response JSON does not contain 'duration'" 59 | -------------------------------------------------------------------------------- /exercises_test_suites/scooter_rental_service/test_scooter_management.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | def test_add_scooter(base_url): 5 | response = requests.post(f"{base_url}/scooters", json={ 6 | "location": "Downtown", 7 | "status": "available" 8 | }) 9 | assert response.status_code == 201, f"Expected status code 201 but got {response.status_code}" 10 | data = response.json() 11 | assert "scooter_id" in data, "Response JSON does not contain 'scooter_id'" 12 | assert data["location"] == "Downtown", f"Expected location 'Downtown' but got {data['location']}" 13 | assert data["status"] == "available", f"Expected status 'available' but got {data['status']}" 14 | 15 | 16 | def test_add_scooter_with_invalid_status(base_url): 17 | response = requests.post(f"{base_url}/scooters", json={ 18 | "location": "Downtown", 19 | "status": "broken" 20 | }) 21 | assert response.status_code == 400, f"Expected status code 400 for invalid status but got {response.status_code}" 22 | -------------------------------------------------------------------------------- /exercises_test_suites/scooter_rental_service/test_user_management.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | def test_add_user(base_url): 5 | response = requests.post(f"{base_url}/users", json={ 6 | "name": "John Doe", 7 | "email": "john.doe@example.com", 8 | "membership_type": "basic" 9 | }) 10 | assert response.status_code == 201, f"Expected status code 201 but got {response.status_code}" 11 | data = response.json() 12 | assert "user_id" in data, "Response JSON does not contain 'user_id'" 13 | assert data["name"] == "John Doe", f"Expected name 'John Doe' but got {data['name']}" 14 | assert data["email"] == "john.doe@example.com", f"Expected email 'john.doe@example.com' but got {data['email']}" 15 | assert data["membership_type"] == "basic", f"Expected membership type 'basic' but got {data['membership_type']}" 16 | 17 | 18 | def test_add_user_invalid_membership(base_url): 19 | response = requests.post(f"{base_url}/users", json={ 20 | "name": "Jane Doe", 21 | "email": "jane.doe@example.com", 22 | "membership_type": "invalid_type" 23 | }) 24 | assert response.status_code == 400, f"Expected status code 400 for invalid membership type but got {response.status_code}" 25 | 26 | 27 | def test_add_user_duplicate_email(base_url, create_user): 28 | user_id = create_user("Alice", "alice@example.com", "basic") 29 | response = requests.post(f"{base_url}/users", json={ 30 | "name": "Alice Clone", 31 | "email": "alice@example.com", 32 | "membership_type": "basic" 33 | }) 34 | assert response.status_code == 400, f"Expected status code 400 for duplicate email but got {response.status_code}" 35 | 36 | 37 | def test_add_user_missing_fields(base_url): 38 | response = requests.post(f"{base_url}/users", json={ 39 | "name": "Incomplete User" 40 | # Missing email and membership_type 41 | }) 42 | assert response.status_code == 400, f"Expected status code 400 for missing" 43 | -------------------------------------------------------------------------------- /exercises_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import subprocess 4 | import sys 5 | from pathlib import Path 6 | 7 | EXERCISES_DIR = Path(__file__).parent / "exercises" 8 | 9 | 10 | class ExercisesUtils: 11 | @staticmethod 12 | def generate_view_names_map(): 13 | all_md_files = list(EXERCISES_DIR.glob("*.md")) 14 | return { 15 | filename.stem: ' '.join(filename.stem.split('_')).title() 16 | for filename in all_md_files 17 | } 18 | 19 | @staticmethod 20 | def open_file_in_explorer(file_path): 21 | system = platform.system() 22 | 23 | if system == "Windows": 24 | os.startfile(file_path) 25 | elif system == "Darwin": # macOS 26 | subprocess.run(["open", file_path]) 27 | else: # Linux and other Unix-like systems 28 | subprocess.run(["xdg-open", file_path]) 29 | 30 | @staticmethod 31 | def get_resource_path(relative_path): 32 | """ Get the absolute path to the resource, accounting for PyInstaller packaging. """ 33 | try: 34 | # PyInstaller creates a temporary folder and stores path in _MEIPASS 35 | base_path = sys._MEIPASS 36 | except AttributeError: 37 | base_path = os.path.abspath(".") 38 | 39 | return os.path.join(base_path, relative_path) 40 | -------------------------------------------------------------------------------- /project_generators/base_project_generator.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from exercises_utils import ExercisesUtils 4 | 5 | 6 | class BaseProjectGenerator: 7 | def generate(self, project_name: str) -> str: 8 | """ 9 | Given a project name, create a new dir and a generic Dockerfile. 10 | Returns the path to the Dockerfile. 11 | """ 12 | # Create the project directory 13 | project_dir = ExercisesUtils.get_resource_path(f"my_solutions/{project_name}") 14 | # project_dir = os.path.join(os.getcwd(), '', project_name) 15 | src_dir = os.path.join(project_dir, 'src') 16 | os.makedirs(src_dir, exist_ok=True) 17 | 18 | # Define a generic Dockerfile content 19 | dockerfile_content = f""" 20 | # Use an official runtime as a parent image 21 | FROM 22 | 23 | # Set the working directory in the container 24 | WORKDIR /{project_name} 25 | 26 | # Install any dependencies 27 | RUN 28 | 29 | # Copy the current directory contents into the container at /app 30 | COPY src/ . 31 | 32 | # Make port 5000 available to the world outside this container 33 | EXPOSE 5000 34 | 35 | # Run the application 36 | CMD [ "" ] 37 | """ 38 | 39 | # Write the Dockerfile to the project directory 40 | dockerfile_path = os.path.join(project_dir, 'Dockerfile') 41 | with open(dockerfile_path, 'w') as dockerfile: 42 | dockerfile.write(dockerfile_content.strip()) 43 | 44 | # Create a README file with guidelines 45 | readme_content = f""" 46 | # {' '.join(project_name.split('_')).title()} 47 | 48 | ## 49 | ## Supported Languages? 50 | 51 | **Every language and every API framework is supported.** 52 | You will be required to correctly write a dockerfile (given the template and the guide below), and the tests are on us. 53 | 54 | ## Project Setup 55 | 1. **Install Dependencies**: Ensure you have Docker installed. 56 | 2. **Build the Docker Image**: Run the following command in the project directory: 57 | ``` 58 | docker build -t {project_name.lower()} . 59 | ``` 60 | 3. **Run the Docker Container**: Start the container using the command: 61 | ``` 62 | docker run -p 5000:5000 {project_name.lower()} 63 | ``` 64 | 65 | ## Dockerfile Guide 66 | 67 | - ``: Replace this with the base image for your chosen language (e.g., `python:3.9-slim`). 68 | - ``: Replace this with the command to install your project dependencies (e.g., `pip install -r requirements.txt`). 69 | - ``: Replace this with the command to start your application (e.g., `flask run --host=0.0.0.0`). 70 | 71 | Adjust the Dockerfile according to your specific project requirements. 72 | """ 73 | 74 | readme_path = os.path.join(project_dir, 'README.md') 75 | with open(readme_path, 'w') as readme: 76 | readme.write(readme_content.strip()) 77 | 78 | return project_dir 79 | -------------------------------------------------------------------------------- /tests/project_generators/test_base_project_generator.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import pytest 4 | 5 | from project_generators.base_project_generator import BaseProjectGenerator 6 | 7 | 8 | @pytest.fixture(scope="function") 9 | def setup_teardown(): 10 | """ 11 | Fixture to set up and tear down the test environment. 12 | """ 13 | generator = BaseProjectGenerator() 14 | project_name = 'TestAPIProject' 15 | project_dir = os.path.join(os.getcwd(), project_name) 16 | yield generator, project_name, project_dir 17 | # Clean up 18 | if os.path.exists(project_dir): 19 | shutil.rmtree(project_dir) 20 | 21 | 22 | def test_generate_creates_directory(setup_teardown): 23 | """ 24 | Test if the generate method creates the project directory. 25 | """ 26 | generator, project_name, project_dir = setup_teardown 27 | generator.generate(project_name) 28 | assert os.path.isdir(project_dir) 29 | 30 | 31 | def test_generate_creates_dockerfile(setup_teardown): 32 | """ 33 | Test if the generate method creates a Dockerfile in the project directory. 34 | """ 35 | generator, project_name, project_dir = setup_teardown 36 | dockerfile_path = generator.generate(project_name) 37 | assert os.path.isfile(dockerfile_path) 38 | with open(dockerfile_path, 'r') as file: 39 | content = file.read() 40 | assert 'FROM ' in content 41 | assert 'WORKDIR /app' in content 42 | assert 'COPY requirements.txt ./' in content 43 | assert 'RUN ' in content 44 | assert 'EXPOSE 5000' in content 45 | assert 'CMD [ "" ]' in content 46 | 47 | 48 | def test_generate_creates_readme(setup_teardown): 49 | """ 50 | Test if the generate method creates a README.md file with guidelines. 51 | """ 52 | generator, project_name, project_dir = setup_teardown 53 | generator.generate(project_name) 54 | readme_path = os.path.join(project_dir, 'README.md') 55 | assert os.path.isfile(readme_path) 56 | with open(readme_path, 'r') as file: 57 | content = file.read() 58 | assert f'# {project_name}' in content 59 | assert '## Project Setup' in content 60 | assert 'docker build -t' in content 61 | assert 'docker run -p 5000:5000' in content 62 | assert '## Dockerfile Guide' in content 63 | assert '' in content 64 | assert '' in content 65 | assert '' in content 66 | --------------------------------------------------------------------------------