├── .dockerignore ├── .editorconfig ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .pylintrc ├── Dockerfile ├── Makefile ├── README.md ├── gunicorn.config.py ├── main.py ├── poetry.lock ├── poetry.toml ├── pyproject.toml ├── service ├── __init__.py ├── api │ ├── __init__.py │ ├── app.py │ ├── exception_handlers.py │ ├── exceptions.py │ ├── middlewares.py │ └── views.py ├── log.py ├── models.py ├── response.py └── settings.py ├── setup.cfg └── tests ├── __init__.py ├── api ├── __init__.py └── test_views.py └── conftest.py /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !service 3 | !gunicorn.config.py 4 | !main.py 5 | !poetry.lock 6 | !pyproject.toml 7 | !README.md 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | max_line_length = 99 11 | tab_width = 4 12 | 13 | [*.py] 14 | max_line_length = 79 15 | 16 | [{*.yml, *.yaml, *.json, *.xml}] 17 | indent_size = 2 18 | 19 | [Makefile] 20 | indent_style = tab 21 | 22 | [*.md] 23 | trim_trailing_whitespace = false 24 | max_line_length = 120 -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-20.04 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | 18 | - name: "Setup python" 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: 3.9 22 | 23 | - name: Install Poetry 24 | uses: snok/install-poetry@v1 25 | with: 26 | virtualenvs-create: true 27 | virtualenvs-in-project: true 28 | 29 | - name: Load cached venv 30 | id: cached-poetry-dependencies 31 | uses: actions/cache@v3 32 | with: 33 | path: .venv 34 | key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} 35 | 36 | - name: Install dependencies 37 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 38 | run: poetry install 39 | 40 | - name: Run tests 41 | run: make lint 42 | 43 | - name: Run linters 44 | run: make test 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editors 2 | .vscode/ 3 | .idea/ 4 | 5 | # Mac/OSX 6 | .DS_Store 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 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 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .nox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | *.py,cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Jupyter Notebook 51 | .ipynb_checkpoints 52 | 53 | # IPython 54 | profile_default/ 55 | ipython_config.py 56 | 57 | # Environments 58 | .env 59 | .venv 60 | env/ 61 | venv/ 62 | ENV/ 63 | env.bak/ 64 | venv.bak/ 65 | 66 | # mypy 67 | .mypy_cache/ 68 | 69 | 70 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions 4 | # may be loaded. Extensions are loading into the active Python interpreter 5 | # and may run arbitrary code. 6 | extension-pkg-whitelist=orjson 7 | 8 | # Add files or directories to the blacklist. 9 | # They should be base names, not paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. 13 | # The regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation 17 | # such as pygtk.require(). 18 | # init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. 21 | # Specifying 0 will auto-detect the number of processors available to use. 22 | jobs=0 23 | 24 | # Control the amount of potential inferred values when inferring 25 | # a single object. This can help the performance when dealing 26 | # with large functions or complex, nested conditions. 27 | limit-inference-results=100 28 | 29 | # List of plugins (as comma separated values of python module names) to load, 30 | # usually to register additional checkers. 31 | load-plugins= 32 | 33 | # Pickle collected data for later comparisons. 34 | persistent=yes 35 | 36 | # Specify a configuration file. 37 | rcfile=.pylintrc 38 | 39 | # When enabled, pylint would attempt to guess common misconfiguration and 40 | # emit user-friendly hints instead of false-positive error messages. 41 | suggestion-mode=yes 42 | 43 | # Allow loading of arbitrary C extensions. Extensions are imported 44 | # into the active Python interpreter and may run arbitrary code. 45 | unsafe-load-any-extension=no 46 | 47 | 48 | [MESSAGES CONTROL] 49 | 50 | # Only show warnings with the listed confidence levels. 51 | # Leave empty to show all. 52 | # Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 53 | confidence= 54 | 55 | # Disable the message, report, category or checker with the given id(s). 56 | disable=arguments-differ, 57 | bad-inline-option, 58 | deprecated-pragma, 59 | duplicate-code, 60 | file-ignored, 61 | invalid-envvar-default, 62 | invalid-name, 63 | locally-disabled, 64 | logging-fstring-interpolation, 65 | missing-class-docstring, 66 | missing-function-docstring, 67 | missing-module-docstring, 68 | no-member, 69 | raise-missing-from, 70 | raw-checker-failed, 71 | suppressed-message, 72 | unused-argument, 73 | use-implicit-booleaness-not-comparison, 74 | use-symbolic-message-instead, 75 | 76 | # Enable the message, report, category or checker with the given id(s). 77 | # You can either give multiple identifier separated by comma (,) or 78 | # put this option multiple time (only on the command line, not in 79 | # the configuration file where it should appear only once). 80 | # See also the "--disable" option for examples. 81 | enable=c-extension-no-member 82 | 83 | 84 | [REPORTS] 85 | 86 | # Python expression which should return a score less than or equal to 10. 87 | # You have access to the variables 'error', 'warning', 'refactor', and 88 | # 'convention' which contain the number of messages in each category, 89 | # as well as 'statement' which is the total number of statements 90 | # analyzed. This score is used by the global evaluation report (RP0004). 91 | # evaluation= 92 | 93 | # Template used to display messages. This is a python new-style format string 94 | # used to format the message information. See doc for all details. 95 | # msg-template= 96 | 97 | # Set the output format. Available formats are text, 98 | # parseable, colorized, json and msvs. 99 | output-format=text 100 | 101 | # Tells whether to display a full report or only the messages. 102 | reports=no 103 | 104 | # Activate the evaluation score. 105 | score=yes 106 | 107 | 108 | [REFACTORING] 109 | 110 | # Maximum number of nested blocks for function / method body 111 | max-nested-blocks=5 112 | 113 | # Complete name of functions that never returns. When checking for 114 | # inconsistent-return-statements if a never returning function 115 | # is called then it will be considered as an explicit return 116 | # statement and no message will be printed. 117 | never-returning-functions=sys.exit 118 | 119 | 120 | [FORMAT] 121 | 122 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 123 | expected-line-ending-format= 124 | 125 | # Regexp for a line that is allowed to be longer than the limit. 126 | ignore-long-lines=^\s*(# )??$ 127 | 128 | # Number of spaces of indent required inside a hanging or continued line. 129 | indent-after-paren=4 130 | 131 | # String used as indentation unit. 132 | # This is usually " " (4 spaces) or "\t" (1 tab). 133 | indent-string=' ' 134 | 135 | # Maximum number of characters on a single line. 136 | max-line-length=120 137 | 138 | # Maximum number of lines in a module. 139 | max-module-lines=1000 140 | 141 | # Allow the body of a class to be on the same line as the declaration 142 | # if body contains single statement. 143 | single-line-class-stmt=no 144 | 145 | # Allow the body of an if to be on the same line as the test 146 | # if there is no else. 147 | single-line-if-stmt=no 148 | 149 | 150 | [BASIC] 151 | 152 | # Naming style matching correct argument names. 153 | argument-naming-style=snake_case 154 | 155 | # Regular expression matching correct argument names. 156 | # Overrides argument-naming-style. 157 | # argument-rgx= 158 | 159 | # Naming style matching correct attribute names. 160 | attr-naming-style=snake_case 161 | 162 | # Regular expression matching correct attribute names. 163 | # Overrides attr-naming-style. 164 | # attr-rgx= 165 | 166 | # Bad variable names which should always be refused, separated by a comma. 167 | bad-names=foo, 168 | bar, 169 | baz, 170 | 171 | # Naming style matching correct class attribute names. 172 | class-attribute-naming-style=any 173 | 174 | # Regular expression matching correct class attribute names. 175 | # Overrides class-attribute-naming-style. 176 | # class-attribute-rgx= 177 | 178 | # Naming style matching correct class names. 179 | class-naming-style=PascalCase 180 | 181 | # Regular expression matching correct class names. 182 | # Overrides class-naming-style. 183 | # class-rgx= 184 | 185 | # Naming style matching correct constant names. 186 | const-naming-style=UPPER_CASE 187 | 188 | # Regular expression matching correct constant names. 189 | # Overrides const-naming-style. 190 | # const-rgx= 191 | 192 | # Minimum line length for functions/classes that require docstrings, 193 | # shorter ones are exempt. 194 | docstring-min-length=-1 195 | 196 | # Naming style matching correct function names. 197 | function-naming-style=snake_case 198 | 199 | # Regular expression matching correct function names. 200 | # Overrides function-naming-style. 201 | # function-rgx= 202 | 203 | # Good variable names which should always be accepted, separated by a comma. 204 | good-names=i, 205 | j, 206 | k, 207 | _, 208 | f, 209 | e, 210 | r, 211 | ok, 212 | id, 213 | db, 214 | tz, 215 | 216 | # Include a hint for the correct naming format with invalid-name. 217 | include-naming-hint=no 218 | 219 | # Naming style matching correct inline iteration names. 220 | inlinevar-naming-style=any 221 | 222 | # Regular expression matching correct inline iteration names. Overrides 223 | # inlinevar-naming-style. 224 | # inlinevar-rgx= 225 | 226 | # Naming style matching correct method names. 227 | method-naming-style=snake_case 228 | 229 | # Regular expression matching correct method names. 230 | # Overrides method-naming-style. 231 | # method-rgx= 232 | 233 | # Naming style matching correct module names. 234 | module-naming-style=snake_case 235 | 236 | # Regular expression matching correct module names. 237 | # Overrides module-naming-style. 238 | # module-rgx= 239 | 240 | # Colon-delimited sets of names that determine each other's 241 | # naming style when the name regexes allow several styles. 242 | name-group= 243 | 244 | # Regular expression which should only match function or 245 | # class names that do not require a docstring. 246 | no-docstring-rgx=^_ 247 | 248 | # List of decorators that produce properties, such as abc.abstractproperty. 249 | # Add to this list to register other decorators that produce valid properties. 250 | # These decorators are taken in consideration only for invalid-name. 251 | property-classes=abc.abstractproperty 252 | 253 | # Naming style matching correct variable names. 254 | variable-naming-style=snake_case 255 | 256 | 257 | # Regular expression matching correct variable names. 258 | # Overrides variable-naming-style. 259 | # variable-rgx= 260 | 261 | 262 | [MISCELLANEOUS] 263 | 264 | # List of note tags to take in consideration, separated by a comma. 265 | notes= 266 | 267 | 268 | [VARIABLES] 269 | 270 | # List of additional names supposed to be defined in builtins. 271 | # Remember that you should avoid defining new builtins when possible. 272 | additional-builtins= 273 | 274 | # Tells whether unused global variables should be treated as a violation. 275 | allow-global-unused-variables=yes 276 | 277 | # List of strings which can identify a callback function by name. 278 | # A callback name must start or end with one of those strings. 279 | callbacks=cb_, 280 | _cb 281 | 282 | # A regular expression matching the name of dummy variables. 283 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 284 | 285 | # Argument names that match this expression will be ignored. 286 | # Default to name with leading underscore. 287 | ignored-argument-names=_.*|^ignored_|^unused_ 288 | 289 | # Tells whether we should check for unused import in __init__ files. 290 | init-import=no 291 | 292 | # List of qualified module names which can have 293 | # objects that can redefine builtins. 294 | redefining-builtins-modules=six.moves, 295 | past.builtins, 296 | future.builtins, 297 | builtins, 298 | io 299 | 300 | 301 | [SPELLING] 302 | 303 | # Limits count of emitted suggestions for spelling mistakes. 304 | max-spelling-suggestions=4 305 | 306 | # Spelling dictionary name. Available dictionaries: none. 307 | # To make it work, install the python-enchant package. 308 | spelling-dict= 309 | 310 | # List of comma separated words that should not be checked. 311 | spelling-ignore-words= 312 | 313 | # A path to a file that contains the private dictionary; one word per line. 314 | spelling-private-dict-file= 315 | 316 | # Tells whether to store unknown words to the private dictionary (see the 317 | # --spelling-private-dict-file option) instead of raising a message. 318 | spelling-store-unknown-words=no 319 | 320 | 321 | [STRING] 322 | 323 | # This flag controls whether the implicit-str-concat-in-sequence 324 | # should generate a warning on implicit string concatenation 325 | # in sequences defined over several lines. 326 | check-str-concat-over-line-jumps=no 327 | 328 | 329 | [TYPECHECK] 330 | 331 | # List of decorators that produce context managers, such as 332 | # contextlib.contextmanager. 333 | # Add to this list to register other decorators 334 | # that produce valid context managers. 335 | contextmanager-decorators=contextlib.contextmanager, 336 | contextlib.asynccontextmanager, 337 | 338 | # List of members which are set dynamically and missed by pylint inference 339 | # system, and so shouldn't trigger E1101 when accessed. Python regular 340 | # expressions are accepted. 341 | generated-members= 342 | 343 | # Tells whether missing members accessed in mixin class should be ignored. 344 | # A mixin class is detected if its name ends with "mixin" (case insensitive). 345 | ignore-mixin-members=yes 346 | 347 | # Tells whether to warn about missing members when 348 | # the owner of the attribute is inferred to be None. 349 | ignore-none=yes 350 | 351 | # This flag controls whether pylint should warn about no-member and similar 352 | # checks whenever an opaque object is returned when inferring. The inference 353 | # can return multiple potential results while evaluating a Python object, but 354 | # some branches might not be evaluated, which results in partial inference. In 355 | # that case, it might be useful to still emit no-member and other checks for 356 | # the rest of the inferred objects. 357 | ignore-on-opaque-inference=yes 358 | 359 | # List of class names for which member attributes should not be checked 360 | # (useful for classes with dynamically set attributes). 361 | # This supports the use of qualified names. 362 | ignored-classes=optparse.Values, 363 | thread._local, 364 | _thread._local, 365 | http.HTTPStatus, 366 | 367 | # List of module names for which member attributes should not be 368 | # checked (useful for modules/projects where namespaces are 369 | # manipulated during runtime and thus existing member attributes 370 | # cannot be deduced by static analysis). 371 | # It supports qualified module names, as well as Unix pattern matching. 372 | ignored-modules= 373 | 374 | # Show a hint with possible names when a member name was not found. 375 | # The aspect of finding the hint is based on edit distance. 376 | missing-member-hint=yes 377 | 378 | # The minimum edit distance a name should have in order to be 379 | # considered a similar match for a missing member name. 380 | missing-member-hint-distance=1 381 | 382 | # The total number of similar names that should be taken in 383 | # consideration when showing a hint for a missing member. 384 | missing-member-max-choices=1 385 | 386 | # List of decorators that change the signature of a decorated function. 387 | signature-mutators= 388 | 389 | 390 | [SIMILARITIES] 391 | 392 | # Ignore comments when computing similarities. 393 | ignore-comments=yes 394 | 395 | # Ignore docstrings when computing similarities. 396 | ignore-docstrings=yes 397 | 398 | # Ignore imports when computing similarities. 399 | ignore-imports=no 400 | 401 | # Minimum lines number of a similarity. 402 | min-similarity-lines=4 403 | 404 | 405 | [LOGGING] 406 | 407 | # Format style used to check logging format string. `old` means using % 408 | # formatting, `new` is for `{}` formatting,and `fstr` is for f-strings. 409 | logging-format-style=old 410 | 411 | # Logging modules to check that the string format arguments are in logging 412 | # function parameter format. 413 | logging-modules=logging 414 | 415 | 416 | [CLASSES] 417 | 418 | # List of method names used to declare (i.e. assign) instance attributes. 419 | defining-attr-methods=__init__, 420 | __new__, 421 | setUp, 422 | __post_init__ 423 | 424 | # List of member names, which should be excluded from 425 | # the protected access warning. 426 | exclude-protected=_asdict, 427 | _fields, 428 | _replace, 429 | _source, 430 | _make 431 | 432 | # List of valid names for the first argument in a class method. 433 | valid-classmethod-first-arg=cls 434 | 435 | # List of valid names for the first argument in a metaclass class method. 436 | valid-metaclass-classmethod-first-arg=cls 437 | 438 | 439 | [DESIGN] 440 | 441 | # Maximum number of arguments for function / method. 442 | max-args=15 443 | 444 | # Maximum number of attributes for a class (see R0902). 445 | max-attributes=12 446 | 447 | # Maximum number of boolean expressions in an if statement (see R0916). 448 | max-bool-expr=2 449 | 450 | # Maximum number of branch for function / method body. 451 | max-branches=9 452 | 453 | # Maximum number of locals for function / method body. 454 | max-locals=22 455 | 456 | # Maximum number of parents for a class (see R0901). 457 | max-parents=10 458 | 459 | # Maximum number of public methods for a class (see R0904). 460 | max-public-methods=22 461 | 462 | # Maximum number of return / yield for function / method body. 463 | max-returns=5 464 | 465 | # Maximum number of statements in function / method body. 466 | max-statements=50 467 | 468 | # Minimum number of public methods for a class (see R0903). 469 | min-public-methods=0 470 | 471 | 472 | [IMPORTS] 473 | 474 | # List of modules that can be imported at any level, 475 | # not just the top level one. 476 | allow-any-import-level= 477 | 478 | # Allow wildcard imports from modules that define __all__. 479 | allow-wildcard-with-all=no 480 | 481 | # Analyse import fallback blocks. This can be used to support 482 | # both Python 2 and 3 compatible code, which means that the block 483 | # might have code that exists only in one or another interpreter, 484 | # leading to false positives when analysed. 485 | analyse-fallback-blocks=no 486 | 487 | # Deprecated modules which should not be used, separated by a comma. 488 | deprecated-modules=optparse,tkinter.tix 489 | 490 | # Create a graph of external dependencies in the given file (report RP0402 must 491 | # not be disabled). 492 | ext-import-graph= 493 | 494 | # Create a graph of every (i.e. internal and external) dependencies in the 495 | # given file (report RP0402 must not be disabled). 496 | import-graph= 497 | 498 | # Create a graph of internal dependencies in the given file 499 | # Report RP0402 must not be disabled. 500 | int-import-graph= 501 | 502 | # Force import order to recognize a module as part of 503 | # the standard compatibility libraries. 504 | known-standard-library= 505 | 506 | # Force import order to recognize a module as part of a third party library. 507 | known-third-party=enchant 508 | 509 | # Couples of modules and preferred modules, separated by a comma. 510 | preferred-modules= 511 | 512 | 513 | [EXCEPTIONS] 514 | 515 | # Exceptions that will emit a warning when being caught. 516 | # Defaults to "BaseException, Exception". 517 | overgeneral-exceptions=builtins.BaseException, 518 | builtins.Exception, 519 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-buster as build 2 | 3 | COPY . . 4 | 5 | RUN pip install -U --no-cache-dir pip poetry setuptools wheel && \ 6 | poetry build -f wheel && \ 7 | poetry export -f requirements.txt -o requirements.txt --without-hashes && \ 8 | pip wheel -w dist -r requirements.txt 9 | 10 | 11 | FROM python:3.8-slim-buster as runtime 12 | 13 | WORKDIR /usr/src/app 14 | 15 | ENV PYTHONOPTIMIZE true 16 | ENV DEBIAN_FRONTEND noninteractive 17 | 18 | # setup timezone 19 | ENV TZ=UTC 20 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 21 | 22 | COPY --from=build dist dist 23 | COPY --from=build main.py gunicorn.config.py ./ 24 | 25 | 26 | RUN pip install -U --no-cache-dir pip dist/*.whl && \ 27 | rm -rf dist 28 | 29 | CMD ["gunicorn", "main:app", "-c", "gunicorn.config.py"] 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VENV := .venv 2 | 3 | PROJECT := service 4 | TESTS := tests 5 | 6 | IMAGE_NAME := reco_service 7 | CONTAINER_NAME := reco_service 8 | 9 | # Prepare 10 | 11 | .venv: 12 | poetry install --no-root 13 | poetry check 14 | 15 | setup: .venv 16 | 17 | 18 | # Clean 19 | 20 | clean: 21 | rm -rf .mypy_cache 22 | rm -rf .pytest_cache 23 | rm -rf $(VENV) 24 | 25 | 26 | # Format 27 | 28 | isort_fix: .venv 29 | poetry run isort $(PROJECT) $(TESTS) 30 | 31 | 32 | black_fix: 33 | poetry run black $(PROJECT) $(TESTS) 34 | 35 | format: isort_fix black_fix 36 | 37 | 38 | # Lint 39 | 40 | isort: .venv 41 | poetry run isort --check $(PROJECT) $(TESTS) 42 | 43 | .black: 44 | poetry run black --check --diff $(PROJECT) $(TESTS) 45 | 46 | flake: .venv 47 | poetry run flake8 $(PROJECT) $(TESTS) 48 | 49 | mypy: .venv 50 | poetry run mypy $(PROJECT) $(TESTS) 51 | 52 | pylint: .venv 53 | poetry run pylint $(PROJECT) $(TESTS) 54 | 55 | lint: isort flake mypy pylint 56 | 57 | 58 | # Test 59 | 60 | .pytest: 61 | poetry run pytest $(TESTS) 62 | 63 | test: .venv .pytest 64 | 65 | 66 | # Docker 67 | 68 | build: 69 | docker build . -t $(IMAGE_NAME) 70 | 71 | run: build 72 | docker run -p 8080:8080 --name $(CONTAINER_NAME) $(IMAGE_NAME) 73 | 74 | # All 75 | 76 | all: setup format lint test run 77 | 78 | .DEFAULT_GOAL = all 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Шаблон сервиса рекомендаций 2 | 3 | ## Подготовка 4 | 5 | ### Python 6 | 7 | В данном шаблоне используется Python3.9, однако вы можете использовать и другие версии, если хотите. 8 | Но мы не гарантируем, что все будет работать. 9 | 10 | ### Make 11 | 12 | [Make](https://www.gnu.org/software/make/) - это очень популярная утилита, 13 | предназначенная для преобразования одних файлов в другие через определенную последовательность команд. 14 | Однако ее можно использовать для исполнения произвольных последовательностей команд. 15 | Команды и правила их исполнения прописываются в `Makefile`. 16 | 17 | Мы будем активно использовать `make` в данном проекте, поэтому рекомендуем познакомится с ней поближе. 18 | 19 | На MacOS и *nix системах `make` обычно идет в комплекте или ее можно легко установить. 20 | Некоторые варианты, как можно поставить `make` на `Windows`, 21 | описаны [здесь](https://stackoverflow.com/questions/32127524/how-to-install-and-use-make-in-windows). 22 | 23 | ### Poetry 24 | 25 | [Poetry](https://python-poetry.org/) - это удобный инструмент для работы с зависимостями в Python. 26 | Мы будем использовать его для подготовки окружения. 27 | 28 | Поэтому перед началом работы необходимо выполнить [шаги по установке](https://python-poetry.org/docs/#installation). 29 | 30 | 31 | ## Виртуальное окружение 32 | 33 | Мы будем работать в виртуальном окружении, которое создадим специально для данного проекта. 34 | Если вы не знакомы с концепцией виртуальных окружений в Python, обязательно 35 | [познакомьтесь](https://docs.python.org/3.8/tutorial/venv.html). 36 | Мы рекомендуем использовать отдельное виртуальное окружение для каждого вашего проекта. 37 | 38 | ### Инициализация окружения 39 | 40 | Выполните команду 41 | ``` 42 | make setup 43 | ``` 44 | 45 | Будет создано новое виртуальное окружение в папке `.venv`. 46 | В него будут установлены пакеты, перечисленные в файле `pyproject.toml`. 47 | 48 | Обратите внимание: если вы один раз выполнили `make setup`, при попытке повторного ее выполнения ничего не произойдет, 49 | поскольку единственная ее зависимость - директория `.venv` - уже существует. 50 | Если вам по какой-то причине нужно пересобрать окружение с нуля, 51 | выполните сначала команду `make clean` - она удалит старое окружение. 52 | 53 | ### Установка/удаление пакетов 54 | 55 | Для установки новых пакетов используйте команду `poetry add`, для удаления - `poetry remove`. 56 | Мы не рекомендуем вручную редактировать секцию с зависимостями в `pyproject.toml`. 57 | 58 | ## Линтеры, тесты и автоформатирование 59 | 60 | ### Автоформатирование 61 | 62 | Командой `make format` можно запустить автоматическое форматирование вашего кода. 63 | 64 | Ее выполнение приведет к запуску [isort](https://github.com/PyCQA/isort) - утилиты 65 | для сортировки импортов в нужном порядке, и [black](https://github.com/psf/black) - одного из самых популярных форматтеров для `Python`. 66 | 67 | 68 | ### Статическая проверка кода 69 | 70 | Командой `make lint` вы запустите проверку линтерами - инструментами для статического анализа кода. 71 | Они помогают выявить ошибки в коде еще до его запуска, а также обнаруживают несоответствия стандарту 72 | [PEP8](https://peps.python.org/pep-0008). 73 | Среди линтеров есть те же `isort` и `black`, только в данном случае они уже ничего не исправляют, а просто проверяют, что код отформатирован правильно. 74 | 75 | ### Тесты 76 | 77 | Командой `make test` вы запустите тесты при помощи утилиты [pytest](https://pytest.org/). 78 | 79 | 80 | ## Запуск приложения 81 | 82 | ### Способ 1: Python + Uvicorn 83 | 84 | ``` 85 | python main.py 86 | ``` 87 | 88 | Приложение запустится локально, в одном процессе. 89 | Хост и порт по умолчанию: `127.0.0.1` и `8080`. 90 | Их можно изменить через переменные окружения `HOST` и `PORT`. 91 | 92 | Управляет процессом легковесный [ASGI](https://asgi.readthedocs.io/en/latest/) server [uvicorn](https://www.uvicorn.org/). 93 | 94 | Обратите внимание: для запуска нужно использовать `python` из окружения проекта. 95 | 96 | ### Способ 2: Uvicorn 97 | 98 | ``` 99 | uvicorn main:app 100 | ``` 101 | 102 | Очень похож на предыдущий, только запуск идет напрямую. 103 | Хост и порт можно передать через аргументы командной строки. 104 | 105 | Обратите внимание: для запуска нужно использовать `uvicorn` из окружения проекта. 106 | 107 | 108 | ### Способ 3: Gunicorn 109 | 110 | ``` 111 | gunicorn main:app -c gunicorn.config.py 112 | ``` 113 | 114 | Способ похож на предыдущий, только вместо `uvicorn` используется 115 | более функциональный сервер [gunicorn](https://gunicorn.org/) (`uvicorn` используется внутри него). 116 | Параметры задаются через конфиг, хост и порт можно задать 117 | через переменные окружения или аргументы командной строки. 118 | 119 | Сервис запускается в несколько параллельных процессов, по умолчанию их число 120 | равно числу ядер процессора. 121 | 122 | Обратите внимание: для запуска нужно использовать `gunicorn` из окружения проекта. 123 | 124 | ### Способ 4: Docker 125 | 126 | Делаем все то же самое, но внутри docker-контейнера. 127 | Если вы не знакомы с [docker](https://www.docker.com/), обязательно познакомьтесь. 128 | 129 | Внутри контейнера можно использовать любой из способов, описанных выше. 130 | В продакшене рекомендуется использовать `gunicorn`. 131 | 132 | Собрать и запустить образ можно командой 133 | 134 | ``` 135 | make run 136 | ``` 137 | 138 | ## CI/CD 139 | 140 | Когда вы выполняете какое-то действие (прописанное в конфиге), запускается процесс CI. 141 | Что именно происходит в этом процессе и как он триггерится, 142 | описывается в специальных `.yaml`/`.yml` конфигах в папке `.github/workflows`. 143 | 144 | Сейчас там есть только один конфиг `test.yml`, который запускает процесс, в котором создается виртуальное окружение, 145 | прогоняются линтеры и тесты. Если что-то пошло не так, процесс падает с ошибкой и в Github появляется красный крестик. 146 | Вам нужно посмотреть логи, исправить ошибку и запушить изменения. 147 | Этот процесс тригеррится при создании и обновлении пулл-реквеста, а также по пушу в `master`. -------------------------------------------------------------------------------- /gunicorn.config.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import cpu_count 2 | from os import getenv as env 3 | 4 | from service import log, settings 5 | 6 | # The socket to bind. 7 | host = env("HOST", "0.0.0.0") 8 | port = int(env("PORT", "8080")) 9 | bind = f"{host}:{port}" 10 | 11 | # The maximum number of pending connections. 12 | backlog = env("GUNICORN_BACKLOG", 2048) 13 | 14 | # The number of worker processes for handling requests. 15 | workers = env("GUNICORN_WORKERS", cpu_count()) 16 | 17 | # The type of workers to use. 18 | worker_class = env("GUNICORN_WORKER_CLASS", "uvicorn.workers.UvicornWorker") 19 | 20 | # The maximum number of requests a worker will process before restarting. 21 | max_requests = env("GUNICORN_MAX_REQUESTS", 1024) 22 | 23 | # Workers silent for more than this many seconds are killed and restarted. 24 | timeout = env("GUNICORN_TIMEOUT", 3600) 25 | 26 | # Timeout for graceful workers restart. 27 | graceful_timeout = env("GUNICORN_GRACEFUL_TIMEOUT", 5) 28 | 29 | # The number of seconds to wait for requests on a Keep-Alive connection. 30 | keepalive = env("GUNICORN_KEEPALIVE", 5) 31 | 32 | # Detaches the server from the controlling terminal and enters the background. 33 | daemon = env("GUNICORN_DAEMON", False) 34 | 35 | # Check the configuration. 36 | check_config = env("GUNICORN_CHECK_CONFIG", False) 37 | 38 | # A base to use with setproctitle for process naming. 39 | proc_name = env("GUNICORN_PROC_NAME", "vertical") 40 | 41 | # Internal setting that is adjusted for each type of application. 42 | default_proc_name = env("GUNICORN_DEFAULT_PROC_NAME", "vertical") 43 | 44 | # The Access log file to write to. 45 | accesslog = env("GUNICORN_ACCESS_LOG", "-") 46 | 47 | # The access log format. 48 | access_log_format = env("GUNICORN_ACCESS_LOG_FORMAT", None) 49 | 50 | # The Error log file to write to. 51 | errorlog = env("GUNICORN_ERRORLOG", "-") 52 | 53 | # The granularity of log output. 54 | loglevel = env("GUNICORN_LOGLEVEL", "INFO") 55 | 56 | # Redirect stdout/stderr to specified file in errorlog. 57 | capture_output = env("GUNICORN_CAPTURE_OUTPUT", False) 58 | 59 | # The log config dictionary to use. 60 | logconfig_dict = log.get_config(settings.get_config()) 61 | 62 | # The maximum size of HTTP request line in bytes. 63 | # This parameter can be used to prevent any DDOS attack. 64 | limit_request_line = env("GUNICORN_LIMIT_REQUEST_LINE", 512) 65 | 66 | # Limit the number of HTTP headers fields in a request. 67 | # This parameter is used to limit the number of headers in a request. 68 | limit_request_fields = env("GUNICORN_LIMIT_REQUEST_FIELDS", 64) 69 | 70 | # Limit the allowed size of an HTTP request header field. 71 | # Setting it to 0 will allow unlimited header field sizes. 72 | limit_request_field_size = env("GUNICORN_LIMIT_REQUEST_FIELD_SIZE", 128) 73 | 74 | # Load application code before the worker processes are forked. 75 | preload_app = env("GUNICORN_PRELOAD_APP", False) 76 | 77 | # Disables the use of sendfile. 78 | sendfile = env("GUNICORN_SENDFILE", True) 79 | 80 | # Set the SO_REUSEPORT flag on the listening socket. 81 | reuse_port = env("GUNICORN_REUSE_PORT", True) 82 | 83 | # A filename to use for the PID file. 84 | # If not set, no PID file will be written. 85 | pidfile = env("GUNICORN_PIDFILE", None) 86 | 87 | # A directory to use for the worker heartbeat temporary file. 88 | # If not set, the default temporary directory will be used. 89 | worker_tmp_dir = env("GUNICORN_WORKER_TMP_DIR", None) 90 | 91 | # Switch worker processes to run as this user. 92 | user = env("GUNICORN_USER", None) 93 | 94 | # Switch worker process to run as this group. 95 | group = env("GUNICORN_GROUP", None) 96 | 97 | # If true, set the worker process’s group access list with all of the groups 98 | # of which the specified username is a member, plus the specified group id. 99 | initgroups = env("GUNICORN_INITGROUPS", None) 100 | 101 | # Directory to store temporary request data as they are read. 102 | tmp_upload_dir = env("GUNICORN_TMP_UPLOAD_DIR", None) 103 | 104 | # Front-end’s IPs from which allowed to handle set secure headers. 105 | forwarded_allow_ips = env("GUNICORN_FORWARDER_ALLOW_IPS", "127.0.0.1") 106 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uvicorn 3 | 4 | from service.api.app import create_app 5 | from service.settings import get_config 6 | 7 | config = get_config() 8 | app = create_app(config) 9 | 10 | 11 | if __name__ == "__main__": 12 | 13 | host = os.getenv("HOST", "127.0.0.1") 14 | port = int(os.getenv("PORT", "8080")) 15 | 16 | uvicorn.run(app, host=host, port=port) 17 | -------------------------------------------------------------------------------- /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 = "annotated-types" 5 | version = "0.6.0" 6 | description = "Reusable constraint types to use with typing.Annotated" 7 | category = "main" 8 | optional = false 9 | python-versions = ">=3.8" 10 | files = [ 11 | {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, 12 | {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, 13 | ] 14 | 15 | [package.dependencies] 16 | typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} 17 | 18 | [[package]] 19 | name = "anyio" 20 | version = "3.7.1" 21 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 22 | category = "main" 23 | optional = false 24 | python-versions = ">=3.7" 25 | files = [ 26 | {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, 27 | {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, 28 | ] 29 | 30 | [package.dependencies] 31 | exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} 32 | idna = ">=2.8" 33 | sniffio = ">=1.1" 34 | 35 | [package.extras] 36 | doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] 37 | test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 38 | trio = ["trio (<0.22)"] 39 | 40 | [[package]] 41 | name = "astroid" 42 | version = "3.0.1" 43 | description = "An abstract syntax tree for Python with inference support." 44 | category = "dev" 45 | optional = false 46 | python-versions = ">=3.8.0" 47 | files = [ 48 | {file = "astroid-3.0.1-py3-none-any.whl", hash = "sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca"}, 49 | {file = "astroid-3.0.1.tar.gz", hash = "sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e"}, 50 | ] 51 | 52 | [package.dependencies] 53 | typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} 54 | 55 | [[package]] 56 | name = "bandit" 57 | version = "1.7.5" 58 | description = "Security oriented static analyser for python code." 59 | category = "dev" 60 | optional = false 61 | python-versions = ">=3.7" 62 | files = [ 63 | {file = "bandit-1.7.5-py3-none-any.whl", hash = "sha256:75665181dc1e0096369112541a056c59d1c5f66f9bb74a8d686c3c362b83f549"}, 64 | {file = "bandit-1.7.5.tar.gz", hash = "sha256:bdfc739baa03b880c2d15d0431b31c658ffc348e907fe197e54e0389dd59e11e"}, 65 | ] 66 | 67 | [package.dependencies] 68 | colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} 69 | GitPython = ">=1.0.1" 70 | PyYAML = ">=5.3.1" 71 | rich = "*" 72 | stevedore = ">=1.20.0" 73 | 74 | [package.extras] 75 | test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)", "tomli (>=1.1.0)"] 76 | toml = ["tomli (>=1.1.0)"] 77 | yaml = ["PyYAML"] 78 | 79 | [[package]] 80 | name = "black" 81 | version = "23.10.1" 82 | description = "The uncompromising code formatter." 83 | category = "dev" 84 | optional = false 85 | python-versions = ">=3.8" 86 | files = [ 87 | {file = "black-23.10.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:ec3f8e6234c4e46ff9e16d9ae96f4ef69fa328bb4ad08198c8cee45bb1f08c69"}, 88 | {file = "black-23.10.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:1b917a2aa020ca600483a7b340c165970b26e9029067f019e3755b56e8dd5916"}, 89 | {file = "black-23.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c74de4c77b849e6359c6f01987e94873c707098322b91490d24296f66d067dc"}, 90 | {file = "black-23.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:7b4d10b0f016616a0d93d24a448100adf1699712fb7a4efd0e2c32bbb219b173"}, 91 | {file = "black-23.10.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b15b75fc53a2fbcac8a87d3e20f69874d161beef13954747e053bca7a1ce53a0"}, 92 | {file = "black-23.10.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:e293e4c2f4a992b980032bbd62df07c1bcff82d6964d6c9496f2cd726e246ace"}, 93 | {file = "black-23.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d56124b7a61d092cb52cce34182a5280e160e6aff3137172a68c2c2c4b76bcb"}, 94 | {file = "black-23.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:3f157a8945a7b2d424da3335f7ace89c14a3b0625e6593d21139c2d8214d55ce"}, 95 | {file = "black-23.10.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:cfcce6f0a384d0da692119f2d72d79ed07c7159879d0bb1bb32d2e443382bf3a"}, 96 | {file = "black-23.10.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:33d40f5b06be80c1bbce17b173cda17994fbad096ce60eb22054da021bf933d1"}, 97 | {file = "black-23.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:840015166dbdfbc47992871325799fd2dc0dcf9395e401ada6d88fe11498abad"}, 98 | {file = "black-23.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:037e9b4664cafda5f025a1728c50a9e9aedb99a759c89f760bd83730e76ba884"}, 99 | {file = "black-23.10.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:7cb5936e686e782fddb1c73f8aa6f459e1ad38a6a7b0e54b403f1f05a1507ee9"}, 100 | {file = "black-23.10.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:7670242e90dc129c539e9ca17665e39a146a761e681805c54fbd86015c7c84f7"}, 101 | {file = "black-23.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed45ac9a613fb52dad3b61c8dea2ec9510bf3108d4db88422bacc7d1ba1243d"}, 102 | {file = "black-23.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6d23d7822140e3fef190734216cefb262521789367fbdc0b3f22af6744058982"}, 103 | {file = "black-23.10.1-py3-none-any.whl", hash = "sha256:d431e6739f727bb2e0495df64a6c7a5310758e87505f5f8cde9ff6c0f2d7e4fe"}, 104 | {file = "black-23.10.1.tar.gz", hash = "sha256:1f8ce316753428ff68749c65a5f7844631aa18c8679dfd3ca9dc1a289979c258"}, 105 | ] 106 | 107 | [package.dependencies] 108 | click = ">=8.0.0" 109 | mypy-extensions = ">=0.4.3" 110 | packaging = ">=22.0" 111 | pathspec = ">=0.9.0" 112 | platformdirs = ">=2" 113 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 114 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 115 | 116 | [package.extras] 117 | colorama = ["colorama (>=0.4.3)"] 118 | d = ["aiohttp (>=3.7.4)"] 119 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 120 | uvloop = ["uvloop (>=0.15.2)"] 121 | 122 | [[package]] 123 | name = "certifi" 124 | version = "2023.7.22" 125 | description = "Python package for providing Mozilla's CA Bundle." 126 | category = "main" 127 | optional = false 128 | python-versions = ">=3.6" 129 | files = [ 130 | {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, 131 | {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, 132 | ] 133 | 134 | [[package]] 135 | name = "charset-normalizer" 136 | version = "3.3.2" 137 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 138 | category = "main" 139 | optional = false 140 | python-versions = ">=3.7.0" 141 | files = [ 142 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 143 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 144 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 145 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 146 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 147 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 148 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 149 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 150 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 151 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 152 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 153 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 154 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 155 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 156 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 157 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 158 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 159 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 160 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 161 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 162 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 163 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 164 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 165 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 166 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 167 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 168 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 169 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 170 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 171 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 172 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 173 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 174 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 175 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 176 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 177 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 178 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 179 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 180 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 181 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 182 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 183 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 184 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 185 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 186 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 187 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 188 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 189 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 190 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 191 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 192 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 193 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 194 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 195 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 196 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 197 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 198 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 199 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 200 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 201 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 202 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 203 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 204 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 205 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 206 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 207 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 208 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 209 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 210 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 211 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 212 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 213 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 214 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 215 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 216 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 217 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 218 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 219 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 220 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 221 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 222 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 223 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 224 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 225 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 226 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 227 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 228 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 229 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 230 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 231 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 232 | ] 233 | 234 | [[package]] 235 | name = "click" 236 | version = "8.1.7" 237 | description = "Composable command line interface toolkit" 238 | category = "main" 239 | optional = false 240 | python-versions = ">=3.7" 241 | files = [ 242 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 243 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 244 | ] 245 | 246 | [package.dependencies] 247 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 248 | 249 | [[package]] 250 | name = "colorama" 251 | version = "0.4.6" 252 | description = "Cross-platform colored terminal text." 253 | category = "main" 254 | optional = false 255 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 256 | files = [ 257 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 258 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 259 | ] 260 | 261 | [[package]] 262 | name = "dill" 263 | version = "0.3.7" 264 | description = "serialize all of Python" 265 | category = "dev" 266 | optional = false 267 | python-versions = ">=3.7" 268 | files = [ 269 | {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, 270 | {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, 271 | ] 272 | 273 | [package.extras] 274 | graph = ["objgraph (>=1.7.2)"] 275 | 276 | [[package]] 277 | name = "exceptiongroup" 278 | version = "1.1.3" 279 | description = "Backport of PEP 654 (exception groups)" 280 | category = "main" 281 | optional = false 282 | python-versions = ">=3.7" 283 | files = [ 284 | {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, 285 | {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, 286 | ] 287 | 288 | [package.extras] 289 | test = ["pytest (>=6)"] 290 | 291 | [[package]] 292 | name = "fastapi" 293 | version = "0.104.1" 294 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 295 | category = "main" 296 | optional = false 297 | python-versions = ">=3.8" 298 | files = [ 299 | {file = "fastapi-0.104.1-py3-none-any.whl", hash = "sha256:752dc31160cdbd0436bb93bad51560b57e525cbb1d4bbf6f4904ceee75548241"}, 300 | {file = "fastapi-0.104.1.tar.gz", hash = "sha256:e5e4540a7c5e1dcfbbcf5b903c234feddcdcd881f191977a1c5dfd917487e7ae"}, 301 | ] 302 | 303 | [package.dependencies] 304 | anyio = ">=3.7.1,<4.0.0" 305 | pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" 306 | starlette = ">=0.27.0,<0.28.0" 307 | typing-extensions = ">=4.8.0" 308 | 309 | [package.extras] 310 | all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "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)"] 311 | 312 | [[package]] 313 | name = "flake8" 314 | version = "6.1.0" 315 | description = "the modular source code checker: pep8 pyflakes and co" 316 | category = "dev" 317 | optional = false 318 | python-versions = ">=3.8.1" 319 | files = [ 320 | {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, 321 | {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, 322 | ] 323 | 324 | [package.dependencies] 325 | mccabe = ">=0.7.0,<0.8.0" 326 | pycodestyle = ">=2.11.0,<2.12.0" 327 | pyflakes = ">=3.1.0,<3.2.0" 328 | 329 | [[package]] 330 | name = "gitdb" 331 | version = "4.0.11" 332 | description = "Git Object Database" 333 | category = "dev" 334 | optional = false 335 | python-versions = ">=3.7" 336 | files = [ 337 | {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"}, 338 | {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"}, 339 | ] 340 | 341 | [package.dependencies] 342 | smmap = ">=3.0.1,<6" 343 | 344 | [[package]] 345 | name = "gitpython" 346 | version = "3.1.40" 347 | description = "GitPython is a Python library used to interact with Git repositories" 348 | category = "dev" 349 | optional = false 350 | python-versions = ">=3.7" 351 | files = [ 352 | {file = "GitPython-3.1.40-py3-none-any.whl", hash = "sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a"}, 353 | {file = "GitPython-3.1.40.tar.gz", hash = "sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4"}, 354 | ] 355 | 356 | [package.dependencies] 357 | gitdb = ">=4.0.1,<5" 358 | 359 | [package.extras] 360 | test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-sugar"] 361 | 362 | [[package]] 363 | name = "gunicorn" 364 | version = "21.2.0" 365 | description = "WSGI HTTP Server for UNIX" 366 | category = "main" 367 | optional = false 368 | python-versions = ">=3.5" 369 | files = [ 370 | {file = "gunicorn-21.2.0-py3-none-any.whl", hash = "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0"}, 371 | {file = "gunicorn-21.2.0.tar.gz", hash = "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033"}, 372 | ] 373 | 374 | [package.dependencies] 375 | packaging = "*" 376 | 377 | [package.extras] 378 | eventlet = ["eventlet (>=0.24.1)"] 379 | gevent = ["gevent (>=1.4.0)"] 380 | setproctitle = ["setproctitle"] 381 | tornado = ["tornado (>=0.2)"] 382 | 383 | [[package]] 384 | name = "h11" 385 | version = "0.12.0" 386 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 387 | category = "main" 388 | optional = false 389 | python-versions = ">=3.6" 390 | files = [ 391 | {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, 392 | {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, 393 | ] 394 | 395 | [[package]] 396 | name = "httpcore" 397 | version = "0.14.7" 398 | description = "A minimal low-level HTTP client." 399 | category = "main" 400 | optional = false 401 | python-versions = ">=3.6" 402 | files = [ 403 | {file = "httpcore-0.14.7-py3-none-any.whl", hash = "sha256:47d772f754359e56dd9d892d9593b6f9870a37aeb8ba51e9a88b09b3d68cfade"}, 404 | {file = "httpcore-0.14.7.tar.gz", hash = "sha256:7503ec1c0f559066e7e39bc4003fd2ce023d01cf51793e3c173b864eb456ead1"}, 405 | ] 406 | 407 | [package.dependencies] 408 | anyio = ">=3.0.0,<4.0.0" 409 | certifi = "*" 410 | h11 = ">=0.11,<0.13" 411 | sniffio = ">=1.0.0,<2.0.0" 412 | 413 | [package.extras] 414 | http2 = ["h2 (>=3,<5)"] 415 | socks = ["socksio (>=1.0.0,<2.0.0)"] 416 | 417 | [[package]] 418 | name = "httpx" 419 | version = "0.22.0" 420 | description = "The next generation HTTP client." 421 | category = "main" 422 | optional = false 423 | python-versions = ">=3.6" 424 | files = [ 425 | {file = "httpx-0.22.0-py3-none-any.whl", hash = "sha256:e35e83d1d2b9b2a609ef367cc4c1e66fd80b750348b20cc9e19d1952fc2ca3f6"}, 426 | {file = "httpx-0.22.0.tar.gz", hash = "sha256:d8e778f76d9bbd46af49e7f062467e3157a5a3d2ae4876a4bbfd8a51ed9c9cb4"}, 427 | ] 428 | 429 | [package.dependencies] 430 | certifi = "*" 431 | charset-normalizer = "*" 432 | httpcore = ">=0.14.5,<0.15.0" 433 | rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} 434 | sniffio = "*" 435 | 436 | [package.extras] 437 | brotli = ["brotli", "brotlicffi"] 438 | cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10.0.0,<11.0.0)"] 439 | http2 = ["h2 (>=3,<5)"] 440 | socks = ["socksio (>=1.0.0,<2.0.0)"] 441 | 442 | [[package]] 443 | name = "idna" 444 | version = "3.4" 445 | description = "Internationalized Domain Names in Applications (IDNA)" 446 | category = "main" 447 | optional = false 448 | python-versions = ">=3.5" 449 | files = [ 450 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, 451 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, 452 | ] 453 | 454 | [[package]] 455 | name = "iniconfig" 456 | version = "2.0.0" 457 | description = "brain-dead simple config-ini parsing" 458 | category = "dev" 459 | optional = false 460 | python-versions = ">=3.7" 461 | files = [ 462 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 463 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 464 | ] 465 | 466 | [[package]] 467 | name = "isort" 468 | version = "5.12.0" 469 | description = "A Python utility / library to sort Python imports." 470 | category = "dev" 471 | optional = false 472 | python-versions = ">=3.8.0" 473 | files = [ 474 | {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, 475 | {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, 476 | ] 477 | 478 | [package.extras] 479 | colors = ["colorama (>=0.4.3)"] 480 | pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] 481 | plugins = ["setuptools"] 482 | requirements-deprecated-finder = ["pip-api", "pipreqs"] 483 | 484 | [[package]] 485 | name = "markdown-it-py" 486 | version = "3.0.0" 487 | description = "Python port of markdown-it. Markdown parsing, done right!" 488 | category = "dev" 489 | optional = false 490 | python-versions = ">=3.8" 491 | files = [ 492 | {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, 493 | {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, 494 | ] 495 | 496 | [package.dependencies] 497 | mdurl = ">=0.1,<1.0" 498 | 499 | [package.extras] 500 | benchmarking = ["psutil", "pytest", "pytest-benchmark"] 501 | code-style = ["pre-commit (>=3.0,<4.0)"] 502 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] 503 | linkify = ["linkify-it-py (>=1,<3)"] 504 | plugins = ["mdit-py-plugins"] 505 | profiling = ["gprof2dot"] 506 | rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] 507 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] 508 | 509 | [[package]] 510 | name = "mccabe" 511 | version = "0.7.0" 512 | description = "McCabe checker, plugin for flake8" 513 | category = "dev" 514 | optional = false 515 | python-versions = ">=3.6" 516 | files = [ 517 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 518 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 519 | ] 520 | 521 | [[package]] 522 | name = "mdurl" 523 | version = "0.1.2" 524 | description = "Markdown URL utilities" 525 | category = "dev" 526 | optional = false 527 | python-versions = ">=3.7" 528 | files = [ 529 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 530 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 531 | ] 532 | 533 | [[package]] 534 | name = "mypy" 535 | version = "1.6.1" 536 | description = "Optional static typing for Python" 537 | category = "dev" 538 | optional = false 539 | python-versions = ">=3.8" 540 | files = [ 541 | {file = "mypy-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c"}, 542 | {file = "mypy-1.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb"}, 543 | {file = "mypy-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e"}, 544 | {file = "mypy-1.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f"}, 545 | {file = "mypy-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c"}, 546 | {file = "mypy-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5"}, 547 | {file = "mypy-1.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245"}, 548 | {file = "mypy-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183"}, 549 | {file = "mypy-1.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0"}, 550 | {file = "mypy-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7"}, 551 | {file = "mypy-1.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f"}, 552 | {file = "mypy-1.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660"}, 553 | {file = "mypy-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7"}, 554 | {file = "mypy-1.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71"}, 555 | {file = "mypy-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a"}, 556 | {file = "mypy-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169"}, 557 | {file = "mypy-1.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143"}, 558 | {file = "mypy-1.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46"}, 559 | {file = "mypy-1.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85"}, 560 | {file = "mypy-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45"}, 561 | {file = "mypy-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208"}, 562 | {file = "mypy-1.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd"}, 563 | {file = "mypy-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332"}, 564 | {file = "mypy-1.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f"}, 565 | {file = "mypy-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30"}, 566 | {file = "mypy-1.6.1-py3-none-any.whl", hash = "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1"}, 567 | {file = "mypy-1.6.1.tar.gz", hash = "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1"}, 568 | ] 569 | 570 | [package.dependencies] 571 | mypy-extensions = ">=1.0.0" 572 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 573 | typing-extensions = ">=4.1.0" 574 | 575 | [package.extras] 576 | dmypy = ["psutil (>=4.0)"] 577 | install-types = ["pip"] 578 | reports = ["lxml"] 579 | 580 | [[package]] 581 | name = "mypy-extensions" 582 | version = "1.0.0" 583 | description = "Type system extensions for programs checked with the mypy type checker." 584 | category = "dev" 585 | optional = false 586 | python-versions = ">=3.5" 587 | files = [ 588 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 589 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 590 | ] 591 | 592 | [[package]] 593 | name = "orjson" 594 | version = "3.9.10" 595 | description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" 596 | category = "main" 597 | optional = false 598 | python-versions = ">=3.8" 599 | files = [ 600 | {file = "orjson-3.9.10-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c18a4da2f50050a03d1da5317388ef84a16013302a5281d6f64e4a3f406aabc4"}, 601 | {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5148bab4d71f58948c7c39d12b14a9005b6ab35a0bdf317a8ade9a9e4d9d0bd5"}, 602 | {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4cf7837c3b11a2dfb589f8530b3cff2bd0307ace4c301e8997e95c7468c1378e"}, 603 | {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c62b6fa2961a1dcc51ebe88771be5319a93fd89bd247c9ddf732bc250507bc2b"}, 604 | {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb3922a7a804755bbe6b5be9b312e746137a03600f488290318936c1a2d4dc"}, 605 | {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1234dc92d011d3554d929b6cf058ac4a24d188d97be5e04355f1b9223e98bbe9"}, 606 | {file = "orjson-3.9.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:06ad5543217e0e46fd7ab7ea45d506c76f878b87b1b4e369006bdb01acc05a83"}, 607 | {file = "orjson-3.9.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4fd72fab7bddce46c6826994ce1e7de145ae1e9e106ebb8eb9ce1393ca01444d"}, 608 | {file = "orjson-3.9.10-cp310-none-win32.whl", hash = "sha256:b5b7d4a44cc0e6ff98da5d56cde794385bdd212a86563ac321ca64d7f80c80d1"}, 609 | {file = "orjson-3.9.10-cp310-none-win_amd64.whl", hash = "sha256:61804231099214e2f84998316f3238c4c2c4aaec302df12b21a64d72e2a135c7"}, 610 | {file = "orjson-3.9.10-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cff7570d492bcf4b64cc862a6e2fb77edd5e5748ad715f487628f102815165e9"}, 611 | {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8bc367f725dfc5cabeed1ae079d00369900231fbb5a5280cf0736c30e2adf7"}, 612 | {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c812312847867b6335cfb264772f2a7e85b3b502d3a6b0586aa35e1858528ab1"}, 613 | {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9edd2856611e5050004f4722922b7b1cd6268da34102667bd49d2a2b18bafb81"}, 614 | {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:674eb520f02422546c40401f4efaf8207b5e29e420c17051cddf6c02783ff5ca"}, 615 | {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0dc4310da8b5f6415949bd5ef937e60aeb0eb6b16f95041b5e43e6200821fb"}, 616 | {file = "orjson-3.9.10-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e99c625b8c95d7741fe057585176b1b8783d46ed4b8932cf98ee145c4facf499"}, 617 | {file = "orjson-3.9.10-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec6f18f96b47299c11203edfbdc34e1b69085070d9a3d1f302810cc23ad36bf3"}, 618 | {file = "orjson-3.9.10-cp311-none-win32.whl", hash = "sha256:ce0a29c28dfb8eccd0f16219360530bc3cfdf6bf70ca384dacd36e6c650ef8e8"}, 619 | {file = "orjson-3.9.10-cp311-none-win_amd64.whl", hash = "sha256:cf80b550092cc480a0cbd0750e8189247ff45457e5a023305f7ef1bcec811616"}, 620 | {file = "orjson-3.9.10-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:602a8001bdf60e1a7d544be29c82560a7b49319a0b31d62586548835bbe2c862"}, 621 | {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f295efcd47b6124b01255d1491f9e46f17ef40d3d7eabf7364099e463fb45f0f"}, 622 | {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92af0d00091e744587221e79f68d617b432425a7e59328ca4c496f774a356071"}, 623 | {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5a02360e73e7208a872bf65a7554c9f15df5fe063dc047f79738998b0506a14"}, 624 | {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:858379cbb08d84fe7583231077d9a36a1a20eb72f8c9076a45df8b083724ad1d"}, 625 | {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666c6fdcaac1f13eb982b649e1c311c08d7097cbda24f32612dae43648d8db8d"}, 626 | {file = "orjson-3.9.10-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3fb205ab52a2e30354640780ce4587157a9563a68c9beaf52153e1cea9aa0921"}, 627 | {file = "orjson-3.9.10-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7ec960b1b942ee3c69323b8721df2a3ce28ff40e7ca47873ae35bfafeb4555ca"}, 628 | {file = "orjson-3.9.10-cp312-none-win_amd64.whl", hash = "sha256:3e892621434392199efb54e69edfff9f699f6cc36dd9553c5bf796058b14b20d"}, 629 | {file = "orjson-3.9.10-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8b9ba0ccd5a7f4219e67fbbe25e6b4a46ceef783c42af7dbc1da548eb28b6531"}, 630 | {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e2ecd1d349e62e3960695214f40939bbfdcaeaaa62ccc638f8e651cf0970e5f"}, 631 | {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f433be3b3f4c66016d5a20e5b4444ef833a1f802ced13a2d852c637f69729c1"}, 632 | {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4689270c35d4bb3102e103ac43c3f0b76b169760aff8bcf2d401a3e0e58cdb7f"}, 633 | {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bd176f528a8151a6efc5359b853ba3cc0e82d4cd1fab9c1300c5d957dc8f48c"}, 634 | {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a2ce5ea4f71681623f04e2b7dadede3c7435dfb5e5e2d1d0ec25b35530e277b"}, 635 | {file = "orjson-3.9.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:49f8ad582da6e8d2cf663c4ba5bf9f83cc052570a3a767487fec6af839b0e777"}, 636 | {file = "orjson-3.9.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2a11b4b1a8415f105d989876a19b173f6cdc89ca13855ccc67c18efbd7cbd1f8"}, 637 | {file = "orjson-3.9.10-cp38-none-win32.whl", hash = "sha256:a353bf1f565ed27ba71a419b2cd3db9d6151da426b61b289b6ba1422a702e643"}, 638 | {file = "orjson-3.9.10-cp38-none-win_amd64.whl", hash = "sha256:e28a50b5be854e18d54f75ef1bb13e1abf4bc650ab9d635e4258c58e71eb6ad5"}, 639 | {file = "orjson-3.9.10-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ee5926746232f627a3be1cc175b2cfad24d0170d520361f4ce3fa2fd83f09e1d"}, 640 | {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a73160e823151f33cdc05fe2cea557c5ef12fdf276ce29bb4f1c571c8368a60"}, 641 | {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c338ed69ad0b8f8f8920c13f529889fe0771abbb46550013e3c3d01e5174deef"}, 642 | {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5869e8e130e99687d9e4be835116c4ebd83ca92e52e55810962446d841aba8de"}, 643 | {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2c1e559d96a7f94a4f581e2a32d6d610df5840881a8cba8f25e446f4d792df3"}, 644 | {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a3a3a72c9811b56adf8bcc829b010163bb2fc308877e50e9910c9357e78521"}, 645 | {file = "orjson-3.9.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7f8fb7f5ecf4f6355683ac6881fd64b5bb2b8a60e3ccde6ff799e48791d8f864"}, 646 | {file = "orjson-3.9.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c943b35ecdf7123b2d81d225397efddf0bce2e81db2f3ae633ead38e85cd5ade"}, 647 | {file = "orjson-3.9.10-cp39-none-win32.whl", hash = "sha256:fb0b361d73f6b8eeceba47cd37070b5e6c9de5beaeaa63a1cb35c7e1a73ef088"}, 648 | {file = "orjson-3.9.10-cp39-none-win_amd64.whl", hash = "sha256:b90f340cb6397ec7a854157fac03f0c82b744abdd1c0941a024c3c29d1340aff"}, 649 | {file = "orjson-3.9.10.tar.gz", hash = "sha256:9ebbdbd6a046c304b1845e96fbcc5559cd296b4dfd3ad2509e33c4d9ce07d6a1"}, 650 | ] 651 | 652 | [[package]] 653 | name = "packaging" 654 | version = "23.2" 655 | description = "Core utilities for Python packages" 656 | category = "main" 657 | optional = false 658 | python-versions = ">=3.7" 659 | files = [ 660 | {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, 661 | {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, 662 | ] 663 | 664 | [[package]] 665 | name = "pathspec" 666 | version = "0.11.2" 667 | description = "Utility library for gitignore style pattern matching of file paths." 668 | category = "dev" 669 | optional = false 670 | python-versions = ">=3.7" 671 | files = [ 672 | {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, 673 | {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, 674 | ] 675 | 676 | [[package]] 677 | name = "pbr" 678 | version = "5.11.1" 679 | description = "Python Build Reasonableness" 680 | category = "dev" 681 | optional = false 682 | python-versions = ">=2.6" 683 | files = [ 684 | {file = "pbr-5.11.1-py2.py3-none-any.whl", hash = "sha256:567f09558bae2b3ab53cb3c1e2e33e726ff3338e7bae3db5dc954b3a44eef12b"}, 685 | {file = "pbr-5.11.1.tar.gz", hash = "sha256:aefc51675b0b533d56bb5fd1c8c6c0522fe31896679882e1c4c63d5e4a0fccb3"}, 686 | ] 687 | 688 | [[package]] 689 | name = "platformdirs" 690 | version = "3.11.0" 691 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 692 | category = "dev" 693 | optional = false 694 | python-versions = ">=3.7" 695 | files = [ 696 | {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, 697 | {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, 698 | ] 699 | 700 | [package.extras] 701 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] 702 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] 703 | 704 | [[package]] 705 | name = "pluggy" 706 | version = "1.3.0" 707 | description = "plugin and hook calling mechanisms for python" 708 | category = "dev" 709 | optional = false 710 | python-versions = ">=3.8" 711 | files = [ 712 | {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, 713 | {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, 714 | ] 715 | 716 | [package.extras] 717 | dev = ["pre-commit", "tox"] 718 | testing = ["pytest", "pytest-benchmark"] 719 | 720 | [[package]] 721 | name = "pycodestyle" 722 | version = "2.11.1" 723 | description = "Python style guide checker" 724 | category = "dev" 725 | optional = false 726 | python-versions = ">=3.8" 727 | files = [ 728 | {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, 729 | {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, 730 | ] 731 | 732 | [[package]] 733 | name = "pydantic" 734 | version = "2.4.2" 735 | description = "Data validation using Python type hints" 736 | category = "main" 737 | optional = false 738 | python-versions = ">=3.7" 739 | files = [ 740 | {file = "pydantic-2.4.2-py3-none-any.whl", hash = "sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1"}, 741 | {file = "pydantic-2.4.2.tar.gz", hash = "sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7"}, 742 | ] 743 | 744 | [package.dependencies] 745 | annotated-types = ">=0.4.0" 746 | pydantic-core = "2.10.1" 747 | typing-extensions = ">=4.6.1" 748 | 749 | [package.extras] 750 | email = ["email-validator (>=2.0.0)"] 751 | 752 | [[package]] 753 | name = "pydantic-core" 754 | version = "2.10.1" 755 | description = "" 756 | category = "main" 757 | optional = false 758 | python-versions = ">=3.7" 759 | files = [ 760 | {file = "pydantic_core-2.10.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63"}, 761 | {file = "pydantic_core-2.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096"}, 762 | {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a"}, 763 | {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175"}, 764 | {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7"}, 765 | {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893"}, 766 | {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e"}, 767 | {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e"}, 768 | {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e"}, 769 | {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6"}, 770 | {file = "pydantic_core-2.10.1-cp310-none-win32.whl", hash = "sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b"}, 771 | {file = "pydantic_core-2.10.1-cp310-none-win_amd64.whl", hash = "sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0"}, 772 | {file = "pydantic_core-2.10.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea"}, 773 | {file = "pydantic_core-2.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4"}, 774 | {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c"}, 775 | {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b"}, 776 | {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f"}, 777 | {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a"}, 778 | {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8"}, 779 | {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4"}, 780 | {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607"}, 781 | {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f"}, 782 | {file = "pydantic_core-2.10.1-cp311-none-win32.whl", hash = "sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6"}, 783 | {file = "pydantic_core-2.10.1-cp311-none-win_amd64.whl", hash = "sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27"}, 784 | {file = "pydantic_core-2.10.1-cp311-none-win_arm64.whl", hash = "sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325"}, 785 | {file = "pydantic_core-2.10.1-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921"}, 786 | {file = "pydantic_core-2.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118"}, 787 | {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab"}, 788 | {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff"}, 789 | {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7"}, 790 | {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c"}, 791 | {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901"}, 792 | {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d"}, 793 | {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f"}, 794 | {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c"}, 795 | {file = "pydantic_core-2.10.1-cp312-none-win32.whl", hash = "sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f"}, 796 | {file = "pydantic_core-2.10.1-cp312-none-win_amd64.whl", hash = "sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430"}, 797 | {file = "pydantic_core-2.10.1-cp312-none-win_arm64.whl", hash = "sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94"}, 798 | {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede"}, 799 | {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698"}, 800 | {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f"}, 801 | {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb"}, 802 | {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904"}, 803 | {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3"}, 804 | {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891"}, 805 | {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221"}, 806 | {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15"}, 807 | {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f"}, 808 | {file = "pydantic_core-2.10.1-cp37-none-win32.whl", hash = "sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c"}, 809 | {file = "pydantic_core-2.10.1-cp37-none-win_amd64.whl", hash = "sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e"}, 810 | {file = "pydantic_core-2.10.1-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc"}, 811 | {file = "pydantic_core-2.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a"}, 812 | {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd"}, 813 | {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468"}, 814 | {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6"}, 815 | {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58"}, 816 | {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302"}, 817 | {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e"}, 818 | {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561"}, 819 | {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de"}, 820 | {file = "pydantic_core-2.10.1-cp38-none-win32.whl", hash = "sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee"}, 821 | {file = "pydantic_core-2.10.1-cp38-none-win_amd64.whl", hash = "sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e"}, 822 | {file = "pydantic_core-2.10.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970"}, 823 | {file = "pydantic_core-2.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b"}, 824 | {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875"}, 825 | {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0"}, 826 | {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531"}, 827 | {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c"}, 828 | {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a"}, 829 | {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429"}, 830 | {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7"}, 831 | {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595"}, 832 | {file = "pydantic_core-2.10.1-cp39-none-win32.whl", hash = "sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a"}, 833 | {file = "pydantic_core-2.10.1-cp39-none-win_amd64.whl", hash = "sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a"}, 834 | {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b"}, 835 | {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521"}, 836 | {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208"}, 837 | {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9"}, 838 | {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb"}, 839 | {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf"}, 840 | {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357"}, 841 | {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada"}, 842 | {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a"}, 843 | {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f"}, 844 | {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e"}, 845 | {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55"}, 846 | {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514"}, 847 | {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2"}, 848 | {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9"}, 849 | {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05"}, 850 | {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52"}, 851 | {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d"}, 852 | {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884"}, 853 | {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b"}, 854 | {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7"}, 855 | {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132"}, 856 | {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402"}, 857 | {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934"}, 858 | {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33"}, 859 | {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5"}, 860 | {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e"}, 861 | {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881"}, 862 | {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5"}, 863 | {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7"}, 864 | {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776"}, 865 | {file = "pydantic_core-2.10.1.tar.gz", hash = "sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82"}, 866 | ] 867 | 868 | [package.dependencies] 869 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 870 | 871 | [[package]] 872 | name = "pydantic-settings" 873 | version = "2.0.3" 874 | description = "Settings management using Pydantic" 875 | category = "main" 876 | optional = false 877 | python-versions = ">=3.7" 878 | files = [ 879 | {file = "pydantic_settings-2.0.3-py3-none-any.whl", hash = "sha256:ddd907b066622bd67603b75e2ff791875540dc485b7307c4fffc015719da8625"}, 880 | {file = "pydantic_settings-2.0.3.tar.gz", hash = "sha256:962dc3672495aad6ae96a4390fac7e593591e144625e5112d359f8f67fb75945"}, 881 | ] 882 | 883 | [package.dependencies] 884 | pydantic = ">=2.0.1" 885 | python-dotenv = ">=0.21.0" 886 | 887 | [[package]] 888 | name = "pyflakes" 889 | version = "3.1.0" 890 | description = "passive checker of Python programs" 891 | category = "dev" 892 | optional = false 893 | python-versions = ">=3.8" 894 | files = [ 895 | {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, 896 | {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, 897 | ] 898 | 899 | [[package]] 900 | name = "pygments" 901 | version = "2.16.1" 902 | description = "Pygments is a syntax highlighting package written in Python." 903 | category = "dev" 904 | optional = false 905 | python-versions = ">=3.7" 906 | files = [ 907 | {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, 908 | {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, 909 | ] 910 | 911 | [package.extras] 912 | plugins = ["importlib-metadata"] 913 | 914 | [[package]] 915 | name = "pylint" 916 | version = "3.0.2" 917 | description = "python code static checker" 918 | category = "dev" 919 | optional = false 920 | python-versions = ">=3.8.0" 921 | files = [ 922 | {file = "pylint-3.0.2-py3-none-any.whl", hash = "sha256:60ed5f3a9ff8b61839ff0348b3624ceeb9e6c2a92c514d81c9cc273da3b6bcda"}, 923 | {file = "pylint-3.0.2.tar.gz", hash = "sha256:0d4c286ef6d2f66c8bfb527a7f8a629009e42c99707dec821a03e1b51a4c1496"}, 924 | ] 925 | 926 | [package.dependencies] 927 | astroid = ">=3.0.1,<=3.1.0-dev0" 928 | colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} 929 | dill = [ 930 | {version = ">=0.2", markers = "python_version < \"3.11\""}, 931 | {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, 932 | {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, 933 | ] 934 | isort = ">=4.2.5,<6" 935 | mccabe = ">=0.6,<0.8" 936 | platformdirs = ">=2.2.0" 937 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 938 | tomlkit = ">=0.10.1" 939 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 940 | 941 | [package.extras] 942 | spelling = ["pyenchant (>=3.2,<4.0)"] 943 | testutils = ["gitpython (>3)"] 944 | 945 | [[package]] 946 | name = "pytest" 947 | version = "7.4.3" 948 | description = "pytest: simple powerful testing with Python" 949 | category = "dev" 950 | optional = false 951 | python-versions = ">=3.7" 952 | files = [ 953 | {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, 954 | {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, 955 | ] 956 | 957 | [package.dependencies] 958 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 959 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 960 | iniconfig = "*" 961 | packaging = "*" 962 | pluggy = ">=0.12,<2.0" 963 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 964 | 965 | [package.extras] 966 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 967 | 968 | [[package]] 969 | name = "python-dotenv" 970 | version = "1.0.0" 971 | description = "Read key-value pairs from a .env file and set them as environment variables" 972 | category = "main" 973 | optional = false 974 | python-versions = ">=3.8" 975 | files = [ 976 | {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, 977 | {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, 978 | ] 979 | 980 | [package.extras] 981 | cli = ["click (>=5.0)"] 982 | 983 | [[package]] 984 | name = "pyyaml" 985 | version = "6.0.1" 986 | description = "YAML parser and emitter for Python" 987 | category = "dev" 988 | optional = false 989 | python-versions = ">=3.6" 990 | files = [ 991 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 992 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 993 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 994 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 995 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 996 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 997 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 998 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 999 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 1000 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 1001 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 1002 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 1003 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 1004 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 1005 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 1006 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 1007 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 1008 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 1009 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 1010 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 1011 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 1012 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 1013 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 1014 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 1015 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 1016 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 1017 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 1018 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 1019 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 1020 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 1021 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 1022 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 1023 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 1024 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 1025 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 1026 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 1027 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 1028 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 1029 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 1030 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 1031 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 1032 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 1033 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 1034 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 1035 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 1036 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 1037 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 1038 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 1039 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 1040 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 1041 | ] 1042 | 1043 | [[package]] 1044 | name = "requests" 1045 | version = "2.31.0" 1046 | description = "Python HTTP for Humans." 1047 | category = "dev" 1048 | optional = false 1049 | python-versions = ">=3.7" 1050 | files = [ 1051 | {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, 1052 | {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, 1053 | ] 1054 | 1055 | [package.dependencies] 1056 | certifi = ">=2017.4.17" 1057 | charset-normalizer = ">=2,<4" 1058 | idna = ">=2.5,<4" 1059 | urllib3 = ">=1.21.1,<3" 1060 | 1061 | [package.extras] 1062 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 1063 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 1064 | 1065 | [[package]] 1066 | name = "rfc3986" 1067 | version = "1.5.0" 1068 | description = "Validating URI References per RFC 3986" 1069 | category = "main" 1070 | optional = false 1071 | python-versions = "*" 1072 | files = [ 1073 | {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, 1074 | {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, 1075 | ] 1076 | 1077 | [package.dependencies] 1078 | idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} 1079 | 1080 | [package.extras] 1081 | idna2008 = ["idna"] 1082 | 1083 | [[package]] 1084 | name = "rich" 1085 | version = "13.6.0" 1086 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 1087 | category = "dev" 1088 | optional = false 1089 | python-versions = ">=3.7.0" 1090 | files = [ 1091 | {file = "rich-13.6.0-py3-none-any.whl", hash = "sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245"}, 1092 | {file = "rich-13.6.0.tar.gz", hash = "sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"}, 1093 | ] 1094 | 1095 | [package.dependencies] 1096 | markdown-it-py = ">=2.2.0" 1097 | pygments = ">=2.13.0,<3.0.0" 1098 | typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} 1099 | 1100 | [package.extras] 1101 | jupyter = ["ipywidgets (>=7.5.1,<9)"] 1102 | 1103 | [[package]] 1104 | name = "smmap" 1105 | version = "5.0.1" 1106 | description = "A pure Python implementation of a sliding window memory map manager" 1107 | category = "dev" 1108 | optional = false 1109 | python-versions = ">=3.7" 1110 | files = [ 1111 | {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, 1112 | {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, 1113 | ] 1114 | 1115 | [[package]] 1116 | name = "sniffio" 1117 | version = "1.3.0" 1118 | description = "Sniff out which async library your code is running under" 1119 | category = "main" 1120 | optional = false 1121 | python-versions = ">=3.7" 1122 | files = [ 1123 | {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, 1124 | {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, 1125 | ] 1126 | 1127 | [[package]] 1128 | name = "starlette" 1129 | version = "0.27.0" 1130 | description = "The little ASGI library that shines." 1131 | category = "main" 1132 | optional = false 1133 | python-versions = ">=3.7" 1134 | files = [ 1135 | {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"}, 1136 | {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"}, 1137 | ] 1138 | 1139 | [package.dependencies] 1140 | anyio = ">=3.4.0,<5" 1141 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 1142 | 1143 | [package.extras] 1144 | full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] 1145 | 1146 | [[package]] 1147 | name = "stevedore" 1148 | version = "5.1.0" 1149 | description = "Manage dynamic plugins for Python applications" 1150 | category = "dev" 1151 | optional = false 1152 | python-versions = ">=3.8" 1153 | files = [ 1154 | {file = "stevedore-5.1.0-py3-none-any.whl", hash = "sha256:8cc040628f3cea5d7128f2e76cf486b2251a4e543c7b938f58d9a377f6694a2d"}, 1155 | {file = "stevedore-5.1.0.tar.gz", hash = "sha256:a54534acf9b89bc7ed264807013b505bf07f74dbe4bcfa37d32bd063870b087c"}, 1156 | ] 1157 | 1158 | [package.dependencies] 1159 | pbr = ">=2.0.0,<2.1.0 || >2.1.0" 1160 | 1161 | [[package]] 1162 | name = "tomli" 1163 | version = "2.0.1" 1164 | description = "A lil' TOML parser" 1165 | category = "dev" 1166 | optional = false 1167 | python-versions = ">=3.7" 1168 | files = [ 1169 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 1170 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 1171 | ] 1172 | 1173 | [[package]] 1174 | name = "tomlkit" 1175 | version = "0.12.2" 1176 | description = "Style preserving TOML library" 1177 | category = "dev" 1178 | optional = false 1179 | python-versions = ">=3.7" 1180 | files = [ 1181 | {file = "tomlkit-0.12.2-py3-none-any.whl", hash = "sha256:eeea7ac7563faeab0a1ed8fe12c2e5a51c61f933f2502f7e9db0241a65163ad0"}, 1182 | {file = "tomlkit-0.12.2.tar.gz", hash = "sha256:df32fab589a81f0d7dc525a4267b6d7a64ee99619cbd1eeb0fae32c1dd426977"}, 1183 | ] 1184 | 1185 | [[package]] 1186 | name = "typing-extensions" 1187 | version = "4.8.0" 1188 | description = "Backported and Experimental Type Hints for Python 3.8+" 1189 | category = "main" 1190 | optional = false 1191 | python-versions = ">=3.8" 1192 | files = [ 1193 | {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, 1194 | {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, 1195 | ] 1196 | 1197 | [[package]] 1198 | name = "urllib3" 1199 | version = "2.0.7" 1200 | description = "HTTP library with thread-safe connection pooling, file post, and more." 1201 | category = "dev" 1202 | optional = false 1203 | python-versions = ">=3.7" 1204 | files = [ 1205 | {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, 1206 | {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, 1207 | ] 1208 | 1209 | [package.extras] 1210 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 1211 | secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] 1212 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 1213 | zstd = ["zstandard (>=0.18.0)"] 1214 | 1215 | [[package]] 1216 | name = "uvicorn" 1217 | version = "0.23.2" 1218 | description = "The lightning-fast ASGI server." 1219 | category = "main" 1220 | optional = false 1221 | python-versions = ">=3.8" 1222 | files = [ 1223 | {file = "uvicorn-0.23.2-py3-none-any.whl", hash = "sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53"}, 1224 | {file = "uvicorn-0.23.2.tar.gz", hash = "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a"}, 1225 | ] 1226 | 1227 | [package.dependencies] 1228 | click = ">=7.0" 1229 | h11 = ">=0.8" 1230 | typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} 1231 | 1232 | [package.extras] 1233 | 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)"] 1234 | 1235 | [[package]] 1236 | name = "uvloop" 1237 | version = "0.19.0" 1238 | description = "Fast implementation of asyncio event loop on top of libuv" 1239 | category = "main" 1240 | optional = false 1241 | python-versions = ">=3.8.0" 1242 | files = [ 1243 | {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"}, 1244 | {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"}, 1245 | {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8"}, 1246 | {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849"}, 1247 | {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957"}, 1248 | {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd"}, 1249 | {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef"}, 1250 | {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2"}, 1251 | {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1"}, 1252 | {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24"}, 1253 | {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533"}, 1254 | {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12"}, 1255 | {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650"}, 1256 | {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec"}, 1257 | {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc"}, 1258 | {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6"}, 1259 | {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593"}, 1260 | {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3"}, 1261 | {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd"}, 1262 | {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd"}, 1263 | {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be"}, 1264 | {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797"}, 1265 | {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d"}, 1266 | {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7"}, 1267 | {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b"}, 1268 | {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67"}, 1269 | {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7"}, 1270 | {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256"}, 1271 | {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17"}, 1272 | {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5"}, 1273 | {file = "uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd"}, 1274 | ] 1275 | 1276 | [package.extras] 1277 | docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] 1278 | test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] 1279 | 1280 | [metadata] 1281 | lock-version = "2.0" 1282 | python-versions = "^3.8.1" 1283 | content-hash = "add0a32ac2cde7c15ee5132b2b1ea1216dab96d9a3d01238822ff4b94e7800a5" 1284 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | create = true 3 | in-project = true 4 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "reco_service" 3 | version = "0.0.1" 4 | description = "" 5 | authors = ["Emiliy Feldman "] 6 | maintainers = ["Emiliy Feldman "] 7 | readme = "README.md" 8 | packages = [ 9 | { include = "service" } 10 | ] 11 | 12 | [tool.poetry.dependencies] 13 | python = "^3.8.1" 14 | fastapi = "^0.104.0" 15 | pydantic = "^2.4.2" 16 | gunicorn = "^21.2.0" 17 | uvloop = "^0.19.0" 18 | uvicorn = "^0.23.0" 19 | orjson = "^3.9.10" 20 | starlette = "^0.27.0" 21 | httpx = "^0.22.0" # for starlette.testclient 22 | pydantic-settings = "^2.0.3" 23 | 24 | [tool.poetry.group.dev.dependencies] 25 | pytest = "7.4.3" 26 | requests = "^2.31.0" 27 | mypy = "^1.6.1" 28 | isort = "^5.12.0" 29 | bandit = "^1.7.5" 30 | flake8 = "^6.1.0" 31 | pylint = "^3.0.2" 32 | black = "^23.10.1" 33 | 34 | [tool.black] 35 | line-length = 120 36 | target-version = ["py38", "py39", "py310"] 37 | 38 | [build-system] 39 | requires = ["poetry>=1.0.5"] 40 | build-backend = "poetry.masonry.api" 41 | -------------------------------------------------------------------------------- /service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feldlime/RecoServiceTemplate/68ffd3f8a6cc73ac40de612861f847364b25c74f/service/__init__.py -------------------------------------------------------------------------------- /service/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feldlime/RecoServiceTemplate/68ffd3f8a6cc73ac40de612861f847364b25c74f/service/api/__init__.py -------------------------------------------------------------------------------- /service/api/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from concurrent.futures.thread import ThreadPoolExecutor 3 | from typing import Any, Dict 4 | 5 | import uvloop 6 | from fastapi import FastAPI 7 | 8 | from ..log import app_logger, setup_logging 9 | from ..settings import ServiceConfig 10 | from .exception_handlers import add_exception_handlers 11 | from .middlewares import add_middlewares 12 | from .views import add_views 13 | 14 | __all__ = ("create_app",) 15 | 16 | 17 | def setup_asyncio(thread_name_prefix: str) -> None: 18 | uvloop.install() 19 | 20 | loop = asyncio.get_event_loop() 21 | 22 | executor = ThreadPoolExecutor(thread_name_prefix=thread_name_prefix) 23 | loop.set_default_executor(executor) 24 | 25 | def handler(_, context: Dict[str, Any]) -> None: 26 | message = "Caught asyncio exception: {message}".format_map(context) 27 | app_logger.warning(message) 28 | 29 | loop.set_exception_handler(handler) 30 | 31 | 32 | def create_app(config: ServiceConfig) -> FastAPI: 33 | setup_logging(config) 34 | setup_asyncio(thread_name_prefix=config.service_name) 35 | 36 | app = FastAPI(debug=False) 37 | app.state.k_recs = config.k_recs 38 | 39 | add_views(app) 40 | add_middlewares(app) 41 | add_exception_handlers(app) 42 | 43 | return app 44 | -------------------------------------------------------------------------------- /service/api/exception_handlers.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from fastapi import FastAPI, Request 4 | from fastapi.exceptions import RequestValidationError 5 | from pydantic import ValidationError 6 | from starlette import status 7 | from starlette.exceptions import HTTPException 8 | from starlette.responses import JSONResponse 9 | 10 | from service.log import app_logger 11 | from service.models import Error 12 | from service.response import create_response, server_error 13 | 14 | from .exceptions import AppException 15 | 16 | 17 | async def default_error_handler( 18 | request: Request, 19 | exc: Exception, 20 | ) -> JSONResponse: 21 | app_logger.error(str(exc)) 22 | error = Error(error_key="server_error", error_message=str(exc)) 23 | return server_error([error]) 24 | 25 | 26 | async def http_error_handler( 27 | request: Request, 28 | exc: HTTPException, 29 | ) -> JSONResponse: 30 | app_logger.error(str(exc)) 31 | error = Error(error_key="http_exception", error_message=exc.detail) 32 | return create_response(status_code=exc.status_code, errors=[error]) 33 | 34 | 35 | async def validation_error_handler( 36 | request: Request, exc: Union[RequestValidationError, ValidationError] 37 | ) -> JSONResponse: 38 | errors = [ 39 | Error( 40 | error_key=err.get("type"), 41 | error_message=err.get("msg"), 42 | error_loc=err.get("loc"), 43 | ) 44 | for err in exc.errors() 45 | ] 46 | app_logger.error(str(errors)) 47 | return create_response(status.HTTP_422_UNPROCESSABLE_ENTITY, errors=errors) 48 | 49 | 50 | async def app_exception_handler( 51 | request: Request, 52 | exc: AppException, 53 | ) -> JSONResponse: 54 | errors = [ 55 | Error( 56 | error_key=exc.error_key, 57 | error_message=exc.error_message, 58 | error_loc=exc.error_loc, 59 | ) 60 | ] 61 | app_logger.error(str(errors)) 62 | return create_response(exc.status_code, errors=errors) 63 | 64 | 65 | def add_exception_handlers(app: FastAPI) -> None: 66 | app.add_exception_handler(HTTPException, http_error_handler) 67 | app.add_exception_handler(ValidationError, validation_error_handler) 68 | app.add_exception_handler(RequestValidationError, validation_error_handler) 69 | app.add_exception_handler(AppException, app_exception_handler) 70 | app.add_exception_handler(Exception, default_error_handler) 71 | -------------------------------------------------------------------------------- /service/api/exceptions.py: -------------------------------------------------------------------------------- 1 | import typing as tp 2 | from http import HTTPStatus 3 | 4 | 5 | class AppException(Exception): 6 | def __init__( 7 | self, 8 | status_code: int, 9 | error_key: str, 10 | error_message: str = "", 11 | error_loc: tp.Optional[tp.Sequence[str]] = None, 12 | ) -> None: 13 | self.error_key = error_key 14 | self.error_message = error_message 15 | self.error_loc = error_loc 16 | self.status_code = status_code 17 | super().__init__() 18 | 19 | 20 | class UserNotFoundError(AppException): 21 | def __init__( 22 | self, 23 | status_code: int = HTTPStatus.NOT_FOUND, 24 | error_key: str = "user_not_found", 25 | error_message: str = "User is unknown", 26 | error_loc: tp.Optional[tp.Sequence[str]] = None, 27 | ): 28 | super().__init__(status_code, error_key, error_message, error_loc) 29 | -------------------------------------------------------------------------------- /service/api/middlewares.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from fastapi import FastAPI, Request 4 | from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint 5 | from starlette.middleware.cors import CORSMiddleware 6 | from starlette.responses import Response 7 | 8 | from service.log import access_logger, app_logger 9 | from service.models import Error 10 | from service.response import server_error 11 | 12 | 13 | class AccessMiddleware(BaseHTTPMiddleware): 14 | async def dispatch( 15 | self, 16 | request: Request, 17 | call_next: RequestResponseEndpoint, 18 | ) -> Response: 19 | started_at = time.perf_counter() 20 | response = await call_next(request) 21 | request_time = time.perf_counter() - started_at 22 | 23 | status_code = response.status_code 24 | 25 | access_logger.info( 26 | msg="", 27 | extra={ 28 | "request_time": round(request_time, 4), 29 | "status_code": status_code, 30 | "requested_url": request.url, 31 | "method": request.method, 32 | }, 33 | ) 34 | return response 35 | 36 | 37 | class ExceptionHandlerMiddleware(BaseHTTPMiddleware): 38 | async def dispatch( 39 | self, 40 | request: Request, 41 | call_next: RequestResponseEndpoint, 42 | ) -> Response: 43 | try: 44 | return await call_next(request) 45 | except Exception as e: # pylint: disable=W0703,W1203 46 | app_logger.exception(msg=f"Caught unhandled {e.__class__} exception: {e}") 47 | error = Error(error_key="server_error", error_message="Internal Server Error") 48 | return server_error([error]) 49 | 50 | 51 | def add_middlewares(app: FastAPI) -> None: 52 | # do not change order 53 | app.add_middleware(ExceptionHandlerMiddleware) 54 | app.add_middleware(AccessMiddleware) 55 | app.add_middleware( 56 | CORSMiddleware, 57 | allow_origins=["*"], 58 | allow_credentials=True, 59 | allow_methods=["*"], 60 | allow_headers=["*"], 61 | ) 62 | -------------------------------------------------------------------------------- /service/api/views.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi import APIRouter, FastAPI, Request 4 | from pydantic import BaseModel 5 | 6 | from service.api.exceptions import UserNotFoundError 7 | from service.log import app_logger 8 | 9 | 10 | class RecoResponse(BaseModel): 11 | user_id: int 12 | items: List[int] 13 | 14 | 15 | router = APIRouter() 16 | 17 | 18 | @router.get( 19 | path="/health", 20 | tags=["Health"], 21 | ) 22 | async def health() -> str: 23 | return "I am alive" 24 | 25 | 26 | @router.get( 27 | path="/reco/{model_name}/{user_id}", 28 | tags=["Recommendations"], 29 | response_model=RecoResponse, 30 | ) 31 | async def get_reco( 32 | request: Request, 33 | model_name: str, 34 | user_id: int, 35 | ) -> RecoResponse: 36 | app_logger.info(f"Request for model: {model_name}, user_id: {user_id}") 37 | 38 | # Write your code here 39 | 40 | if user_id > 10**9: 41 | raise UserNotFoundError(error_message=f"User {user_id} not found") 42 | 43 | k_recs = request.app.state.k_recs 44 | reco = list(range(k_recs)) 45 | return RecoResponse(user_id=user_id, items=reco) 46 | 47 | 48 | def add_views(app: FastAPI) -> None: 49 | app.include_router(router) 50 | -------------------------------------------------------------------------------- /service/log.py: -------------------------------------------------------------------------------- 1 | import logging.config 2 | import typing as tp 3 | 4 | from .settings import ServiceConfig 5 | 6 | app_logger = logging.getLogger("app") 7 | access_logger = logging.getLogger("access") 8 | 9 | 10 | class ServiceNameFilter(logging.Filter): 11 | def __init__(self, name: str = "", service_name: str = "") -> None: 12 | self.service_name = service_name 13 | 14 | super().__init__(name) 15 | 16 | def filter(self, record: logging.LogRecord) -> bool: 17 | setattr(record, "service_name", self.service_name) 18 | 19 | return super().filter(record) 20 | 21 | 22 | def get_config(service_config: ServiceConfig) -> tp.Dict[str, tp.Any]: 23 | level = service_config.log_config.level 24 | datetime_format = service_config.log_config.datetime_format 25 | 26 | config = { 27 | "version": 1, 28 | "disable_existing_loggers": True, 29 | "loggers": { 30 | "root": { 31 | "level": level, 32 | "handlers": ["console"], 33 | "propagate": False, 34 | }, 35 | app_logger.name: { 36 | "level": level, 37 | "handlers": ["console"], 38 | "propagate": False, 39 | }, 40 | access_logger.name: { 41 | "level": level, 42 | "handlers": ["access"], 43 | "propagate": False, 44 | }, 45 | "gunicorn.error": { 46 | "level": "INFO", 47 | "handlers": [ 48 | "console", 49 | ], 50 | "propagate": False, 51 | }, 52 | "gunicorn.access": { 53 | "level": "ERROR", 54 | "handlers": [ 55 | "gunicorn.access", 56 | ], 57 | "propagate": False, 58 | }, 59 | "uvicorn.error": { 60 | "level": "INFO", 61 | "handlers": [ 62 | "console", 63 | ], 64 | "propagate": False, 65 | }, 66 | "uvicorn.access": { 67 | "level": "ERROR", 68 | "handlers": [ 69 | "gunicorn.access", 70 | ], 71 | "propagate": False, 72 | }, 73 | }, 74 | "handlers": { 75 | "console": { 76 | "formatter": "console", 77 | "class": "logging.StreamHandler", 78 | "stream": "ext://sys.stdout", 79 | "filters": ["service_name"], 80 | }, 81 | "access": { 82 | "formatter": "access", 83 | "class": "logging.StreamHandler", 84 | "stream": "ext://sys.stdout", 85 | "filters": ["service_name"], 86 | }, 87 | "gunicorn.access": { 88 | "class": "logging.StreamHandler", 89 | "formatter": "gunicorn.access", 90 | "stream": "ext://sys.stdout", 91 | "filters": ["service_name"], 92 | }, 93 | }, 94 | "formatters": { 95 | "console": { 96 | "format": ( 97 | 'time="%(asctime)s" ' 98 | 'level="%(levelname)s" ' 99 | 'service_name="%(service_name)s" ' 100 | 'logger="%(name)s" ' 101 | 'pid="%(process)d" ' 102 | 'message="%(message)s" ' 103 | ), 104 | "datefmt": datetime_format, 105 | }, 106 | "access": { 107 | "format": ( 108 | 'time="%(asctime)s" ' 109 | 'level="%(levelname)s" ' 110 | 'service_name="%(service_name)s" ' 111 | 'logger="%(name)s" ' 112 | 'pid="%(process)d" ' 113 | 'method="%(method)s" ' 114 | 'requested_url="%(requested_url)s" ' 115 | 'status_code="%(status_code)s" ' 116 | 'request_time="%(request_time)s" ' 117 | ), 118 | "datefmt": datetime_format, 119 | }, 120 | "gunicorn.access": { 121 | "format": ( 122 | 'time="%(asctime)s" ' 123 | 'level="%(levelname)s" ' 124 | 'logger="%(name)s" ' 125 | 'pid="%(process)d" ' 126 | '"%(message)s"' 127 | ), 128 | "datefmt": datetime_format, 129 | }, 130 | }, 131 | "filters": { 132 | "service_name": { 133 | "()": "service.log.ServiceNameFilter", 134 | "service_name": service_config.service_name, 135 | }, 136 | }, 137 | } 138 | 139 | return config 140 | 141 | 142 | def setup_logging(service_config: ServiceConfig) -> None: 143 | config = get_config(service_config) 144 | logging.config.dictConfig(config) 145 | -------------------------------------------------------------------------------- /service/models.py: -------------------------------------------------------------------------------- 1 | import typing as tp 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class Error(BaseModel): 7 | error_key: str 8 | error_message: str 9 | error_loc: tp.Optional[tp.Any] = None 10 | -------------------------------------------------------------------------------- /service/response.py: -------------------------------------------------------------------------------- 1 | import json 2 | import typing as tp 3 | from http import HTTPStatus 4 | 5 | import orjson 6 | from fastapi.responses import JSONResponse 7 | from pydantic import BaseModel 8 | 9 | from service.models import Error 10 | 11 | 12 | class EnhancedJSONEncoder(json.JSONEncoder): 13 | def default(self, o: tp.Any) -> tp.Any: 14 | if isinstance(o, BaseModel): 15 | return o.model_dump() 16 | try: 17 | orjson.dumps(o) 18 | except TypeError: 19 | return str(o) 20 | return super().default(o) 21 | 22 | 23 | class DataclassJSONResponse(JSONResponse): 24 | media_type = "application/json" 25 | 26 | def render(self, content: tp.Any) -> bytes: 27 | return json.dumps( 28 | content, 29 | ensure_ascii=False, 30 | allow_nan=False, 31 | indent=None, 32 | separators=(",", ":"), 33 | cls=EnhancedJSONEncoder, 34 | ).encode("utf-8") 35 | 36 | 37 | def create_response( 38 | status_code: int, 39 | message: tp.Optional[str] = None, 40 | data: tp.Optional[tp.Any] = None, 41 | errors: tp.Optional[tp.List[Error]] = None, 42 | ) -> JSONResponse: 43 | content: tp.Dict[str, tp.Any] = {} 44 | 45 | if message is not None: 46 | content["message"] = message 47 | 48 | if data is not None: 49 | content["data"] = data 50 | 51 | if errors is not None: 52 | content["errors"] = errors 53 | 54 | return DataclassJSONResponse(content, status_code=status_code) 55 | 56 | 57 | def server_error(errors: tp.List[Error]) -> JSONResponse: 58 | return create_response(HTTPStatus.INTERNAL_SERVER_ERROR, errors=errors) 59 | -------------------------------------------------------------------------------- /service/settings.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings, SettingsConfigDict 2 | 3 | 4 | class Config(BaseSettings): 5 | model_config = SettingsConfigDict(case_sensitive=False) 6 | 7 | 8 | class LogConfig(Config): 9 | model_config = SettingsConfigDict(case_sensitive=False, env_prefix="log_") 10 | level: str = "INFO" 11 | datetime_format: str = "%Y-%m-%d %H:%M:%S" 12 | 13 | 14 | class ServiceConfig(Config): 15 | service_name: str = "reco_service" 16 | k_recs: int = 10 17 | 18 | log_config: LogConfig 19 | 20 | 21 | def get_config() -> ServiceConfig: 22 | return ServiceConfig( 23 | log_config=LogConfig(), 24 | ) 25 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | # Sets the console output style while running tests. 3 | console_output_style = progress 4 | 5 | # Sets list of directories that should be searched for tests when no 6 | # specific directories, files or test ids are given in the command 7 | # line when executing pytest from the root dir directory. 8 | testpaths = tests 9 | 10 | junit_family = xunit2 11 | 12 | [flake8] 13 | # Set the maximum allowed McCabe complexity value for a block of code. 14 | max-complexity = 10 15 | 16 | # Set the maximum length that any line may be. 17 | max-line-length = 120 18 | 19 | # Set the maximum length that a comment or docstring line may be. 20 | max-doc-length = 79 21 | 22 | [mypy] 23 | # Enables PEP 420 style namespace packages. 24 | namespace_packages = False 25 | 26 | # Suppresses error messages about imports that cannot be resolved. 27 | ignore_missing_imports = True 28 | 29 | # Directs what to do with imports when the imported module is found as a .py file 30 | # and not part of the files, modules and packages provided on the command line. 31 | follow_imports = normal 32 | 33 | # Determines whether to respect the follow_imports setting even for stub (.pyi) files. 34 | follow_imports_for_stubs = True 35 | 36 | # Specifies the path to the Python executable to inspect 37 | # to collect a list of available PEP 561 packages. 38 | python_executable = .venv/bin/python 39 | 40 | # Enables reporting error messages generated within PEP 561 compliant packages. 41 | no_silence_site_packages = False 42 | 43 | # Specifies the Python version used to parse and check the target program. 44 | python_version = 3.8 45 | 46 | # Disallows usage of types that come from unfollowed imports. 47 | disallow_any_unimported = False 48 | 49 | # Disallows all expressions in the module that have type Any. 50 | disallow_any_expr = False 51 | 52 | # Disallows functions that have Any in their signature after decorator transformation. 53 | disallow_any_decorated = False 54 | 55 | # Disallows explicit Any in type positions such as type annotations and generic type parameters. 56 | disallow_any_explicit = False 57 | 58 | # Disallows usage of generic types that do not specify explicit type parameters. 59 | disallow_any_generics = False 60 | 61 | # Disallows subclassing a value of type Any. 62 | disallow_subclassing_any = False 63 | 64 | # Disallows calling functions without type annotations from functions with type annotations. 65 | disallow_untyped_calls = True 66 | 67 | # Disallows defining functions without type annotations or with incomplete type annotations. 68 | disallow_untyped_defs = False 69 | 70 | # Disallows defining functions with incomplete type annotations. 71 | disallow_incomplete_defs = False 72 | 73 | # Type-checks the interior of functions without type annotations. 74 | check_untyped_defs = True 75 | 76 | # Reports an error whenever a function with type annotations 77 | # is decorated with a decorator without annotations. 78 | disallow_untyped_decorators = False 79 | 80 | # Changes the treatment of arguments with a default value of None 81 | # by not implicitly making their type Optional. 82 | no_implicit_optional = False 83 | 84 | # Enables or disables strict Optional checks. 85 | # If False, mypy treats None as compatible with every type. 86 | strict_optional = False 87 | 88 | # Warns about casting an expression to its inferred type. 89 | warn_redundant_casts = False 90 | 91 | # Warns about unneeded # type: ignore comments. 92 | warn_unused_ignores = True 93 | 94 | # Shows errors for missing return statements on some execution paths. 95 | warn_no_return = True 96 | 97 | # Shows a warning when returning a value with type Any 98 | # from a function declared with a non- Any return type. 99 | warn_return_any = False 100 | 101 | # Shows a warning when encountering any code inferred to be 102 | # unreachable or redundant after performing type analysis. 103 | warn_unreachable = True 104 | 105 | # Ignores all non-fatal errors. 106 | ignore_errors = False 107 | 108 | # Causes mypy to suppress errors caused by not being able to 109 | # fully infer the types of global and class variables. 110 | allow_untyped_globals = True 111 | 112 | # Allows variables to be redefined with an arbitrary type, as long as the redefinition 113 | # is in the same block and nesting level as the original definition. 114 | allow_redefinition = False 115 | 116 | # By default, imported values to a module are treated as exported 117 | # and mypy allows other modules to import them. 118 | implicit_reexport = True 119 | 120 | # Prohibit equality checks, identity checks, and container checks between non-overlapping types. 121 | strict_equality = True 122 | 123 | # Prefixes each error with the relevant context. 124 | show_error_context = True 125 | 126 | # Shows column numbers in error messages. 127 | show_column_numbers = True 128 | 129 | # Shows error codes in error messages. See Error codes for more information. 130 | show_error_codes = True 131 | 132 | # Use visually nicer output in error messages: use soft word wrap, 133 | # show source code snippets, and show error location markers. 134 | pretty = True 135 | 136 | # Shows error messages with color enabled. 137 | color_output = True 138 | 139 | # Shows a short summary line after error messages. 140 | error_summary = True 141 | 142 | # Show absolute paths to files. 143 | show_absolute_path = False 144 | 145 | # Enables incremental mode. 146 | incremental = True 147 | 148 | # Specifies the location where mypy stores incremental cache info. 149 | cache_dir = .mypy_cache 150 | 151 | # Use an SQLite database to store the cache. 152 | sqlite_cache = False 153 | 154 | # Include fine-grained dependency information in the cache for the mypy daemon. 155 | cache_fine_grained = False 156 | 157 | # Makes mypy use incremental cache data even if it was generated by a different version of mypy. 158 | skip_version_check = False 159 | 160 | # Skip cache internal consistency checks based on mtime. 161 | skip_cache_mtime_checks = False 162 | 163 | [isort] 164 | # An integer that represents the longest line-length you want a single import to take. 165 | line_length = 120 166 | 167 | # An integer that represents the longest line-length you want when wrapping. 168 | wrap_length = 120 169 | 170 | # Virtual environment to use for determining whether a package is third-party. 171 | virtual_env = .venv 172 | 173 | # An integer that represents how you want imports to be displayed 174 | # if they're long enough to span multiple lines. 175 | multi_line_output = 3 176 | 177 | # An integer that represents the number of spaces you would like 178 | # to indent by or Tab to indent by a single tab 179 | indent = 4 180 | 181 | # Force from imports to be grid wrapped regardless of line length, 182 | # where the value given is the number of imports allowed before wrapping occurs. 183 | force_grid_wrap = false 184 | 185 | # If set to True - isort will only change a file in place 186 | # if the resulting file has correct Python syntax. 187 | atomic = True 188 | 189 | # If set to True - ensures that if a star import is present, 190 | # nothing else is imported from that namespace. 191 | combine_star = True 192 | 193 | # If set to True - isort will print out verbose information. 194 | verbose = false 195 | 196 | # Will set isort to automatically add a trailing comma to the end of from imports. 197 | include_trailing_comma = True 198 | 199 | # Tells isort to use parenthesis for line continuation 200 | # instead of \ for lines over the allotted line length limit. 201 | use_parentheses = True 202 | 203 | # If set, import sorting will take case in consideration when sorting. 204 | case_sensitive = True 205 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feldlime/RecoServiceTemplate/68ffd3f8a6cc73ac40de612861f847364b25c74f/tests/__init__.py -------------------------------------------------------------------------------- /tests/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feldlime/RecoServiceTemplate/68ffd3f8a6cc73ac40de612861f847364b25c74f/tests/api/__init__.py -------------------------------------------------------------------------------- /tests/api/test_views.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | 3 | from starlette.testclient import TestClient 4 | 5 | from service.settings import ServiceConfig 6 | 7 | GET_RECO_PATH = "/reco/{model_name}/{user_id}" 8 | 9 | 10 | def test_health( 11 | client: TestClient, 12 | ) -> None: 13 | with client: 14 | response = client.get("/health") 15 | assert response.status_code == HTTPStatus.OK 16 | 17 | 18 | def test_get_reco_success( 19 | client: TestClient, 20 | service_config: ServiceConfig, 21 | ) -> None: 22 | user_id = 123 23 | path = GET_RECO_PATH.format(model_name="some_model", user_id=user_id) 24 | with client: 25 | response = client.get(path) 26 | assert response.status_code == HTTPStatus.OK 27 | response_json = response.json() 28 | assert response_json["user_id"] == user_id 29 | assert len(response_json["items"]) == service_config.k_recs 30 | assert all(isinstance(item_id, int) for item_id in response_json["items"]) 31 | 32 | 33 | def test_get_reco_for_unknown_user( 34 | client: TestClient, 35 | ) -> None: 36 | user_id = 10**10 37 | path = GET_RECO_PATH.format(model_name="some_model", user_id=user_id) 38 | with client: 39 | response = client.get(path) 40 | assert response.status_code == HTTPStatus.NOT_FOUND 41 | assert response.json()["errors"][0]["error_key"] == "user_not_found" 42 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=redefined-outer-name 2 | import pytest 3 | from fastapi import FastAPI 4 | from starlette.testclient import TestClient 5 | 6 | from service.api.app import create_app 7 | from service.settings import ServiceConfig, get_config 8 | 9 | 10 | @pytest.fixture 11 | def service_config() -> ServiceConfig: 12 | return get_config() 13 | 14 | 15 | @pytest.fixture 16 | def app( 17 | service_config: ServiceConfig, 18 | ) -> FastAPI: 19 | app = create_app(service_config) 20 | return app 21 | 22 | 23 | @pytest.fixture 24 | def client(app: FastAPI) -> TestClient: 25 | return TestClient(app=app) 26 | --------------------------------------------------------------------------------