├── .dockerignore ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .idea ├── .gitignore ├── apyr.iml ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── pylint.xml ├── vcs.xml └── webResources.xml ├── .pydocstyle ├── .pylintrc ├── Dockerfile ├── LICENSE ├── README.md ├── apyr ├── __init__.py ├── dependencies.py ├── exceptions.py ├── function_handler.py ├── functions.py ├── main.py ├── models.py ├── routers │ ├── apyr_endpoints.py │ └── endpoints.py └── utils.py ├── assets └── help.html ├── docker-compose.yaml ├── endpoints.yaml ├── poetry.lock ├── poetry.toml ├── pyproject.toml └── tests ├── __init__.py ├── data ├── string1.txt └── string2.txt ├── integration ├── __init__.py ├── conftest.py └── test_endpoints.py └── unit ├── __init__.py ├── test_functions.py ├── test_utils.py └── test_validators.py /.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | *.pyo 4 | *.pyd 5 | .Python 6 | env 7 | pip-log.txt 8 | pip-delete-this-directory.txt 9 | .tox 10 | .coverage 11 | .coverage.* 12 | .cache 13 | nosetests.xml 14 | coverage.xml 15 | *,cover 16 | *.log 17 | .git -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | #---------------------------------------------- 13 | # check-out repo and set-up python 14 | #---------------------------------------------- 15 | - name: Check out repository 16 | uses: actions/checkout@v2 17 | - name: Set up python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: 3.7 21 | #---------------------------------------------- 22 | # ----- install & configure poetry ----- 23 | #---------------------------------------------- 24 | - name: Install Poetry 25 | uses: snok/install-poetry@v1.1.1 26 | with: 27 | virtualenvs-create: true 28 | virtualenvs-in-project: true 29 | #---------------------------------------------- 30 | # load cached venv if cache exists 31 | #---------------------------------------------- 32 | - name: Load cached venv 33 | id: cached-poetry-dependencies 34 | uses: actions/cache@v2 35 | with: 36 | path: .venv 37 | key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} 38 | #---------------------------------------------- 39 | # install dependencies if cache does not exist 40 | #---------------------------------------------- 41 | - name: Install dependencies 42 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 43 | run: poetry install --no-interaction --no-root 44 | #---------------------------------------------- 45 | # install your root project, if required 46 | #---------------------------------------------- 47 | - name: Install library 48 | run: poetry install --no-interaction 49 | #---------------------------------------------- 50 | # run test suite 51 | #---------------------------------------------- 52 | - name: Run tests 53 | run: | 54 | source .venv/bin/activate 55 | pytest tests/ 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/pycharm,vscode,python,macos 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=pycharm,vscode,python,macos 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | ### PyCharm ### 35 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 36 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 37 | 38 | # User-specific stuff 39 | .idea/**/workspace.xml 40 | .idea/**/tasks.xml 41 | .idea/**/usage.statistics.xml 42 | .idea/**/dictionaries 43 | .idea/**/shelf 44 | 45 | # Generated files 46 | .idea/**/contentModel.xml 47 | 48 | # Sensitive or high-churn files 49 | .idea/**/dataSources/ 50 | .idea/**/dataSources.ids 51 | .idea/**/dataSources.local.xml 52 | .idea/**/sqlDataSources.xml 53 | .idea/**/dynamic.xml 54 | .idea/**/uiDesigner.xml 55 | .idea/**/dbnavigator.xml 56 | 57 | # Gradle 58 | .idea/**/gradle.xml 59 | .idea/**/libraries 60 | 61 | # Gradle and Maven with auto-import 62 | # When using Gradle or Maven with auto-import, you should exclude module files, 63 | # since they will be recreated, and may cause churn. Uncomment if using 64 | # auto-import. 65 | # .idea/artifacts 66 | # .idea/compiler.xml 67 | # .idea/jarRepositories.xml 68 | # .idea/modules.xml 69 | # .idea/*.iml 70 | # .idea/modules 71 | # *.iml 72 | # *.ipr 73 | 74 | # CMake 75 | cmake-build-*/ 76 | 77 | # Mongo Explorer plugin 78 | .idea/**/mongoSettings.xml 79 | 80 | # File-based project format 81 | *.iws 82 | 83 | # IntelliJ 84 | out/ 85 | 86 | # mpeltonen/sbt-idea plugin 87 | .idea_modules/ 88 | 89 | # JIRA plugin 90 | atlassian-ide-plugin.xml 91 | 92 | # Cursive Clojure plugin 93 | .idea/replstate.xml 94 | 95 | # Crashlytics plugin (for Android Studio and IntelliJ) 96 | com_crashlytics_export_strings.xml 97 | crashlytics.properties 98 | crashlytics-build.properties 99 | fabric.properties 100 | 101 | # Editor-based Rest Client 102 | .idea/httpRequests 103 | 104 | # Android studio 3.1+ serialized cache file 105 | .idea/caches/build_file_checksums.ser 106 | 107 | ### PyCharm Patch ### 108 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 109 | 110 | # *.iml 111 | # modules.xml 112 | # .idea/misc.xml 113 | # *.ipr 114 | 115 | # Sonarlint plugin 116 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 117 | .idea/**/sonarlint/ 118 | 119 | # SonarQube Plugin 120 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 121 | .idea/**/sonarIssues.xml 122 | 123 | # Markdown Navigator plugin 124 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 125 | .idea/**/markdown-navigator.xml 126 | .idea/**/markdown-navigator-enh.xml 127 | .idea/**/markdown-navigator/ 128 | 129 | # Cache file creation bug 130 | # See https://youtrack.jetbrains.com/issue/JBR-2257 131 | .idea/$CACHE_FILE$ 132 | 133 | # CodeStream plugin 134 | # https://plugins.jetbrains.com/plugin/12206-codestream 135 | .idea/codestream.xml 136 | 137 | ### Python ### 138 | # Byte-compiled / optimized / DLL files 139 | __pycache__/ 140 | *.py[cod] 141 | *$py.class 142 | 143 | # C extensions 144 | *.so 145 | 146 | # Distribution / packaging 147 | .Python 148 | build/ 149 | develop-eggs/ 150 | dist/ 151 | downloads/ 152 | eggs/ 153 | .eggs/ 154 | lib/ 155 | lib64/ 156 | parts/ 157 | sdist/ 158 | var/ 159 | wheels/ 160 | pip-wheel-metadata/ 161 | share/python-wheels/ 162 | *.egg-info/ 163 | .installed.cfg 164 | *.egg 165 | MANIFEST 166 | 167 | # PyInstaller 168 | # Usually these files are written by a python script from a template 169 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 170 | *.manifest 171 | *.spec 172 | 173 | # Installer logs 174 | pip-log.txt 175 | pip-delete-this-directory.txt 176 | 177 | # Unit test / coverage reports 178 | htmlcov/ 179 | .tox/ 180 | .nox/ 181 | .coverage 182 | .coverage.* 183 | .cache 184 | nosetests.xml 185 | coverage.xml 186 | *.cover 187 | *.py,cover 188 | .hypothesis/ 189 | .pytest_cache/ 190 | pytestdebug.log 191 | 192 | # Translations 193 | *.mo 194 | *.pot 195 | 196 | # Django stuff: 197 | *.log 198 | local_settings.py 199 | db.sqlite3 200 | db.sqlite3-journal 201 | 202 | # Flask stuff: 203 | instance/ 204 | .webassets-cache 205 | 206 | # Scrapy stuff: 207 | .scrapy 208 | 209 | # Sphinx documentation 210 | docs/_build/ 211 | doc/_build/ 212 | 213 | # PyBuilder 214 | target/ 215 | 216 | # Jupyter Notebook 217 | .ipynb_checkpoints 218 | 219 | # IPython 220 | profile_default/ 221 | ipython_config.py 222 | 223 | # pyenv 224 | .python-version 225 | 226 | # pipenv 227 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 228 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 229 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 230 | # install all needed dependencies. 231 | #Pipfile.lock 232 | 233 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 234 | __pypackages__/ 235 | 236 | # Celery stuff 237 | celerybeat-schedule 238 | celerybeat.pid 239 | 240 | # SageMath parsed files 241 | *.sage.py 242 | 243 | # Environments 244 | .env 245 | .venv 246 | env/ 247 | venv/ 248 | ENV/ 249 | env.bak/ 250 | venv.bak/ 251 | pythonenv* 252 | 253 | # Spyder project settings 254 | .spyderproject 255 | .spyproject 256 | 257 | # Rope project settings 258 | .ropeproject 259 | 260 | # mkdocs documentation 261 | /site 262 | 263 | # mypy 264 | .mypy_cache/ 265 | .dmypy.json 266 | dmypy.json 267 | 268 | # Pyre type checker 269 | .pyre/ 270 | 271 | # pytype static type analyzer 272 | .pytype/ 273 | 274 | # profiling data 275 | .prof 276 | 277 | ### vscode ### 278 | .vscode/* 279 | !.vscode/settings.json 280 | !.vscode/tasks.json 281 | !.vscode/launch.json 282 | !.vscode/extensions.json 283 | *.code-workspace 284 | 285 | # End of https://www.toptal.com/developers/gitignore/api/pycharm,vscode,python,macos -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /.idea/apyr.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 19 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/pylint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/webResources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.pydocstyle: -------------------------------------------------------------------------------- 1 | [pydocstyle] 2 | inherit = false 3 | ignore = D100,D101,D102,D103,D104,D107,D213 4 | match = .*\.py -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-whitelist= 7 | 8 | # Specify a score threshold to be exceeded before program exits with error. 9 | fail-under=10.0 10 | 11 | # Add files or directories to the blacklist. They should be base names, not 12 | # paths. 13 | ignore=CVS 14 | 15 | # Add files or directories matching the regex patterns to the blacklist. The 16 | # regex matches against base names, not paths. 17 | ignore-patterns=tests 18 | 19 | # Python code to execute, usually for sys.path manipulation such as 20 | # pygtk.require(). 21 | #init-hook= 22 | 23 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 24 | # number of processors available to use. 25 | jobs=1 26 | 27 | # Control the amount of potential inferred values when inferring a single 28 | # object. This can help the performance when dealing with large functions or 29 | # complex, nested conditions. 30 | limit-inference-results=100 31 | 32 | # List of plugins (as comma separated values of python module names) to load, 33 | # usually to register additional checkers. 34 | load-plugins= 35 | 36 | # Pickle collected data for later comparisons. 37 | persistent=yes 38 | 39 | # When enabled, pylint would attempt to guess common misconfiguration and emit 40 | # user-friendly hints instead of false-positive error messages. 41 | suggestion-mode=yes 42 | 43 | # Allow loading of arbitrary C extensions. Extensions are imported into the 44 | # 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. Leave empty to show 51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 52 | confidence= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once). You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use "--disable=all --enable=classes 62 | # --disable=W". 63 | disable=print-statement, 64 | parameter-unpacking, 65 | unpacking-in-except, 66 | old-raise-syntax, 67 | backtick, 68 | long-suffix, 69 | old-ne-operator, 70 | old-octal-literal, 71 | import-star-module-level, 72 | non-ascii-bytes-literal, 73 | raw-checker-failed, 74 | bad-inline-option, 75 | locally-disabled, 76 | file-ignored, 77 | suppressed-message, 78 | useless-suppression, 79 | deprecated-pragma, 80 | use-symbolic-message-instead, 81 | apply-builtin, 82 | basestring-builtin, 83 | buffer-builtin, 84 | cmp-builtin, 85 | coerce-builtin, 86 | execfile-builtin, 87 | file-builtin, 88 | long-builtin, 89 | raw_input-builtin, 90 | reduce-builtin, 91 | standarderror-builtin, 92 | unicode-builtin, 93 | xrange-builtin, 94 | coerce-method, 95 | delslice-method, 96 | getslice-method, 97 | setslice-method, 98 | no-absolute-import, 99 | old-division, 100 | dict-iter-method, 101 | dict-view-method, 102 | next-method-called, 103 | metaclass-assignment, 104 | indexing-exception, 105 | raising-string, 106 | reload-builtin, 107 | oct-method, 108 | hex-method, 109 | nonzero-method, 110 | cmp-method, 111 | input-builtin, 112 | round-builtin, 113 | intern-builtin, 114 | unichr-builtin, 115 | map-builtin-not-iterating, 116 | zip-builtin-not-iterating, 117 | range-builtin-not-iterating, 118 | filter-builtin-not-iterating, 119 | using-cmp-argument, 120 | eq-without-hash, 121 | div-method, 122 | idiv-method, 123 | rdiv-method, 124 | exception-message-attribute, 125 | invalid-str-codec, 126 | sys-max-int, 127 | bad-python3-import, 128 | deprecated-string-function, 129 | deprecated-str-translate-call, 130 | deprecated-itertools-function, 131 | deprecated-types-field, 132 | next-method-defined, 133 | dict-items-not-iterating, 134 | dict-keys-not-iterating, 135 | dict-values-not-iterating, 136 | deprecated-operator-function, 137 | deprecated-urllib-function, 138 | xreadlines-attribute, 139 | deprecated-sys-function, 140 | exception-escape, 141 | comprehension-escape, 142 | missing-module-docstring, 143 | missing-function-docstring, 144 | missing-class-docstring, 145 | too-few-public-methods 146 | 147 | # Enable the message, report, category or checker with the given id(s). You can 148 | # either give multiple identifier separated by comma (,) or put this option 149 | # multiple time (only on the command line, not in the configuration file where 150 | # it should appear only once). See also the "--disable" option for examples. 151 | enable=c-extension-no-member 152 | 153 | 154 | [REPORTS] 155 | 156 | # Python expression which should return a score less than or equal to 10. You 157 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 158 | # which contain the number of messages in each category, as well as 'statement' 159 | # which is the total number of statements analyzed. This score is used by the 160 | # global evaluation report (RP0004). 161 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 162 | 163 | # Template used to display messages. This is a python new-style format string 164 | # used to format the message information. See doc for all details. 165 | #msg-template= 166 | 167 | # Set the output format. Available formats are text, parseable, colorized, json 168 | # and msvs (visual studio). You can also give a reporter class, e.g. 169 | # mypackage.mymodule.MyReporterClass. 170 | output-format=text 171 | 172 | # Tells whether to display a full report or only the messages. 173 | reports=no 174 | 175 | # Activate the evaluation score. 176 | score=yes 177 | 178 | 179 | [REFACTORING] 180 | 181 | # Maximum number of nested blocks for function / method body 182 | max-nested-blocks=5 183 | 184 | # Complete name of functions that never returns. When checking for 185 | # inconsistent-return-statements if a never returning function is called then 186 | # it will be considered as an explicit return statement and no message will be 187 | # printed. 188 | never-returning-functions=sys.exit 189 | 190 | 191 | [LOGGING] 192 | 193 | # The type of string formatting that logging methods do. `old` means using % 194 | # formatting, `new` is for `{}` formatting. 195 | logging-format-style=old 196 | 197 | # Logging modules to check that the string format arguments are in logging 198 | # function parameter format. 199 | logging-modules=logging 200 | 201 | 202 | [SPELLING] 203 | 204 | # Limits count of emitted suggestions for spelling mistakes. 205 | max-spelling-suggestions=4 206 | 207 | # Spelling dictionary name. Available dictionaries: none. To make it work, 208 | # install the python-enchant package. 209 | spelling-dict= 210 | 211 | # List of comma separated words that should not be checked. 212 | spelling-ignore-words= 213 | 214 | # A path to a file that contains the private dictionary; one word per line. 215 | spelling-private-dict-file= 216 | 217 | # Tells whether to store unknown words to the private dictionary (see the 218 | # --spelling-private-dict-file option) instead of raising a message. 219 | spelling-store-unknown-words=no 220 | 221 | 222 | [MISCELLANEOUS] 223 | 224 | # List of note tags to take in consideration, separated by a comma. 225 | notes=FIXME, 226 | XXX, 227 | TODO 228 | 229 | # Regular expression of note tags to take in consideration. 230 | #notes-rgx= 231 | 232 | 233 | [TYPECHECK] 234 | 235 | # List of decorators that produce context managers, such as 236 | # contextlib.contextmanager. Add to this list to register other decorators that 237 | # produce valid context managers. 238 | contextmanager-decorators=contextlib.contextmanager 239 | 240 | # List of members which are set dynamically and missed by pylint inference 241 | # system, and so shouldn't trigger E1101 when accessed. Python regular 242 | # expressions are accepted. 243 | generated-members= 244 | 245 | # Tells whether missing members accessed in mixin class should be ignored. A 246 | # mixin class is detected if its name ends with "mixin" (case insensitive). 247 | ignore-mixin-members=yes 248 | 249 | # Tells whether to warn about missing members when the owner of the attribute 250 | # is inferred to be None. 251 | ignore-none=yes 252 | 253 | # This flag controls whether pylint should warn about no-member and similar 254 | # checks whenever an opaque object is returned when inferring. The inference 255 | # can return multiple potential results while evaluating a Python object, but 256 | # some branches might not be evaluated, which results in partial inference. In 257 | # that case, it might be useful to still emit no-member and other checks for 258 | # the rest of the inferred objects. 259 | ignore-on-opaque-inference=yes 260 | 261 | # List of class names for which member attributes should not be checked (useful 262 | # for classes with dynamically set attributes). This supports the use of 263 | # qualified names. 264 | ignored-classes=optparse.Values,thread._local,_thread._local 265 | 266 | # List of module names for which member attributes should not be checked 267 | # (useful for modules/projects where namespaces are manipulated during runtime 268 | # and thus existing member attributes cannot be deduced by static analysis). It 269 | # supports qualified module names, as well as Unix pattern matching. 270 | ignored-modules= 271 | 272 | # Show a hint with possible names when a member name was not found. The aspect 273 | # of finding the hint is based on edit distance. 274 | missing-member-hint=yes 275 | 276 | # The minimum edit distance a name should have in order to be considered a 277 | # similar match for a missing member name. 278 | missing-member-hint-distance=1 279 | 280 | # The total number of similar names that should be taken in consideration when 281 | # showing a hint for a missing member. 282 | missing-member-max-choices=1 283 | 284 | # List of decorators that change the signature of a decorated function. 285 | signature-mutators= 286 | 287 | 288 | [VARIABLES] 289 | 290 | # List of additional names supposed to be defined in builtins. Remember that 291 | # you should avoid defining new builtins when possible. 292 | additional-builtins= 293 | 294 | # Tells whether unused global variables should be treated as a violation. 295 | allow-global-unused-variables=yes 296 | 297 | # List of strings which can identify a callback function by name. A callback 298 | # name must start or end with one of those strings. 299 | callbacks=cb_, 300 | _cb 301 | 302 | # A regular expression matching the name of dummy variables (i.e. expected to 303 | # not be used). 304 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 305 | 306 | # Argument names that match this expression will be ignored. Default to name 307 | # with leading underscore. 308 | ignored-argument-names=_.*|^ignored_|^unused_ 309 | 310 | # Tells whether we should check for unused import in __init__ files. 311 | init-import=no 312 | 313 | # List of qualified module names which can have objects that can redefine 314 | # builtins. 315 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 316 | 317 | 318 | [FORMAT] 319 | 320 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 321 | expected-line-ending-format= 322 | 323 | # Regexp for a line that is allowed to be longer than the limit. 324 | ignore-long-lines=^\s*(# )??$ 325 | 326 | # Number of spaces of indent required inside a hanging or continued line. 327 | indent-after-paren=4 328 | 329 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 330 | # tab). 331 | indent-string=' ' 332 | 333 | # Maximum number of characters on a single line. 334 | max-line-length=100 335 | 336 | # Maximum number of lines in a module. 337 | max-module-lines=1000 338 | 339 | # Allow the body of a class to be on the same line as the declaration if body 340 | # contains single statement. 341 | single-line-class-stmt=no 342 | 343 | # Allow the body of an if to be on the same line as the test if there is no 344 | # else. 345 | single-line-if-stmt=no 346 | 347 | 348 | [SIMILARITIES] 349 | 350 | # Ignore comments when computing similarities. 351 | ignore-comments=yes 352 | 353 | # Ignore docstrings when computing similarities. 354 | ignore-docstrings=yes 355 | 356 | # Ignore imports when computing similarities. 357 | ignore-imports=no 358 | 359 | # Minimum lines number of a similarity. 360 | min-similarity-lines=4 361 | 362 | 363 | [BASIC] 364 | 365 | # Naming style matching correct argument names. 366 | argument-naming-style=snake_case 367 | 368 | # Regular expression matching correct argument names. Overrides argument- 369 | # naming-style. 370 | #argument-rgx= 371 | 372 | # Naming style matching correct attribute names. 373 | attr-naming-style=snake_case 374 | 375 | # Regular expression matching correct attribute names. Overrides attr-naming- 376 | # style. 377 | #attr-rgx= 378 | 379 | # Bad variable names which should always be refused, separated by a comma. 380 | bad-names=foo, 381 | bar, 382 | baz, 383 | toto, 384 | tutu, 385 | tata 386 | 387 | # Bad variable names regexes, separated by a comma. If names match any regex, 388 | # they will always be refused 389 | bad-names-rgxs= 390 | 391 | # Naming style matching correct class attribute names. 392 | class-attribute-naming-style=any 393 | 394 | # Regular expression matching correct class attribute names. Overrides class- 395 | # attribute-naming-style. 396 | #class-attribute-rgx= 397 | 398 | # Naming style matching correct class names. 399 | class-naming-style=PascalCase 400 | 401 | # Regular expression matching correct class names. Overrides class-naming- 402 | # style. 403 | #class-rgx= 404 | 405 | # Naming style matching correct constant names. 406 | const-naming-style=UPPER_CASE 407 | 408 | # Regular expression matching correct constant names. Overrides const-naming- 409 | # style. 410 | #const-rgx= 411 | 412 | # Minimum line length for functions/classes that require docstrings, shorter 413 | # ones are exempt. 414 | docstring-min-length=-1 415 | 416 | # Naming style matching correct function names. 417 | function-naming-style=snake_case 418 | 419 | # Regular expression matching correct function names. Overrides function- 420 | # naming-style. 421 | #function-rgx= 422 | 423 | # Good variable names which should always be accepted, separated by a comma. 424 | good-names=i, 425 | j, 426 | k, 427 | ex, 428 | Run, 429 | _ 430 | 431 | # Good variable names regexes, separated by a comma. If names match any regex, 432 | # they will always be accepted 433 | good-names-rgxs= 434 | 435 | # Include a hint for the correct naming format with invalid-name. 436 | include-naming-hint=no 437 | 438 | # Naming style matching correct inline iteration names. 439 | inlinevar-naming-style=any 440 | 441 | # Regular expression matching correct inline iteration names. Overrides 442 | # inlinevar-naming-style. 443 | #inlinevar-rgx= 444 | 445 | # Naming style matching correct method names. 446 | method-naming-style=snake_case 447 | 448 | # Regular expression matching correct method names. Overrides method-naming- 449 | # style. 450 | #method-rgx= 451 | 452 | # Naming style matching correct module names. 453 | module-naming-style=snake_case 454 | 455 | # Regular expression matching correct module names. Overrides module-naming- 456 | # style. 457 | #module-rgx= 458 | 459 | # Colon-delimited sets of names that determine each other's naming style when 460 | # the name regexes allow several styles. 461 | name-group= 462 | 463 | # Regular expression which should only match function or class names that do 464 | # not require a docstring. 465 | no-docstring-rgx=^_ 466 | 467 | # List of decorators that produce properties, such as abc.abstractproperty. Add 468 | # to this list to register other decorators that produce valid properties. 469 | # These decorators are taken in consideration only for invalid-name. 470 | property-classes=abc.abstractproperty 471 | 472 | # Naming style matching correct variable names. 473 | variable-naming-style=snake_case 474 | 475 | # Regular expression matching correct variable names. Overrides variable- 476 | # naming-style. 477 | #variable-rgx= 478 | 479 | 480 | [STRING] 481 | 482 | # This flag controls whether inconsistent-quotes generates a warning when the 483 | # character used as a quote delimiter is used inconsistently within a module. 484 | check-quote-consistency=no 485 | 486 | # This flag controls whether the implicit-str-concat should generate a warning 487 | # on implicit string concatenation in sequences defined over several lines. 488 | check-str-concat-over-line-jumps=no 489 | 490 | 491 | [IMPORTS] 492 | 493 | # List of modules that can be imported at any level, not just the top level 494 | # one. 495 | allow-any-import-level= 496 | 497 | # Allow wildcard imports from modules that define __all__. 498 | allow-wildcard-with-all=no 499 | 500 | # Analyse import fallback blocks. This can be used to support both Python 2 and 501 | # 3 compatible code, which means that the block might have code that exists 502 | # only in one or another interpreter, leading to false positives when analysed. 503 | analyse-fallback-blocks=no 504 | 505 | # Deprecated modules which should not be used, separated by a comma. 506 | deprecated-modules=optparse,tkinter.tix 507 | 508 | # Create a graph of external dependencies in the given file (report RP0402 must 509 | # not be disabled). 510 | ext-import-graph= 511 | 512 | # Create a graph of every (i.e. internal and external) dependencies in the 513 | # given file (report RP0402 must not be disabled). 514 | import-graph= 515 | 516 | # Create a graph of internal dependencies in the given file (report RP0402 must 517 | # not be disabled). 518 | int-import-graph= 519 | 520 | # Force import order to recognize a module as part of the standard 521 | # compatibility libraries. 522 | known-standard-library= 523 | 524 | # Force import order to recognize a module as part of a third party library. 525 | known-third-party=enchant 526 | 527 | # Couples of modules and preferred modules, separated by a comma. 528 | preferred-modules= 529 | 530 | 531 | [CLASSES] 532 | 533 | # List of method names used to declare (i.e. assign) instance attributes. 534 | defining-attr-methods=__init__, 535 | __new__, 536 | setUp, 537 | __post_init__ 538 | 539 | # List of member names, which should be excluded from the protected access 540 | # warning. 541 | exclude-protected=_asdict, 542 | _fields, 543 | _replace, 544 | _source, 545 | _make 546 | 547 | # List of valid names for the first argument in a class method. 548 | valid-classmethod-first-arg=cls 549 | 550 | # List of valid names for the first argument in a metaclass class method. 551 | valid-metaclass-classmethod-first-arg=cls 552 | 553 | 554 | [DESIGN] 555 | 556 | # Maximum number of arguments for function / method. 557 | max-args=5 558 | 559 | # Maximum number of attributes for a class (see R0902). 560 | max-attributes=7 561 | 562 | # Maximum number of boolean expressions in an if statement (see R0916). 563 | max-bool-expr=5 564 | 565 | # Maximum number of branch for function / method body. 566 | max-branches=12 567 | 568 | # Maximum number of locals for function / method body. 569 | max-locals=15 570 | 571 | # Maximum number of parents for a class (see R0901). 572 | max-parents=7 573 | 574 | # Maximum number of public methods for a class (see R0904). 575 | max-public-methods=20 576 | 577 | # Maximum number of return / yield for function / method body. 578 | max-returns=6 579 | 580 | # Maximum number of statements in function / method body. 581 | max-statements=50 582 | 583 | # Minimum number of public methods for a class (see R0903). 584 | min-public-methods=2 585 | 586 | 587 | [EXCEPTIONS] 588 | 589 | # Exceptions that will emit a warning when being caught. Defaults to 590 | # "BaseException, Exception". 591 | overgeneral-exceptions=BaseException, 592 | Exception 593 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7 2 | 3 | ENV PYTHONFAULTHANDLER=1 \ 4 | PYTHONUNBUFFERED=1 \ 5 | PYTHONHASHSEED=random \ 6 | PIP_NO_CACHE_DIR=off \ 7 | PIP_DISABLE_PIP_VERSION_CHECK=on \ 8 | PIP_DEFAULT_TIMEOUT=100 9 | 10 | RUN pip install poetry 11 | 12 | WORKDIR /apyr 13 | COPY poetry.lock pyproject.toml /apyr/ 14 | 15 | RUN poetry config virtualenvs.create false && poetry install --no-interaction --no-ansi 16 | 17 | COPY . /apyr 18 | 19 | CMD ["poetry", "run", "uvicorn", "--host", "0.0.0.0", "--port", "8000", "apyr.main:app"] 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Umut Seven 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # apyr ![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/umutseven92/apyr?label=version) ![tests](https://github.com/umutseven92/apyr/workflows/tests/badge.svg?branch=master) 2 | 3 | **apyr** (all lowercase) is a simple & easy to use mock API server. 4 | 5 | It's great for front-end development when your API is not ready, or when you are prototyping an API. It's also very 6 | useful for demos & hackathons. 7 | 8 | ## Installation 9 | 10 | * Clone the project; 11 | 12 | ```bash 13 | git clone https://github.com/umutseven92/apyr.git 14 | ``` 15 | 16 | * Edit `endpoints.yaml` with your endpoints (details below). 17 | 18 | ### Via poetry 19 | 20 | * Install [poetry](https://python-poetry.org/docs/#installation). 21 | 22 | ```bash 23 | cd apyr 24 | poetry install # Install dependencies 25 | poetry run apyr # Run apyr 26 | ``` 27 | 28 | ### Via Docker 29 | 30 | ```bash 31 | cd apyr 32 | docker-compose up --build -d 33 | ``` 34 | 35 | ## Configuration 36 | 37 | Your endpoints are defined in `endpoints.yaml`. An example `endpoints.yaml` comes with the project; feel free to edit 38 | it. 39 | 40 | | Syntax | Required | Default | Description | 41 | | :--- | :---: | :--- | :-------- | 42 | | `method` | ✅ | | [HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) of the endpoint | | 43 | | `path` | ✅ | | Path to the endpoint, appended to the base URL | | 44 | | `status_code` | ✅ | | [Status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) of the response | 45 | | `media_type` | ❌ | `application/json` | [Mime Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#important_mime_types_for_web_developers) of the response | 46 | | `content` | ❌ | | Body of the response | 47 | | `content_path` | ❌ | | Path to the response body | 48 | 49 | Both `content` and `content_path` can't be set at the same time. 50 | 51 | ### Example endpoints.yaml 52 | 53 | ```yaml 54 | # A GET method that returns a list of employees. 55 | - method: GET 56 | path: test/employees 57 | status_code: 200 58 | content: > 59 | [ 60 | { "first_name": "Peter", "last_name": "Venkman" }, 61 | { "first_name": "Ray", "last_name": "Stantz" }, 62 | { "first_name": "Egon", "last_name": "Spengler" }, 63 | ] 64 | # A GET method that returns an employee. 65 | # Take note of the two %functions%- the employee's first name, last name and age will be random at every response. 66 | - method: GET 67 | path: test/employee/2 68 | status_code: 200 69 | content: > 70 | { 71 | "first_name": "%random_first_name(female)%", 72 | "last_name": "%random_last_name()%", 73 | "age": %random_int(20, 50)% 74 | } 75 | # A POST method that returns a 500. Great for testing error pages. 76 | - method: POST 77 | path: test/employee 78 | media_type: text 79 | status_code: 500 80 | content: An unexpected error occured while creating the employee. 81 | # A PUT method that returns a 201. Does not return a body- content is optional. 82 | - method: PUT 83 | path: test/employee/3 84 | status_code: 201 85 | # A GET method that returns an HTML page. 86 | - method: GET 87 | path: test/help 88 | status_code: 200 89 | media_type: text/html 90 | content: > 91 | 92 | 93 | 94 |

