├── .env.sample ├── .github ├── dependabot.yaml └── workflows │ ├── automerge.yml │ └── create-pr.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── app ├── __init__.py ├── api │ ├── __init__.py │ ├── health.py │ └── v1 │ │ └── sample_resource.py ├── common │ ├── __init__.py │ ├── error.py │ └── util.py ├── conf │ ├── __init__.py │ ├── config.py │ ├── logging.py │ └── logging.yaml ├── db │ ├── __init__.py │ └── db.py ├── main.py ├── models │ ├── __init__.py │ ├── create_sample_resource.py │ ├── get_sample_resource.py │ ├── mongo_model.py │ └── sample_resource_common.py └── schemas │ └── sample_resource.py ├── docker-compose.yml ├── frontend └── static │ ├── images │ ├── parallax1.jpg │ └── parallax2.jpg │ ├── js │ └── init.js │ └── style.css ├── poetry.lock ├── pyproject.toml ├── requirements.txt ├── start.sh └── tests ├── __init__.py ├── conftest.py ├── mock_data └── sample_resource.json ├── mongo_client.py ├── test_create_sample_resource.py ├── test_delete_sample_resource.py ├── test_get_sample_resource.py ├── test_health.py └── test_update_sample_resource.py /.env.sample: -------------------------------------------------------------------------------- 1 | LOG_LEVEL=WARN 2 | 3 | FLASK_DEBUG=True 4 | SECRET_KEY=create_key_here 5 | 6 | # FastAPI Config 7 | # CLIENT_ID=1 8 | # CLIENT_SECRET=2 9 | # DOMAIN=http://localhost:8000 10 | 11 | 12 | # Sendgrid API 13 | SENDGRID_API_KEY= 14 | 15 | SENTRY_DSN= 16 | SENTRY_ENVIRONMENT=development 17 | SENTRY_DEBUG=False 18 | 19 | # MongoDB Config 20 | MONGO_URI=mongodb://localhost:27017 21 | MONGO_URL=mongodb://localhost 22 | MONGO_USER=user 23 | MONGO_PASSWORD=password 24 | MAX_CONNECTIONS_COUNT=10 25 | MIN_CONNECTIONS_COUNT=3 26 | MONGO_DB=db_name 27 | MONGO_DB_TEST=db_test -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "pip" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | 13 | - package-ecosystem: "poetry" 14 | directory: "/" 15 | schedule: 16 | interval: "daily" 17 | open-pull-requests-limit: 10 18 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: Auto Merge and Close Pull Request 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | jobs: 8 | auto_merge_and_close: 9 | runs-on: ubuntu-latest 10 | if: github.event.pull_request.head.repo.full_name == github.repository 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v3 14 | 15 | - name: Enable Pull Request Automerge 16 | uses: peter-evans/enable-pull-request-automerge@v2 17 | id: automerge 18 | with: 19 | token: ${{ secrets.PAT }} 20 | pull-request-number: ${{ github.event.pull_request.number }} 21 | merge-method: merge 22 | 23 | - name: Close Pull Request 24 | if: steps.automerge.outputs.merged == 'true' 25 | uses: peter-evans/close-pull@v2 26 | with: 27 | token: ${{ secrets.PAT }} 28 | pull-request-number: ${{ github.event.pull_request.number }} 29 | comment: Auto-closing pull request 30 | delete-branch: false 31 | 32 | -------------------------------------------------------------------------------- /.github/workflows/create-pr.yml: -------------------------------------------------------------------------------- 1 | name: Auto Pull Request on Push 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | # if: "!contains(github.ref, 'main')" 8 | 9 | jobs: 10 | create_pull_request: 11 | runs-on: ubuntu-latest 12 | if: github.ref != 'refs/heads/main' 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v3 16 | 17 | - name: Create Pull Request 18 | uses: peter-evans/create-pull-request@v4.2.3 19 | with: 20 | token: ${{ secrets.PR_TOKEN }} 21 | branch: ${{ github.ref }} 22 | base: main 23 | commit-message: 'Automated pull request from ${{ github.ref }} on ${{ format(github.event.head_commit.timestamp) }}' 24 | title: New pull request from ${{ github.ref }} on ${{ format(github.event.head_commit.timestamp, 'DD/MM/YY HH:mm:ss') }} 25 | body: Please review the changes made from ${{ github.ref }} on ${{ format(github.event.head_commit.timestamp, 'DD/MM/YY HH:mm:ss') }} 26 | draft: false 27 | # if: github.event.pull_request.head.ref != 'main' 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .mypy_cache 3 | docker-stack.yml 4 | 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | pip-wheel-metadata/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py,cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 100 | __pypackages__/ 101 | 102 | # Celery stuff 103 | celerybeat-schedule 104 | celerybeat.pid 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | 136 | .DS_Store 137 | zbak/ 138 | .idea/ 139 | 140 | 141 | # Logs 142 | logs 143 | *.log 144 | npm-debug.log* 145 | yarn-debug.log* 146 | yarn-error.log* 147 | lerna-debug.log* 148 | .pnpm-debug.log* 149 | 150 | # Diagnostic reports (https://nodejs.org/api/report.html) 151 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 152 | 153 | # Runtime data 154 | pids 155 | *.pid 156 | *.seed 157 | *.pid.lock 158 | 159 | # Directory for instrumented libs generated by jscoverage/JSCover 160 | lib-cov 161 | 162 | # Coverage directory used by tools like istanbul 163 | coverage 164 | *.lcov 165 | 166 | # nyc test coverage 167 | .nyc_output 168 | 169 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 170 | .grunt 171 | 172 | # Bower dependency directory (https://bower.io/) 173 | bower_components 174 | 175 | # node-waf configuration 176 | .lock-wscript 177 | 178 | # Compiled binary addons (https://nodejs.org/api/addons.html) 179 | build/Release 180 | 181 | # Dependency directories 182 | node_modules/ 183 | jspm_packages/ 184 | 185 | # Snowpack dependency directory (https://snowpack.dev/) 186 | web_modules/ 187 | 188 | # TypeScript cache 189 | *.tsbuildinfo 190 | 191 | # Optional npm cache directory 192 | .npm 193 | 194 | # Optional eslint cache 195 | .eslintcache 196 | 197 | # Optional stylelint cache 198 | .stylelintcache 199 | 200 | # Microbundle cache 201 | .rpt2_cache/ 202 | .rts2_cache_cjs/ 203 | .rts2_cache_es/ 204 | .rts2_cache_umd/ 205 | 206 | # Optional REPL history 207 | .node_repl_history 208 | 209 | # Output of 'npm pack' 210 | *.tgz 211 | 212 | # Yarn Integrity file 213 | .yarn-integrity 214 | 215 | # dotenv environment variable files 216 | .env 217 | .env.development.local 218 | .env.test.local 219 | .env.production.local 220 | .env.local 221 | 222 | # parcel-bundler cache (https://parceljs.org/) 223 | .cache 224 | .parcel-cache 225 | 226 | # Next.js build output 227 | .next 228 | out 229 | 230 | # Nuxt.js build / generate output 231 | .nuxt 232 | dist 233 | 234 | # Gatsby files 235 | .cache/ 236 | # Comment in the public line in if your project uses Gatsby and not Next.js 237 | # https://nextjs.org/blog/next-9-1#public-directory-support 238 | # public 239 | 240 | # vuepress build output 241 | .vuepress/dist 242 | 243 | # vuepress v2.x temp and cache directory 244 | .temp 245 | .cache 246 | 247 | # Docusaurus cache and generated files 248 | .docusaurus 249 | 250 | # Serverless directories 251 | .serverless/ 252 | 253 | # FuseBox cache 254 | .fusebox/ 255 | 256 | # DynamoDB Local files 257 | .dynamodb/ 258 | 259 | # TernJS port file 260 | .tern-port 261 | 262 | # Stores VSCode versions used for testing VSCode extensions 263 | .vscode-test 264 | 265 | # yarn v2 266 | .yarn/cache 267 | .yarn/unplugged 268 | .yarn/build-state.yml 269 | .yarn/install-state.gz 270 | .pnp.* 271 | 272 | # Django # 273 | *.log 274 | *.pot 275 | *.pyc 276 | __pycache__ 277 | db.sqlite3 278 | 279 | # Backup files # 280 | *.bak 281 | 282 | # If you are using PyCharm # 283 | # User-specific stuff 284 | .idea/**/workspace.xml 285 | .idea/**/tasks.xml 286 | .idea/**/usage.statistics.xml 287 | .idea/**/dictionaries 288 | .idea/**/shelf 289 | 290 | # AWS User-specific 291 | .idea/**/aws.xml 292 | 293 | # Generated files 294 | .idea/**/contentModel.xml 295 | 296 | # Sensitive or high-churn files 297 | .idea/**/dataSources/ 298 | .idea/**/dataSources.ids 299 | .idea/**/dataSources.local.xml 300 | .idea/**/sqlDataSources.xml 301 | .idea/**/dynamic.xml 302 | .idea/**/uiDesigner.xml 303 | .idea/**/dbnavigator.xml 304 | 305 | # Gradle 306 | .idea/**/gradle.xml 307 | .idea/**/libraries 308 | 309 | # File-based project format 310 | *.iws 311 | 312 | # IntelliJ 313 | out/ 314 | 315 | # JIRA plugin 316 | atlassian-ide-plugin.xml 317 | 318 | # Python # 319 | *.py[cod] 320 | *$py.class 321 | 322 | # Distribution / packaging 323 | .Python build/ 324 | develop-eggs/ 325 | downloads/ 326 | eggs/ 327 | .eggs/ 328 | lib/ 329 | lib64/ 330 | parts/ 331 | sdist/ 332 | var/ 333 | wheels/ 334 | *.egg-info/ 335 | .installed.cfg 336 | *.egg 337 | *.manifest 338 | *.spec 339 | 340 | # Installer logs 341 | pip-log.txt 342 | pip-delete-this-directory.txt 343 | 344 | # Unit test / coverage reports 345 | htmlcov/ 346 | .tox/ 347 | .coverage 348 | .coverage.* 349 | .cache 350 | .pytest_cache/ 351 | nosetests.xml 352 | coverage.xml 353 | *.cover 354 | .hypothesis/ 355 | 356 | # Jupyter Notebook 357 | .ipynb_checkpoints 358 | 359 | # pyenv 360 | .python-version 361 | 362 | # celery 363 | celerybeat-schedule.* 364 | 365 | # SageMath parsed files 366 | *.sage.py 367 | 368 | # Environments 369 | .env 370 | .venv 371 | env/ 372 | venv/ 373 | ENV/ 374 | env.bak/ 375 | venv.bak/ 376 | 377 | # mkdocs documentation 378 | /site 379 | 380 | # mypy 381 | .mypy_cache/ 382 | 383 | # Sublime Text # 384 | *.tmlanguage.cache 385 | *.tmPreferences.cache 386 | *.stTheme.cache 387 | *.sublime-workspace 388 | *.sublime-project 389 | 390 | # sftp configuration file 391 | sftp-config.json 392 | 393 | # Package control specific files Package 394 | Control.last-run 395 | Control.ca-list 396 | Control.ca-bundle 397 | Control.system-ca-bundle 398 | GitHub.sublime-settings 399 | 400 | # Visual Studio Code # 401 | .vscode/* 402 | !.vscode/settings.json 403 | !.vscode/tasks.json 404 | !.vscode/launch.json 405 | !.vscode/extensions.json 406 | .history 407 | 408 | node_modules 409 | yarn.lock 410 | 411 | _unused/ 412 | data.db 413 | 414 | # Django # 415 | *.log 416 | *.pot 417 | *.pyc 418 | __pycache__ 419 | db.sqlite3 420 | 421 | # Backup files # 422 | *.bak 423 | 424 | # If you are using PyCharm # 425 | # User-specific stuff 426 | .idea/**/workspace.xml 427 | .idea/**/tasks.xml 428 | .idea/**/usage.statistics.xml 429 | .idea/**/dictionaries 430 | .idea/**/shelf 431 | 432 | # AWS User-specific 433 | .idea/**/aws.xml 434 | 435 | # Generated files 436 | .idea/**/contentModel.xml 437 | 438 | # Sensitive or high-churn files 439 | .idea/**/dataSources/ 440 | .idea/**/dataSources.ids 441 | .idea/**/dataSources.local.xml 442 | .idea/**/sqlDataSources.xml 443 | .idea/**/dynamic.xml 444 | .idea/**/uiDesigner.xml 445 | .idea/**/dbnavigator.xml 446 | 447 | # Gradle 448 | .idea/**/gradle.xml 449 | .idea/**/libraries 450 | 451 | # File-based project format 452 | *.iws 453 | 454 | # IntelliJ 455 | out/ 456 | 457 | # JIRA plugin 458 | atlassian-ide-plugin.xml 459 | 460 | # Python # 461 | *.py[cod] 462 | *$py.class 463 | 464 | # Distribution / packaging 465 | .Python build/ 466 | develop-eggs/ 467 | downloads/ 468 | eggs/ 469 | .eggs/ 470 | lib/ 471 | lib64/ 472 | parts/ 473 | sdist/ 474 | var/ 475 | wheels/ 476 | *.egg-info/ 477 | .installed.cfg 478 | *.egg 479 | *.manifest 480 | *.spec 481 | 482 | # Installer logs 483 | pip-log.txt 484 | pip-delete-this-directory.txt 485 | 486 | # Unit test / coverage reports 487 | htmlcov/ 488 | .tox/ 489 | .coverage 490 | .coverage.* 491 | .cache 492 | .pytest_cache/ 493 | nosetests.xml 494 | coverage.xml 495 | *.cover 496 | .hypothesis/ 497 | 498 | # Jupyter Notebook 499 | .ipynb_checkpoints 500 | 501 | # pyenv 502 | .python-version 503 | 504 | # celery 505 | celerybeat-schedule.* 506 | 507 | # SageMath parsed files 508 | *.sage.py 509 | 510 | # Environments 511 | .env 512 | .venv 513 | env/ 514 | venv/ 515 | ENV/ 516 | env.bak/ 517 | venv.bak/ 518 | 519 | # mkdocs documentation 520 | /site 521 | 522 | # mypy 523 | .mypy_cache/ 524 | 525 | # Sublime Text # 526 | *.tmlanguage.cache 527 | *.tmPreferences.cache 528 | *.stTheme.cache 529 | *.sublime-workspace 530 | *.sublime-project 531 | 532 | # sftp configuration file 533 | sftp-config.json 534 | 535 | # Package control specific files Package 536 | Control.last-run 537 | Control.ca-list 538 | Control.ca-bundle 539 | Control.system-ca-bundle 540 | GitHub.sublime-settings 541 | 542 | # Visual Studio Code # 543 | .vscode/* 544 | !.vscode/settings.json 545 | !.vscode/tasks.json 546 | !.vscode/launch.json 547 | !.vscode/extensions.json 548 | .history 549 | 550 | assets 551 | node_modules 552 | yarn.lock 553 | 554 | _unused/ 555 | data/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11.2-slim 2 | 3 | # RUN apt-get update \ 4 | # && apt-get upgrade -y \ 5 | # && apt-get install -y --no-install-recommends curl git build-essential python3-setuptools \ 6 | # && apt-get autoremove -y 7 | 8 | ENV PYTHONDONTWRITEBYTECODE 1 9 | ENV PYTHONUNBUFFERED 1 10 | 11 | RUN adduser --disabled-password --gecos '' appuser 12 | WORKDIR /server 13 | 14 | RUN pip install --upgrade pip 15 | RUN pip install poetry 16 | 17 | COPY pyproject.toml poetry.lock ./ 18 | RUN poetry export --without-hashes --without dev -f requirements.txt -o requirements.txt && \ 19 | chown appuser:appuser requirements.txt && \ 20 | pip install -r requirements.txt 21 | 22 | USER appuser 23 | COPY --chown=appuser:appuser start.sh /server/ 24 | COPY --chown=appuser:appuser app /server/app 25 | 26 | RUN chmod +x /server/start.sh 27 | ENTRYPOINT [ "/server/start.sh" ] 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alexander Kim 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test dev docker-build docker-compose-up docker-compose-down 2 | 3 | VERSION := 0.1.0 4 | IMAGE_NAME := releads-docker:$(VERSION) 5 | 6 | test: 7 | @docker run -itd --rm \ 8 | --name mongodb \ 9 | -p 27017:27017 \ 10 | -v $(shell pwd)/data:/data/db \ 11 | mongo:6.0 12 | @env $(shell cat .env) poetry run pytest -s -vv 13 | @docker stop mongodb 14 | 15 | dev: 16 | @docker run -itd --rm \ 17 | --name mongodb \ 18 | -p 27017:27017 \ 19 | -v $(shell pwd)/data:/data/db \ 20 | mongo:6.0 21 | @env $(shell cat .env) poetry run gunicorn -k uvicorn.workers.UvicornWorker --reload --bind 0.0.0.0:8888 -w 4 app.main:app 22 | 23 | docker-build: 24 | @docker build -t $(IMAGE_NAME) . 25 | 26 | docker-compose-up: 27 | @docker-compose up -d --build --remove-orphans --force-recreate 28 | 29 | docker-compose-down: 30 | @docker-compose down -v --remove-orphans --rmi all 31 | @docker-compose rm -f -v 32 | 33 | docker-compose-update: 34 | @docker-compose down -v --remove-orphans --rmi all 35 | @docker-compose rm -f -v 36 | @docker-compose up -d --build --remove-orphans --force-recreate 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Boilerplate for FastAPI, MongoDB, Motor Projects 2 | ![Python3.11.2](https://img.shields.io/badge/Python-3.11.2-brightgreen.svg?style=flat-square) 3 | ![MongoDB](https://img.shields.io/badge/MongoDB-6.0-brightgreen.svg?style=flat-square) 4 | ![FastAPI](https://img.shields.io/badge/FastAPI-0.92.0-brightgreen.svg?style=flat-square) 5 | ![Motor](https://img.shields.io/badge/Motor-3.1.1-brightgreen.svg?style=flat-square) 6 | 7 | 8 | ## Features 9 | A new backend project created with this boilerplate provides: 10 | - [x] Asynchronous high-performance RESTful APIs built upon [FastAPI](https://fastapi.tiangolo.com/) framework. 11 | - [x] Asynchronous CRUD operations for a sample resource built upon [Motor](https://motor.readthedocs.io/en/stable/) driver for MongoDB, providing high performance and efficiency. 12 | - [x] API documentation with [Swagger UI](https://swagger.io/tools/swagger-ui/). 13 | - [x] API testing with [pytest](https://docs.pytest.org/en/7.1.x/) and [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio). 14 | - [x] Dockerfile for containerization and docker-compose support. 15 | - [x] Easy creation of new backend services with [cookiecutter](https://github.com/cookiecutter/cookiecutter). 16 | - [x] Easy package menagement with [Poetry](https://python-poetry.org/). 17 | - [x] Health API for service health checking. 18 | - [x] Easy configuration with environment variables. 19 | - [x] Easy testing, develop running, docker build, docker-compose up and down with Makefile. 20 | - [x] Proper logging with ID masking. 21 | 22 | ## Prerequisites 23 | - Python 3.11+ 24 | - [Poetry](https://python-poetry.org/) installed 25 | - Docker installed 26 | - GNU Make 27 | 28 | ## Getting Started 29 | 30 | ### Edit Environment Variables 31 | Edit the `.env` file within the project folder. 32 | 33 | ### Run Tests 34 | ```sh 35 | make test 36 | ``` 37 | (This may not work at this time. Please use docker-compose instead.) 38 | 39 | ### Build Docker Image 40 | ```sh 41 | make docker-build 42 | ``` 43 | 44 | ### Docker-compose 45 | ```sh 46 | make docker-compose-up 47 | make docker-compose-down 48 | ``` 49 | 50 | ### Run Service Locally 51 | ```sh 52 | make dev 53 | ``` 54 | This will create a MongoDB container as well. 55 | (This may not work at this time. Please use docker-compose instead.) 56 | 57 | ### Check Swagger API Document 58 | Go to ` http://localhost:8888/docs`. 59 | 60 | ## Contributing 61 | Pull requests are welcome. For major changes, please open an issue first to discuss what you. 62 | 63 | ## Credit: 64 | Forked from https://github.com/klee1611/cookiecutter-fastapi-mongo -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexk1919/fastapi-motor-mongo-template/659af1b939829fe2962adcb9d0191c83941f7893/app/__init__.py -------------------------------------------------------------------------------- /app/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexk1919/fastapi-motor-mongo-template/659af1b939829fe2962adcb9d0191c83941f7893/app/api/__init__.py -------------------------------------------------------------------------------- /app/api/health.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends 2 | from motor.motor_asyncio import AsyncIOMotorClient 3 | import platform 4 | import psutil 5 | 6 | from app.db.db import get_db 7 | 8 | router = APIRouter() 9 | 10 | 11 | @router.get('/', include_in_schema=False) 12 | @router.get('') 13 | async def health(db: AsyncIOMotorClient = Depends(get_db)): 14 | try: 15 | # Check if the database is responsive 16 | await db.command('ping') 17 | db_status = 'up' 18 | except Exception: 19 | db_status = 'down' 20 | 21 | # Get system information 22 | system_info = { 23 | "system": platform.system(), 24 | "processor": platform.processor(), 25 | "architecture": platform.architecture(), 26 | "memory": psutil.virtual_memory()._asdict(), 27 | "disk": psutil.disk_usage('/')._asdict() 28 | } 29 | 30 | return { 31 | "database": db_status, 32 | "system_info": system_info 33 | } 34 | -------------------------------------------------------------------------------- /app/api/v1/sample_resource.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, Response 2 | import logging 3 | from uuid import UUID 4 | 5 | from app.db.db import get_db, AsyncIOMotorClient 6 | from app.schemas.sample_resource import create_sample_resource as \ 7 | db_create_sample_resouce, get_sample_resource as \ 8 | db_get_sample_resource, update_sample_resource as \ 9 | db_update_sample_resource, delete_sample_resource as \ 10 | db_delete_sample_resource 11 | from app.common.util import uuid_masker 12 | from app.common.error import UnprocessableError 13 | 14 | from app.models.create_sample_resource import \ 15 | CreateSampleResourceReq, CreateSampleResourceResp 16 | from app.models.get_sample_resource import GetSampleResourceResp 17 | 18 | router = APIRouter() 19 | 20 | 21 | @router.post('/', include_in_schema=False, status_code=201) 22 | @router.post('', response_model=CreateSampleResourceResp, status_code=201, 23 | responses={ 24 | 400: {} 25 | } 26 | ) 27 | async def create_sample_resource( 28 | sample_resource_data: CreateSampleResourceReq, 29 | db: AsyncIOMotorClient = Depends(get_db) 30 | ): 31 | logging.info('Receive create sample resource request') 32 | 33 | sample_resource_db = await db_create_sample_resouce( 34 | db, 35 | sample_resource_data.name 36 | ) 37 | 38 | return CreateSampleResourceResp(id=sample_resource_db.id) 39 | 40 | 41 | @router.get('/', include_in_schema=False, status_code=200) 42 | @router.get('', response_model=GetSampleResourceResp, status_code=200, 43 | responses={ 44 | 400: {} 45 | } 46 | ) 47 | async def get_sample_resource( 48 | resource: UUID, 49 | db: AsyncIOMotorClient = Depends(get_db), 50 | ): 51 | logging.info( 52 | f'Receive get sample resource {uuid_masker(resource)} request' 53 | ) 54 | 55 | sample_resource = await db_get_sample_resource( 56 | db, 57 | resource 58 | ) 59 | 60 | if None is sample_resource: 61 | return Response(status_code=204) 62 | 63 | return GetSampleResourceResp(name=sample_resource.get("name")) 64 | 65 | 66 | @router.put('/{resource_id}', include_in_schema=False, status_code=200) 67 | @router.put('/{resource_id}', status_code=200, 68 | responses={ 69 | 400: {} 70 | } 71 | ) 72 | async def update_sample_resource( 73 | resource_id: UUID, 74 | sample_resource_data: CreateSampleResourceReq, 75 | db: AsyncIOMotorClient = Depends(get_db), 76 | ): 77 | logging.info( 78 | f'Receive update sample resource {uuid_masker(resource_id)} request' 79 | ) 80 | 81 | sample_resource = await db_update_sample_resource( 82 | db, 83 | resource_id, 84 | sample_resource_data.dict() 85 | ) 86 | if None is sample_resource: 87 | raise UnprocessableError([]) 88 | 89 | return {} 90 | 91 | 92 | @router.delete('/{resource_id}', include_in_schema=False, status_code=200) 93 | @router.delete('/{resource_id}', status_code=200, 94 | responses={ 95 | 400: {} 96 | } 97 | ) 98 | async def delete_sample_resource( 99 | resource_id: UUID, 100 | db: AsyncIOMotorClient = Depends(get_db), 101 | ): 102 | logging.info( 103 | f'Receive delete sample resource {uuid_masker(resource_id)} request' 104 | ) 105 | 106 | sample_resource = await db_delete_sample_resource( 107 | db, 108 | resource_id, 109 | ) 110 | if None is sample_resource: 111 | raise UnprocessableError([]) 112 | 113 | return {} 114 | -------------------------------------------------------------------------------- /app/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexk1919/fastapi-motor-mongo-template/659af1b939829fe2962adcb9d0191c83941f7893/app/common/__init__.py -------------------------------------------------------------------------------- /app/common/error.py: -------------------------------------------------------------------------------- 1 | from fastapi.responses import JSONResponse 2 | 3 | 4 | class BaseErrResp(Exception): 5 | def __init__(self, status: int, title: str, details: list) -> None: 6 | self.__status = status 7 | self.__title = title 8 | self.__detail = details 9 | 10 | def gen_err_resp(self) -> JSONResponse: 11 | return JSONResponse( 12 | status_code=self.__status, 13 | content={ 14 | "type": "about:blank", 15 | 'title': self.__title, 16 | 'status': self.__status, 17 | 'detail': self.__detail 18 | } 19 | ) 20 | 21 | 22 | class BadRequest(BaseErrResp): 23 | def __init__(self, details: list): 24 | super(BadRequest, self).__init__(400, 'Bad Request', details) 25 | 26 | 27 | class InternalError(BaseErrResp): 28 | def __init__(self, details: list): 29 | super(InternalError, self).__init__(500, 'Internal Error', details) 30 | 31 | 32 | class UnprocessableError(BaseErrResp): 33 | def __init__(self, details: list): 34 | super(UnprocessableError, self).__init__( 35 | 422, 36 | 'Unprocessable Entity', 37 | details 38 | ) 39 | -------------------------------------------------------------------------------- /app/common/util.py: -------------------------------------------------------------------------------- 1 | import re 2 | from uuid import UUID 3 | 4 | 5 | def uuid_masker(exposed_uuid: str | UUID) -> str: 6 | uuid_str = str(exposed_uuid) 7 | return re.sub( 8 | r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-", 9 | '********-****-****-****-', 10 | uuid_str 11 | ) 12 | -------------------------------------------------------------------------------- /app/conf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexk1919/fastapi-motor-mongo-template/659af1b939829fe2962adcb9d0191c83941f7893/app/conf/__init__.py -------------------------------------------------------------------------------- /app/conf/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | import logging 4 | 5 | from app.common.error import InternalError 6 | 7 | load_dotenv() 8 | 9 | 10 | class Config: 11 | version = "0.1.0" 12 | title = "releads" 13 | 14 | app_settings = { 15 | 'db_name': os.getenv('MONGO_DB'), 16 | 'mongodb_url': os.getenv('MONGO_URL'), 17 | 'db_username': os.getenv('MONGO_USER'), 18 | 'db_password': os.getenv('MONGO_PASSWORD'), 19 | 'max_db_conn_count': os.getenv('MAX_CONNECTIONS_COUNT'), 20 | 'min_db_conn_count': os.getenv('MIN_CONNECTIONS_COUNT'), 21 | } 22 | 23 | @classmethod 24 | def app_settings_validate(cls): 25 | for k, v in cls.app_settings.items(): 26 | if None is v: 27 | logging.error(f'Config variable error. {k} cannot be None') 28 | raise InternalError([{"message": "Server configure error"}]) 29 | else: 30 | logging.info(f'Config variable {k} is {v}') 31 | 32 | -------------------------------------------------------------------------------- /app/conf/logging.py: -------------------------------------------------------------------------------- 1 | import os 2 | import yaml 3 | import logging 4 | import logging.config 5 | 6 | 7 | class SensitiveInfoFilter(logging.Filter): 8 | """ 9 | A filter to mask sensitive information in log messages. 10 | """ 11 | 12 | def __init__(self, sensitive_words): 13 | super().__init__() 14 | self.sensitive_words = sensitive_words 15 | 16 | def filter(self, record): 17 | """ 18 | Check if a log record contains sensitive information, and if so, mask it. 19 | """ 20 | msg = record.getMessage() 21 | for word in self.sensitive_words: 22 | msg = msg.replace(word, "*" * len(word)) 23 | record.msg = msg 24 | return True 25 | 26 | 27 | def replace_env_for_config(log_conf: dict) -> None: 28 | for k, v in log_conf.items(): 29 | if isinstance(v, dict): 30 | replace_env_for_config(v) 31 | elif isinstance(v, str) and v[0] == '$': 32 | log_conf[k] = os.environ.get(v[1:]) 33 | 34 | 35 | def create_log_config(log_path: str) -> dict: 36 | with open(log_path, 'r') as f: 37 | log_config = yaml.load(f, Loader=yaml.CLoader) 38 | replace_env_for_config(log_config) 39 | return log_config 40 | 41 | 42 | def setup_logging(): 43 | log_config = create_log_config('app/conf/logging.yaml') 44 | logging.config.dictConfig(log_config) 45 | -------------------------------------------------------------------------------- /app/conf/logging.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | disable_existing_loggers: False 4 | 5 | formatters: 6 | simple: 7 | format: "[%(asctime)s] [%(levelname)s] %(module)s - %(message)s" 8 | 9 | handlers: 10 | console_handler: 11 | class: logging.StreamHandler 12 | level: $LOG_LEVEL 13 | formatter: simple 14 | stream: ext://sys.stdout 15 | filters: [sensitive_info] 16 | file_handler: 17 | class: logging.FileHandler 18 | filename: app.log 19 | level: $LOG_LEVEL 20 | formatter: simple 21 | filters: [sensitive_info] 22 | 23 | filters: 24 | sensitive_info: 25 | (): app.conf.logging.SensitiveInfoFilter 26 | sensitive_words: [password, secret, username, token, key] 27 | 28 | root: 29 | level: $LOG_LEVEL 30 | handlers: [console_handler, file_handler] 31 | -------------------------------------------------------------------------------- /app/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexk1919/fastapi-motor-mongo-template/659af1b939829fe2962adcb9d0191c83941f7893/app/db/__init__.py -------------------------------------------------------------------------------- /app/db/db.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase 4 | import logging 5 | 6 | from app.conf.config import Config 7 | 8 | load_dotenv() 9 | 10 | db_client: AsyncIOMotorClient = None 11 | 12 | 13 | async def get_db() -> AsyncIOMotorClient: 14 | db_name = Config.app_settings.get('db_name') 15 | return db_client[db_name] 16 | 17 | 18 | async def connect_and_init_db(): 19 | global db_client 20 | try: 21 | db_client = AsyncIOMotorClient( 22 | Config.app_settings.get('mongodb_url'), 23 | username=Config.app_settings.get('db_username'), 24 | password=Config.app_settings.get('db_password'), 25 | maxPoolSize=Config.app_settings.get('max_db_conn_count'), 26 | minPoolSize=Config.app_settings.get('min_db_conn_count'), 27 | uuidRepresentation="standard", 28 | ) 29 | logging.info('Connected to mongo.') 30 | except Exception as e: 31 | logging.exception(f'Could not connect to mongo: {e}') 32 | raise 33 | 34 | 35 | async def close_db_connect(): 36 | global db_client 37 | if db_client is None: 38 | logging.warning('Connection is None, nothing to close.') 39 | return 40 | db_client.close() 41 | db_client = None 42 | logging.info('Mongo connection closed.') 43 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request 2 | from fastapi.exceptions import RequestValidationError 3 | from fastapi.responses import JSONResponse 4 | from fastapi.openapi.utils import get_openapi 5 | from fastapi.staticfiles import StaticFiles 6 | 7 | from app.common.error import BadRequest, UnprocessableError 8 | from app.conf.config import Config 9 | from app.conf.logging import setup_logging 10 | from app.db.db import connect_and_init_db, close_db_connect 11 | 12 | from app.api import health 13 | from app.api.v1 import sample_resource as sample_resource_v1 14 | 15 | 16 | # Logging 17 | setup_logging() 18 | 19 | # app 20 | app = FastAPI() 21 | 22 | # mounting static folder on serve for fetching static files 23 | app.mount("/static", StaticFiles(directory="frontend/static"), name="static") 24 | 25 | 26 | # DB Events 27 | app.add_event_handler("startup", Config.app_settings_validate) 28 | app.add_event_handler("startup", connect_and_init_db) 29 | app.add_event_handler("shutdown", close_db_connect) 30 | 31 | 32 | # openapi schema 33 | def custom_openapi(): 34 | if app.openapi_schema: 35 | return app.openapi_schema 36 | openapi_schema = get_openapi( 37 | title=Config.title, 38 | version=Config.version, 39 | routes=app.routes 40 | ) 41 | app.openapi_schema = openapi_schema 42 | return app.openapi_schema 43 | 44 | 45 | app.openapi = custom_openapi 46 | 47 | 48 | # HTTP error responses 49 | @app.exception_handler(BadRequest) 50 | async def bad_request_handler(req: Request, exc: BadRequest) -> JSONResponse: 51 | return exc.gen_err_resp() 52 | 53 | 54 | @app.exception_handler(RequestValidationError) 55 | async def invalid_req_handler( 56 | req: Request, 57 | exc: RequestValidationError 58 | ) -> JSONResponse: 59 | logging.error(f'Request invalid. {str(exc)}') 60 | return JSONResponse( 61 | status_code=400, 62 | content={ 63 | "type": "about:blank", 64 | 'title': 'Bad Request', 65 | 'status': 400, 66 | 'detail': [str(exc)] 67 | } 68 | ) 69 | 70 | 71 | @app.exception_handler(UnprocessableError) 72 | async def unprocessable_error_handler( 73 | req: Request, 74 | exc: UnprocessableError 75 | ) -> JSONResponse: 76 | return exc.gen_err_resp() 77 | 78 | 79 | # API Path 80 | app.include_router( 81 | health.router, 82 | prefix='/health', 83 | tags=["health"] 84 | ) 85 | app.include_router( 86 | sample_resource_v1.router, 87 | prefix='/api/sample-resource-app/v1/sample-resource', 88 | tags=["sample resource v1"] 89 | ) 90 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexk1919/fastapi-motor-mongo-template/659af1b939829fe2962adcb9d0191c83941f7893/app/models/__init__.py -------------------------------------------------------------------------------- /app/models/create_sample_resource.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | 3 | from .sample_resource_common import SampleResourceBase, to_lower_camel_case 4 | from .mongo_model import MongoModel 5 | 6 | 7 | class CreateSampleResourceReq(SampleResourceBase): 8 | class Config: 9 | alias_generator = to_lower_camel_case 10 | 11 | 12 | class CreateSampleResourceResp(MongoModel): 13 | id: UUID 14 | -------------------------------------------------------------------------------- /app/models/get_sample_resource.py: -------------------------------------------------------------------------------- 1 | from .mongo_model import MongoModel 2 | 3 | 4 | class GetSampleResourceResp(MongoModel): 5 | name: str 6 | -------------------------------------------------------------------------------- /app/models/mongo_model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class MongoModel(BaseModel): 5 | @classmethod 6 | def from_mongo(cls, data: dict): 7 | if not data: 8 | return data 9 | id = data.pop('_id', None) 10 | return cls(**dict(data, id=id)) 11 | 12 | def mongo(self, **kwargs): 13 | exclude_unset = kwargs.pop('exclude_unset', True) 14 | by_alias = kwargs.pop('by_alias', True) 15 | 16 | parsed = self.dict( 17 | exclude_unset=exclude_unset, 18 | by_alias=by_alias, 19 | **kwargs, 20 | ) 21 | 22 | if '_id' not in parsed and 'id' in parsed: 23 | parsed['_id'] = parsed.pop('id') 24 | 25 | return parsed 26 | -------------------------------------------------------------------------------- /app/models/sample_resource_common.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, constr 2 | from datetime import datetime 3 | from uuid import UUID 4 | 5 | from .mongo_model import MongoModel 6 | 7 | 8 | def to_lower_camel_case(string: str) -> str: 9 | split_str = string.split('_') 10 | return split_str[0] + ''.join(word.capitalize() for word in split_str[1:]) 11 | 12 | 13 | class SampleResourceBase(BaseModel): 14 | name: constr(max_length=255) 15 | 16 | 17 | class SampleResourceDB(SampleResourceBase, MongoModel): 18 | id: UUID 19 | create_time: datetime 20 | update_time: datetime 21 | deleted: bool 22 | -------------------------------------------------------------------------------- /app/schemas/sample_resource.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4, UUID 2 | from datetime import datetime 3 | import logging 4 | from pymongo import ReturnDocument 5 | 6 | from app.conf.config import Config 7 | from app.db.db import AsyncIOMotorClient 8 | from app.models.sample_resource_common import SampleResourceDB 9 | from app.common.util import uuid_masker 10 | 11 | 12 | __db_name = Config.app_settings.get('db_name') 13 | __db_collection = 'sample_resource' 14 | 15 | 16 | async def create_sample_resource( 17 | conn: AsyncIOMotorClient, 18 | name: str 19 | ) -> SampleResourceDB: 20 | new_sample_resource = SampleResourceDB( 21 | id=uuid4(), 22 | name=name, 23 | create_time=datetime.utcnow(), 24 | update_time=datetime.utcnow(), 25 | deleted=False, 26 | ) 27 | logging.info( 28 | f'Inserting sample resource {name} into db...' 29 | ) 30 | await conn[__db_name][__db_collection].insert_one( 31 | new_sample_resource.mongo() 32 | ) 33 | logging.info( 34 | f"Sample resource {name} has inserted into db" 35 | ) 36 | return new_sample_resource 37 | 38 | 39 | async def get_sample_resource( 40 | conn: AsyncIOMotorClient, 41 | resource_id: UUID 42 | ) -> SampleResourceDB | None: 43 | logging.info(f"Getting sample resource {uuid_masker(resource_id)}...") 44 | sample_resource = await conn[__db_name][__db_collection].find_one( 45 | {"$and": [ 46 | {'_id': resource_id}, 47 | {'deleted': False}, 48 | ]}, 49 | ) 50 | if None is sample_resource: 51 | logging.info(f"Resource {uuid_masker(resource_id)} is None") 52 | return sample_resource 53 | 54 | 55 | async def update_sample_resource( 56 | conn: AsyncIOMotorClient, 57 | resource_id: UUID, 58 | resource_data: dict 59 | ) -> SampleResourceDB | None: 60 | logging.info( 61 | f'Updating sample resource {uuid_masker(str(resource_id))}...' 62 | ) 63 | sample_resource = \ 64 | await conn[__db_name][__db_collection].find_one_and_update( 65 | {"$and": [ 66 | {'_id': resource_id}, 67 | {'deleted': False}, 68 | ]}, 69 | {'$set': { 70 | **resource_data, 71 | "update_time": datetime.utcnow(), 72 | }}, 73 | return_document=ReturnDocument.AFTER, 74 | ) 75 | if None is sample_resource: 76 | logging.error( 77 | f"Sample resource {uuid_masker(str(resource_id))} not exist" 78 | ) 79 | else: 80 | logging.info( 81 | f'Sample resource {uuid_masker(str(resource_id))} updated' 82 | ) 83 | return sample_resource 84 | 85 | 86 | async def delete_sample_resource( 87 | conn: AsyncIOMotorClient, 88 | resource_id: UUID, 89 | ) -> SampleResourceDB | None: 90 | logging.info( 91 | f"Deleting sample resource {uuid_masker(str(resource_id))}..." 92 | ) 93 | 94 | sample_resource = await conn[__db_name][__db_collection].\ 95 | find_one_and_update( 96 | {"$and": [ 97 | {'_id': resource_id}, 98 | {'deleted': False}, 99 | ]}, 100 | {'$set': { 101 | "deleted": True, 102 | "update_time": datetime.utcnow(), 103 | }}, 104 | return_document=ReturnDocument.AFTER, 105 | ) 106 | 107 | if None is sample_resource: 108 | logging.error( 109 | f"Sample resource {uuid_masker(str(resource_id))} not exist" 110 | ) 111 | else: 112 | logging.info( 113 | f'Sample resource {uuid_masker(str(resource_id))} deleted' 114 | ) 115 | return sample_resource 116 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | server: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | container_name: releads 8 | ports: 9 | - 8888:8888 10 | environment: 11 | - LOG_LEVEL=INFO 12 | - MONGODB_URL=mongodb://mongodb/releads_fast?readPreference=secondaryPreferred&connectTimeoutMS=4000&maxIdleTimeMS=90000&heartbeatFrequencyMS=12000&w=majority&wTimeoutMS=6000 13 | - MONGODB_DBNAME=releads_fast 14 | - MAX_CONNECTIONS_COUNT=10 15 | - MIN_CONNECTIONS_COUNT=3 16 | depends_on: 17 | - mongodb 18 | 19 | mongodb: 20 | image: mongo:latest 21 | container_name: mongodb 22 | volumes: 23 | - /tmp/mongo:/data/db 24 | ports: 25 | - 27017:27017 26 | -------------------------------------------------------------------------------- /frontend/static/images/parallax1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexk1919/fastapi-motor-mongo-template/659af1b939829fe2962adcb9d0191c83941f7893/frontend/static/images/parallax1.jpg -------------------------------------------------------------------------------- /frontend/static/images/parallax2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexk1919/fastapi-motor-mongo-template/659af1b939829fe2962adcb9d0191c83941f7893/frontend/static/images/parallax2.jpg -------------------------------------------------------------------------------- /frontend/static/js/init.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | $(function(){ 3 | 4 | $('.sidenav').sidenav(); 5 | $('.parallax').parallax(); 6 | 7 | }); // end of document ready 8 | })(jQuery); // end of jQuery name space 9 | -------------------------------------------------------------------------------- /frontend/static/style.css: -------------------------------------------------------------------------------- 1 | 2 | *{ 3 | font-family: -apple-system, BlinkMacSystemFont, Roboto, 'Segoe UI',Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 4 | } 5 | .dark-mode-text{ 6 | color: #e0e0e0; 7 | } 8 | 9 | .logo-gradient{ 10 | background: linear-gradient(to right, #ff5252 0%, #ff9800 100%); 11 | -webkit-background-clip: text; 12 | -webkit-text-fill-color: transparent; 13 | font-weight: 600; 14 | } 15 | 16 | .icon-block p{ 17 | font-weight: 100; 18 | } 19 | 20 | #nav-header:hover, #nav-header:focus{ 21 | background-color: #404040; 22 | } 23 | 24 | .btn{ 25 | border-radius: 10px; 26 | } 27 | 28 | .btn:hover, .btn:focus{ 29 | background-color:#757575; 30 | box-shadow: 0 0 10px #eee; 31 | transition: 0.5s; 32 | } 33 | 34 | #icons{ 35 | color: #f57c00; 36 | } 37 | 38 | 39 | .btn-grad { 40 | background-image: linear-gradient(to right, #ff5252 0%, #ff9800 100%); 41 | text-align: center; 42 | transition: 0.5s; 43 | background-size: auto; 44 | color: white; 45 | } 46 | 47 | .btn-grad:hover { 48 | background-position: right center; /* change the direction of the change here */ 49 | box-shadow: 0 0 20px #eee; 50 | text-decoration: none; 51 | } 52 | 53 | .nav-wrapper{ 54 | padding: 0 50; 55 | } 56 | 57 | nav ul a{ 58 | color: white; 59 | } 60 | 61 | nav, .sidenav{ 62 | background-color: #202020; 63 | } 64 | 65 | .sidenav-trigger { 66 | color: #404040; 67 | } 68 | 69 | .drk-bg{ 70 | background-color: #202020; 71 | } 72 | 73 | .dropdown-content{ 74 | margin-top: 20px; 75 | } 76 | 77 | body{ 78 | background-color: #000000 ; 79 | } 80 | 81 | .parallax 82 | { 83 | background-color: #000000; 84 | } 85 | 86 | .dark-card{ 87 | background-color: #202020; 88 | } 89 | 90 | p { 91 | line-height: 2rem; 92 | } 93 | 94 | .parallax-container { 95 | min-height: 380px; 96 | line-height: 0; 97 | height: auto; 98 | color: rgba(255,255,255,.9); 99 | } 100 | .parallax-container .section { 101 | width: 100%; 102 | } 103 | 104 | .icon-block { 105 | padding: 3 15 0 15; 106 | margin-top: 20; 107 | background-color: #202020; 108 | border-radius: 10px; 109 | min-height: 320; 110 | } 111 | 112 | .icon-block p{ 113 | color: #e9e9e9; 114 | } 115 | 116 | .icon-block .material-icons { 117 | font-size: inherit; 118 | } 119 | 120 | 121 | footer.page-footer { 122 | margin: 0; 123 | } 124 | 125 | 126 | footer{ 127 | background-color: #202020; 128 | padding-top: 10px; 129 | } 130 | 131 | .upload-dashboard{ 132 | background-color: #202020; 133 | /* height: 100; */ 134 | border-radius: 10px; 135 | } 136 | 137 | /* .upload-option-dropdown-content{ 138 | transition: max-height 1s; 139 | max-height: 0; 140 | } */ 141 | 142 | .upload-option .collapsible-header{ 143 | padding-bottom: 10px; 144 | border-radius: 10px; 145 | } 146 | 147 | .upload-option .collapsible-body{ 148 | padding: 0px 20; 149 | padding-bottom: 30px; 150 | } 151 | 152 | 153 | .file-dashboard-content{ 154 | height: 600; 155 | border: #e9e9e9 solid 1px; 156 | border-radius: 10px; 157 | margin-right: 16; 158 | } 159 | 160 | #file-list{ 161 | overflow-y: scroll; 162 | /* background-color: #000000; */ 163 | height: 420; 164 | } 165 | 166 | .scroll{ 167 | position: relative; 168 | } 169 | 170 | .shade{ 171 | position:absolute; 172 | height: 27px; 173 | left: 0px; 174 | right: 0px; 175 | top: 403px; 176 | z-index:10; 177 | /* background: -moz-linear-gradient(top, rgba(255,255,255,0) 0%, rgba(152,152,152,0) 40%, rgba(23,23,23,1) 90%, rgba(0,0,0,1) 99%); /* FF3.6+ */ 178 | /* background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,0)), color-stop(40%,rgba(152,152,152,0)), color-stop(90%,rgba(23,23,23,1)), color-stop(99%,rgba(0,0,0,1))); Chrome,Safari4+ */ 179 | /* background: -webkit-linear-gradient(top, rgba(255,255,255,0) 0%,rgba(152,152,152,0) 40%,rgba(23,23,23,1) 90%,rgba(0,0,0,1) 99%); Chrome10+,Safari5.1+ */ 180 | /* background: -o-linear-gradient(top, rgba(255,255,255,0) 0%,rgba(152,152,152,0) 40%,rgba(23,23,23,1) 90%,rgba(0,0,0,1) 99%); Opera 11.10+ */ 181 | /* background: -ms-linear-gradient(top, rgba(255,255,255,0) 0%,rgba(152,152,152,0) 40%,rgba(23,23,23,1) 90%,rgba(0,0,0,1) 99%); IE10+ */ 182 | background: linear-gradient(to bottom, rgb(0 0 0/ 0%) 0%,rgb(0 0 0 / 100%) 70%); /* W3C */ 183 | } 184 | 185 | .file-dashboard-content .card{ 186 | 187 | transition: 0.4s; 188 | border-radius: 10px; 189 | border: solid 1px #e9e9e9; 190 | background-color: #000000; 191 | } 192 | 193 | .file-dashboard-content .card:hover{ 194 | box-shadow: 0px 10px 30px 0px rgba(230, 19, 19, 0.25); 195 | background-color: #202020; 196 | /* box-shadow: rgba(82, 140, 234, 0.2) 0px 10px 30px 2px, rgba(82, 140, 234, 0.2) 10px 2px 30px 2px, rgba(82, 140, 234, 0.2) 0px 20px 30px 2px; */ 197 | } 198 | 199 | /* .file-dashboard .card:hover{ 200 | box-shadow: 0 0 5px #979797; 201 | } */ 202 | 203 | .wrap-text{ 204 | overflow-wrap: break-word; 205 | } 206 | 207 | ::-webkit-scrollbar{ 208 | display: none; 209 | } 210 | 211 | @media only screen and (max-width : 992px) { 212 | .parallax-container .section { 213 | position: absolute; 214 | top: 40%; 215 | } 216 | #index-banner .section { 217 | top: 10%; 218 | } 219 | } 220 | 221 | @media only screen and (max-width : 600px) { 222 | #index-banner .section { 223 | top: 0; 224 | } 225 | 226 | .file-dashboard{ 227 | /* margin-top: 20px; */ 228 | margin: 20px; 229 | } 230 | 231 | .nav-wrapper{ 232 | padding: 0 20; 233 | } 234 | 235 | .upload-dashboard{ 236 | /* margin-top: 20px; */ 237 | margin: 10px; 238 | } 239 | 240 | .file-dashboard-content{ 241 | margin: 10px; 242 | } 243 | 244 | .icon-block{ 245 | min-height: 350; 246 | } 247 | #connect-section { 248 | text-align: left; 249 | } 250 | } -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "anyio" 5 | version = "3.6.2" 6 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 7 | category = "main" 8 | optional = false 9 | python-versions = ">=3.6.2" 10 | files = [ 11 | {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, 12 | {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, 13 | ] 14 | 15 | [package.dependencies] 16 | idna = ">=2.8" 17 | sniffio = ">=1.1" 18 | 19 | [package.extras] 20 | doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 21 | test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] 22 | trio = ["trio (>=0.16,<0.22)"] 23 | 24 | [[package]] 25 | name = "attrs" 26 | version = "22.2.0" 27 | description = "Classes Without Boilerplate" 28 | category = "dev" 29 | optional = false 30 | python-versions = ">=3.6" 31 | files = [ 32 | {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, 33 | {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, 34 | ] 35 | 36 | [package.extras] 37 | cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] 38 | dev = ["attrs[docs,tests]"] 39 | docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] 40 | tests = ["attrs[tests-no-zope]", "zope.interface"] 41 | tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] 42 | 43 | [[package]] 44 | name = "certifi" 45 | version = "2022.12.7" 46 | description = "Python package for providing Mozilla's CA Bundle." 47 | category = "dev" 48 | optional = false 49 | python-versions = ">=3.6" 50 | files = [ 51 | {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, 52 | {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, 53 | ] 54 | 55 | [[package]] 56 | name = "charset-normalizer" 57 | version = "3.1.0" 58 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 59 | category = "dev" 60 | optional = false 61 | python-versions = ">=3.7.0" 62 | files = [ 63 | {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, 64 | {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, 65 | {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, 66 | {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, 67 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, 68 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, 69 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, 70 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, 71 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, 72 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, 73 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, 74 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, 75 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, 76 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, 77 | {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, 78 | {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, 79 | {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, 80 | {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, 81 | {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, 82 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, 83 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, 84 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, 85 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, 86 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, 87 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, 88 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, 89 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, 90 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, 91 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, 92 | {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, 93 | {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, 94 | {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, 95 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, 96 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, 97 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, 98 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, 99 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, 100 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, 101 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, 102 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, 103 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, 104 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, 105 | {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, 106 | {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, 107 | {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, 108 | {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, 109 | {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, 110 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, 111 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, 112 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, 113 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, 114 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, 115 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, 116 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, 117 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, 118 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, 119 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, 120 | {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, 121 | {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, 122 | {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, 123 | {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, 124 | {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, 125 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, 126 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, 127 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, 128 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, 129 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, 130 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, 131 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, 132 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, 133 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, 134 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, 135 | {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, 136 | {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, 137 | {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, 138 | ] 139 | 140 | [[package]] 141 | name = "click" 142 | version = "8.1.3" 143 | description = "Composable command line interface toolkit" 144 | category = "main" 145 | optional = false 146 | python-versions = ">=3.7" 147 | files = [ 148 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 149 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 150 | ] 151 | 152 | [package.dependencies] 153 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 154 | 155 | [[package]] 156 | name = "colorama" 157 | version = "0.4.6" 158 | description = "Cross-platform colored terminal text." 159 | category = "main" 160 | optional = false 161 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 162 | files = [ 163 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 164 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 165 | ] 166 | 167 | [[package]] 168 | name = "dnspython" 169 | version = "2.3.0" 170 | description = "DNS toolkit" 171 | category = "main" 172 | optional = false 173 | python-versions = ">=3.7,<4.0" 174 | files = [ 175 | {file = "dnspython-2.3.0-py3-none-any.whl", hash = "sha256:89141536394f909066cabd112e3e1a37e4e654db00a25308b0f130bc3152eb46"}, 176 | {file = "dnspython-2.3.0.tar.gz", hash = "sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9"}, 177 | ] 178 | 179 | [package.extras] 180 | curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] 181 | dnssec = ["cryptography (>=2.6,<40.0)"] 182 | doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.11.0)"] 183 | doq = ["aioquic (>=0.9.20)"] 184 | idna = ["idna (>=2.1,<4.0)"] 185 | trio = ["trio (>=0.14,<0.23)"] 186 | wmi = ["wmi (>=1.5.1,<2.0.0)"] 187 | 188 | [[package]] 189 | name = "fastapi" 190 | version = "0.92.0" 191 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 192 | category = "main" 193 | optional = false 194 | python-versions = ">=3.7" 195 | files = [ 196 | {file = "fastapi-0.92.0-py3-none-any.whl", hash = "sha256:ae7b97c778e2f2ec3fb3cb4fb14162129411d99907fb71920f6d69a524340ebf"}, 197 | {file = "fastapi-0.92.0.tar.gz", hash = "sha256:023a0f5bd2c8b2609014d3bba1e14a1d7df96c6abea0a73070621c9862b9a4de"}, 198 | ] 199 | 200 | [package.dependencies] 201 | pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" 202 | starlette = ">=0.25.0,<0.26.0" 203 | 204 | [package.extras] 205 | all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] 206 | dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (>=0.12.0,<0.21.0)"] 207 | doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer[all] (>=0.6.1,<0.8.0)"] 208 | test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.10.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.6.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] 209 | 210 | [[package]] 211 | name = "gunicorn" 212 | version = "20.1.0" 213 | description = "WSGI HTTP Server for UNIX" 214 | category = "main" 215 | optional = false 216 | python-versions = ">=3.5" 217 | files = [ 218 | {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, 219 | {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, 220 | ] 221 | 222 | [package.dependencies] 223 | setuptools = ">=3.0" 224 | 225 | [package.extras] 226 | eventlet = ["eventlet (>=0.24.1)"] 227 | gevent = ["gevent (>=1.4.0)"] 228 | setproctitle = ["setproctitle"] 229 | tornado = ["tornado (>=0.2)"] 230 | 231 | [[package]] 232 | name = "h11" 233 | version = "0.14.0" 234 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 235 | category = "main" 236 | optional = false 237 | python-versions = ">=3.7" 238 | files = [ 239 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 240 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 241 | ] 242 | 243 | [[package]] 244 | name = "idna" 245 | version = "3.4" 246 | description = "Internationalized Domain Names in Applications (IDNA)" 247 | category = "main" 248 | optional = false 249 | python-versions = ">=3.5" 250 | files = [ 251 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, 252 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, 253 | ] 254 | 255 | [[package]] 256 | name = "iniconfig" 257 | version = "2.0.0" 258 | description = "brain-dead simple config-ini parsing" 259 | category = "dev" 260 | optional = false 261 | python-versions = ">=3.7" 262 | files = [ 263 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 264 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 265 | ] 266 | 267 | [[package]] 268 | name = "motor" 269 | version = "3.1.1" 270 | description = "Non-blocking MongoDB driver for Tornado or asyncio" 271 | category = "main" 272 | optional = false 273 | python-versions = ">=3.7" 274 | files = [ 275 | {file = "motor-3.1.1-py3-none-any.whl", hash = "sha256:01d93d7c512810dcd85f4d634a7244ba42ff6be7340c869791fe793561e734da"}, 276 | {file = "motor-3.1.1.tar.gz", hash = "sha256:a4bdadf8a08ebb186ba16e557ba432aa867f689a42b80f2e9f8b24bbb1604742"}, 277 | ] 278 | 279 | [package.dependencies] 280 | pymongo = ">=4.1,<5" 281 | 282 | [package.extras] 283 | aws = ["pymongo[aws] (>=4.1,<5)"] 284 | encryption = ["pymongo[encryption] (>=4.1,<5)"] 285 | gssapi = ["pymongo[gssapi] (>=4.1,<5)"] 286 | ocsp = ["pymongo[ocsp] (>=4.1,<5)"] 287 | snappy = ["pymongo[snappy] (>=4.1,<5)"] 288 | srv = ["pymongo[srv] (>=4.1,<5)"] 289 | zstd = ["pymongo[zstd] (>=4.1,<5)"] 290 | 291 | [[package]] 292 | name = "mypy" 293 | version = "1.1.1" 294 | description = "Optional static typing for Python" 295 | category = "dev" 296 | optional = false 297 | python-versions = ">=3.7" 298 | files = [ 299 | {file = "mypy-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39c7119335be05630611ee798cc982623b9e8f0cff04a0b48dfc26100e0b97af"}, 300 | {file = "mypy-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61bf08362e93b6b12fad3eab68c4ea903a077b87c90ac06c11e3d7a09b56b9c1"}, 301 | {file = "mypy-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbb19c9f662e41e474e0cff502b7064a7edc6764f5262b6cd91d698163196799"}, 302 | {file = "mypy-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:315ac73cc1cce4771c27d426b7ea558fb4e2836f89cb0296cbe056894e3a1f78"}, 303 | {file = "mypy-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5cb14ff9919b7df3538590fc4d4c49a0f84392237cbf5f7a816b4161c061829e"}, 304 | {file = "mypy-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:26cdd6a22b9b40b2fd71881a8a4f34b4d7914c679f154f43385ca878a8297389"}, 305 | {file = "mypy-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b5f81b40d94c785f288948c16e1f2da37203c6006546c5d947aab6f90aefef2"}, 306 | {file = "mypy-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b437be1c02712a605591e1ed1d858aba681757a1e55fe678a15c2244cd68a5"}, 307 | {file = "mypy-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d809f88734f44a0d44959d795b1e6f64b2bbe0ea4d9cc4776aa588bb4229fc1c"}, 308 | {file = "mypy-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:a380c041db500e1410bb5b16b3c1c35e61e773a5c3517926b81dfdab7582be54"}, 309 | {file = "mypy-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b7c7b708fe9a871a96626d61912e3f4ddd365bf7f39128362bc50cbd74a634d5"}, 310 | {file = "mypy-1.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1c10fa12df1232c936830839e2e935d090fc9ee315744ac33b8a32216b93707"}, 311 | {file = "mypy-1.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0a28a76785bf57655a8ea5eb0540a15b0e781c807b5aa798bd463779988fa1d5"}, 312 | {file = "mypy-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ef6a01e563ec6a4940784c574d33f6ac1943864634517984471642908b30b6f7"}, 313 | {file = "mypy-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d64c28e03ce40d5303450f547e07418c64c241669ab20610f273c9e6290b4b0b"}, 314 | {file = "mypy-1.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64cc3afb3e9e71a79d06e3ed24bb508a6d66f782aff7e56f628bf35ba2e0ba51"}, 315 | {file = "mypy-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce61663faf7a8e5ec6f456857bfbcec2901fbdb3ad958b778403f63b9e606a1b"}, 316 | {file = "mypy-1.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2b0c373d071593deefbcdd87ec8db91ea13bd8f1328d44947e88beae21e8d5e9"}, 317 | {file = "mypy-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:2888ce4fe5aae5a673386fa232473014056967f3904f5abfcf6367b5af1f612a"}, 318 | {file = "mypy-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:19ba15f9627a5723e522d007fe708007bae52b93faab00f95d72f03e1afa9598"}, 319 | {file = "mypy-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:59bbd71e5c58eed2e992ce6523180e03c221dcd92b52f0e792f291d67b15a71c"}, 320 | {file = "mypy-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9401e33814cec6aec8c03a9548e9385e0e228fc1b8b0a37b9ea21038e64cdd8a"}, 321 | {file = "mypy-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b398d8b1f4fba0e3c6463e02f8ad3346f71956b92287af22c9b12c3ec965a9f"}, 322 | {file = "mypy-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:69b35d1dcb5707382810765ed34da9db47e7f95b3528334a3c999b0c90fe523f"}, 323 | {file = "mypy-1.1.1-py3-none-any.whl", hash = "sha256:4e4e8b362cdf99ba00c2b218036002bdcdf1e0de085cdb296a49df03fb31dfc4"}, 324 | {file = "mypy-1.1.1.tar.gz", hash = "sha256:ae9ceae0f5b9059f33dbc62dea087e942c0ccab4b7a003719cb70f9b8abfa32f"}, 325 | ] 326 | 327 | [package.dependencies] 328 | mypy-extensions = ">=1.0.0" 329 | typing-extensions = ">=3.10" 330 | 331 | [package.extras] 332 | dmypy = ["psutil (>=4.0)"] 333 | install-types = ["pip"] 334 | python2 = ["typed-ast (>=1.4.0,<2)"] 335 | reports = ["lxml"] 336 | 337 | [[package]] 338 | name = "mypy-extensions" 339 | version = "1.0.0" 340 | description = "Type system extensions for programs checked with the mypy type checker." 341 | category = "dev" 342 | optional = false 343 | python-versions = ">=3.5" 344 | files = [ 345 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 346 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 347 | ] 348 | 349 | [[package]] 350 | name = "packaging" 351 | version = "23.0" 352 | description = "Core utilities for Python packages" 353 | category = "dev" 354 | optional = false 355 | python-versions = ">=3.7" 356 | files = [ 357 | {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, 358 | {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, 359 | ] 360 | 361 | [[package]] 362 | name = "pluggy" 363 | version = "1.0.0" 364 | description = "plugin and hook calling mechanisms for python" 365 | category = "dev" 366 | optional = false 367 | python-versions = ">=3.6" 368 | files = [ 369 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 370 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 371 | ] 372 | 373 | [package.extras] 374 | dev = ["pre-commit", "tox"] 375 | testing = ["pytest", "pytest-benchmark"] 376 | 377 | [[package]] 378 | name = "psutil" 379 | version = "5.9.4" 380 | description = "Cross-platform lib for process and system monitoring in Python." 381 | category = "main" 382 | optional = false 383 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 384 | files = [ 385 | {file = "psutil-5.9.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8"}, 386 | {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe"}, 387 | {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549"}, 388 | {file = "psutil-5.9.4-cp27-cp27m-win32.whl", hash = "sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad"}, 389 | {file = "psutil-5.9.4-cp27-cp27m-win_amd64.whl", hash = "sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94"}, 390 | {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24"}, 391 | {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7"}, 392 | {file = "psutil-5.9.4-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7"}, 393 | {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1"}, 394 | {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08"}, 395 | {file = "psutil-5.9.4-cp36-abi3-win32.whl", hash = "sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff"}, 396 | {file = "psutil-5.9.4-cp36-abi3-win_amd64.whl", hash = "sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4"}, 397 | {file = "psutil-5.9.4-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e"}, 398 | {file = "psutil-5.9.4.tar.gz", hash = "sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62"}, 399 | ] 400 | 401 | [package.extras] 402 | test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] 403 | 404 | [[package]] 405 | name = "pydantic" 406 | version = "1.10.5" 407 | description = "Data validation and settings management using python type hints" 408 | category = "main" 409 | optional = false 410 | python-versions = ">=3.7" 411 | files = [ 412 | {file = "pydantic-1.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5920824fe1e21cbb3e38cf0f3dd24857c8959801d1031ce1fac1d50857a03bfb"}, 413 | {file = "pydantic-1.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3bb99cf9655b377db1a9e47fa4479e3330ea96f4123c6c8200e482704bf1eda2"}, 414 | {file = "pydantic-1.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2185a3b3d98ab4506a3f6707569802d2d92c3a7ba3a9a35683a7709ea6c2aaa2"}, 415 | {file = "pydantic-1.10.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f582cac9d11c227c652d3ce8ee223d94eb06f4228b52a8adaafa9fa62e73d5c9"}, 416 | {file = "pydantic-1.10.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c9e5b778b6842f135902e2d82624008c6a79710207e28e86966cd136c621bfee"}, 417 | {file = "pydantic-1.10.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72ef3783be8cbdef6bca034606a5de3862be6b72415dc5cb1fb8ddbac110049a"}, 418 | {file = "pydantic-1.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:45edea10b75d3da43cfda12f3792833a3fa70b6eee4db1ed6aed528cef17c74e"}, 419 | {file = "pydantic-1.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:63200cd8af1af2c07964546b7bc8f217e8bda9d0a2ef0ee0c797b36353914984"}, 420 | {file = "pydantic-1.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:305d0376c516b0dfa1dbefeae8c21042b57b496892d721905a6ec6b79494a66d"}, 421 | {file = "pydantic-1.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fd326aff5d6c36f05735c7c9b3d5b0e933b4ca52ad0b6e4b38038d82703d35b"}, 422 | {file = "pydantic-1.10.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bb0452d7b8516178c969d305d9630a3c9b8cf16fcf4713261c9ebd465af0d73"}, 423 | {file = "pydantic-1.10.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9a9d9155e2a9f38b2eb9374c88f02fd4d6851ae17b65ee786a87d032f87008f8"}, 424 | {file = "pydantic-1.10.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f836444b4c5ece128b23ec36a446c9ab7f9b0f7981d0d27e13a7c366ee163f8a"}, 425 | {file = "pydantic-1.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:8481dca324e1c7b715ce091a698b181054d22072e848b6fc7895cd86f79b4449"}, 426 | {file = "pydantic-1.10.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:87f831e81ea0589cd18257f84386bf30154c5f4bed373b7b75e5cb0b5d53ea87"}, 427 | {file = "pydantic-1.10.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ce1612e98c6326f10888df951a26ec1a577d8df49ddcaea87773bfbe23ba5cc"}, 428 | {file = "pydantic-1.10.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58e41dd1e977531ac6073b11baac8c013f3cd8706a01d3dc74e86955be8b2c0c"}, 429 | {file = "pydantic-1.10.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6a4b0aab29061262065bbdede617ef99cc5914d1bf0ddc8bcd8e3d7928d85bd6"}, 430 | {file = "pydantic-1.10.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:36e44a4de37b8aecffa81c081dbfe42c4d2bf9f6dff34d03dce157ec65eb0f15"}, 431 | {file = "pydantic-1.10.5-cp37-cp37m-win_amd64.whl", hash = "sha256:261f357f0aecda005934e413dfd7aa4077004a174dafe414a8325e6098a8e419"}, 432 | {file = "pydantic-1.10.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b429f7c457aebb7fbe7cd69c418d1cd7c6fdc4d3c8697f45af78b8d5a7955760"}, 433 | {file = "pydantic-1.10.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:663d2dd78596c5fa3eb996bc3f34b8c2a592648ad10008f98d1348be7ae212fb"}, 434 | {file = "pydantic-1.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51782fd81f09edcf265823c3bf43ff36d00db246eca39ee765ef58dc8421a642"}, 435 | {file = "pydantic-1.10.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c428c0f64a86661fb4873495c4fac430ec7a7cef2b8c1c28f3d1a7277f9ea5ab"}, 436 | {file = "pydantic-1.10.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:76c930ad0746c70f0368c4596020b736ab65b473c1f9b3872310a835d852eb19"}, 437 | {file = "pydantic-1.10.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3257bd714de9db2102b742570a56bf7978e90441193acac109b1f500290f5718"}, 438 | {file = "pydantic-1.10.5-cp38-cp38-win_amd64.whl", hash = "sha256:f5bee6c523d13944a1fdc6f0525bc86dbbd94372f17b83fa6331aabacc8fd08e"}, 439 | {file = "pydantic-1.10.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:532e97c35719f137ee5405bd3eeddc5c06eb91a032bc755a44e34a712420daf3"}, 440 | {file = "pydantic-1.10.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ca9075ab3de9e48b75fa8ccb897c34ccc1519177ad8841d99f7fd74cf43be5bf"}, 441 | {file = "pydantic-1.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd46a0e6296346c477e59a954da57beaf9c538da37b9df482e50f836e4a7d4bb"}, 442 | {file = "pydantic-1.10.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3353072625ea2a9a6c81ad01b91e5c07fa70deb06368c71307529abf70d23325"}, 443 | {file = "pydantic-1.10.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3f9d9b2be177c3cb6027cd67fbf323586417868c06c3c85d0d101703136e6b31"}, 444 | {file = "pydantic-1.10.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b473d00ccd5c2061fd896ac127b7755baad233f8d996ea288af14ae09f8e0d1e"}, 445 | {file = "pydantic-1.10.5-cp39-cp39-win_amd64.whl", hash = "sha256:5f3bc8f103b56a8c88021d481410874b1f13edf6e838da607dcb57ecff9b4594"}, 446 | {file = "pydantic-1.10.5-py3-none-any.whl", hash = "sha256:7c5b94d598c90f2f46b3a983ffb46ab806a67099d118ae0da7ef21a2a4033b28"}, 447 | {file = "pydantic-1.10.5.tar.gz", hash = "sha256:9e337ac83686645a46db0e825acceea8e02fca4062483f40e9ae178e8bd1103a"}, 448 | ] 449 | 450 | [package.dependencies] 451 | typing-extensions = ">=4.2.0" 452 | 453 | [package.extras] 454 | dotenv = ["python-dotenv (>=0.10.4)"] 455 | email = ["email-validator (>=1.0.3)"] 456 | 457 | [[package]] 458 | name = "pymongo" 459 | version = "4.3.3" 460 | description = "Python driver for MongoDB " 461 | category = "main" 462 | optional = false 463 | python-versions = ">=3.7" 464 | files = [ 465 | {file = "pymongo-4.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:74731c9e423c93cbe791f60c27030b6af6a948cef67deca079da6cd1bb583a8e"}, 466 | {file = "pymongo-4.3.3-cp310-cp310-manylinux1_i686.whl", hash = "sha256:66413c50d510e5bcb0afc79880d1693a2185bcea003600ed898ada31338c004e"}, 467 | {file = "pymongo-4.3.3-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:9b87b23570565a6ddaa9244d87811c2ee9cffb02a753c8a2da9c077283d85845"}, 468 | {file = "pymongo-4.3.3-cp310-cp310-manylinux2014_i686.whl", hash = "sha256:695939036a320f4329ccf1627edefbbb67cc7892b8222d297b0dd2313742bfee"}, 469 | {file = "pymongo-4.3.3-cp310-cp310-manylinux2014_ppc64le.whl", hash = "sha256:ffcc8394123ea8d43fff8e5d000095fe7741ce3f8988366c5c919c4f5eb179d3"}, 470 | {file = "pymongo-4.3.3-cp310-cp310-manylinux2014_s390x.whl", hash = "sha256:943f208840777f34312c103a2d1caab02d780c4e9be26b3714acf6c4715ba7e1"}, 471 | {file = "pymongo-4.3.3-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:01f7cbe88d22440b6594c955e37312d932fd632ffed1a86d0c361503ca82cc9d"}, 472 | {file = "pymongo-4.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdb87309de97c63cb9a69132e1cb16be470e58cffdfbad68fdd1dc292b22a840"}, 473 | {file = "pymongo-4.3.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d86c35d94b5499689354ccbc48438a79f449481ee6300f3e905748edceed78e7"}, 474 | {file = "pymongo-4.3.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a966d5304b7d90c45c404914e06bbf02c5bf7e99685c6c12f0047ef2aa837142"}, 475 | {file = "pymongo-4.3.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be1d2ce7e269215c3ee9a215e296b7a744aff4f39233486d2c4d77f5f0c561a6"}, 476 | {file = "pymongo-4.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55b6163dac53ef1e5d834297810c178050bd0548a4136cd4e0f56402185916ca"}, 477 | {file = "pymongo-4.3.3-cp310-cp310-win32.whl", hash = "sha256:dc0cff74cd36d7e1edba91baa09622c35a8a57025f2f2b7a41e3f83b1db73186"}, 478 | {file = "pymongo-4.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:cafa52873ae12baa512a8721afc20de67a36886baae6a5f394ddef0ce9391f91"}, 479 | {file = "pymongo-4.3.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:599d3f6fbef31933b96e2d906b0f169b3371ff79ea6aaf6ecd76c947a3508a3d"}, 480 | {file = "pymongo-4.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0640b4e9d008e13956b004d1971a23377b3d45491f87082161c92efb1e6c0d6"}, 481 | {file = "pymongo-4.3.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:341221e2f2866a5960e6f8610f4cbac0bb13097f3b1a289aa55aba984fc0d969"}, 482 | {file = "pymongo-4.3.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7fac06a539daef4fcf5d8288d0d21b412f9b750454cd5a3cf90484665db442a"}, 483 | {file = "pymongo-4.3.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3a51901066696c4af38c6c63a1f0aeffd5e282367ff475de8c191ec9609b56d"}, 484 | {file = "pymongo-4.3.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3055510fdfdb1775bc8baa359783022f70bb553f2d46e153c094dfcb08578ff"}, 485 | {file = "pymongo-4.3.3-cp311-cp311-win32.whl", hash = "sha256:524d78673518dcd352a91541ecd2839c65af92dc883321c2109ef6e5cd22ef23"}, 486 | {file = "pymongo-4.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:b8a03af1ce79b902a43f5f694c4ca8d92c2a4195db0966f08f266549e2fc49bc"}, 487 | {file = "pymongo-4.3.3-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:39b03045c71f761aee96a12ebfbc2f4be89e724ff6f5e31c2574c1a0e2add8bd"}, 488 | {file = "pymongo-4.3.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6fcfbf435eebf8a1765c6d1f46821740ebe9f54f815a05c8fc30d789ef43cb12"}, 489 | {file = "pymongo-4.3.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:7d43ac9c7eeda5100fb0a7152fab7099c9cf9e5abd3bb36928eb98c7d7a339c6"}, 490 | {file = "pymongo-4.3.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3b93043b14ba7eb08c57afca19751658ece1cfa2f0b7b1fb5c7a41452fbb8482"}, 491 | {file = "pymongo-4.3.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:c09956606c08c4a7c6178a04ba2dd9388fcc5db32002ade9c9bc865ab156ab6d"}, 492 | {file = "pymongo-4.3.3-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:b0cfe925610f2fd59555bb7fc37bd739e4b197d33f2a8b2fae7b9c0c6640318c"}, 493 | {file = "pymongo-4.3.3-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:4d00b91c77ceb064c9b0459f0d6ea5bfdbc53ea9e17cf75731e151ef25a830c7"}, 494 | {file = "pymongo-4.3.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:c6258a3663780ae47ba73d43eb63c79c40ffddfb764e09b56df33be2f9479837"}, 495 | {file = "pymongo-4.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29e758f0e734e1e90357ae01ec9c6daf19ff60a051192fe110d8fb25c62600e"}, 496 | {file = "pymongo-4.3.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f3621a46cdc7a9ba8080422262398a91762a581d27e0647746588d3f995c88"}, 497 | {file = "pymongo-4.3.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:47f7aa217b25833cd6f0e72b0d224be55393c2692b4f5e0561cb3beeb10296e9"}, 498 | {file = "pymongo-4.3.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2fdc855149efe7cdcc2a01ca02bfa24761c640203ea94df467f3baf19078be"}, 499 | {file = "pymongo-4.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5effd87c7d363890259eac16c56a4e8da307286012c076223997f8cc4a8c435b"}, 500 | {file = "pymongo-4.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6dd1cf2995fdbd64fc0802313e8323f5fa18994d51af059b5b8862b73b5e53f0"}, 501 | {file = "pymongo-4.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:bb869707d8e30645ed6766e44098600ca6cdf7989c22a3ea2b7966bb1d98d4b2"}, 502 | {file = "pymongo-4.3.3-cp37-cp37m-win32.whl", hash = "sha256:49210feb0be8051a64d71691f0acbfbedc33e149f0a5d6e271fddf6a12493fed"}, 503 | {file = "pymongo-4.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:54c377893f2cbbffe39abcff5ff2e917b082c364521fa079305f6f064e1a24a9"}, 504 | {file = "pymongo-4.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c184ec5be465c0319440734491e1aa4709b5f3ba75fdfc9dbbc2ae715a7f6829"}, 505 | {file = "pymongo-4.3.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:dca34367a4e77fcab0693e603a959878eaf2351585e7d752cac544bc6b2dee46"}, 506 | {file = "pymongo-4.3.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cd6a4afb20fb3c26a7bfd4611a0bbb24d93cbd746f5eb881f114b5e38fd55501"}, 507 | {file = "pymongo-4.3.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0c466710871d0026c190fc4141e810cf9d9affbf4935e1d273fbdc7d7cda6143"}, 508 | {file = "pymongo-4.3.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:d07d06dba5b5f7d80f9cc45501456e440f759fe79f9895922ed486237ac378a8"}, 509 | {file = "pymongo-4.3.3-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:711bc52cb98e7892c03e9b669bebd89c0a890a90dbc6d5bb2c47f30239bac6e9"}, 510 | {file = "pymongo-4.3.3-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:34b040e095e1671df0c095ec0b04fc4ebb19c4c160f87c2b55c079b16b1a6b00"}, 511 | {file = "pymongo-4.3.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4ed00f96e147f40b565fe7530d1da0b0f3ab803d5dd5b683834500fa5d195ec4"}, 512 | {file = "pymongo-4.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef888f48eb9203ee1e04b9fb27429017b290fb916f1e7826c2f7808c88798394"}, 513 | {file = "pymongo-4.3.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:316498b642c00401370b2156b5233b256f9b33799e0a8d9d0b8a7da217a20fca"}, 514 | {file = "pymongo-4.3.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa7e202feb683dad74f00dea066690448d0cfa310f8a277db06ec8eb466601b5"}, 515 | {file = "pymongo-4.3.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52896e22115c97f1c829db32aa2760b0d61839cfe08b168c2b1d82f31dbc5f55"}, 516 | {file = "pymongo-4.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c051fe37c96b9878f37fa58906cb53ecd13dcb7341d3a85f1e2e2f6b10782d9"}, 517 | {file = "pymongo-4.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5134d33286c045393c7beb51be29754647cec5ebc051cf82799c5ce9820a2ca2"}, 518 | {file = "pymongo-4.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a9c2885b4a8e6e39db5662d8b02ca6dcec796a45e48c2de12552841f061692ba"}, 519 | {file = "pymongo-4.3.3-cp38-cp38-win32.whl", hash = "sha256:a6cd6f1db75eb07332bd3710f58f5fce4967eadbf751bad653842750a61bda62"}, 520 | {file = "pymongo-4.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:d5571b6978750601f783cea07fb6b666837010ca57e5cefa389c1d456f6222e2"}, 521 | {file = "pymongo-4.3.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:81d1a7303bd02ca1c5be4aacd4db73593f573ba8e0c543c04c6da6275fd7a47e"}, 522 | {file = "pymongo-4.3.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:016c412118e1c23fef3a1eada4f83ae6e8844fd91986b2e066fc1b0013cdd9ae"}, 523 | {file = "pymongo-4.3.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:8fd6e191b92a10310f5a6cfe10d6f839d79d192fb02480bda325286bd1c7b385"}, 524 | {file = "pymongo-4.3.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:e2961b05f9c04a53da8bfc72f1910b6aec7205fcf3ac9c036d24619979bbee4b"}, 525 | {file = "pymongo-4.3.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:b38a96b3eed8edc515b38257f03216f382c4389d022a8834667e2bc63c0c0c31"}, 526 | {file = "pymongo-4.3.3-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:c1a70c51da9fa95bd75c167edb2eb3f3c4d27bc4ddd29e588f21649d014ec0b7"}, 527 | {file = "pymongo-4.3.3-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:8a06a0c02f5606330e8f2e2f3b7949877ca7e4024fa2bff5a4506bec66c49ec7"}, 528 | {file = "pymongo-4.3.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:6c2216d8b6a6d019c6f4b1ad55f890e5e77eb089309ffc05b6911c09349e7474"}, 529 | {file = "pymongo-4.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eac0a143ef4f28f49670bf89cb15847eb80b375d55eba401ca2f777cd425f338"}, 530 | {file = "pymongo-4.3.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:08fc250b5552ee97ceeae0f52d8b04f360291285fc7437f13daa516ce38fdbc6"}, 531 | {file = "pymongo-4.3.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704d939656e21b073bfcddd7228b29e0e8a93dd27b54240eaafc0b9a631629a6"}, 532 | {file = "pymongo-4.3.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1074f1a6f23e28b983c96142f2d45be03ec55d93035b471c26889a7ad2365db3"}, 533 | {file = "pymongo-4.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b16250238de8dafca225647608dddc7bbb5dce3dd53b4d8e63c1cc287394c2f"}, 534 | {file = "pymongo-4.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7761cacb8745093062695b11574effea69db636c2fd0a9269a1f0183712927b4"}, 535 | {file = "pymongo-4.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fd7bb378d82b88387dc10227cfd964f6273eb083e05299e9b97cbe075da12d11"}, 536 | {file = "pymongo-4.3.3-cp39-cp39-win32.whl", hash = "sha256:dc24d245026a72d9b4953729d31813edd4bd4e5c13622d96e27c284942d33f24"}, 537 | {file = "pymongo-4.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:fc28e8d85d392a06434e9a934908d97e2cf453d69488d2bcd0bfb881497fd975"}, 538 | {file = "pymongo-4.3.3.tar.gz", hash = "sha256:34e95ffb0a68bffbc3b437f2d1f25fc916fef3df5cdeed0992da5f42fae9b807"}, 539 | ] 540 | 541 | [package.dependencies] 542 | dnspython = ">=1.16.0,<3.0.0" 543 | 544 | [package.extras] 545 | aws = ["pymongo-auth-aws (<2.0.0)"] 546 | encryption = ["pymongo-auth-aws (<2.0.0)", "pymongocrypt (>=1.3.0,<2.0.0)"] 547 | gssapi = ["pykerberos"] 548 | ocsp = ["certifi", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] 549 | snappy = ["python-snappy"] 550 | zstd = ["zstandard"] 551 | 552 | [[package]] 553 | name = "pytest" 554 | version = "7.2.2" 555 | description = "pytest: simple powerful testing with Python" 556 | category = "dev" 557 | optional = false 558 | python-versions = ">=3.7" 559 | files = [ 560 | {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"}, 561 | {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"}, 562 | ] 563 | 564 | [package.dependencies] 565 | attrs = ">=19.2.0" 566 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 567 | iniconfig = "*" 568 | packaging = "*" 569 | pluggy = ">=0.12,<2.0" 570 | 571 | [package.extras] 572 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 573 | 574 | [[package]] 575 | name = "pytest-asyncio" 576 | version = "0.20.3" 577 | description = "Pytest support for asyncio" 578 | category = "dev" 579 | optional = false 580 | python-versions = ">=3.7" 581 | files = [ 582 | {file = "pytest-asyncio-0.20.3.tar.gz", hash = "sha256:83cbf01169ce3e8eb71c6c278ccb0574d1a7a3bb8eaaf5e50e0ad342afb33b36"}, 583 | {file = "pytest_asyncio-0.20.3-py3-none-any.whl", hash = "sha256:f129998b209d04fcc65c96fc85c11e5316738358909a8399e93be553d7656442"}, 584 | ] 585 | 586 | [package.dependencies] 587 | pytest = ">=6.1.0" 588 | 589 | [package.extras] 590 | docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] 591 | testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] 592 | 593 | [[package]] 594 | name = "python-dotenv" 595 | version = "1.0.0" 596 | description = "Read key-value pairs from a .env file and set them as environment variables" 597 | category = "main" 598 | optional = false 599 | python-versions = ">=3.8" 600 | files = [ 601 | {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, 602 | {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, 603 | ] 604 | 605 | [package.extras] 606 | cli = ["click (>=5.0)"] 607 | 608 | [[package]] 609 | name = "pyyaml" 610 | version = "6.0" 611 | description = "YAML parser and emitter for Python" 612 | category = "main" 613 | optional = false 614 | python-versions = ">=3.6" 615 | files = [ 616 | {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, 617 | {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, 618 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, 619 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, 620 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, 621 | {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, 622 | {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, 623 | {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, 624 | {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, 625 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, 626 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, 627 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, 628 | {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, 629 | {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, 630 | {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, 631 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, 632 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, 633 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, 634 | {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, 635 | {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, 636 | {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, 637 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, 638 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, 639 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, 640 | {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, 641 | {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, 642 | {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, 643 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, 644 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, 645 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, 646 | {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, 647 | {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, 648 | {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, 649 | {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, 650 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, 651 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, 652 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, 653 | {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, 654 | {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, 655 | {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, 656 | ] 657 | 658 | [[package]] 659 | name = "requests" 660 | version = "2.28.2" 661 | description = "Python HTTP for Humans." 662 | category = "dev" 663 | optional = false 664 | python-versions = ">=3.7, <4" 665 | files = [ 666 | {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, 667 | {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, 668 | ] 669 | 670 | [package.dependencies] 671 | certifi = ">=2017.4.17" 672 | charset-normalizer = ">=2,<4" 673 | idna = ">=2.5,<4" 674 | urllib3 = ">=1.21.1,<1.27" 675 | 676 | [package.extras] 677 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 678 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 679 | 680 | [[package]] 681 | name = "setuptools" 682 | version = "67.5.1" 683 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 684 | category = "main" 685 | optional = false 686 | python-versions = ">=3.7" 687 | files = [ 688 | {file = "setuptools-67.5.1-py3-none-any.whl", hash = "sha256:1c39d42bda4cb89f7fdcad52b6762e3c309ec8f8715b27c684176b7d71283242"}, 689 | {file = "setuptools-67.5.1.tar.gz", hash = "sha256:15136a251127da2d2e77ac7a1bc231eb504654f7e3346d93613a13f2e2787535"}, 690 | ] 691 | 692 | [package.extras] 693 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 694 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 695 | testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 696 | 697 | [[package]] 698 | name = "sniffio" 699 | version = "1.3.0" 700 | description = "Sniff out which async library your code is running under" 701 | category = "main" 702 | optional = false 703 | python-versions = ">=3.7" 704 | files = [ 705 | {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, 706 | {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, 707 | ] 708 | 709 | [[package]] 710 | name = "starlette" 711 | version = "0.25.0" 712 | description = "The little ASGI library that shines." 713 | category = "main" 714 | optional = false 715 | python-versions = ">=3.7" 716 | files = [ 717 | {file = "starlette-0.25.0-py3-none-any.whl", hash = "sha256:774f1df1983fd594b9b6fb3ded39c2aa1979d10ac45caac0f4255cbe2acb8628"}, 718 | {file = "starlette-0.25.0.tar.gz", hash = "sha256:854c71e73736c429c2bdb07801f2c76c9cba497e7c3cf4988fde5e95fe4cdb3c"}, 719 | ] 720 | 721 | [package.dependencies] 722 | anyio = ">=3.4.0,<5" 723 | 724 | [package.extras] 725 | full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] 726 | 727 | [[package]] 728 | name = "typing-extensions" 729 | version = "4.5.0" 730 | description = "Backported and Experimental Type Hints for Python 3.7+" 731 | category = "main" 732 | optional = false 733 | python-versions = ">=3.7" 734 | files = [ 735 | {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, 736 | {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, 737 | ] 738 | 739 | [[package]] 740 | name = "urllib3" 741 | version = "1.26.14" 742 | description = "HTTP library with thread-safe connection pooling, file post, and more." 743 | category = "dev" 744 | optional = false 745 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 746 | files = [ 747 | {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, 748 | {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, 749 | ] 750 | 751 | [package.extras] 752 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 753 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 754 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 755 | 756 | [[package]] 757 | name = "uvicorn" 758 | version = "0.20.0" 759 | description = "The lightning-fast ASGI server." 760 | category = "main" 761 | optional = false 762 | python-versions = ">=3.7" 763 | files = [ 764 | {file = "uvicorn-0.20.0-py3-none-any.whl", hash = "sha256:c3ed1598a5668208723f2bb49336f4509424ad198d6ab2615b7783db58d919fd"}, 765 | {file = "uvicorn-0.20.0.tar.gz", hash = "sha256:a4e12017b940247f836bc90b72e725d7dfd0c8ed1c51eb365f5ba30d9f5127d8"}, 766 | ] 767 | 768 | [package.dependencies] 769 | click = ">=7.0" 770 | h11 = ">=0.8" 771 | 772 | [package.extras] 773 | standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] 774 | 775 | [metadata] 776 | lock-version = "2.0" 777 | python-versions = "^3.11.2" 778 | content-hash = "98838c6f9b894249dac3e73385c4d85f0731e642233786b6fa9a4a29464283a1" 779 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "project name" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["alexk1919"] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.11.2" 9 | fastapi = "^0.92.0" 10 | motor = "^3.1.1" 11 | gunicorn = "^20.1.0" 12 | PyYAML = "^6.0" 13 | uvicorn = "^0.20.0" 14 | python-dotenv = "^1.0.0" 15 | psutil = "^5.9.4" 16 | 17 | [tool.poetry.dev-dependencies] 18 | pytest-asyncio = "^0.20.3" 19 | mypy = "^1.1.1" 20 | requests = "^2.28.2" 21 | 22 | [build-system] 23 | requires = ["poetry-core>=1.5.1"] 24 | build-backend = "poetry.core.masonry.api" 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==3.6.2 ; python_full_version >= "3.11.2" and python_full_version < "4.0.0" 2 | click==8.1.3 ; python_full_version >= "3.11.2" and python_full_version < "4.0.0" 3 | colorama==0.4.6 ; python_full_version >= "3.11.2" and python_full_version < "4.0.0" and platform_system == "Windows" 4 | dnspython==2.3.0 ; python_full_version >= "3.11.2" and python_version < "4.0" 5 | fastapi==0.92.0 ; python_full_version >= "3.11.2" and python_full_version < "4.0.0" 6 | gunicorn==20.1.0 ; python_full_version >= "3.11.2" and python_full_version < "4.0.0" 7 | h11==0.14.0 ; python_full_version >= "3.11.2" and python_full_version < "4.0.0" 8 | idna==3.4 ; python_full_version >= "3.11.2" and python_full_version < "4.0.0" 9 | motor==3.1.1 ; python_full_version >= "3.11.2" and python_full_version < "4.0.0" 10 | psutil==5.9.4 ; python_full_version >= "3.11.2" and python_full_version < "4.0.0" 11 | pydantic==1.10.5 ; python_full_version >= "3.11.2" and python_full_version < "4.0.0" 12 | pymongo==4.3.3 ; python_full_version >= "3.11.2" and python_full_version < "4.0.0" 13 | python-dotenv==1.0.0 ; python_full_version >= "3.11.2" and python_full_version < "4.0.0" 14 | pyyaml==6.0 ; python_full_version >= "3.11.2" and python_full_version < "4.0.0" 15 | setuptools==67.5.1 ; python_full_version >= "3.11.2" and python_full_version < "4.0.0" 16 | sniffio==1.3.0 ; python_full_version >= "3.11.2" and python_full_version < "4.0.0" 17 | starlette==0.25.0 ; python_full_version >= "3.11.2" and python_full_version < "4.0.0" 18 | typing-extensions==4.5.0 ; python_full_version >= "3.11.2" and python_full_version < "4.0.0" 19 | uvicorn==0.20.0 ; python_full_version >= "3.11.2" and python_full_version < "4.0.0" 20 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | gunicorn -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8888 -w 4 app.main:app 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexk1919/fastapi-motor-mongo-template/659af1b939829fe2962adcb9d0191c83941f7893/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest_asyncio 2 | from fastapi.testclient import TestClient 3 | import os 4 | 5 | from .mongo_client import MongoClient 6 | 7 | 8 | @pytest_asyncio.fixture() 9 | def env_setup(): 10 | os.environ["MONGODB_DBNAME"] = os.environ.get("TEST_DB_NAME") 11 | os.environ["MONGODB_URL"] = os.environ.get("TEST_MONGODB_URL") 12 | 13 | 14 | @pytest_asyncio.fixture() 15 | def test_client(env_setup): 16 | from app.main import app 17 | with TestClient(app) as test_client: 18 | yield test_client 19 | 20 | 21 | @pytest_asyncio.fixture() 22 | async def mongo_client(env_setup): 23 | print('\033[92mSetting test db\033[0m') 24 | async with MongoClient( 25 | os.environ.get("TEST_DB_NAME"), 26 | 'sample_resource' 27 | ) as mongo_client: 28 | yield mongo_client 29 | -------------------------------------------------------------------------------- /tests/mock_data/sample_resource.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_id": "5e8e793e-b7b6-4ad0-ba78-94445ef2a286", 4 | "name": "name A", 5 | "create_time": "2021-11-10 06:12:32", 6 | "update_time": "2021-11-14 23:01:24", 7 | "deleted": false 8 | }, 9 | { 10 | "_id": "cef4485f-fed7-48e3-99c6-47da4c04a894", 11 | "name": "name B", 12 | "create_time": "2021-11-12 15:23:24", 13 | "update_time": "2021-11-16 14:32:16", 14 | "deleted": false 15 | }, 16 | { 17 | "_id": "c0a207d1-1734-4052-b127-4845eb9d40bb", 18 | "name": "name C", 19 | "create_time": "2022-01-03 11:13:22", 20 | "update_time": "2021-02-10 09:30:11", 21 | "deleted": true 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /tests/mongo_client.py: -------------------------------------------------------------------------------- 1 | from motor.motor_asyncio import AsyncIOMotorClient 2 | import os 3 | from uuid import UUID 4 | import logging 5 | import json 6 | from datetime import datetime 7 | 8 | 9 | class MongoHandler(): 10 | def __init__(self, db_name: str, collection_name: str): 11 | self.__db_name = db_name 12 | self.__collection_name = collection_name 13 | self.__db_client = AsyncIOMotorClient( 14 | os.environ.get('TEST_MONGODB_URL') 15 | ) 16 | 17 | async def get_sample_resource(self, resource_id: UUID): 18 | return await self.__db_client[self.__db_name][self.__collection_name]\ 19 | .find_one({'_id': UUID(resource_id)}) 20 | 21 | async def insert_sample_resource(self, sample_resource: dict): 22 | await self.__db_client[self.__db_name][self.__collection_name]\ 23 | .insert_one(sample_resource) 24 | 25 | async def drop_database(self): 26 | await self.__db_client.drop_database(self.__db_name) 27 | 28 | def close_conn(self): 29 | self.__db_client.close() 30 | 31 | 32 | class MongoClient(): 33 | def __init__(self, db_name: str, collection_name: str): 34 | self.__db_handler = MongoHandler(db_name, collection_name) 35 | 36 | async def __aenter__(self): 37 | await self.__create_mock_data() 38 | return self.__db_handler 39 | 40 | async def __create_mock_data(self): 41 | with open('tests/mock_data/sample_resource.json', 'r') as f: 42 | sample_resource_json = json.load(f) 43 | for sample_resource in sample_resource_json: 44 | sample_resource['create_time'] = datetime.strptime( 45 | sample_resource['create_time'], '%Y-%m-%d %H:%M:%S' 46 | ) 47 | sample_resource['update_time'] = datetime.strptime( 48 | sample_resource['update_time'], '%Y-%m-%d %H:%M:%S' 49 | ) 50 | sample_resource['_id'] = UUID(sample_resource['_id']) 51 | await self.__db_handler.insert_sample_resource(sample_resource) 52 | 53 | async def __aexit__( 54 | self, exception_type, 55 | exception_value, exception_traceback 56 | ): 57 | if exception_type: 58 | logging.error(exception_value) 59 | 60 | await self.__db_handler.drop_database() 61 | self.__db_handler.close_conn() 62 | return False 63 | -------------------------------------------------------------------------------- /tests/test_create_sample_resource.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.asyncio 5 | @pytest.mark.parametrize( 6 | "name, expected_status", 7 | [ 8 | ('John Doe', 201), 9 | (None, 400), 10 | ] 11 | ) 12 | async def test_create_sample_resource( 13 | test_client, mongo_client, name: str, expected_status: int 14 | ): 15 | req_json = {} 16 | if None is not name: 17 | req_json["name"] = name 18 | 19 | resp = test_client.post( 20 | '/api/sample-resource-app/v1/sample-resource', 21 | json=req_json 22 | ) 23 | assert resp.status_code == expected_status 24 | 25 | if 201 == expected_status: 26 | assert 'id' in resp.json() 27 | resource_id = resp.json().get('id') 28 | resource_db = await mongo_client.get_sample_resource(resource_id) 29 | assert resource_db.get('name') == name 30 | assert False is resource_db.get('deleted') 31 | -------------------------------------------------------------------------------- /tests/test_delete_sample_resource.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from uuid import UUID 3 | 4 | 5 | @pytest.mark.asyncio 6 | @pytest.mark.parametrize( 7 | "resource_id, expected_status", 8 | [ 9 | ("5e8e793e-b7b6-4ad0-ba78-94445ef2a286", 200), 10 | ("cef4485f-fed7-48e3-99c6-47da4c04a894", 200), 11 | ("c0a207d1-1734-4052-b127-4845eb9d40bb", 422), 12 | (None, 405), 13 | ] 14 | ) 15 | async def test_delete_sample_resource( 16 | test_client, mongo_client, resource_id: UUID, expected_status: int 17 | ): 18 | 19 | path = '/api/sample-resource-app/v1/sample-resource' 20 | if None is not resource_id: 21 | path = f'{path}/{resource_id}' 22 | 23 | resp = test_client.delete( 24 | path, 25 | ) 26 | assert resp.status_code == expected_status 27 | 28 | if 200 == resp.status_code: 29 | resource_db = await mongo_client.get_sample_resource(resource_id) 30 | assert True is resource_db.get('deleted') 31 | -------------------------------------------------------------------------------- /tests/test_get_sample_resource.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from uuid import UUID 3 | 4 | 5 | @pytest.mark.asyncio 6 | @pytest.mark.parametrize( 7 | "resource_id, expected_status, expected_name", 8 | [ 9 | ("5e8e793e-b7b6-4ad0-ba78-94445ef2a286", 200, "name A"), 10 | ("cef4485f-fed7-48e3-99c6-47da4c04a894", 200, "name B"), 11 | (None, 400, None), 12 | ("76805766-c436-405f-beb2-735075124a6e", 204, None), 13 | ("c0a207d1-1734-4052-b127-4845eb9d40bb", 204, None), 14 | ] 15 | ) 16 | async def test_create_sample_resource( 17 | test_client, mongo_client, resource_id: UUID, 18 | expected_status: int, expected_name: str 19 | ): 20 | req_params = {} 21 | if None is not resource_id: 22 | req_params["resource"] = resource_id 23 | 24 | resp = test_client.get( 25 | '/api/sample-resource-app/v1/sample-resource', 26 | params=req_params, 27 | ) 28 | assert resp.status_code == expected_status 29 | 30 | if 200 == expected_status: 31 | assert 'name' in resp.json() 32 | assert expected_name == resp.json().get('name') 33 | -------------------------------------------------------------------------------- /tests/test_health.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.asyncio 5 | async def test_health(test_client): 6 | resp = test_client.get('/health') 7 | assert 200 == resp.status_code 8 | -------------------------------------------------------------------------------- /tests/test_update_sample_resource.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from uuid import UUID 3 | 4 | 5 | @pytest.mark.asyncio 6 | @pytest.mark.parametrize( 7 | "resource_id, name, expected_status", 8 | [ 9 | ("5e8e793e-b7b6-4ad0-ba78-94445ef2a286", 'John Doe', 200), 10 | ("cef4485f-fed7-48e3-99c6-47da4c04a894", 'Jane Doe', 200), 11 | ("cddd67bf-48c0-4e4c-95c6-cec9ec1981b6", 'John Doe', 422), 12 | ("c0a207d1-1734-4052-b127-4845eb9d40bb", 'John Doe', 422), 13 | ("5e8e793e-b7b6-4ad0-ba78-94445ef2a286", None, 400), 14 | ] 15 | ) 16 | async def test_update_sample_resource( 17 | test_client, mongo_client, resource_id: UUID, 18 | name: str, expected_status: int 19 | ): 20 | req_json = {} 21 | if None is not name: 22 | req_json["name"] = name 23 | 24 | resp = test_client.put( 25 | f'/api/sample-resource-app/v1/sample-resource/{resource_id}', 26 | json=req_json 27 | ) 28 | assert resp.status_code == expected_status 29 | 30 | if 200 == expected_status: 31 | resource_db = await mongo_client.get_sample_resource(resource_id) 32 | assert resource_db.get('name') == name 33 | --------------------------------------------------------------------------------