I've quit better jobs than this.

95 |

Ghostbusters, whaddya want.

96 | 97 | 98 | # The same method as above, but the content is referenced from another file. Path is relative to project root. 99 | - method: GET 100 | path: test/help2 101 | status_code: 200 102 | media_type: text/html 103 | content_path: assets/help.html 104 | ``` 105 | 106 | ### Example usage 107 | 108 | An example of making a `curl` request to our second endpoint defined above: 109 | 110 | ```bash 111 | ~ λ curl 0.0.0.0:8000/test/employee/2 -v 112 | > GET /test/employee/2 HTTP/1.1 113 | > 114 | < HTTP/1.1 200 OK 115 | < server: uvicorn 116 | < content-length: 52 117 | < content-type: application/json 118 | < 119 | { "first_name": "Geoffrey", "last_name": "Greeley", "age": 28 } 120 | ``` 121 | 122 | No need to restart **apyr** after editing `endpoints.yaml`- it's all taken care of! 123 | 124 | ## Functions 125 | 126 | **apyr** supports different kinds of functions inside the content parameter. 127 | 128 | Currently supported functions are: 129 | 130 | | Name | Parameters | Description | Examples | 131 | | :--- | :--- | :--- | :--- | 132 | | `%random_first_name(gender)%` | `gender`: Optional string. Can be `male` or `female`. If left empty, will default to both | Will be replaced by a random first name | `%random_first_name(male)%`, `%random_first_name(female)%`, `%random_first_name()%` 133 | | `%random_last_name()%` | | Will be replaced by a random last name | `%random_last_name()%` | 134 | | `%random_int(start, end)%` | `start`: Required int, `end`: Required int | Will be replaced by a random integer between `start` and `end` (both inclusive) | `%random_int(0, 20)%`, `%random_int(20, 50)%` | 135 | 136 | ## Contributing 137 | 138 | If you like this project, please consider [donating to the Electronic Frontier Foundation](https://supporters.eff.org/donate). -------------------------------------------------------------------------------- /apyr/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutseven92/apyr/5b73482db03b273443d0d67dcf03d50ab74f0205/apyr/__init__.py -------------------------------------------------------------------------------- /apyr/dependencies.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import List 3 | 4 | import yaml 5 | from fastapi import HTTPException 6 | from starlette.responses import Response 7 | 8 | from apyr.exceptions import ( 9 | TooManyEndpointsException, 10 | NoEndpointsException, 11 | FunctionException, 12 | ) 13 | from apyr.function_handler import FunctionHandler 14 | from apyr.functions import FUNCTIONS 15 | from apyr.models import Endpoint 16 | from apyr.utils import get_project_root, get_digest, load_file 17 | 18 | 19 | class EndpointsRepo: 20 | def __init__(self): 21 | self.last_hash: str = "" 22 | self.endpoints: List[Endpoint] = [] 23 | self.endpoints_path = get_project_root().joinpath("endpoints.yaml") 24 | 25 | def _load_endpoints(self): 26 | stream = open(self.endpoints_path, "r") 27 | endpoints = yaml.full_load(stream) 28 | 29 | self.endpoints = [Endpoint(**endpoint) for endpoint in endpoints] 30 | 31 | def _check_if_file_changed(self, path: Path) -> bool: 32 | """Check to see if the file changed. 33 | 34 | We hash the file and compare it to the previous hash. 35 | """ 36 | new_hash = get_digest(str(path)) 37 | if new_hash != self.last_hash: 38 | self.last_hash = new_hash 39 | return True 40 | 41 | return False 42 | 43 | def get_response(self, path: str, method: str) -> Response: 44 | # Do not reload endpoints if the file has not changed 45 | if self._check_if_file_changed(self.endpoints_path): 46 | self._load_endpoints() 47 | 48 | def _filter_endpoints(endpoint: Endpoint): 49 | return endpoint.path == path and endpoint.method.lower() == method.lower() 50 | 51 | filtered: List[Endpoint] = list(filter(_filter_endpoints, self.endpoints)) 52 | 53 | if len(filtered) > 1: 54 | raise TooManyEndpointsException() 55 | if len(filtered) == 0: 56 | raise NoEndpointsException() 57 | 58 | filtered_endpoint = filtered[0] 59 | 60 | if filtered_endpoint.content is None and filtered_endpoint.content_path is None: 61 | return Response( 62 | status_code=filtered_endpoint.status_code, 63 | ) 64 | 65 | content: str = "" 66 | if filtered_endpoint.content: 67 | content = filtered_endpoint.content 68 | elif filtered_endpoint.content_path: 69 | full_path = get_project_root().joinpath(filtered_endpoint.content_path) 70 | content = load_file(str(full_path)) 71 | 72 | try: 73 | body = FunctionHandler.run(content, FUNCTIONS) 74 | except FunctionException as ex: 75 | raise HTTPException( 76 | status_code=500, detail={"error": ex.error, "reason": ex.detail} 77 | ) from ex 78 | 79 | return Response( 80 | status_code=filtered_endpoint.status_code, 81 | media_type=filtered_endpoint.media_type, 82 | content=body, 83 | ) 84 | -------------------------------------------------------------------------------- /apyr/exceptions.py: -------------------------------------------------------------------------------- 1 | class EndpointException(Exception): 2 | pass 3 | 4 | 5 | class TooManyEndpointsException(EndpointException): 6 | def __init__(self): 7 | self.message = "There are too many endpoints matching the conditions." 8 | super().__init__(self.message) 9 | 10 | 11 | class NoEndpointsException(EndpointException): 12 | def __init__(self): 13 | self.message = "There are no endpoints matching the conditions." 14 | super().__init__(self.message) 15 | 16 | 17 | class FunctionException(Exception): 18 | def __init__(self, fun_name: str, detail: str): 19 | self.error = f"Error in function {fun_name}" 20 | self.detail = detail 21 | super().__init__(detail) 22 | -------------------------------------------------------------------------------- /apyr/function_handler.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import List, Tuple, Dict 3 | 4 | from apyr.exceptions import FunctionException 5 | from apyr.models import ContentFunction 6 | 7 | 8 | class FunctionHandler: 9 | @staticmethod 10 | def parse_content(content: str) -> List[ContentFunction]: 11 | regex = re.compile( 12 | r"(?P%(?P[^%]+?)\((?P.*?)\)%)", flags=re.M 13 | ) 14 | match: List[Tuple[str, str, str]] = regex.findall(content) 15 | 16 | functions: List[ContentFunction] = [] 17 | 18 | for capture in match: 19 | full, name, params = capture 20 | param_mapped = list( 21 | map(lambda x: x.strip(), params.split(",")) 22 | ) # Remove whitespace from params 23 | param_list = list( 24 | filter(lambda x: x != "", param_mapped) 25 | ) # Filter out empty params 26 | functions.append(ContentFunction(full=full, name=name, params=param_list)) 27 | 28 | return functions 29 | 30 | @staticmethod 31 | def execute_functions( 32 | content_functions: List[ContentFunction], functions: Dict, content: str 33 | ) -> str: 34 | for content_function in content_functions: 35 | fun = functions.get(content_function.name) 36 | 37 | if fun is None: 38 | print(f"Function {content_function.name} not found. Skipping..") 39 | continue 40 | 41 | try: 42 | result = fun(*content_function.params) 43 | except Exception as ex: 44 | raise FunctionException(content_function.full, str(ex)) from ex 45 | 46 | content = content.replace(content_function.full, str(result)) 47 | 48 | return content 49 | 50 | @staticmethod 51 | def run(content: str, functions: Dict) -> str: 52 | content_functions = FunctionHandler.parse_content(content) 53 | return FunctionHandler.execute_functions(content_functions, functions, content) 54 | -------------------------------------------------------------------------------- /apyr/functions.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import Optional 3 | 4 | import names 5 | 6 | 7 | def random_first_name(gender: Optional[str]) -> str: 8 | return names.get_first_name(gender) 9 | 10 | 11 | def random_last_name() -> str: 12 | return names.get_last_name() 13 | 14 | 15 | def random_int(start: str, end: str) -> int: 16 | return random.randint(int(start), int(end)) 17 | 18 | 19 | FUNCTIONS = { 20 | "random_first_name": random_first_name, 21 | "random_last_name": random_last_name, 22 | "random_int": random_int, 23 | } 24 | -------------------------------------------------------------------------------- /apyr/main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | import uvicorn 4 | from fastapi import FastAPI 5 | 6 | from apyr.routers.apyr_endpoints import apyr_router 7 | from apyr.routers.endpoints import endpoint_router 8 | 9 | app = FastAPI() 10 | 11 | app.include_router(apyr_router) 12 | app.include_router(endpoint_router) 13 | 14 | 15 | def run(): 16 | uvicorn.run(app, host="0.0.0.0", port=8000) 17 | 18 | 19 | if __name__ == "__main__": 20 | run() 21 | -------------------------------------------------------------------------------- /apyr/models.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=no-name-in-module 2 | from enum import Enum 3 | from typing import Optional, List 4 | 5 | from pydantic import BaseModel, validator 6 | 7 | 8 | class Method(str, Enum): 9 | get = "GET" 10 | head = "HEAD" 11 | post = "POST" 12 | put = "PUT" 13 | delete = "DELETE" 14 | options = "OPTIONS" 15 | trace = "TRACE" 16 | patch = "PATCH" 17 | 18 | 19 | class Endpoint(BaseModel): 20 | method: Method 21 | path: str 22 | status_code: int 23 | media_type: str = "application/json" 24 | content: Optional[str] 25 | content_path: Optional[str] 26 | 27 | @validator("content_path", pre=True, always=True) 28 | def content_path_correct( 29 | cls, value, values 30 | ): # pylint:disable=no-self-argument, no-self-use 31 | if values["content"] is not None and value is not None: 32 | raise ValueError("Cannot set both content and content_path.") 33 | return value 34 | 35 | 36 | class ContentFunction(BaseModel): 37 | full: str # E.g. %random_first_name(female)% 38 | name: str # E.g. random_first_name 39 | params: List[str] # E.g. female 40 | -------------------------------------------------------------------------------- /apyr/routers/apyr_endpoints.py: -------------------------------------------------------------------------------- 1 | """ Endpoints used by apyr itself. 2 | 3 | Prefixed by `/apyr` so as to not block user defined endpoints from being mocked. 4 | """ 5 | from fastapi import APIRouter 6 | 7 | from starlette.responses import Response 8 | from starlette.status import HTTP_200_OK 9 | 10 | apyr_router = APIRouter(prefix="/apyr") 11 | 12 | 13 | @apyr_router.get("/status") 14 | async def status(): 15 | """Status check for the apyr service. Used mainly as a health check.""" 16 | return Response(status_code=HTTP_200_OK) 17 | -------------------------------------------------------------------------------- /apyr/routers/endpoints.py: -------------------------------------------------------------------------------- 1 | """ Endpoints that are defined by user. """ 2 | 3 | from functools import lru_cache 4 | 5 | from fastapi import APIRouter, Depends, Request, HTTPException 6 | 7 | from apyr.dependencies import EndpointsRepo 8 | from apyr.exceptions import EndpointException 9 | 10 | endpoint_router = APIRouter() 11 | 12 | 13 | @lru_cache() 14 | def endpoints_dependency() -> EndpointsRepo: 15 | return EndpointsRepo() 16 | 17 | 18 | @endpoint_router.get("/{path:path}") 19 | @endpoint_router.head("/{path:path}") 20 | @endpoint_router.post("/{path:path}") 21 | @endpoint_router.put("/{path:path}") 22 | @endpoint_router.delete("/{path:path}") 23 | @endpoint_router.options("/{path:path}") 24 | @endpoint_router.trace("/{path:path}") 25 | @endpoint_router.patch("/{path:path}") 26 | async def all_endpoints( 27 | path: str, request: Request, repo: EndpointsRepo = Depends(endpoints_dependency) 28 | ): 29 | try: 30 | response = repo.get_response(path, request.method) 31 | except EndpointException as ex: 32 | raise HTTPException(status_code=404, detail=str(ex)) from ex 33 | 34 | return response 35 | -------------------------------------------------------------------------------- /apyr/utils.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from pathlib import Path 3 | 4 | 5 | def get_project_root() -> Path: 6 | return Path(__file__).parent.parent 7 | 8 | 9 | def get_digest(file_path: str) -> str: 10 | hash_256 = hashlib.sha256() 11 | 12 | with open(file_path, "rb") as file: 13 | while True: 14 | chunk = file.read(hash_256.block_size) 15 | if not chunk: 16 | break 17 | hash_256.update(chunk) 18 | 19 | return hash_256.hexdigest() 20 | 21 | 22 | def load_file(file_path: str) -> str: 23 | with open(file_path, "r") as file: 24 | return file.read() 25 | -------------------------------------------------------------------------------- /assets/help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

I've quit better jobs than this.

5 |

Ghostbusters, whaddya want.

6 | 7 | 8 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | apyr: 4 | build: . 5 | ports: 6 | - "8000:8000" 7 | volumes: 8 | - "./endpoints.yaml:/apyr/endpoints.yaml" 9 | -------------------------------------------------------------------------------- /endpoints.yaml: -------------------------------------------------------------------------------- 1 | # A GET method that returns a list of employees. 2 | - method: GET 3 | path: test/employees 4 | status_code: 200 5 | content: > 6 | [ 7 | { "first_name": "Peter", "last_name": "Venkman" }, 8 | { "first_name": "Ray", "last_name": "Stantz" }, 9 | { "first_name": "Egon", "last_name": "Spengler" } 10 | ] 11 | # A GET method that returns an employee. 12 | # Take note of the two %functions%- the employee's first name, last name and age will be random at every response. 13 | - method: GET 14 | path: test/employee/2 15 | status_code: 200 16 | content: > 17 | { 18 | "first_name": "%random_first_name(female)%", 19 | "last_name": "%random_last_name()%", 20 | "age": %random_int(20, 50)% 21 | } 22 | # A POST method that returns a 500. Great for testing error pages. 23 | - method: POST 24 | path: test/employee 25 | media_type: text 26 | status_code: 500 27 | content: An unexpected error occurred while creating the employee. 28 | # A PUT method that returns a 201. Does not return a body- content is optional. 29 | - method: PUT 30 | path: test/employee/3 31 | status_code: 201 32 | # A GET method that returns an HTML page. 33 | - method: GET 34 | path: test/help 35 | status_code: 200 36 | media_type: text/html 37 | content: |- 38 | 39 | 40 | 41 |

I've quit better jobs than this.

42 |

Ghostbusters, whaddya want.

43 | 44 | 45 | # The same method as above, but the content is referenced from another file. Path is relative to project root. 46 | - method: GET 47 | path: test/help2 48 | status_code: 200 49 | media_type: text/html 50 | content_path: assets/help.html 51 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "appdirs" 3 | version = "1.4.4" 4 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "astroid" 11 | version = "2.4.2" 12 | description = "An abstract syntax tree for Python with inference support." 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=3.5" 16 | 17 | [package.dependencies] 18 | lazy-object-proxy = ">=1.4.0,<1.5.0" 19 | six = ">=1.12,<2.0" 20 | typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} 21 | wrapt = ">=1.11,<2.0" 22 | 23 | [[package]] 24 | name = "atomicwrites" 25 | version = "1.4.0" 26 | description = "Atomic file writes." 27 | category = "dev" 28 | optional = false 29 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 30 | 31 | [[package]] 32 | name = "attrs" 33 | version = "20.3.0" 34 | description = "Classes Without Boilerplate" 35 | category = "dev" 36 | optional = false 37 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 38 | 39 | [package.extras] 40 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] 41 | docs = ["furo", "sphinx", "zope.interface"] 42 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 43 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] 44 | 45 | [[package]] 46 | name = "bcrypt" 47 | version = "3.2.0" 48 | description = "Modern password hashing for your software and your servers" 49 | category = "dev" 50 | optional = false 51 | python-versions = ">=3.6" 52 | 53 | [package.dependencies] 54 | cffi = ">=1.1" 55 | six = ">=1.4.1" 56 | 57 | [package.extras] 58 | tests = ["pytest (>=3.2.1,!=3.3.0)"] 59 | typecheck = ["mypy"] 60 | 61 | [[package]] 62 | name = "black" 63 | version = "20.8b1" 64 | description = "The uncompromising code formatter." 65 | category = "dev" 66 | optional = false 67 | python-versions = ">=3.6" 68 | 69 | [package.dependencies] 70 | appdirs = "*" 71 | click = ">=7.1.2" 72 | mypy-extensions = ">=0.4.3" 73 | pathspec = ">=0.6,<1" 74 | regex = ">=2020.1.8" 75 | toml = ">=0.10.1" 76 | typed-ast = ">=1.4.0" 77 | typing-extensions = ">=3.7.4" 78 | 79 | [package.extras] 80 | colorama = ["colorama (>=0.4.3)"] 81 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 82 | 83 | [[package]] 84 | name = "cached-property" 85 | version = "1.5.2" 86 | description = "A decorator for caching properties in classes." 87 | category = "dev" 88 | optional = false 89 | python-versions = "*" 90 | 91 | [[package]] 92 | name = "certifi" 93 | version = "2020.12.5" 94 | description = "Python package for providing Mozilla's CA Bundle." 95 | category = "dev" 96 | optional = false 97 | python-versions = "*" 98 | 99 | [[package]] 100 | name = "cffi" 101 | version = "1.14.5" 102 | description = "Foreign Function Interface for Python calling C code." 103 | category = "dev" 104 | optional = false 105 | python-versions = "*" 106 | 107 | [package.dependencies] 108 | pycparser = "*" 109 | 110 | [[package]] 111 | name = "chardet" 112 | version = "4.0.0" 113 | description = "Universal encoding detector for Python 2 and 3" 114 | category = "dev" 115 | optional = false 116 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 117 | 118 | [[package]] 119 | name = "click" 120 | version = "7.1.2" 121 | description = "Composable command line interface toolkit" 122 | category = "main" 123 | optional = false 124 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 125 | 126 | [[package]] 127 | name = "colorama" 128 | version = "0.4.4" 129 | description = "Cross-platform colored terminal text." 130 | category = "main" 131 | optional = false 132 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 133 | 134 | [[package]] 135 | name = "cryptography" 136 | version = "3.4.6" 137 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 138 | category = "dev" 139 | optional = false 140 | python-versions = ">=3.6" 141 | 142 | [package.dependencies] 143 | cffi = ">=1.12" 144 | 145 | [package.extras] 146 | docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] 147 | docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] 148 | pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] 149 | sdist = ["setuptools-rust (>=0.11.4)"] 150 | ssh = ["bcrypt (>=3.1.5)"] 151 | test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] 152 | 153 | [[package]] 154 | name = "distro" 155 | version = "1.5.0" 156 | description = "Distro - an OS platform information API" 157 | category = "dev" 158 | optional = false 159 | python-versions = "*" 160 | 161 | [[package]] 162 | name = "docker" 163 | version = "4.4.4" 164 | description = "A Python library for the Docker Engine API." 165 | category = "dev" 166 | optional = false 167 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 168 | 169 | [package.dependencies] 170 | paramiko = {version = ">=2.4.2", optional = true, markers = "extra == \"ssh\""} 171 | pywin32 = {version = "227", markers = "sys_platform == \"win32\""} 172 | requests = ">=2.14.2,<2.18.0 || >2.18.0" 173 | six = ">=1.4.0" 174 | websocket-client = ">=0.32.0" 175 | 176 | [package.extras] 177 | ssh = ["paramiko (>=2.4.2)"] 178 | tls = ["pyOpenSSL (>=17.5.0)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"] 179 | 180 | [[package]] 181 | name = "docker-compose" 182 | version = "1.28.5" 183 | description = "Multi-container orchestration for Docker" 184 | category = "dev" 185 | optional = false 186 | python-versions = ">=3.4" 187 | 188 | [package.dependencies] 189 | cached-property = ">=1.2.0,<2" 190 | colorama = {version = ">=0.4,<1", markers = "sys_platform == \"win32\""} 191 | distro = ">=1.5.0,<2" 192 | docker = {version = ">=4.4.4,<5", extras = ["ssh"]} 193 | dockerpty = ">=0.4.1,<1" 194 | docopt = ">=0.6.1,<1" 195 | jsonschema = ">=2.5.1,<4" 196 | python-dotenv = ">=0.13.0,<1" 197 | PyYAML = ">=3.10,<6" 198 | requests = ">=2.20.0,<3" 199 | texttable = ">=0.9.0,<2" 200 | websocket-client = ">=0.32.0,<1" 201 | 202 | [package.extras] 203 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2)"] 204 | tests = ["ddt (>=1.2.2,<2)", "pytest (<6)"] 205 | 206 | [[package]] 207 | name = "dockerpty" 208 | version = "0.4.1" 209 | description = "Python library to use the pseudo-tty of a docker container" 210 | category = "dev" 211 | optional = false 212 | python-versions = "*" 213 | 214 | [package.dependencies] 215 | six = ">=1.3.0" 216 | 217 | [[package]] 218 | name = "docopt" 219 | version = "0.6.2" 220 | description = "Pythonic argument parser, that will make you smile" 221 | category = "dev" 222 | optional = false 223 | python-versions = "*" 224 | 225 | [[package]] 226 | name = "fastapi" 227 | version = "0.63.0" 228 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 229 | category = "main" 230 | optional = false 231 | python-versions = ">=3.6" 232 | 233 | [package.dependencies] 234 | pydantic = ">=1.0.0,<2.0.0" 235 | starlette = "0.13.6" 236 | 237 | [package.extras] 238 | all = ["requests (>=2.24.0,<3.0.0)", "aiofiles (>=0.5.0,<0.6.0)", "jinja2 (>=2.11.2,<3.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<2.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "graphene (>=2.1.8,<3.0.0)", "ujson (>=3.0.0,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.14.0)", "async_exit_stack (>=1.0.1,<2.0.0)", "async_generator (>=1.10,<2.0.0)"] 239 | dev = ["python-jose[cryptography] (>=3.1.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.14.0)", "graphene (>=2.1.8,<3.0.0)"] 240 | doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=6.1.4,<7.0.0)", "markdown-include (>=0.5.1,<0.6.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.2.0)", "typer-cli (>=0.0.9,<0.0.10)", "pyyaml (>=5.3.1,<6.0.0)"] 241 | test = ["pytest (==5.4.3)", "pytest-cov (==2.10.0)", "pytest-asyncio (>=0.14.0,<0.15.0)", "mypy (==0.790)", "flake8 (>=3.8.3,<4.0.0)", "black (==20.8b1)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.15.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.4.0)", "orjson (>=3.2.1,<4.0.0)", "async_exit_stack (>=1.0.1,<2.0.0)", "async_generator (>=1.10,<2.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "aiofiles (>=0.5.0,<0.6.0)", "flask (>=1.1.2,<2.0.0)"] 242 | 243 | [[package]] 244 | name = "h11" 245 | version = "0.12.0" 246 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 247 | category = "main" 248 | optional = false 249 | python-versions = ">=3.6" 250 | 251 | [[package]] 252 | name = "httptools" 253 | version = "0.1.1" 254 | description = "A collection of framework independent HTTP protocol utils." 255 | category = "main" 256 | optional = false 257 | python-versions = "*" 258 | 259 | [package.extras] 260 | test = ["Cython (==0.29.14)"] 261 | 262 | [[package]] 263 | name = "idna" 264 | version = "2.10" 265 | description = "Internationalized Domain Names in Applications (IDNA)" 266 | category = "dev" 267 | optional = false 268 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 269 | 270 | [[package]] 271 | name = "importlib-metadata" 272 | version = "3.4.0" 273 | description = "Read metadata from Python packages" 274 | category = "dev" 275 | optional = false 276 | python-versions = ">=3.6" 277 | 278 | [package.dependencies] 279 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 280 | zipp = ">=0.5" 281 | 282 | [package.extras] 283 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 284 | testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 285 | 286 | [[package]] 287 | name = "iniconfig" 288 | version = "1.1.1" 289 | description = "iniconfig: brain-dead simple config-ini parsing" 290 | category = "dev" 291 | optional = false 292 | python-versions = "*" 293 | 294 | [[package]] 295 | name = "isort" 296 | version = "5.7.0" 297 | description = "A Python utility / library to sort Python imports." 298 | category = "dev" 299 | optional = false 300 | python-versions = ">=3.6,<4.0" 301 | 302 | [package.extras] 303 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 304 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 305 | colors = ["colorama (>=0.4.3,<0.5.0)"] 306 | 307 | [[package]] 308 | name = "jsonschema" 309 | version = "3.2.0" 310 | description = "An implementation of JSON Schema validation for Python" 311 | category = "dev" 312 | optional = false 313 | python-versions = "*" 314 | 315 | [package.dependencies] 316 | attrs = ">=17.4.0" 317 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 318 | pyrsistent = ">=0.14.0" 319 | six = ">=1.11.0" 320 | 321 | [package.extras] 322 | format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"] 323 | format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator (>0.1.0)", "rfc3339-validator"] 324 | 325 | [[package]] 326 | name = "lazy-object-proxy" 327 | version = "1.4.3" 328 | description = "A fast and thorough lazy object proxy." 329 | category = "dev" 330 | optional = false 331 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 332 | 333 | [[package]] 334 | name = "mccabe" 335 | version = "0.6.1" 336 | description = "McCabe checker, plugin for flake8" 337 | category = "dev" 338 | optional = false 339 | python-versions = "*" 340 | 341 | [[package]] 342 | name = "mypy-extensions" 343 | version = "0.4.3" 344 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 345 | category = "dev" 346 | optional = false 347 | python-versions = "*" 348 | 349 | [[package]] 350 | name = "names" 351 | version = "0.3.0" 352 | description = "Generate random names" 353 | category = "main" 354 | optional = false 355 | python-versions = "*" 356 | 357 | [[package]] 358 | name = "packaging" 359 | version = "20.9" 360 | description = "Core utilities for Python packages" 361 | category = "dev" 362 | optional = false 363 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 364 | 365 | [package.dependencies] 366 | pyparsing = ">=2.0.2" 367 | 368 | [[package]] 369 | name = "paramiko" 370 | version = "2.7.2" 371 | description = "SSH2 protocol library" 372 | category = "dev" 373 | optional = false 374 | python-versions = "*" 375 | 376 | [package.dependencies] 377 | bcrypt = ">=3.1.3" 378 | cryptography = ">=2.5" 379 | pynacl = ">=1.0.1" 380 | 381 | [package.extras] 382 | all = ["pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "bcrypt (>=3.1.3)", "invoke (>=1.3)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] 383 | ed25519 = ["pynacl (>=1.0.1)", "bcrypt (>=3.1.3)"] 384 | gssapi = ["pyasn1 (>=0.1.7)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] 385 | invoke = ["invoke (>=1.3)"] 386 | 387 | [[package]] 388 | name = "pathspec" 389 | version = "0.8.1" 390 | description = "Utility library for gitignore style pattern matching of file paths." 391 | category = "dev" 392 | optional = false 393 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 394 | 395 | [[package]] 396 | name = "pluggy" 397 | version = "0.13.1" 398 | description = "plugin and hook calling mechanisms for python" 399 | category = "dev" 400 | optional = false 401 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 402 | 403 | [package.dependencies] 404 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 405 | 406 | [package.extras] 407 | dev = ["pre-commit", "tox"] 408 | 409 | [[package]] 410 | name = "py" 411 | version = "1.10.0" 412 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 413 | category = "dev" 414 | optional = false 415 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 416 | 417 | [[package]] 418 | name = "pycparser" 419 | version = "2.20" 420 | description = "C parser in Python" 421 | category = "dev" 422 | optional = false 423 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 424 | 425 | [[package]] 426 | name = "pydantic" 427 | version = "1.7.3" 428 | description = "Data validation and settings management using python 3.6 type hinting" 429 | category = "main" 430 | optional = false 431 | python-versions = ">=3.6" 432 | 433 | [package.extras] 434 | dotenv = ["python-dotenv (>=0.10.4)"] 435 | email = ["email-validator (>=1.0.3)"] 436 | typing_extensions = ["typing-extensions (>=3.7.2)"] 437 | 438 | [[package]] 439 | name = "pydocstyle" 440 | version = "5.1.1" 441 | description = "Python docstring style checker" 442 | category = "dev" 443 | optional = false 444 | python-versions = ">=3.5" 445 | 446 | [package.dependencies] 447 | snowballstemmer = "*" 448 | 449 | [[package]] 450 | name = "pylint" 451 | version = "2.6.0" 452 | description = "python code static checker" 453 | category = "dev" 454 | optional = false 455 | python-versions = ">=3.5.*" 456 | 457 | [package.dependencies] 458 | astroid = ">=2.4.0,<=2.5" 459 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 460 | isort = ">=4.2.5,<6" 461 | mccabe = ">=0.6,<0.7" 462 | toml = ">=0.7.1" 463 | 464 | [[package]] 465 | name = "pynacl" 466 | version = "1.4.0" 467 | description = "Python binding to the Networking and Cryptography (NaCl) library" 468 | category = "dev" 469 | optional = false 470 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 471 | 472 | [package.dependencies] 473 | cffi = ">=1.4.1" 474 | six = "*" 475 | 476 | [package.extras] 477 | docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] 478 | tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] 479 | 480 | [[package]] 481 | name = "pyparsing" 482 | version = "2.4.7" 483 | description = "Python parsing module" 484 | category = "dev" 485 | optional = false 486 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 487 | 488 | [[package]] 489 | name = "pyrsistent" 490 | version = "0.17.3" 491 | description = "Persistent/Functional/Immutable data structures" 492 | category = "dev" 493 | optional = false 494 | python-versions = ">=3.5" 495 | 496 | [[package]] 497 | name = "pytest" 498 | version = "6.2.2" 499 | description = "pytest: simple powerful testing with Python" 500 | category = "dev" 501 | optional = false 502 | python-versions = ">=3.6" 503 | 504 | [package.dependencies] 505 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 506 | attrs = ">=19.2.0" 507 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 508 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 509 | iniconfig = "*" 510 | packaging = "*" 511 | pluggy = ">=0.12,<1.0.0a1" 512 | py = ">=1.8.2" 513 | toml = "*" 514 | 515 | [package.extras] 516 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 517 | 518 | [[package]] 519 | name = "pytest-dependency" 520 | version = "0.5.1" 521 | description = "Manage dependencies of tests" 522 | category = "dev" 523 | optional = false 524 | python-versions = "*" 525 | 526 | [package.dependencies] 527 | pytest = ">=3.6.0" 528 | 529 | [[package]] 530 | name = "pytest-docker" 531 | version = "0.10.1" 532 | description = "Simple pytest fixtures for Docker and docker-compose based tests" 533 | category = "dev" 534 | optional = false 535 | python-versions = ">=3.6" 536 | 537 | [package.dependencies] 538 | attrs = ">=19,<21" 539 | docker-compose = ">=1.27.3,<2.0" 540 | pytest = ">=4.0,<7.0" 541 | 542 | [package.extras] 543 | tests = ["requests (>=2.22.0,<3.0)", "pytest-pylint (>=0.14.1,<1.0)", "pytest-pycodestyle (>=2.0.0,<3.0)"] 544 | 545 | [[package]] 546 | name = "pytest-mock" 547 | version = "3.5.1" 548 | description = "Thin-wrapper around the mock package for easier use with pytest" 549 | category = "dev" 550 | optional = false 551 | python-versions = ">=3.5" 552 | 553 | [package.dependencies] 554 | pytest = ">=5.0" 555 | 556 | [package.extras] 557 | dev = ["pre-commit", "tox", "pytest-asyncio"] 558 | 559 | [[package]] 560 | name = "python-dotenv" 561 | version = "0.15.0" 562 | description = "Add .env support to your django/flask apps in development and deployments" 563 | category = "main" 564 | optional = false 565 | python-versions = "*" 566 | 567 | [package.extras] 568 | cli = ["click (>=5.0)"] 569 | 570 | [[package]] 571 | name = "pywin32" 572 | version = "227" 573 | description = "Python for Window Extensions" 574 | category = "dev" 575 | optional = false 576 | python-versions = "*" 577 | 578 | [[package]] 579 | name = "pyyaml" 580 | version = "5.4.1" 581 | description = "YAML parser and emitter for Python" 582 | category = "main" 583 | optional = false 584 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 585 | 586 | [[package]] 587 | name = "regex" 588 | version = "2020.11.13" 589 | description = "Alternative regular expression module, to replace re." 590 | category = "dev" 591 | optional = false 592 | python-versions = "*" 593 | 594 | [[package]] 595 | name = "requests" 596 | version = "2.25.1" 597 | description = "Python HTTP for Humans." 598 | category = "dev" 599 | optional = false 600 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 601 | 602 | [package.dependencies] 603 | certifi = ">=2017.4.17" 604 | chardet = ">=3.0.2,<5" 605 | idna = ">=2.5,<3" 606 | urllib3 = ">=1.21.1,<1.27" 607 | 608 | [package.extras] 609 | security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] 610 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 611 | 612 | [[package]] 613 | name = "six" 614 | version = "1.15.0" 615 | description = "Python 2 and 3 compatibility utilities" 616 | category = "dev" 617 | optional = false 618 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 619 | 620 | [[package]] 621 | name = "snowballstemmer" 622 | version = "2.1.0" 623 | description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." 624 | category = "dev" 625 | optional = false 626 | python-versions = "*" 627 | 628 | [[package]] 629 | name = "starlette" 630 | version = "0.13.6" 631 | description = "The little ASGI library that shines." 632 | category = "main" 633 | optional = false 634 | python-versions = ">=3.6" 635 | 636 | [package.extras] 637 | full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "ujson"] 638 | 639 | [[package]] 640 | name = "texttable" 641 | version = "1.6.3" 642 | description = "module for creating simple ASCII tables" 643 | category = "dev" 644 | optional = false 645 | python-versions = "*" 646 | 647 | [[package]] 648 | name = "toml" 649 | version = "0.10.2" 650 | description = "Python Library for Tom's Obvious, Minimal Language" 651 | category = "dev" 652 | optional = false 653 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 654 | 655 | [[package]] 656 | name = "typed-ast" 657 | version = "1.4.2" 658 | description = "a fork of Python 2 and 3 ast modules with type comment support" 659 | category = "dev" 660 | optional = false 661 | python-versions = "*" 662 | 663 | [[package]] 664 | name = "typing-extensions" 665 | version = "3.7.4.3" 666 | description = "Backported and Experimental Type Hints for Python 3.5+" 667 | category = "main" 668 | optional = false 669 | python-versions = "*" 670 | 671 | [[package]] 672 | name = "urllib3" 673 | version = "1.26.3" 674 | description = "HTTP library with thread-safe connection pooling, file post, and more." 675 | category = "dev" 676 | optional = false 677 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 678 | 679 | [package.extras] 680 | brotli = ["brotlipy (>=0.6.0)"] 681 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 682 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 683 | 684 | [[package]] 685 | name = "uvicorn" 686 | version = "0.13.4" 687 | description = "The lightning-fast ASGI server." 688 | category = "main" 689 | optional = false 690 | python-versions = "*" 691 | 692 | [package.dependencies] 693 | click = ">=7.0.0,<8.0.0" 694 | colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} 695 | h11 = ">=0.8" 696 | httptools = {version = ">=0.1.0,<0.2.0", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} 697 | python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} 698 | PyYAML = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} 699 | typing-extensions = {version = "*", markers = "python_version < \"3.8\""} 700 | uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} 701 | watchgod = {version = ">=0.6", optional = true, markers = "extra == \"standard\""} 702 | websockets = {version = ">=8.0.0,<9.0.0", optional = true, markers = "extra == \"standard\""} 703 | 704 | [package.extras] 705 | standard = ["websockets (>=8.0.0,<9.0.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "httptools (>=0.1.0,<0.2.0)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] 706 | 707 | [[package]] 708 | name = "uvloop" 709 | version = "0.14.0" 710 | description = "Fast implementation of asyncio event loop on top of libuv" 711 | category = "main" 712 | optional = false 713 | python-versions = "*" 714 | 715 | [[package]] 716 | name = "watchgod" 717 | version = "0.6" 718 | description = "Simple, modern file watching and code reload in python." 719 | category = "main" 720 | optional = false 721 | python-versions = ">=3.5" 722 | 723 | [[package]] 724 | name = "websocket-client" 725 | version = "0.58.0" 726 | description = "WebSocket client for Python with low level API options" 727 | category = "dev" 728 | optional = false 729 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 730 | 731 | [package.dependencies] 732 | six = "*" 733 | 734 | [[package]] 735 | name = "websockets" 736 | version = "8.1" 737 | description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" 738 | category = "main" 739 | optional = false 740 | python-versions = ">=3.6.1" 741 | 742 | [[package]] 743 | name = "wrapt" 744 | version = "1.12.1" 745 | description = "Module for decorators, wrappers and monkey patching." 746 | category = "dev" 747 | optional = false 748 | python-versions = "*" 749 | 750 | [[package]] 751 | name = "zipp" 752 | version = "3.4.0" 753 | description = "Backport of pathlib-compatible object wrapper for zip files" 754 | category = "dev" 755 | optional = false 756 | python-versions = ">=3.6" 757 | 758 | [package.extras] 759 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 760 | testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 761 | 762 | [metadata] 763 | lock-version = "1.1" 764 | python-versions = "^3.7" 765 | content-hash = "2526f915c45a42c10faec5531b1aebe3e79e9e63d04ab66f8dee381dbe6876d6" 766 | 767 | [metadata.files] 768 | appdirs = [ 769 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 770 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 771 | ] 772 | astroid = [ 773 | {file = "astroid-2.4.2-py3-none-any.whl", hash = "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"}, 774 | {file = "astroid-2.4.2.tar.gz", hash = "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703"}, 775 | ] 776 | atomicwrites = [ 777 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 778 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 779 | ] 780 | attrs = [ 781 | {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, 782 | {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, 783 | ] 784 | bcrypt = [ 785 | {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, 786 | {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"}, 787 | {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"}, 788 | {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"}, 789 | {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"}, 790 | {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"}, 791 | {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, 792 | ] 793 | black = [ 794 | {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, 795 | ] 796 | cached-property = [ 797 | {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, 798 | {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, 799 | ] 800 | certifi = [ 801 | {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, 802 | {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, 803 | ] 804 | cffi = [ 805 | {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"}, 806 | {file = "cffi-1.14.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1"}, 807 | {file = "cffi-1.14.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa"}, 808 | {file = "cffi-1.14.5-cp27-cp27m-win32.whl", hash = "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3"}, 809 | {file = "cffi-1.14.5-cp27-cp27m-win_amd64.whl", hash = "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5"}, 810 | {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482"}, 811 | {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6"}, 812 | {file = "cffi-1.14.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045"}, 813 | {file = "cffi-1.14.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa"}, 814 | {file = "cffi-1.14.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406"}, 815 | {file = "cffi-1.14.5-cp35-cp35m-win32.whl", hash = "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369"}, 816 | {file = "cffi-1.14.5-cp35-cp35m-win_amd64.whl", hash = "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315"}, 817 | {file = "cffi-1.14.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892"}, 818 | {file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"}, 819 | {file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"}, 820 | {file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"}, 821 | {file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"}, 822 | {file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"}, 823 | {file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"}, 824 | {file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"}, 825 | {file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"}, 826 | {file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"}, 827 | {file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"}, 828 | {file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"}, 829 | {file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"}, 830 | {file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"}, 831 | {file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"}, 832 | {file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"}, 833 | {file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"}, 834 | {file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"}, 835 | {file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"}, 836 | {file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"}, 837 | {file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"}, 838 | {file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"}, 839 | {file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"}, 840 | {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"}, 841 | {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"}, 842 | ] 843 | chardet = [ 844 | {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, 845 | {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, 846 | ] 847 | click = [ 848 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 849 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 850 | ] 851 | colorama = [ 852 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 853 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 854 | ] 855 | cryptography = [ 856 | {file = "cryptography-3.4.6-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:57ad77d32917bc55299b16d3b996ffa42a1c73c6cfa829b14043c561288d2799"}, 857 | {file = "cryptography-3.4.6-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:4169a27b818de4a1860720108b55a2801f32b6ae79e7f99c00d79f2a2822eeb7"}, 858 | {file = "cryptography-3.4.6-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:93cfe5b7ff006de13e1e89830810ecbd014791b042cbe5eec253be11ac2b28f3"}, 859 | {file = "cryptography-3.4.6-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:5ecf2bcb34d17415e89b546dbb44e73080f747e504273e4d4987630493cded1b"}, 860 | {file = "cryptography-3.4.6-cp36-abi3-manylinux2014_x86_64.whl", hash = "sha256:fec7fb46b10da10d9e1d078d1ff8ed9e05ae14f431fdbd11145edd0550b9a964"}, 861 | {file = "cryptography-3.4.6-cp36-abi3-win32.whl", hash = "sha256:df186fcbf86dc1ce56305becb8434e4b6b7504bc724b71ad7a3239e0c9d14ef2"}, 862 | {file = "cryptography-3.4.6-cp36-abi3-win_amd64.whl", hash = "sha256:66b57a9ca4b3221d51b237094b0303843b914b7d5afd4349970bb26518e350b0"}, 863 | {file = "cryptography-3.4.6-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:066bc53f052dfeda2f2d7c195cf16fb3e5ff13e1b6b7415b468514b40b381a5b"}, 864 | {file = "cryptography-3.4.6-pp36-pypy36_pp73-manylinux2014_x86_64.whl", hash = "sha256:600cf9bfe75e96d965509a4c0b2b183f74a4fa6f5331dcb40fb7b77b7c2484df"}, 865 | {file = "cryptography-3.4.6-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:0923ba600d00718d63a3976f23cab19aef10c1765038945628cd9be047ad0336"}, 866 | {file = "cryptography-3.4.6-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:9e98b452132963678e3ac6c73f7010fe53adf72209a32854d55690acac3f6724"}, 867 | {file = "cryptography-3.4.6.tar.gz", hash = "sha256:2d32223e5b0ee02943f32b19245b61a62db83a882f0e76cc564e1cec60d48f87"}, 868 | ] 869 | distro = [ 870 | {file = "distro-1.5.0-py2.py3-none-any.whl", hash = "sha256:df74eed763e18d10d0da624258524ae80486432cd17392d9c3d96f5e83cd2799"}, 871 | {file = "distro-1.5.0.tar.gz", hash = "sha256:0e58756ae38fbd8fc3020d54badb8eae17c5b9dcbed388b17bb55b8a5928df92"}, 872 | ] 873 | docker = [ 874 | {file = "docker-4.4.4-py2.py3-none-any.whl", hash = "sha256:f3607d5695be025fa405a12aca2e5df702a57db63790c73b927eb6a94aac60af"}, 875 | {file = "docker-4.4.4.tar.gz", hash = "sha256:d3393c878f575d3a9ca3b94471a3c89a6d960b35feb92f033c0de36cc9d934db"}, 876 | ] 877 | docker-compose = [ 878 | {file = "docker-compose-1.28.5.tar.gz", hash = "sha256:b3ff8f0352eb4055c4c483cb498aeff7c90195fa679f3caf7098a2d6fa6030e5"}, 879 | {file = "docker_compose-1.28.5-py2.py3-none-any.whl", hash = "sha256:2c09c6a7d320f1191d14ae6e7d93190d459313c8393cc5c74cb15f9205a8f23f"}, 880 | ] 881 | dockerpty = [ 882 | {file = "dockerpty-0.4.1.tar.gz", hash = "sha256:69a9d69d573a0daa31bcd1c0774eeed5c15c295fe719c61aca550ed1393156ce"}, 883 | ] 884 | docopt = [ 885 | {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, 886 | ] 887 | fastapi = [ 888 | {file = "fastapi-0.63.0-py3-none-any.whl", hash = "sha256:98d8ea9591d8512fdadf255d2a8fa56515cdd8624dca4af369da73727409508e"}, 889 | {file = "fastapi-0.63.0.tar.gz", hash = "sha256:63c4592f5ef3edf30afa9a44fa7c6b7ccb20e0d3f68cd9eba07b44d552058dcb"}, 890 | ] 891 | h11 = [ 892 | {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, 893 | {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, 894 | ] 895 | httptools = [ 896 | {file = "httptools-0.1.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce"}, 897 | {file = "httptools-0.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4"}, 898 | {file = "httptools-0.1.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6"}, 899 | {file = "httptools-0.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c"}, 900 | {file = "httptools-0.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a"}, 901 | {file = "httptools-0.1.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f"}, 902 | {file = "httptools-0.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2"}, 903 | {file = "httptools-0.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009"}, 904 | {file = "httptools-0.1.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437"}, 905 | {file = "httptools-0.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d"}, 906 | {file = "httptools-0.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be"}, 907 | {file = "httptools-0.1.1.tar.gz", hash = "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce"}, 908 | ] 909 | idna = [ 910 | {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, 911 | {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, 912 | ] 913 | importlib-metadata = [ 914 | {file = "importlib_metadata-3.4.0-py3-none-any.whl", hash = "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771"}, 915 | {file = "importlib_metadata-3.4.0.tar.gz", hash = "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"}, 916 | ] 917 | iniconfig = [ 918 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 919 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 920 | ] 921 | isort = [ 922 | {file = "isort-5.7.0-py3-none-any.whl", hash = "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc"}, 923 | {file = "isort-5.7.0.tar.gz", hash = "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e"}, 924 | ] 925 | jsonschema = [ 926 | {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, 927 | {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"}, 928 | ] 929 | lazy-object-proxy = [ 930 | {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"}, 931 | {file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"}, 932 | {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win32.whl", hash = "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4"}, 933 | {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a"}, 934 | {file = "lazy_object_proxy-1.4.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d"}, 935 | {file = "lazy_object_proxy-1.4.3-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a"}, 936 | {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win32.whl", hash = "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e"}, 937 | {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win_amd64.whl", hash = "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357"}, 938 | {file = "lazy_object_proxy-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50"}, 939 | {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db"}, 940 | {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449"}, 941 | {file = "lazy_object_proxy-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156"}, 942 | {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531"}, 943 | {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb"}, 944 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08"}, 945 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383"}, 946 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142"}, 947 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea"}, 948 | {file = "lazy_object_proxy-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62"}, 949 | {file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"}, 950 | {file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"}, 951 | ] 952 | mccabe = [ 953 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 954 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 955 | ] 956 | mypy-extensions = [ 957 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 958 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 959 | ] 960 | names = [ 961 | {file = "names-0.3.0.tar.gz", hash = "sha256:726e46254f2ed03f1ffb5d941dae3bc67c35123941c29becd02d48d0caa2a671"}, 962 | ] 963 | packaging = [ 964 | {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, 965 | {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, 966 | ] 967 | paramiko = [ 968 | {file = "paramiko-2.7.2-py2.py3-none-any.whl", hash = "sha256:4f3e316fef2ac628b05097a637af35685183111d4bc1b5979bd397c2ab7b5898"}, 969 | {file = "paramiko-2.7.2.tar.gz", hash = "sha256:7f36f4ba2c0d81d219f4595e35f70d56cc94f9ac40a6acdf51d6ca210ce65035"}, 970 | ] 971 | pathspec = [ 972 | {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, 973 | {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, 974 | ] 975 | pluggy = [ 976 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 977 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 978 | ] 979 | py = [ 980 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 981 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 982 | ] 983 | pycparser = [ 984 | {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, 985 | {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, 986 | ] 987 | pydantic = [ 988 | {file = "pydantic-1.7.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c59ea046aea25be14dc22d69c97bee629e6d48d2b2ecb724d7fe8806bf5f61cd"}, 989 | {file = "pydantic-1.7.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a4143c8d0c456a093387b96e0f5ee941a950992904d88bc816b4f0e72c9a0009"}, 990 | {file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:d8df4b9090b595511906fa48deda47af04e7d092318bfb291f4d45dfb6bb2127"}, 991 | {file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:514b473d264671a5c672dfb28bdfe1bf1afd390f6b206aa2ec9fed7fc592c48e"}, 992 | {file = "pydantic-1.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:dba5c1f0a3aeea5083e75db9660935da90216f8a81b6d68e67f54e135ed5eb23"}, 993 | {file = "pydantic-1.7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59e45f3b694b05a69032a0d603c32d453a23f0de80844fb14d55ab0c6c78ff2f"}, 994 | {file = "pydantic-1.7.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:5b24e8a572e4b4c18f614004dda8c9f2c07328cb5b6e314d6e1bbd536cb1a6c1"}, 995 | {file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:b2b054d095b6431cdda2f852a6d2f0fdec77686b305c57961b4c5dd6d863bf3c"}, 996 | {file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:025bf13ce27990acc059d0c5be46f416fc9b293f45363b3d19855165fee1874f"}, 997 | {file = "pydantic-1.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6e3874aa7e8babd37b40c4504e3a94cc2023696ced5a0500949f3347664ff8e2"}, 998 | {file = "pydantic-1.7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e682f6442ebe4e50cb5e1cfde7dda6766fb586631c3e5569f6aa1951fd1a76ef"}, 999 | {file = "pydantic-1.7.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:185e18134bec5ef43351149fe34fda4758e53d05bb8ea4d5928f0720997b79ef"}, 1000 | {file = "pydantic-1.7.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:f5b06f5099e163295b8ff5b1b71132ecf5866cc6e7f586d78d7d3fd6e8084608"}, 1001 | {file = "pydantic-1.7.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:24ca47365be2a5a3cc3f4a26dcc755bcdc9f0036f55dcedbd55663662ba145ec"}, 1002 | {file = "pydantic-1.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:d1fe3f0df8ac0f3a9792666c69a7cd70530f329036426d06b4f899c025aca74e"}, 1003 | {file = "pydantic-1.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f6864844b039805add62ebe8a8c676286340ba0c6d043ae5dea24114b82a319e"}, 1004 | {file = "pydantic-1.7.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ecb54491f98544c12c66ff3d15e701612fc388161fd455242447083350904730"}, 1005 | {file = "pydantic-1.7.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:ffd180ebd5dd2a9ac0da4e8b995c9c99e7c74c31f985ba090ee01d681b1c4b95"}, 1006 | {file = "pydantic-1.7.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8d72e814c7821125b16f1553124d12faba88e85405b0864328899aceaad7282b"}, 1007 | {file = "pydantic-1.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:475f2fa134cf272d6631072554f845d0630907fce053926ff634cc6bc45bf1af"}, 1008 | {file = "pydantic-1.7.3-py3-none-any.whl", hash = "sha256:38be427ea01a78206bcaf9a56f835784afcba9e5b88fbdce33bbbfbcd7841229"}, 1009 | {file = "pydantic-1.7.3.tar.gz", hash = "sha256:213125b7e9e64713d16d988d10997dabc6a1f73f3991e1ff8e35ebb1409c7dc9"}, 1010 | ] 1011 | pydocstyle = [ 1012 | {file = "pydocstyle-5.1.1-py3-none-any.whl", hash = "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678"}, 1013 | {file = "pydocstyle-5.1.1.tar.gz", hash = "sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325"}, 1014 | ] 1015 | pylint = [ 1016 | {file = "pylint-2.6.0-py3-none-any.whl", hash = "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f"}, 1017 | {file = "pylint-2.6.0.tar.gz", hash = "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210"}, 1018 | ] 1019 | pynacl = [ 1020 | {file = "PyNaCl-1.4.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff"}, 1021 | {file = "PyNaCl-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514"}, 1022 | {file = "PyNaCl-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574"}, 1023 | {file = "PyNaCl-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80"}, 1024 | {file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, 1025 | {file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, 1026 | {file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, 1027 | {file = "PyNaCl-1.4.0-cp35-abi3-win32.whl", hash = "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634"}, 1028 | {file = "PyNaCl-1.4.0-cp35-abi3-win_amd64.whl", hash = "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6"}, 1029 | {file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, 1030 | {file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"}, 1031 | {file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"}, 1032 | {file = "PyNaCl-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6"}, 1033 | {file = "PyNaCl-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f"}, 1034 | {file = "PyNaCl-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f"}, 1035 | {file = "PyNaCl-1.4.0-cp38-cp38-win32.whl", hash = "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96"}, 1036 | {file = "PyNaCl-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420"}, 1037 | {file = "PyNaCl-1.4.0.tar.gz", hash = "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505"}, 1038 | ] 1039 | pyparsing = [ 1040 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 1041 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 1042 | ] 1043 | pyrsistent = [ 1044 | {file = "pyrsistent-0.17.3.tar.gz", hash = "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"}, 1045 | ] 1046 | pytest = [ 1047 | {file = "pytest-6.2.2-py3-none-any.whl", hash = "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839"}, 1048 | {file = "pytest-6.2.2.tar.gz", hash = "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9"}, 1049 | ] 1050 | pytest-dependency = [ 1051 | {file = "pytest-dependency-0.5.1.tar.gz", hash = "sha256:c2a892906192663f85030a6ab91304e508e546cddfe557d692d61ec57a1d946b"}, 1052 | ] 1053 | pytest-docker = [ 1054 | {file = "pytest-docker-0.10.1.tar.gz", hash = "sha256:7a0544dc00c83fd7808907a55835b1b7c07e3ea2d22ee558b5a233247e462735"}, 1055 | {file = "pytest_docker-0.10.1-py3-none-any.whl", hash = "sha256:9faa9c87e6e0920612005c8d187c58b3d336f3af49a878f358578de125963858"}, 1056 | ] 1057 | pytest-mock = [ 1058 | {file = "pytest-mock-3.5.1.tar.gz", hash = "sha256:a1e2aba6af9560d313c642dae7e00a2a12b022b80301d9d7fc8ec6858e1dd9fc"}, 1059 | {file = "pytest_mock-3.5.1-py3-none-any.whl", hash = "sha256:379b391cfad22422ea2e252bdfc008edd08509029bcde3c25b2c0bd741e0424e"}, 1060 | ] 1061 | python-dotenv = [ 1062 | {file = "python-dotenv-0.15.0.tar.gz", hash = "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0"}, 1063 | {file = "python_dotenv-0.15.0-py2.py3-none-any.whl", hash = "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e"}, 1064 | ] 1065 | pywin32 = [ 1066 | {file = "pywin32-227-cp27-cp27m-win32.whl", hash = "sha256:371fcc39416d736401f0274dd64c2302728c9e034808e37381b5e1b22be4a6b0"}, 1067 | {file = "pywin32-227-cp27-cp27m-win_amd64.whl", hash = "sha256:4cdad3e84191194ea6d0dd1b1b9bdda574ff563177d2adf2b4efec2a244fa116"}, 1068 | {file = "pywin32-227-cp35-cp35m-win32.whl", hash = "sha256:f4c5be1a293bae0076d93c88f37ee8da68136744588bc5e2be2f299a34ceb7aa"}, 1069 | {file = "pywin32-227-cp35-cp35m-win_amd64.whl", hash = "sha256:a929a4af626e530383a579431b70e512e736e9588106715215bf685a3ea508d4"}, 1070 | {file = "pywin32-227-cp36-cp36m-win32.whl", hash = "sha256:300a2db938e98c3e7e2093e4491439e62287d0d493fe07cce110db070b54c0be"}, 1071 | {file = "pywin32-227-cp36-cp36m-win_amd64.whl", hash = "sha256:9b31e009564fb95db160f154e2aa195ed66bcc4c058ed72850d047141b36f3a2"}, 1072 | {file = "pywin32-227-cp37-cp37m-win32.whl", hash = "sha256:47a3c7551376a865dd8d095a98deba954a98f326c6fe3c72d8726ca6e6b15507"}, 1073 | {file = "pywin32-227-cp37-cp37m-win_amd64.whl", hash = "sha256:31f88a89139cb2adc40f8f0e65ee56a8c585f629974f9e07622ba80199057511"}, 1074 | {file = "pywin32-227-cp38-cp38-win32.whl", hash = "sha256:7f18199fbf29ca99dff10e1f09451582ae9e372a892ff03a28528a24d55875bc"}, 1075 | {file = "pywin32-227-cp38-cp38-win_amd64.whl", hash = "sha256:7c1ae32c489dc012930787f06244426f8356e129184a02c25aef163917ce158e"}, 1076 | {file = "pywin32-227-cp39-cp39-win32.whl", hash = "sha256:c054c52ba46e7eb6b7d7dfae4dbd987a1bb48ee86debe3f245a2884ece46e295"}, 1077 | {file = "pywin32-227-cp39-cp39-win_amd64.whl", hash = "sha256:f27cec5e7f588c3d1051651830ecc00294f90728d19c3bf6916e6dba93ea357c"}, 1078 | ] 1079 | pyyaml = [ 1080 | {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, 1081 | {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, 1082 | {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, 1083 | {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, 1084 | {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, 1085 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, 1086 | {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, 1087 | {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, 1088 | {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, 1089 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, 1090 | {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, 1091 | {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, 1092 | {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, 1093 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, 1094 | {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, 1095 | {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, 1096 | {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, 1097 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, 1098 | {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, 1099 | {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, 1100 | {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, 1101 | ] 1102 | regex = [ 1103 | {file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"}, 1104 | {file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"}, 1105 | {file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"}, 1106 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"}, 1107 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"}, 1108 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"}, 1109 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"}, 1110 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"}, 1111 | {file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"}, 1112 | {file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"}, 1113 | {file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"}, 1114 | {file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"}, 1115 | {file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"}, 1116 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"}, 1117 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"}, 1118 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"}, 1119 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"}, 1120 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"}, 1121 | {file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"}, 1122 | {file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"}, 1123 | {file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"}, 1124 | {file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"}, 1125 | {file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"}, 1126 | {file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"}, 1127 | {file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"}, 1128 | {file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"}, 1129 | {file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"}, 1130 | {file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"}, 1131 | {file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"}, 1132 | {file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"}, 1133 | {file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"}, 1134 | {file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"}, 1135 | {file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"}, 1136 | {file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"}, 1137 | {file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"}, 1138 | {file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"}, 1139 | {file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"}, 1140 | {file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"}, 1141 | {file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"}, 1142 | {file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"}, 1143 | {file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"}, 1144 | ] 1145 | requests = [ 1146 | {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, 1147 | {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, 1148 | ] 1149 | six = [ 1150 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, 1151 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, 1152 | ] 1153 | snowballstemmer = [ 1154 | {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, 1155 | {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, 1156 | ] 1157 | starlette = [ 1158 | {file = "starlette-0.13.6-py3-none-any.whl", hash = "sha256:bd2ffe5e37fb75d014728511f8e68ebf2c80b0fa3d04ca1479f4dc752ae31ac9"}, 1159 | {file = "starlette-0.13.6.tar.gz", hash = "sha256:ebe8ee08d9be96a3c9f31b2cb2a24dbdf845247b745664bd8a3f9bd0c977fdbc"}, 1160 | ] 1161 | texttable = [ 1162 | {file = "texttable-1.6.3-py2.py3-none-any.whl", hash = "sha256:f802f2ef8459058736264210f716c757cbf85007a30886d8541aa8c3404f1dda"}, 1163 | {file = "texttable-1.6.3.tar.gz", hash = "sha256:ce0faf21aa77d806bbff22b107cc22cce68dc9438f97a2df32c93e9afa4ce436"}, 1164 | ] 1165 | toml = [ 1166 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 1167 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 1168 | ] 1169 | typed-ast = [ 1170 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"}, 1171 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"}, 1172 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"}, 1173 | {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"}, 1174 | {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"}, 1175 | {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"}, 1176 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"}, 1177 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"}, 1178 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"}, 1179 | {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"}, 1180 | {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"}, 1181 | {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"}, 1182 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"}, 1183 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"}, 1184 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"}, 1185 | {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"}, 1186 | {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"}, 1187 | {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"}, 1188 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"}, 1189 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"}, 1190 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"}, 1191 | {file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"}, 1192 | {file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"}, 1193 | {file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"}, 1194 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"}, 1195 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"}, 1196 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"}, 1197 | {file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"}, 1198 | {file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"}, 1199 | {file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"}, 1200 | ] 1201 | typing-extensions = [ 1202 | {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, 1203 | {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, 1204 | {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, 1205 | ] 1206 | urllib3 = [ 1207 | {file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"}, 1208 | {file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"}, 1209 | ] 1210 | uvicorn = [ 1211 | {file = "uvicorn-0.13.4-py3-none-any.whl", hash = "sha256:7587f7b08bd1efd2b9bad809a3d333e972f1d11af8a5e52a9371ee3a5de71524"}, 1212 | {file = "uvicorn-0.13.4.tar.gz", hash = "sha256:3292251b3c7978e8e4a7868f4baf7f7f7bb7e40c759ecc125c37e99cdea34202"}, 1213 | ] 1214 | uvloop = [ 1215 | {file = "uvloop-0.14.0-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd"}, 1216 | {file = "uvloop-0.14.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726"}, 1217 | {file = "uvloop-0.14.0-cp36-cp36m-macosx_10_11_x86_64.whl", hash = "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7"}, 1218 | {file = "uvloop-0.14.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"}, 1219 | {file = "uvloop-0.14.0-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891"}, 1220 | {file = "uvloop-0.14.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95"}, 1221 | {file = "uvloop-0.14.0-cp38-cp38-macosx_10_11_x86_64.whl", hash = "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5"}, 1222 | {file = "uvloop-0.14.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09"}, 1223 | {file = "uvloop-0.14.0.tar.gz", hash = "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e"}, 1224 | ] 1225 | watchgod = [ 1226 | {file = "watchgod-0.6-py35.py36.py37-none-any.whl", hash = "sha256:59700dab7445aa8e6067a5b94f37bae90fc367554549b1ed2e9d0f4f38a90d2a"}, 1227 | {file = "watchgod-0.6.tar.gz", hash = "sha256:e9cca0ab9c63f17fc85df9fd8bd18156ff00aff04ebe5976cee473f4968c6858"}, 1228 | ] 1229 | websocket-client = [ 1230 | {file = "websocket_client-0.58.0-py2.py3-none-any.whl", hash = "sha256:44b5df8f08c74c3d82d28100fdc81f4536809ce98a17f0757557813275fbb663"}, 1231 | {file = "websocket_client-0.58.0.tar.gz", hash = "sha256:63509b41d158ae5b7f67eb4ad20fecbb4eee99434e73e140354dc3ff8e09716f"}, 1232 | ] 1233 | websockets = [ 1234 | {file = "websockets-8.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c"}, 1235 | {file = "websockets-8.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170"}, 1236 | {file = "websockets-8.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8"}, 1237 | {file = "websockets-8.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb"}, 1238 | {file = "websockets-8.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5"}, 1239 | {file = "websockets-8.1-cp36-cp36m-win32.whl", hash = "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a"}, 1240 | {file = "websockets-8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5"}, 1241 | {file = "websockets-8.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989"}, 1242 | {file = "websockets-8.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d"}, 1243 | {file = "websockets-8.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779"}, 1244 | {file = "websockets-8.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8"}, 1245 | {file = "websockets-8.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422"}, 1246 | {file = "websockets-8.1-cp37-cp37m-win32.whl", hash = "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc"}, 1247 | {file = "websockets-8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308"}, 1248 | {file = "websockets-8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092"}, 1249 | {file = "websockets-8.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485"}, 1250 | {file = "websockets-8.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1"}, 1251 | {file = "websockets-8.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55"}, 1252 | {file = "websockets-8.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824"}, 1253 | {file = "websockets-8.1-cp38-cp38-win32.whl", hash = "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36"}, 1254 | {file = "websockets-8.1-cp38-cp38-win_amd64.whl", hash = "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"}, 1255 | {file = "websockets-8.1.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"}, 1256 | ] 1257 | wrapt = [ 1258 | {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, 1259 | ] 1260 | zipp = [ 1261 | {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"}, 1262 | {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"}, 1263 | ] 1264 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | create = true 3 | in-project = true -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "apyr" 3 | version = "1.1.0" 4 | description = "Mock that API" 5 | license = "MIT" 6 | readme = "README.md" 7 | homepage = "https://github.com/umutseven92/apyr" 8 | repository = "https://github.com/umutseven92/apyr" 9 | keywords = ["api", "mock", "fastapi"] 10 | authors = ["Umut Seven "] 11 | maintainers = ["Umut Seven "] 12 | documentation = "https://github.com/umutseven92/apyr/blob/master/README.md" 13 | 14 | [tool.poetry.dependencies] 15 | python = "^3.7" 16 | fastapi = "^0.63.0" 17 | uvicorn = { extras = ["standard"], version = "^0.13.3" } 18 | names = "^0.3.0" 19 | 20 | [tool.poetry.dev-dependencies] 21 | black = "^20.8b1" 22 | pylint = "^2.6.0" 23 | pydocstyle = "^5.1.1" 24 | pytest = "^6.2.2" 25 | pytest-dependency = "^0.5.1" 26 | pytest-mock = "^3.5.1" 27 | pytest-docker = "^0.10.1" 28 | 29 | [build-system] 30 | requires = ["poetry-core>=1.0.0"] 31 | build-backend = "poetry.core.masonry.api" 32 | 33 | [tool.poetry.urls] 34 | issues = "https://github.com/umutseven92/apyr/issues" 35 | 36 | [tool.poetry.scripts] 37 | apyr = "apyr.main:run" -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutseven92/apyr/5b73482db03b273443d0d67dcf03d50ab74f0205/tests/__init__.py -------------------------------------------------------------------------------- /tests/data/string1.txt: -------------------------------------------------------------------------------- 1 | On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee the pain and trouble that are bound to ensue; and equal blame belongs to those who fail in their duty through weakness of will, which is the same as saying through shrinking from toil and pain. These cases are perfectly simple and easy to distinguish. -------------------------------------------------------------------------------- /tests/data/string2.txt: -------------------------------------------------------------------------------- 1 | In a free hour, when our power of choice is untrammelled and when nothing prevents our being able to do what we like best, every pleasure is to be welcomed and every pain avoided. But in certain circumstances and owing to the claims of duty or the obligations of business it will frequently occur that pleasures have to be repudiated and annoyances accepted. The wise man therefore always holds in these matters to this principle of selection: he rejects pleasures to secure other greater pleasures, or else he endures pains to avoid worse pains. -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutseven92/apyr/5b73482db03b273443d0d67dcf03d50ab74f0205/tests/integration/__init__.py -------------------------------------------------------------------------------- /tests/integration/conftest.py: -------------------------------------------------------------------------------- 1 | from http.client import RemoteDisconnected 2 | 3 | import pytest 4 | import requests 5 | 6 | from apyr.utils import get_project_root 7 | 8 | 9 | @pytest.fixture(scope="session") 10 | def docker_compose_file(pytestconfig): 11 | del pytestconfig 12 | return get_project_root().joinpath("docker-compose.yaml") 13 | 14 | 15 | @pytest.fixture(scope="session") 16 | def apyr_service(docker_ip, docker_services): 17 | del docker_ip 18 | port = docker_services.port_for("apyr", 8000) 19 | base_url = f"http://0.0.0.0:{port}" 20 | 21 | def is_responsive(): 22 | status_url = f"{base_url}/apyr/status" 23 | 24 | try: 25 | response = requests.get(status_url) 26 | if response.status_code == 200: 27 | return True 28 | except: 29 | return False 30 | 31 | docker_services.wait_until_responsive( 32 | timeout=30.0, pause=0.1, check=lambda: is_responsive() 33 | ) 34 | return base_url 35 | -------------------------------------------------------------------------------- /tests/integration/test_endpoints.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import pytest 4 | import requests 5 | 6 | 7 | def strip_whitespace_html(content: str) -> str: 8 | """Strips whitespace from in-between HTML tags. Useful for when comparing HTML files.""" 9 | return re.sub(r">\s*<", "><", content).strip() 10 | 11 | 12 | class TestEndpoints: 13 | """Test class for the default `endpoints.yaml` file, which should cover all types of requests.""" 14 | 15 | def test_get_employees(self, apyr_service): 16 | expected_body = [ 17 | {"first_name": "Peter", "last_name": "Venkman"}, 18 | {"first_name": "Ray", "last_name": "Stantz"}, 19 | {"first_name": "Egon", "last_name": "Spengler"}, 20 | ] 21 | response = requests.get(apyr_service + "/test/employees") 22 | 23 | assert response.status_code == 200 24 | assert response.headers.get("content-type") == "application/json" 25 | 26 | body = response.json() 27 | 28 | assert body == expected_body 29 | 30 | def test_get_employee(self, apyr_service): 31 | response = requests.get(apyr_service + "/test/employee/2") 32 | 33 | assert response.status_code == 200 34 | assert response.headers.get("content-type") == "application/json" 35 | 36 | body = response.json() 37 | 38 | assert "first_name" in body 39 | assert "last_name" in body 40 | assert "age" in body 41 | assert 20 <= body["age"] <= 50 42 | 43 | def test_post_employee(self, apyr_service): 44 | expected_body = "An unexpected error occurred while creating the employee." 45 | response = requests.post(apyr_service + "/test/employee") 46 | 47 | assert response.status_code == 500 48 | assert response.headers.get("content-type") == "text" 49 | 50 | body = response.text 51 | 52 | assert body == expected_body 53 | 54 | def test_put_employee(self, apyr_service): 55 | response = requests.put(apyr_service + "/test/employee/3") 56 | 57 | assert response.status_code == 201 58 | assert "content-type" not in response.headers 59 | assert response.text == "" 60 | 61 | @pytest.mark.parametrize("path", ["/test/help", "/test/help2"]) 62 | def test_get_help(self, apyr_service, path: str): 63 | expected_body = """ 64 | 65 | 66 | 67 |

I've quit better jobs than this.

68 |

Ghostbusters, whaddya want.

69 | 70 | 71 | """ 72 | 73 | response = requests.get(apyr_service + path) 74 | 75 | assert response.status_code == 200 76 | assert response.headers.get("content-type") == "text/html; charset=utf-8" 77 | 78 | body = response.text 79 | 80 | assert strip_whitespace_html(expected_body) == strip_whitespace_html(body) 81 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutseven92/apyr/5b73482db03b273443d0d67dcf03d50ab74f0205/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/test_functions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from apyr.exceptions import FunctionException 4 | from apyr.function_handler import FunctionHandler 5 | 6 | 7 | class TestFunctions: 8 | def test_can_parse_single_line_content(self): 9 | content = """%random_last_name(first, second)%""" 10 | functions = FunctionHandler.parse_content(content) 11 | 12 | assert len(functions) == 1 13 | function = functions[0] 14 | 15 | assert function.full == "%random_last_name(first, second)%" 16 | assert function.name == "random_last_name" 17 | assert function.params == ["first", "second"] 18 | 19 | def test_can_parse_multi_line_content(self): 20 | content = """ 21 | %random_last_name(first, second)% 22 | %random_first_name(third, fourth)% 23 | """ 24 | functions = FunctionHandler.parse_content(content) 25 | 26 | assert len(functions) == 2 27 | 28 | assert functions[0].full == "%random_last_name(first, second)%" 29 | assert functions[0].name == "random_last_name" 30 | assert functions[0].params == ["first", "second"] 31 | 32 | assert functions[1].full == "%random_first_name(third, fourth)%" 33 | assert functions[1].name == "random_first_name" 34 | assert functions[1].params == ["third", "fourth"] 35 | 36 | def test_wont_parse_incorrect_functions(self): 37 | content = """ 38 | %% 39 | %random_first_name% 40 | %random_first_name()% 41 | %random_first_%name()% 42 | %random_first_name(first)% 43 | %random_first_name%(first, second)% 44 | %random_first_name(first, second)% 45 | %random_first_name%(first,second)% 46 | %random_first_name(first,second)% 47 | 48 | { "first_name": "%random_first_name()%", "last_name": "%random_last_name(abc)%" } 49 | """ 50 | 51 | functions = FunctionHandler.parse_content(content) 52 | 53 | assert len(functions) == 7 54 | 55 | assert functions[0].full == "%random_first_name()%" 56 | assert functions[0].name == "random_first_name" 57 | assert functions[0].params == [] 58 | 59 | assert functions[1].full == "%name()%" 60 | assert functions[1].name == "name" 61 | assert functions[1].params == [] 62 | 63 | assert functions[2].full == "%random_first_name(first)%" 64 | assert functions[2].name == "random_first_name" 65 | assert functions[2].params == ["first"] 66 | 67 | assert functions[3].full == "%random_first_name(first, second)%" # Space 68 | assert functions[3].name == "random_first_name" 69 | assert functions[3].params == ["first", "second"] 70 | 71 | assert functions[4].full == "%random_first_name(first,second)%" # No space 72 | assert functions[4].name == "random_first_name" 73 | assert functions[4].params == ["first", "second"] 74 | 75 | assert functions[5].full == "%random_first_name()%" 76 | assert functions[5].name == "random_first_name" 77 | assert functions[5].params == [] 78 | 79 | assert functions[6].full == "%random_last_name(abc)%" 80 | assert functions[6].name == "random_last_name" 81 | assert functions[6].params == ["abc"] 82 | 83 | def test_can_execute_functions(self): 84 | content = """ 85 | { 86 | "first_name": "%random_first_name(m)%", 87 | "last_name": "%random_last_name()%", 88 | "age": %random_int(0, 50)% 89 | } 90 | """ 91 | 92 | mock_functions = { 93 | "random_first_name": lambda x: "Harry", 94 | "random_last_name": lambda: "Canyon", 95 | "random_int": lambda x, y: 42, 96 | } 97 | 98 | exec_content = FunctionHandler.run(content, mock_functions) 99 | 100 | assert ( 101 | exec_content 102 | == """ 103 | { 104 | "first_name": "Harry", 105 | "last_name": "Canyon", 106 | "age": 42 107 | } 108 | """ 109 | ) 110 | 111 | def test_will_raise_function_exception(self): 112 | content = """ 113 | { 114 | "age": "%random_int(5, abc)%", 115 | } 116 | """ 117 | 118 | def add_two(x, y): 119 | val = int(x) + int(y) 120 | return val 121 | 122 | mock_functions = {"random_int": add_two} # Should fail 123 | 124 | with pytest.raises(FunctionException): 125 | FunctionHandler.run(content, mock_functions) 126 | -------------------------------------------------------------------------------- /tests/unit/test_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from apyr.utils import get_digest, get_project_root 6 | 7 | 8 | class TestUtils: 9 | @pytest.mark.dependency(name="project_root") 10 | def test_can_get_project_root(self): 11 | base_dir = get_project_root() 12 | existing_path = base_dir.joinpath("pyproject.toml") 13 | 14 | assert os.path.isfile(existing_path) 15 | 16 | @pytest.mark.dependency(depends=["project_root"]) 17 | def test_hash_same_file(self): 18 | string1_path = str(get_project_root().joinpath("tests/data/string1.txt")) 19 | 20 | initial_hash = get_digest(string1_path) 21 | duplicate_hash = get_digest(string1_path) 22 | 23 | assert initial_hash == duplicate_hash 24 | 25 | @pytest.mark.dependency(depends=["project_root"]) 26 | def test_hash_diff_file(self): 27 | string1_path = str(get_project_root().joinpath("tests/data/string1.txt")) 28 | string2_path = str(get_project_root().joinpath("tests/data/string2.txt")) 29 | 30 | first_hash = get_digest(string1_path) 31 | second_hash = get_digest(string2_path) 32 | 33 | assert first_hash != second_hash 34 | -------------------------------------------------------------------------------- /tests/unit/test_validators.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from apyr.models import Endpoint 4 | 5 | 6 | class TestValidators: 7 | def test_setting_both_content_and_content_path_raises_value_error(self): 8 | endpoint_json = { 9 | "method": "POST", 10 | "path": "test/employee", 11 | "media_type": "text", 12 | "status_code": 500, 13 | "content": "Test", 14 | "content_path": "Test2", 15 | } 16 | 17 | with pytest.raises(ValueError): 18 | Endpoint(**endpoint_json) 19 | 20 | def test_setting_only_content_is_successful(self): 21 | endpoint_json = { 22 | "method": "POST", 23 | "path": "test/employee", 24 | "media_type": "text", 25 | "status_code": 500, 26 | "content": "Test", 27 | } 28 | 29 | endpoint = Endpoint(**endpoint_json) 30 | 31 | assert endpoint.content == "Test" 32 | assert endpoint.content_path is None 33 | 34 | def test_setting_only_content_path_is_successful(self): 35 | endpoint_json = { 36 | "method": "POST", 37 | "path": "test/employee", 38 | "media_type": "text", 39 | "status_code": 500, 40 | "content_path": "Test", 41 | } 42 | 43 | endpoint = Endpoint(**endpoint_json) 44 | 45 | assert endpoint.content_path == "Test" 46 | assert endpoint.content is None 47 | 48 | def test_setting_none_is_successful(self): 49 | endpoint_json = { 50 | "method": "POST", 51 | "path": "test/employee", 52 | "media_type": "text", 53 | "status_code": 500, 54 | } 55 | 56 | endpoint = Endpoint(**endpoint_json) 57 | 58 | assert endpoint.content_path is None 59 | assert endpoint.content is None 60 | --------------------------------------------------------------------------------