├── .bumpversion.cfg ├── .coveragerc ├── .deepsource.toml ├── .editorconfig ├── .env ├── .github └── FUNDING.yml ├── .gitignore ├── .isort.cfg ├── .travis.yml ├── ACKNOWLEDGEMENTS.md ├── ARCHITECTURE.md ├── CHANGELOG.md ├── CODING_STANDARD.md ├── CONTRIBUTING.md ├── EXTENDING.md ├── FAQ.md ├── LICENSE ├── MANIFEST.in ├── Pipfile ├── README.md ├── SECURITY.md ├── appveyor.yml ├── artwork ├── example.gif ├── koala.png ├── logo.png ├── logo.xcf ├── t-shirt.png └── t-shirt.xcf ├── benchmarks ├── http │ ├── .env │ ├── RESULTS.md │ ├── bobo_test.py │ ├── bottle_test.py │ ├── cherrypy_test.py │ ├── falcon_test.py │ ├── flask_test.py │ ├── hug_test.py │ ├── muffin_test.py │ ├── pyramid_test.py │ ├── requirements.txt │ ├── runner.sh │ └── tornado_test.py └── internal │ └── argument_populating.py ├── docker ├── app │ └── Dockerfile ├── docker-compose.yml ├── gunicorn │ └── Dockerfile ├── template │ ├── __init__.py │ └── handlers │ │ ├── birthday.py │ │ └── hello.py └── workspace │ └── Dockerfile ├── documentation ├── AUTHENTICATION.md ├── CUSTOM_CONTEXT.md ├── DIRECTIVES.md ├── OUTPUT_FORMATS.md ├── ROUTING.md └── TYPE_ANNOTATIONS.md ├── examples ├── authentication.py ├── cli.py ├── cli_multiple.py ├── cli_object.py ├── cors_middleware.py ├── cors_per_route.py ├── docker_compose_with_mongodb │ ├── Dockerfile │ ├── README.md │ ├── app.py │ ├── docker-compose.yml │ └── requirements.txt ├── docker_nginx │ ├── Dockerfile │ ├── Makefile │ ├── README.md │ ├── api │ │ ├── __init__.py │ │ └── __main__.py │ ├── config │ │ └── nginx │ │ │ └── nginx.conf │ ├── docker-compose.dev.yml │ ├── docker-compose.yml │ └── setup.py ├── document.html ├── file_upload_example.py ├── force_https.py ├── happy_birthday.py ├── hello_world.py ├── html_serve.py ├── image_serve.py ├── import_example │ ├── __init__.py │ ├── example_resource.py │ └── import_example_server.py ├── marshmallow_example.py ├── matplotlib │ ├── additional_requirements.txt │ └── plot.py ├── multi_file_cli │ ├── __init__.py │ ├── api.py │ └── sub_api.py ├── multiple_files │ ├── README.md │ ├── __init__.py │ ├── api.py │ ├── part_1.py │ └── part_2.py ├── on_startup.py ├── override_404.py ├── pil_example │ ├── additional_requirements.txt │ └── pill.py ├── post_body_example.py ├── quick_server.py ├── quick_start │ ├── first_step_1.py │ ├── first_step_2.py │ └── first_step_3.py ├── redirects.py ├── return_400.py ├── secure_auth_with_db_example.py ├── sink_example.py ├── smtp_envelope_example.py ├── sqlalchemy_example │ ├── Dockerfile │ ├── demo │ │ ├── api.py │ │ ├── app.py │ │ ├── authentication.py │ │ ├── base.py │ │ ├── context.py │ │ ├── directives.py │ │ ├── models.py │ │ └── validation.py │ ├── docker-compose.yml │ └── requirements.txt ├── static_serve.py ├── streaming_movie_server │ ├── movie.mp4 │ └── movie_server.py ├── test_happy_birthday.py ├── unicode_output.py ├── use_socket.py ├── versioning.py └── write_once.py ├── hug ├── __init__.py ├── __main__.py ├── _empty.py ├── _version.py ├── api.py ├── authentication.py ├── decorators.py ├── defaults.py ├── development_runner.py ├── directives.py ├── exceptions.py ├── format.py ├── input_format.py ├── interface.py ├── introspect.py ├── json_module.py ├── middleware.py ├── output_format.py ├── redirect.py ├── route.py ├── routing.py ├── store.py ├── test.py ├── this.py ├── transform.py ├── types.py ├── use.py └── validate.py ├── pyproject.toml ├── requirements ├── build.txt ├── build_common.txt ├── build_style_tools.txt ├── build_windows.txt ├── common.txt ├── development.txt └── release.txt ├── scripts ├── before_install.sh └── install.sh ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── conftest.py ├── constants.py ├── data │ └── index.html ├── fixtures.py ├── module_fake.py ├── module_fake_http_and_cli.py ├── module_fake_many_methods.py ├── module_fake_post.py ├── module_fake_simple.py ├── test_api.py ├── test_async.py ├── test_authentication.py ├── test_context_factory.py ├── test_coroutines.py ├── test_decorators.py ├── test_directives.py ├── test_documentation.py ├── test_exceptions.py ├── test_full_request.py ├── test_global_context.py ├── test_input_format.py ├── test_interface.py ├── test_introspect.py ├── test_main.py ├── test_middleware.py ├── test_output_format.py ├── test_redirect.py ├── test_route.py ├── test_routing.py ├── test_store.py ├── test_test.py ├── test_this.py ├── test_transform.py ├── test_types.py ├── test_use.py └── test_validate.py └── tox.ini /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 2.6.1 3 | 4 | [bumpversion:file:.env] 5 | 6 | [bumpversion:file:setup.py] 7 | 8 | [bumpversion:file:hug/_version.py] 9 | 10 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | include = hug/*.py 3 | omit = hug/development_runner.py 4 | exclude_lines = def hug 5 | def serve 6 | def _start_api 7 | sys.stdout.buffer.write 8 | class Socket 9 | pragma: no cover 10 | except ImportError: 11 | if MARSHMALLOW_MAJOR_VERSION is None or MARSHMALLOW_MAJOR_VERSION == 2: 12 | -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | test_patterns = ["tests/**"] 4 | 5 | exclude_patterns = [ 6 | "examples/**", 7 | "benchmarks/**" 8 | ] 9 | 10 | [[analyzers]] 11 | name = "python" 12 | enabled = true 13 | 14 | [analyzers.meta] 15 | runtime_version = "3.x.x" -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.py] 4 | max_line_length = 100 5 | indent_style = space 6 | indent_size = 4 7 | ignore_frosted_errors = E103 8 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | OPEN_PROJECT_NAME="hug" 3 | 4 | if [ "$PROJECT_NAME" = "$OPEN_PROJECT_NAME" ]; then 5 | return 6 | fi 7 | 8 | if [ ! -f ".env" ]; then 9 | return 10 | fi 11 | 12 | export PROJECT_NAME=$OPEN_PROJECT_NAME 13 | export PROJECT_DIR="$PWD" 14 | export PROJECT_VERSION="2.6.1" 15 | 16 | if [ ! -d "venv" ]; then 17 | if ! hash pyvenv 2>/dev/null; then 18 | function pyvenv() 19 | { 20 | if hash python3.7 2>/dev/null; then 21 | python3.7 -m venv $@ 22 | elif hash pyvenv-3.6 2>/dev/null; then 23 | pyvenv-3.6 $@ 24 | elif hash pyvenv-3.5 2>/dev/null; then 25 | pyvenv-3.5 $@ 26 | elif hash pyvenv-3.4 2>/dev/null; then 27 | pyvenv-3.4 $@ 28 | elif hash pyvenv-3.3 2>/dev/null; then 29 | pyvenv-3.3 $@ 30 | elif hash pyvenv-3.2 2>/dev/null; then 31 | pyvenv-3.2 $@ 32 | else 33 | python3 -m venv $@ 34 | fi 35 | } 36 | fi 37 | 38 | echo "Making venv for $PROJECT_NAME" 39 | pyvenv venv 40 | . venv/bin/activate 41 | pip install -r requirements/development.txt 42 | python setup.py install 43 | fi 44 | 45 | . venv/bin/activate 46 | 47 | # Let's make sure this is a hubflow enabled repo 48 | yes | git hf init >/dev/null 2>/dev/null 49 | 50 | # Quick directory switching 51 | alias root="cd $PROJECT_DIR" 52 | alias project="root; cd $PROJECT_NAME" 53 | alias tests="root; cd tests" 54 | alias examples="root; cd examples" 55 | alias requirements="root; cd requirements" 56 | alias run_tests="_test" 57 | 58 | 59 | function open { 60 | (root 61 | $CODE_EDITOR hug/*.py setup.py tests/*.py examples/*.py examples/*/*.py README.md tox.ini .gitignore CHANGELOG.md setup.cfg .editorconfig .env .coveragerc .travis.yml requirements/*.txt) 62 | } 63 | 64 | 65 | function clean { 66 | (root 67 | isort hug/*.py setup.py tests/*.py 68 | black -l 100 hug) 69 | } 70 | 71 | 72 | function check { 73 | (root 74 | frosted hug/*.py) 75 | } 76 | 77 | 78 | function _test { 79 | (root 80 | tox) 81 | } 82 | 83 | 84 | function coverage { 85 | (root 86 | $BROWSER htmlcov/index.html) 87 | } 88 | 89 | 90 | function load { 91 | (root 92 | python setup.py install) 93 | } 94 | 95 | 96 | function unload { 97 | (root 98 | pip uninstall hug) 99 | } 100 | 101 | 102 | function install { 103 | (root 104 | sudo python setup.py install) 105 | } 106 | 107 | 108 | function update { 109 | (root 110 | pip install -r requirements/development.txt -U) 111 | } 112 | 113 | 114 | function distribute { 115 | (root 116 | pip install pypandoc 117 | python -c "import pypandoc; pypandoc.convert('README.md', 'rst')" || exit 1 118 | python setup.py sdist upload) 119 | } 120 | 121 | 122 | function version() 123 | { 124 | echo $PROJECT_VERSION 125 | } 126 | 127 | 128 | function new_version() 129 | { 130 | (root 131 | if [ -z "$1" ]; then 132 | echo "You must supply a new version to replace the old version with" 133 | return 134 | fi 135 | 136 | sed -i "s/$PROJECT_VERSION/$1/" .env setup.py hug/_version.py) 137 | export PROJECT_VERSION=$1 138 | } 139 | 140 | 141 | function new_version_patch() 142 | { 143 | (root 144 | bumpversion --allow-dirty patch) 145 | } 146 | 147 | 148 | function new_version_minor() 149 | { 150 | (root 151 | bumpversion --allow-dirty minor) 152 | } 153 | 154 | 155 | function new_version_major() 156 | { 157 | (root 158 | bumpversion --allow-dirty major) 159 | } 160 | 161 | 162 | function leave { 163 | export PROJECT_NAME="" 164 | export PROJECT_DIR="" 165 | 166 | unalias root 167 | unalias project 168 | unalias tests 169 | unalias examples 170 | unalias requirements 171 | unalias test 172 | 173 | unset -f _start 174 | unset -f _end 175 | 176 | 177 | unset -f open 178 | unset -f clean 179 | unset -f _test 180 | unset -f coverage 181 | unset -f load 182 | unset -f unload 183 | unset -f install 184 | unset -f update 185 | unset -f distribute 186 | unset -f version 187 | unset -f new_version 188 | 189 | unset -f leave 190 | 191 | deactivate 192 | } 193 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | tidelift: "pypi/hug" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | .DS_Store 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | build 10 | eggs 11 | .eggs 12 | parts 13 | var 14 | sdist 15 | develop-eggs 16 | .installed.cfg 17 | lib 18 | lib64 19 | MANIFEST 20 | 21 | # Installer logs 22 | pip-log.txt 23 | npm-debug.log 24 | pip-selfcheck.json 25 | 26 | # Unit test / coverage reports 27 | .coverage 28 | .tox 29 | nosetests.xml 30 | htmlcov 31 | .cache 32 | .pytest_cache 33 | 34 | # Translations 35 | *.mo 36 | 37 | # Mr Developer 38 | .mr.developer.cfg 39 | .project 40 | .pydevproject 41 | 42 | # SQLite 43 | test_exp_framework 44 | 45 | # npm 46 | node_modules/ 47 | 48 | # dolphin 49 | .directory 50 | libpeerconnection.log 51 | 52 | # setuptools 53 | dist 54 | 55 | # IDE Files 56 | atlassian-ide-plugin.xml 57 | .idea/ 58 | *.swp 59 | *.kate-swp 60 | .ropeproject/ 61 | 62 | # Python3 Venv Files 63 | .venv/ 64 | bin/ 65 | include/ 66 | lib/ 67 | lib64 68 | pyvenv.cfg 69 | share/ 70 | venv/ 71 | 72 | # Cython 73 | *.c 74 | 75 | # Emacs backup 76 | *~ 77 | 78 | # VSCode 79 | /.vscode 80 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | multi_line_output=3 3 | include_trailing_comma=True 4 | force_grid_wrap=0 5 | use_parentheses=True 6 | line_length=100 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | cache: pip 4 | matrix: 5 | include: 6 | - os: linux 7 | sudo: required 8 | python: 3.5 9 | - os: linux 10 | sudo: required 11 | python: 3.6 12 | - os: linux 13 | sudo: required 14 | python: 3.8 15 | - os: linux 16 | sudo: required 17 | python: 3.7 18 | env: TOXENV=py37-marshmallow2 19 | - os: linux 20 | sudo: required 21 | python: pypy3.5-6.0 22 | env: TOXENV=pypy3-marshmallow2 23 | - os: linux 24 | sudo: required 25 | python: 3.7 26 | env: TOXENV=py37-marshmallow3 27 | - os: linux 28 | sudo: required 29 | python: 3.7 30 | env: TOXENV=py37-black 31 | - os: linux 32 | sudo: required 33 | python: 3.7 34 | env: TOXENV=py37-flake8 35 | - os: linux 36 | sudo: required 37 | python: 3.7 38 | env: TOXENV=py37-bandit 39 | - os: linux 40 | sudo: required 41 | python: 3.7 42 | env: TOXENV=py37-vulture 43 | - os: linux 44 | sudo: required 45 | python: 3.7 46 | env: TOXENV=py37-isort 47 | - os: linux 48 | sudo: required 49 | python: 3.7 50 | env: TOXENV=py37-safety 51 | - os: linux 52 | sudo: required 53 | python: pypy3.5-6.0 54 | env: TOXENV=pypy3-marshmallow3 55 | - os: osx 56 | language: generic 57 | env: TOXENV=py36-marshmallow2 58 | - os: osx 59 | language: generic 60 | env: TOXENV=py36-marshmallow3 61 | before_install: 62 | - "./scripts/before_install.sh" 63 | install: 64 | - source ./scripts/install.sh 65 | - pip install tox tox-travis coveralls 66 | script: tox 67 | after_success: coveralls 68 | deploy: 69 | provider: pypi 70 | user: timothycrosley 71 | distributions: sdist bdist_wheel 72 | skip_existing: true 73 | on: 74 | tags: false 75 | branch: master 76 | condition: "$TOXENV = py37-marshmallow2" 77 | password: 78 | secure: Zb8jwvUzsiXNxU+J0cuP/7ZIUfsw9qoENAlIEI5qyly8MFyHTM/HvdriQJM0IFCKiOSU4PnCtkL6Yt+M4oA7QrjsMrxxDo2ekZq2EbsxjTNxzXnnyetTYh94AbQfZyzliMyeccJe4iZJdoJqYG92BwK0cDyRV/jSsIL6ibkZgjKuBP7WAKbZcUVDwOgL4wEfKztTnQcAYUCmweoEGt8r0HP1PXvb0jt5Rou3qwMpISZpBYU01z38h23wtOi8jylSvYu/LiFdV8fKslAgDyDUhRdbj9DMBVBlvYT8dlWNpnrpphortJ6H+G82NbFT53qtV75CrB1j/wGik1HQwUYfhfDFP1RYgdXfHeKYEMWiokp+mX3O9uv/AoArAX5Q4auFBR8VG3BB6H96BtNQk5x/Lax7eWMZI0yzsGuJtWiDyeI5Ah5EBOs89bX+tlIhYDH5jm44ekmkKJJlRiiry1k2oSqQL35sLI3S68vqzo0vswsMhLq0/dGhdUxf1FH9jJHHbSxSV3HRSk045w9OYpLC2GULytSO9IBOFFOaTJqb8MXFZwyb9wqZbQxELBrfH3VocVq85E1ZJUT4hsDkODNfe6LAeaDmdl8V1T8d+KAs62pX+4BHDED+LmHI/7Ha/bf6MkXloJERKg3ocpjr69QADc3x3zuyArQ2ab1ncrer+yk= 79 | -------------------------------------------------------------------------------- /ACKNOWLEDGEMENTS.md: -------------------------------------------------------------------------------- 1 | Core Developers 2 | =================== 3 | - Timothy Edmund Crosley (@timothycrosley) 4 | - Brandon Hoffman (@BrandonHoffman) 5 | - Jason Tyler (@jay-tyler) 6 | - Fabian Kochem (@vortec) 7 | 8 | Notable Bug Reporters 9 | =================== 10 | - Michael Buckner (@michaelbuckner) 11 | - Carl Neuhaus (@carlneuhaus) 12 | - Eirik Rye (@eirikrye) 13 | - Matteo Bertini (@naufraghi) 14 | - Erwin Haasnoot (@ErwinHaasnoot) 15 | - Aris Pikeas (@pikeas) 16 | 17 | Code Contributors 18 | =================== 19 | - Kostas Dizas (@kostasdizas) 20 | - Ali-Akber Saifee (@alisaifee) 21 | - @arpesenti 22 | - Eirik Rye (@eirikrye) 23 | - Matteo Bertini (@naufraghi) 24 | - Trevor Scheitrum (@trevorscheitrum) 25 | - Ian Wagner (@ianthetechie) 26 | - Erwin Haasnoot (@ErwinHaasnoot) 27 | - Kirk Leon Guerrero (@kirklg) 28 | - Ergo_ (@johnlam) 29 | - Rodrigue Cloutier (@rodcloutier) 30 | - KhanhIceTea (@khanhicetea) 31 | - Prashant Sinha (@PrashntS) 32 | - Alan Lu (@cag) 33 | - Soloman Weng (@soloman1124) 34 | - Evan Owen (@thatGuy0923) 35 | - Gemedet (@gemedet) 36 | - Garrett Squire (@gsquire) 37 | - Haïkel Guémar (@hguemar) 38 | - Eshin Kunishima (@mikoim) 39 | - Mike Adams (@mikeadamz) 40 | - Michal Bultrowicz (@butla) 41 | - Bogdan (@spock) 42 | - @banteg 43 | - Philip Bjorge (@philipbjorge) 44 | - Daniel Metz (@danielmmetz) 45 | - Alessandro Amici (@alexamici) 46 | - Trevor Bekolay (@tbekolay) 47 | - Elijah Wilson (@tizz98) 48 | - Chelsea Dole (@chelseadole) 49 | - Antti Kaihola (@akaihola) 50 | - Christopher Goes (@GhostOfGoes) 51 | - Stanislav (@atmo) 52 | - Lordran (@xzycn) 53 | - Stephan Fitzpatrick (@knowsuchagency) 54 | - Edvard Majakari (@EdvardM) 55 | - Sai Charan (@mrsaicharan1) 56 | 57 | Documenters 58 | =================== 59 | - Timothy Cyrus (@tcyrus) 60 | - M.Yasoob Ullah Khalid (@yasoob) 61 | - Lionel Montrieux (@lmontrieux) 62 | - Ian Wagner (@ianthetechie) 63 | - Andrew Murray (@radarhere) 64 | - Tim (@timlyo) 65 | - Sven-Hendrik Haase (@svenstaro) 66 | - Matt Caldwell (@mattcaldwell) 67 | - berdario (@berdario) 68 | - Cory Taylor (@coryandrewtaylor) 69 | - James C. (@JamesMCo) 70 | - Ally Weir (@allyjweir) 71 | - Steven Loria (@sloria) 72 | - Patrick Abeya (@wombat2k) 73 | - Ergo_ (@johnlam) 74 | - Adeel Khan (@adeel) 75 | - Benjamin Williams (@benjaminjosephw) 76 | - @gdw2 77 | - Thierry Colsenet (@ThierryCols) 78 | - Shawn Q Jackson (@gt50) 79 | - Bernhard E. Reiter (@bernhardreiter) 80 | - Adam McCarthy (@mccajm) 81 | - Sobolev Nikita (@sobolevn) 82 | - Chris (@ckhrysze) 83 | - Amanda Crosley (@waddlelikeaduck) 84 | - Chelsea Dole (@chelseadole) 85 | - Joshua Crowgey (@jcrowgey) 86 | - Antti Kaihola (@akaihola) 87 | - Simon Ince (@Simon-Ince) 88 | - Edvard Majakari (@EdvardM) 89 | 90 | 91 | 92 | -------------------------------------------- 93 | 94 | A sincere thanks to everyone who has helped make hug the great Python3 project it is today! 95 | 96 | ~Timothy Crosley 97 | -------------------------------------------------------------------------------- /CODING_STANDARD.md: -------------------------------------------------------------------------------- 1 | Coding Standard 2 | ========= 3 | Any submission to this project should closely follow the [PEP 8](https://www.python.org/dev/peps/pep-0008/) coding guidelines with the exceptions: 4 | 5 | 1. Lines can be up to 100 characters long. 6 | 2. Single letter or otherwise nondescript variable names are prohibited. 7 | 8 | Standards for new hug modules 9 | ========= 10 | New modules added to the hug project should all live directly within the `hug/` directory without nesting. 11 | If the modules are meant only for internal use within hug they should be prefixed with a leading underscore. For example, `def _internal_function`. 12 | Modules should contain a doc string at the top that gives a general explanation of the purpose and then 13 | restates the project's use of the MIT license. 14 | 15 | There should be a `tests/test_$MODULE_NAME.py` file created to correspond to every new module that contains 16 | test coverage for the module. Ideally, tests should be 1:1 (one test object per code object, one test method 17 | per code method) to the extent cleanly possible. 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to hug 2 | ========= 3 | Looking for a growing and useful open source project to contribute to? 4 | Want your contributions to be warmly welcomed and acknowledged? 5 | Want a free project t-shirt to show you're a contributor? 6 | Welcome! You have found the right place. 7 | 8 | hug is growing quickly and needs awesome contributors like *you* to help the project reach its full potential. 9 | From reporting issues, writing documentation, implementing new features, fixing bugs and creating logos to providing additional usage examples - any contribution you can provide will be greatly appreciated and acknowledged. 10 | 11 | Getting hug set up for local development 12 | ========= 13 | The first step when contributing to any project is getting it set up on your local machine. hug aims to make this as simple as possible. 14 | 15 | Account Requirements: 16 | 17 | - [A valid GitHub account](https://github.com/join) 18 | 19 | Base System Requirements: 20 | 21 | - Python3.5+ 22 | - Python3-venv (included with most Python3 installations but some Ubuntu systems require that it be installed separately) 23 | - bash or a bash compatible shell (should be auto-installed on Linux / Mac) 24 | - [autoenv](https://github.com/kennethreitz/autoenv) (optional) 25 | 26 | Once you have verified that you system matches the base requirements you can start to get the project working by following these steps: 27 | 28 | 1. [Fork the project on GitHub](https://github.com/timothycrosley/hug/fork). 29 | 2. Clone your fork to your local file system: 30 | `git clone https://github.com/$GITHUB_ACCOUNT/hug.git` 31 | 3. `cd hug` 32 | - Create a virtual environment using [`python3 -m venv $ENV_NAME`](https://docs.python.org/3/library/venv.html) or `mkvirtualenv` (from [virtualenvwrapper](https://virtualenvwrapper.readthedocs.io/en/latest/)) 33 | - If you have autoenv set-up correctly, simply press Y and then wait for the environment to be set up for you. 34 | - If you don't have autoenv set-up, run `source .env` to set up the local environment. You will need to run this script every time you want to work on the project - though it will not cause the entire set up process to re-occur. 35 | 4. Run `test` to verify your everything is set up correctly. If the tests all pass, you have successfully set up hug for local development! If not, you can ask for help diagnosing the error [here](https://gitter.im/timothycrosley/hug). 36 | 37 | Install dependencies by running `pip install -r requirements/release.txt`, 38 | and optional build dependencies 39 | by running `pip install -r requirements/build.txt` 40 | or `pip install -r requirements/build_windows.txt`. 41 | 42 | Install Hug itself with `pip install .` or `pip install -e .` (for editable mode). 43 | This will compile all modules with [Cython](https://cython.org/) if it's installed in the environment. 44 | You can skip Cython compilation using `pip install --install-option=--without-cython .` (this works with `-e` as well). 45 | 46 | Making a contribution 47 | ========= 48 | Congrats! You're now ready to make a contribution! Use the following as a guide to help you reach a successful pull-request: 49 | 50 | 1. Check the [issues page](https://github.com/timothycrosley/hug/issues) on GitHub to see if the task you want to complete is listed there. 51 | - If it's listed there, write a comment letting others know you are working on it. 52 | - If it's not listed in GitHub issues, go ahead and log a new issue. Then add a comment letting everyone know you have it under control. 53 | - If you're not sure if it's something that is good for the main hug project and want immediate feedback, you can discuss it [here](https://gitter.im/timothycrosley/hug). 54 | 2. Create an issue branch for your local work `git checkout -b issue/$ISSUE-NUMBER`. 55 | 3. Do your magic here. 56 | 4. Run `clean` to automatically sort your imports according to pep-8 guidelines. 57 | 5. Ensure your code matches hug's latest coding standards defined [here](https://github.com/timothycrosley/hug/blob/develop/CODING_STANDARD.md). It's important to focus to focus on making your code efficient as hug is used as a base framework for several performance critical APIs. 58 | 7. Submit a pull request to the main project repository via GitHub. 59 | 60 | Thanks for the contribution! It will quickly get reviewed, and, once accepted, will result in your name being added to the ACKNOWLEDGEMENTS.md list :). 61 | 62 | Getting a free t-shirt 63 | ========= 64 | ![hug t-shirt](https://raw.github.com/timothycrosley/hug/develop/artwork/t-shirt.png) 65 | 66 | Once you have finished contributing to the project, send your mailing address and shirt size to timothy.crosley@gmail.com, with the title hug Shirt for @$GITHUB_USER_NAME. 67 | 68 | When the project has reached 100 contributors, I will be sending every one of the original hundred contributors a t-shirt to commemorate their awesome work. 69 | 70 | Thank you! 71 | ========= 72 | I can not tell you how thankful I am for the hard work done by hug contributors like you. hug could not be the exciting and useful framework it is today without your help. 73 | 74 | Thank you! 75 | 76 | ~Timothy Crosley 77 | -------------------------------------------------------------------------------- /EXTENDING.md: -------------------------------------------------------------------------------- 1 | Building hug extensions 2 | ========= 3 | Want to extend hug to tackle new problems? Integrate a new form of authentication? Add new useful types? 4 | Awesome! Here are some guidlines to help you get going and make a world class hug extension 5 | that you will be proud to have showcased to all hug users. 6 | 7 | How are extensions built? 8 | ========= 9 | hug extensions should be built like any other python project and uploaded to PYPI. What makes a hug extension a *hug* extension is simply it's name and the fact it contains within its Python code utilities and classes that extend hugs capabilties. 10 | 11 | Naming your extension 12 | ========= 13 | All hug extensions should be prefixed with `hug_` for easy disscovery on PYPI. Additionally, there are a few more exact prefixes that can be optionally be added to help steer users to what your extensions accomplishes: 14 | 15 | - `hug_types_` should be used if your extensions is used primarily to add new types to hug (for example: hug_types_numpy). 16 | - `hug_authentication_` if your extension is used primarily to add a new authentication type to hug (for example: hug_authentication_oath2) 17 | - `hug_output_format_` if your extension is used primarily to add a new output format to hug (for example: hug_output_format_svg) 18 | - `hug_input_format_` if your extension is used primarily to add a new input format to hug (for example: hug_input_format_html) 19 | - `hug_validate_` if your extension is used primarily to add a new overall validator to hug (for example: hug_validate_no_null). 20 | - `hug_transform_` if your extension is used primarily to add a new hug transformer (for example: hug_transform_add_time) 21 | - `hug_middleware_` if your extension is used primarily to add a middleware to hug (for example: hug_middleware_redis_session) 22 | 23 | For any more complex or general use case that doesn't fit into these predefined categories or combines many of them, it 24 | is perfectly suitable to simply prefix your extension with `hug_`. For example: hug_geo could combine hug types, hug input formats, and hug output formats making it a good use case for a simply prefixed extension. 25 | 26 | Building Recommendations 27 | ========= 28 | Ideally, hug extensions should be built in the same manner as hug itself. This means 100% test coverage using pytest, decent performance, pep8 compliance, and built in optional compiling with Cython. None of this is strictly required, but will help give users of your extension faith that it wont slow things down or break their setup unexpectedly. 29 | 30 | Registering your extension 31 | ========= 32 | Once you have finished developing and testing your extension, you can help increase others ability to discover it by registering it. The first place an extension should be registered is on PYPI, just like any other Python Package. In addition to that you can add your extension to the list of extensions on hug's [github wiki](https://github.com/timothycrosley/hug/wiki/Hug-Extensions). 33 | 34 | Thank you 35 | ========= 36 | A sincere thanks to anyone that takes the time to develop and register an extension for hug. You are helping to make hug a more complete eco-system for everyuser out there, and paving the way for a solid foundation into the future. 37 | 38 | Thanks! 39 | 40 | ~Timothy Crosley 41 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions about Hug 2 | 3 | For more examples, check out Hug's [documentation](https://github.com/timothycrosley/hug/tree/develop/documentation) and [examples](https://github.com/timothycrosley/hug/tree/develop/examples) Github directories, and its [website](http://www.hug.rest/). 4 | 5 | ## General Questions 6 | 7 | Q: *Can I use Hug with a web framework -- Django for example?* 8 | 9 | A: You can use Hug alongside Django or the web framework of your choice, but it does have drawbacks. You would need to run hug on a separate, hug-exclusive server. You can also [mount Hug as a WSGI app](https://pythonhosted.org/django-wsgi/embedded-apps.html), embedded within your normal Django app. 10 | 11 | Q: *Is Hug compatabile with Python 2?* 12 | 13 | A: Python 2 is not supported by Hug. However, if you need to account for backwards compatability, there are workarounds. For example, you can wrap the decorators: 14 | 15 | ```Python 16 | def my_get_fn(func, *args, **kwargs): 17 | if 'hug' in globals(): 18 | return hug.get(func, *args, **kwargs) 19 | return func 20 | ``` 21 | 22 | ## Technical Questions 23 | 24 | Q: *I need to ensure the security of my data. Can Hug be used over HTTPS?* 25 | 26 | A: Not directly, but you can utilize [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) with nginx to transmit sensitive data. HTTPS is not part of the standard WSGI application layer, so you must use a WSGI HTTP server (such as uWSGI) to run in production. With this setup, Nginx handles SSL connections, and transfers requests to uWSGI. 27 | 28 | Q: *How can I serve static files from a directory using Hug?* 29 | 30 | A: For a static HTML page, you can just set the proper output format as: `output=hug.output_format.html`. To see other examples, check out the [html_serve](https://github.com/timothycrosley/hug/blob/develop/examples/html_serve.py) example, the [image_serve](https://github.com/timothycrosley/hug/blob/develop/examples/image_serve.py) example, and the more general [static_serve](https://github.com/timothycrosley/hug/blob/develop/examples/static_serve.py) example within `hug/examples`. 31 | 32 | Most basic examples will use a format that looks something like this: 33 | 34 | ```Python 35 | @hug.static('/static') 36 | def my_static_dirs(): 37 |  return('/home/www/path-to-static-dir') 38 | ``` 39 | 40 | Q: *Does Hug support autoreloading?* 41 | 42 | A: Hug supports any WSGI server that uses autoreloading, for example Gunicorn and uWSGI. The scripts for initializing autoreload for them are, respectively: 43 | 44 | Gunicorn: `gunicorn --reload app:__hug_wsgi__` 45 | uWSGI: `--py-autoreload 1 --http :8000 -w app:__hug_wsgi__` 46 | 47 | Q: *How can I access a list of my routes?* 48 | 49 | A: You can access a list of your routes by using the routes object on the HTTP API: 50 | 51 | `__hug_wsgi__.http.routes` 52 | 53 | It will return to you a structure of "base_url -> url -> HTTP method -> Version -> Python Handler". Therefore, for example, if you have no base_url set and you want to see the list of all URLS, you could run: 54 | 55 | `__hug_wsgi__.http.routes[''].keys()` 56 | 57 | Q: *How can I configure a unique 404 route?* 58 | 59 | A: By default, Hug will call `documentation_404()` if no HTTP route is found. However, if you want to configure other options (such as routing to a directiory, or routing everything else to a landing page) you can use the `@hug.sink('/')` decorator to create a "catch-all" route: 60 | 61 | ```Python 62 | import hug 63 | 64 | @hug.sink('/all') 65 | def my_sink(request): 66 | return request.path.replace('/all', '') 67 | ``` 68 | 69 | For more information, check out the ROUTING.md file within the `hug/documentation` directory. 70 | 71 | Q: *How can I enable CORS* 72 | 73 | A: There are many solutions depending on the specifics of your application. 74 | For most applications, you can use the included cors middleware: 75 | 76 | ``` 77 | import hug 78 | 79 | api = hug.API(__name__) 80 | api.http.add_middleware(hug.middleware.CORSMiddleware(api, max_age=10)) 81 | 82 | 83 | @hug.get("/demo") 84 | def get_demo(): 85 | return {"result": "Hello World"} 86 | ``` 87 | For cases that are more complex then the middleware handles 88 | 89 | [This comment](https://github.com/hugapi/hug/issues/114#issuecomment-342493165) (and the discussion around it) give a good starting off point. 90 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Timothy Crosley 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include *.md -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | 8 | [dev-packages] 9 | 10 | [requires] 11 | python_version = "3.7" 12 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | hug takes security and quality seriously. This focus is why we depend only on thoroughly tested components and utilize static analysis tools (such as bandit and safety) to verify the security of our code base. 4 | If you find or encounter any potential security issues, please let us know right away so we can resolve them. 5 | 6 | ## Supported Versions 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 2.5.6 | :white_check_mark: | 11 | 12 | Currently, only the latest version of hug will receive security fixes. 13 | 14 | ## Reporting a Vulnerability 15 | 16 | To report a security vulnerability, please use the 17 | [Tidelift security contact](https://tidelift.com/security). 18 | Tidelift will coordinate the fix and disclosure. 19 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the 3 | # /E:ON and /V:ON options are not enabled in the batch script intepreter 4 | # See: http://stackoverflow.com/a/13751649/163740 5 | CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\appveyor\\run_with_env.cmd" 6 | TOX_ENV: "pywin" 7 | 8 | matrix: 9 | - PYTHON: "C:\\Python35-x64" 10 | PYTHON_VERSION: "3.5.1" 11 | PYTHON_ARCH: "64" 12 | 13 | install: 14 | # Download setup scripts and unzip 15 | - ps: "wget https://github.com/cloudify-cosmo/appveyor-utils/archive/master.zip -OutFile ./master.zip" 16 | - "7z e master.zip */appveyor/* -oappveyor" 17 | 18 | # Install Python (from the official .msi of http://python.org) and pip when 19 | # not already installed. 20 | - "powershell ./appveyor/install.ps1" 21 | 22 | # Prepend newly installed Python to the PATH of this build (this cannot be 23 | # done from inside the powershell script as it would require to restart 24 | # the parent CMD process). 25 | - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" 26 | 27 | # Check that we have the expected version and architecture for Python 28 | - "python --version" 29 | - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" 30 | 31 | build: false # Not a C# project, build stuff at the test step instead. 32 | 33 | before_test: 34 | - "%CMD_IN_ENV% pip install tox" 35 | 36 | test_script: 37 | - "%CMD_IN_ENV% tox -e %TOX_ENV%" 38 | -------------------------------------------------------------------------------- /artwork/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugapi/hug/e4a3fa40f98487a67351311d0da659a6c9ce88a6/artwork/example.gif -------------------------------------------------------------------------------- /artwork/koala.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugapi/hug/e4a3fa40f98487a67351311d0da659a6c9ce88a6/artwork/koala.png -------------------------------------------------------------------------------- /artwork/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugapi/hug/e4a3fa40f98487a67351311d0da659a6c9ce88a6/artwork/logo.png -------------------------------------------------------------------------------- /artwork/logo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugapi/hug/e4a3fa40f98487a67351311d0da659a6c9ce88a6/artwork/logo.xcf -------------------------------------------------------------------------------- /artwork/t-shirt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugapi/hug/e4a3fa40f98487a67351311d0da659a6c9ce88a6/artwork/t-shirt.png -------------------------------------------------------------------------------- /artwork/t-shirt.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugapi/hug/e4a3fa40f98487a67351311d0da659a6c9ce88a6/artwork/t-shirt.xcf -------------------------------------------------------------------------------- /benchmarks/http/.env: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | OPEN_PROJECT_NAME="hug_benchmark_http" 3 | 4 | if [ "$PROJECT_NAME" = "$OPEN_PROJECT_NAME" ]; then 5 | return 6 | fi 7 | 8 | if [ ! -f ".env" ]; then 9 | return 10 | fi 11 | 12 | export PROJECT_NAME=$OPEN_PROJECT_NAME 13 | export PROJECT_DIR="$PWD" 14 | export PROJECT_VERSION="1.0.0" 15 | 16 | if [ ! -d "venv" ]; then 17 | if ! hash pyvenv 2>/dev/null; then 18 | function pyvenv() 19 | { 20 | if hash pyvenv-3.5 2>/dev/null; then 21 | pyvenv-3.5 $@ 22 | fi 23 | if hash pyvenv-3.4 2>/dev/null; then 24 | pyvenv-3.4 $@ 25 | fi 26 | if hash pyvenv-3.3 2>/dev/null; then 27 | pyvenv-3.3 $@ 28 | fi 29 | if hash pyvenv-3.2 2>/dev/null; then 30 | pyvenv-3.2 $@ 31 | fi 32 | } 33 | fi 34 | 35 | echo "Making venv for $PROJECT_NAME" 36 | pyvenv venv 37 | . venv/bin/activate 38 | pip install -r requirements.txt 39 | fi 40 | 41 | . venv/bin/activate 42 | 43 | # Quick directory switching 44 | alias root="cd $PROJECT_DIR" 45 | 46 | 47 | function run { 48 | (root 49 | . runner.sh) 50 | } 51 | 52 | 53 | function update { 54 | pip install -r requirements.txt 55 | } 56 | 57 | 58 | function leave { 59 | export PROJECT_NAME="" 60 | export PROJECT_DIR="" 61 | 62 | unalias root 63 | 64 | unset -f run 65 | unset -f update 66 | 67 | unset -f leave 68 | 69 | deactivate 70 | } 71 | -------------------------------------------------------------------------------- /benchmarks/http/RESULTS.md: -------------------------------------------------------------------------------- 1 | Latest benchmark results: 2 | 3 | hug_test: 4 | Requests per second: 7518.20 [#/sec] (mean) 5 | Complete requests: 20000 6 | falcon_test: 7 | Requests per second: 8186.67 [#/sec] (mean) 8 | Complete requests: 20000 9 | flask_test: 10 | Requests per second: 5536.62 [#/sec] (mean) 11 | Complete requests: 20000 12 | bobo_test: 13 | Requests per second: 6572.28 [#/sec] (mean) 14 | Complete requests: 20000 15 | cherrypy_test: 16 | Requests per second: 3404.87 [#/sec] (mean) 17 | Complete requests: 20000 18 | pyramid_test: 19 | Requests per second: 5961.53 [#/sec] (mean) 20 | Complete requests: 20000 21 | 22 | -------------------------------------------------------------------------------- /benchmarks/http/bobo_test.py: -------------------------------------------------------------------------------- 1 | import bobo 2 | 3 | 4 | @bobo.query("/text", content_type="text/plain") 5 | def text(): 6 | return "Hello, world!" 7 | 8 | 9 | app = bobo.Application(bobo_resources=__name__) 10 | -------------------------------------------------------------------------------- /benchmarks/http/bottle_test.py: -------------------------------------------------------------------------------- 1 | import bottle 2 | 3 | app = bottle.Bottle() 4 | 5 | 6 | @app.route("/text") 7 | def text(): 8 | return "Hello, world!" 9 | -------------------------------------------------------------------------------- /benchmarks/http/cherrypy_test.py: -------------------------------------------------------------------------------- 1 | import cherrypy 2 | 3 | 4 | class Root(object): 5 | @cherrypy.expose 6 | def text(self): 7 | return "Hello, world!" 8 | 9 | 10 | app = cherrypy.tree.mount(Root()) 11 | cherrypy.log.screen = False 12 | -------------------------------------------------------------------------------- /benchmarks/http/falcon_test.py: -------------------------------------------------------------------------------- 1 | import falcon 2 | 3 | 4 | class Resource(object): 5 | def on_get(self, req, resp): 6 | resp.status = falcon.HTTP_200 7 | resp.content_type = "text/plain" 8 | resp.body = "Hello, world!" 9 | 10 | 11 | app = falcon.API() 12 | app.add_route("/text", Resource()) 13 | -------------------------------------------------------------------------------- /benchmarks/http/flask_test.py: -------------------------------------------------------------------------------- 1 | import flask 2 | 3 | app = flask.Flask(__name__) 4 | 5 | 6 | @app.route("/text") 7 | def text(): 8 | return "Hello, world!" 9 | -------------------------------------------------------------------------------- /benchmarks/http/hug_test.py: -------------------------------------------------------------------------------- 1 | import hug 2 | 3 | 4 | @hug.get("/text", output_format=hug.output_format.text, parse_body=False) 5 | def text(): 6 | return "Hello, World!" 7 | 8 | 9 | app = hug.API(__name__).http.server() 10 | -------------------------------------------------------------------------------- /benchmarks/http/muffin_test.py: -------------------------------------------------------------------------------- 1 | import muffin 2 | 3 | app = muffin.Application("web") 4 | 5 | 6 | @app.register("/text") 7 | def text(request): 8 | return "Hello, World!" 9 | -------------------------------------------------------------------------------- /benchmarks/http/pyramid_test.py: -------------------------------------------------------------------------------- 1 | from pyramid.view import view_config 2 | from pyramid.config import Configurator 3 | 4 | 5 | @view_config(route_name="text", renderer="string") 6 | def text(request): 7 | return "Hello, World!" 8 | 9 | 10 | config = Configurator() 11 | 12 | config.add_route("text", "/text") 13 | 14 | config.scan() 15 | app = config.make_wsgi_app() 16 | -------------------------------------------------------------------------------- /benchmarks/http/requirements.txt: -------------------------------------------------------------------------------- 1 | Cython==0.24 2 | bobo 3 | bottle 4 | cherrypy 5 | falcon 6 | flask 7 | muffin 8 | pyramid 9 | hug 10 | gunicorn 11 | -------------------------------------------------------------------------------- /benchmarks/http/runner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | output="Test results:\n" 4 | 5 | for app in hug_test falcon_test flask_test bobo_test cherrypy_test pyramid_test bottle_test; 6 | do 7 | echo "TEST: $app" 8 | killall gunicorn 9 | fuser -k 8000/tcp 10 | gunicorn -w 2 $app:app & 11 | sleep 5 12 | ab -n 1000 -c 5 http://localhost:8000/text 13 | sleep 5 14 | ab_out=`ab -n 20000 -c 5 http://localhost:8000/text` 15 | killall gunicorn 16 | rps=`echo "$ab_out" | grep "Requests per second"` 17 | crs=`echo "$ab_out" | grep "Complete requests"` 18 | output="$output\n$app:\n\t$rps\n\t$crs" 19 | done 20 | 21 | echo -e "$output" 22 | 23 | 24 | -------------------------------------------------------------------------------- /benchmarks/http/tornado_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import tornado.ioloop 3 | import tornado.web 4 | import json 5 | 6 | 7 | class TextHandler(tornado.web.RequestHandler): 8 | def get(self): 9 | self.write("Hello, world!") 10 | 11 | 12 | application = tornado.web.Application([(r"/text", TextHandler)]) 13 | 14 | if __name__ == "__main__": 15 | application.listen(8000) 16 | tornado.ioloop.IOLoop.current().start() 17 | -------------------------------------------------------------------------------- /benchmarks/internal/argument_populating.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from hug.decorators import auto_kwargs 4 | from hug.introspect import generate_accepted_kwargs 5 | 6 | DATA = {"request": None} 7 | 8 | 9 | class Timer(object): 10 | def __init__(self, name): 11 | self.name = name 12 | 13 | def __enter__(self): 14 | self.start = time.time() 15 | 16 | def __exit__(self, *args): 17 | print("{0} took {1}".format(self.name, time.clock() - self.start)) 18 | 19 | 20 | def my_method(name, request=None): 21 | pass 22 | 23 | 24 | def my_method_with_kwargs(name, request=None, **kwargs): 25 | pass 26 | 27 | 28 | with Timer("generate_kwargs"): 29 | accept_kwargs = generate_accepted_kwargs(my_method, ("request", "response", "version")) 30 | 31 | for test in range(100000): 32 | my_method(test, **accept_kwargs(DATA)) 33 | 34 | 35 | with Timer("auto_kwargs"): 36 | wrapped_method = auto_kwargs(my_method) 37 | 38 | for test in range(100000): 39 | wrapped_method(test, **DATA) 40 | 41 | 42 | with Timer("native_kwargs"): 43 | for test in range(100000): 44 | my_method_with_kwargs(test, **DATA) 45 | 46 | 47 | with Timer("no_kwargs"): 48 | for test in range(100000): 49 | my_method(test, request=None) 50 | -------------------------------------------------------------------------------- /docker/app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:alpine 2 | MAINTAINER Housni Yakoob 3 | 4 | CMD ["true"] -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | gunicorn: 4 | build: ./gunicorn 5 | volumes_from: 6 | - app 7 | ports: 8 | - "8000:8000" 9 | links: 10 | - app 11 | app: 12 | build: ./app 13 | volumes: 14 | - ./template/:/src 15 | workspace: 16 | build: ./workspace 17 | volumes_from: 18 | - app 19 | tty: true -------------------------------------------------------------------------------- /docker/gunicorn/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:alpine 2 | MAINTAINER Housni Yakoob 3 | 4 | EXPOSE 8000 5 | 6 | RUN pip3 install gunicorn 7 | RUN pip3 install hug -U 8 | WORKDIR /src 9 | CMD gunicorn --reload --bind=0.0.0.0:8000 __init__:__hug_wsgi__ -------------------------------------------------------------------------------- /docker/template/__init__.py: -------------------------------------------------------------------------------- 1 | import hug 2 | from handlers import birthday, hello 3 | 4 | 5 | @hug.extend_api("") 6 | def api(): 7 | return [hello, birthday] 8 | -------------------------------------------------------------------------------- /docker/template/handlers/birthday.py: -------------------------------------------------------------------------------- 1 | import hug 2 | 3 | 4 | @hug.get("/birthday") 5 | def home(name: str): 6 | return "Happy Birthday, {name}".format(name=name) 7 | -------------------------------------------------------------------------------- /docker/template/handlers/hello.py: -------------------------------------------------------------------------------- 1 | import hug 2 | 3 | 4 | @hug.get("/hello") 5 | def hello(name: str = "World"): 6 | return "Hello, {name}".format(name=name) 7 | -------------------------------------------------------------------------------- /docker/workspace/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:alpine 2 | MAINTAINER Housni Yakoob 3 | 4 | RUN apk update && apk upgrade 5 | RUN apk add bash \ 6 | && sed -i -e "s/bin\/ash/bin\/bash/" /etc/passwd 7 | 8 | CMD ["true"] -------------------------------------------------------------------------------- /documentation/AUTHENTICATION.md: -------------------------------------------------------------------------------- 1 | Authentication in *hug* 2 | ===================== 3 | 4 | Hug supports a number of authentication methods which handle the http headers for you and lets you very simply link them with your own authentication logic. 5 | 6 | To use hug's authentication, when defining an interface, you add a `requires` keyword argument to your `@get` (or other http verb) decorator. The argument to `requires` is a *function*, which returns either `False`, if the authentication fails, or a python object which represents the user. The function is wrapped by a wrapper from the `hug.authentication.*` module which handles the http header fields. 7 | 8 | That python object can be anything. In very simple cases it could be a string containing the user's username. If your application is using a database with an ORM such as [peewee](http://docs.peewee-orm.com/en/latest/), then this object can be more complex and map to a row in a database table. 9 | 10 | To access the user object, you need to use the `hug.directives.user` directive in your declaration. 11 | 12 | @hug.get(requires=) 13 | def handler(user: hug.directives.user) 14 | 15 | This directive supplies the user object. Hug will have already handled the authentication, and rejected any requests with bad credentials with a 401 code, so you can just assume that the user is valid in your logic. 16 | 17 | 18 | Type of Authentication | Hug Authenticator Wrapper | Header Name | Header Content | Arguments to wrapped verification function 19 | ----------------------------|----------------------------------|-----------------|-------------------------|------------ 20 | Basic Authentication | `hug.authenticaton.basic` | Authorization | "Basic XXXX" where XXXX is username:password encoded in Base64| username, password 21 | Token Authentication | `hug.authentication.token` | Authorization | the token as a string| token 22 | API Key Authentication | `hug.authentication.api_key` | X-Api-Key | the API key as a string | api-key 23 | -------------------------------------------------------------------------------- /documentation/CUSTOM_CONTEXT.md: -------------------------------------------------------------------------------- 1 | Context factory in hug 2 | ====================== 3 | 4 | There is a concept of a 'context' in falcon, which is a dict that lives through the whole request. It is used to integrate 5 | for example SQLAlchemy library. However, in hug's case you would expect the context to work in each interface, not 6 | only the http one based on falcon. That is why hug provides its own context, that can be used in all interfaces. 7 | If you want to see the context in action, see the examples. 8 | 9 | ## Create context 10 | 11 | By default, the hug creates also a simple dict object as the context. However, you are able to define your own context 12 | by using the context_factory decorator. 13 | 14 | ```py 15 | @hug.create_context() 16 | def context_factory(*args, **kwargs): 17 | return dict() 18 | ``` 19 | 20 | Arguments that are provided to the factory are almost the same as the ones provided to the directive 21 | (api, api_version, interface and interface specific arguments). For exact arguments, go to the interface definition. 22 | 23 | ## Delete context 24 | 25 | After the call is finished, the context is deleted. If you want to do something else with the context at the end, you 26 | can override the default behaviour by the delete_context decorator. 27 | 28 | ```py 29 | @hug.delete_context() 30 | def delete_context(context, exception=None, errors=None, lacks_requirement=None): 31 | pass 32 | ``` 33 | 34 | This function takes the context and some arguments that informs us about the result of the call's execution. 35 | If the call missed the requirements, the reason will be in lacks_requirements, errors will contain the result of the 36 | validation (None if call has passed the validation) and exception if there was any exception in the call. 37 | Note that if you use cli interface, the errors will contain a string with the first not passed validation. Otherwise, 38 | you will get a dict with errors. 39 | 40 | 41 | Where can I use the context? 42 | ============================ 43 | 44 | The context can be used in the authentication, directives and validation. The function used as an api endpoint 45 | should not get to the context directly, only using the directives. 46 | 47 | ## Authentication 48 | 49 | To use the context in the authentication function, you need to add an additional argument as the context. 50 | Using the context, you can for example check if the credentials meet the criteria basing on the connection with the 51 | database. 52 | Here are the examples: 53 | 54 | ```py 55 | @hug.authentication.basic 56 | def context_basic_authentication(username, password, context): 57 | if username == context['username'] and password == context['password']: 58 | return True 59 | 60 | @hug.authentication.api_key 61 | def context_api_key_authentication(api_key, context): 62 | if api_key == 'Bacon': 63 | return 'Timothy' 64 | 65 | @hug.authentication.token 66 | def context_token_authentication(token, context): 67 | if token == precomptoken: 68 | return 'Timothy' 69 | ``` 70 | 71 | ## Directives 72 | 73 | Here is an example of a directive that has access to the context: 74 | 75 | 76 | ```py 77 | @hug.directive() 78 | def custom_directive(context=None, **kwargs): 79 | return 'custom' 80 | ``` 81 | 82 | ## Validation 83 | 84 | ### Hug types 85 | 86 | You can get the context by creating your own custom hug type. You can extend a regular hug type, as in example below: 87 | 88 | 89 | ```py 90 | @hug.type(chain=True, extend=hug.types.number, accept_context=True) 91 | def check_if_near_the_right_number(value, context): 92 | the_only_right_number = context['the_only_right_number'] 93 | if value not in [ 94 | the_only_right_number - 1, 95 | the_only_right_number, 96 | the_only_right_number + 1, 97 | ]: 98 | raise ValueError('Not near the right number') 99 | return value 100 | ``` 101 | 102 | You can also chain extend a custom hug type that you created before. Keep in mind that if you marked that 103 | the type that you are extending is using the context, all the types that are extending it should also use the context. 104 | 105 | 106 | ```py 107 | @hug.type(chain=True, extend=check_if_near_the_right_number, accept_context=True) 108 | def check_if_the_only_right_number(value, context): 109 | if value != context['the_only_right_number']: 110 | raise ValueError('Not the right number') 111 | return value 112 | ``` 113 | 114 | It is possible to extend a hug type without the chain option, but still using the context: 115 | 116 | 117 | ```py 118 | @hug.type(chain=False, extend=hug.types.number, accept_context=True) 119 | def check_if_string_has_right_value(value, context): 120 | if str(context['the_only_right_number']) not in value: 121 | raise ValueError('The value does not contain the only right number') 122 | return value 123 | ``` 124 | 125 | ### Marshmallow schema 126 | 127 | Marshmallow library also have a concept of the context, so hug also populates the context here. 128 | 129 | 130 | ```py 131 | class MarshmallowContextSchema(Schema): 132 | name = fields.String() 133 | 134 | @validates_schema 135 | def check_context(self, data): 136 | self.context['marshmallow'] += 1 137 | 138 | @hug.get() 139 | def made_up_hello(test: MarshmallowContextSchema()): 140 | return 'hi' 141 | ``` 142 | 143 | What can be a context? 144 | ====================== 145 | 146 | Basically, the answer is everything. For example you can keep all the necessary database sessions in the context 147 | and also you can keep there all the resources that need to be dealt with after the execution of the endpoint. 148 | In delete_context function you can resolve all the dependencies between the databases' management. 149 | See the examples to see what can be achieved. Do not forget to add your own example if you find an another usage! 150 | -------------------------------------------------------------------------------- /documentation/DIRECTIVES.md: -------------------------------------------------------------------------------- 1 | hug directives (automatic argument injection) 2 | =================== 3 | 4 | Oftentimes you'll find yourself needing something particular to an interface (say a header, a session, or content_type), but don't want to tie your function 5 | to a single interface. To support this, hug introduces the concept of `directives`. In hug, directives are simply arguments that have been registered to automatically provide a parameter value based on knowledge known to the interface. 6 | 7 | For example, this is the built-in session directive: 8 | 9 | 10 | @hug.directive() 11 | def session(context_name='session', request=None, **kwargs): 12 | """Returns the session associated with the current request""" 13 | return request and request.context.get(context_name, None) or None 14 | 15 | Then, when using this directive in your code, you can either specify the directive via type annotation: 16 | 17 | @hug.get() 18 | def my_endpoint(session: hug.directives.session): 19 | session # is here automatically, without needing to be passed in 20 | 21 | Or by prefixing the argument with `hug_`: 22 | 23 | @hug.get() 24 | def my_endpoint(hug_session): 25 | session # is here automatically, without needing to be passed in 26 | 27 | You can then specify a different location for the hug session, simply by providing a default for the argument: 28 | 29 | @hug.get() 30 | def my_endpoint(hug_session='alternative_session_key'): 31 | session # is here automatically, without needing to be passed in 32 | 33 | Built-in directives 34 | =================== 35 | 36 | hug provides a handful of directives for commonly needed attributes: 37 | 38 | - hug.directives.Timer (hug_timer=precision): Stores the time the interface was initially called, returns how much time has passed since the function was called, if casted as a float. Automatically converts to the time taken when returned as part of a JSON structure. The default value specifies the float precision desired when keeping track of the time passed. 39 | - hug.directives.module (hug_module): Passes along the module that contains the API associated with this endpoint. 40 | - hug.directives.api (hug_api): Passes along the hug API singleton associated with this endpoint. 41 | - hug.directives.api_version (hug_api_version): Passes along the version of the API being called. 42 | - hug.directives.documentation (hug_documentation): Generates and passes along the entire set of documentation for the API that contains the endpoint. 43 | - hug.directives.session (hug_session=context_name): Passes along the session associated with the current request. The default value provides a different key whose value is stored on the request.context object. 44 | - hug.directives.user (hug_user): Passes along the user object associated with the request. 45 | - hug.directives.CurrentAPI (hug_current_api): Passes along a smart, version-aware API caller, to enable calling other functions within your API, with reassurance that the correct function is being called for the version of the API being requested. 46 | 47 | Building custom directives 48 | =================== 49 | 50 | hug provides the `@hug.directive()` to enable creation of new directives. It takes one argument: apply_globally, which defaults to False. 51 | If you set this parameter to True, the hug directive will be automatically made available as a magic `hug_` argument on all endpoints outside of your defined API. This is not a concern if you're applying directives via type annotation. 52 | 53 | The most basic directive will take an optional default value, as well as **kwargs: 54 | 55 | @hug.directive() 56 | def basic(default=False, **kwargs): 57 | return str(default) + ' there!' 58 | 59 | 60 | This directive could then be used like this: 61 | 62 | @hug.local() 63 | def endpoint(hug_basic='hi'): 64 | return hug_basic 65 | 66 | assert endpoint() == 'hi there!' 67 | 68 | It's important to always accept **kwargs for directive functions, as each interface gets to decide its own set of 69 | keyword arguments to send to the directive, which can then be used to pull in information for the directive. 70 | 71 | Common directive key word parameters 72 | =================== 73 | 74 | Independent of interface, the following key word arguments will be passed to the directive: 75 | 76 | - `interface` - The interface that the directive is being run through. Useful for conditionally injecting data (via the decorator) depending on the interface it is being called through, as demonstrated at the bottom of this section. 77 | - `api` - The API singleton associated with this endpoint. 78 | 79 | Interface Example: 80 | 81 | @directive() 82 | def my_directive(default=None, interface=None, **kwargs): 83 | if interface == hug.interface.CLI: 84 | return 'CLI specific' 85 | elif interface == hug.interface.HTTP: 86 | return 'HTTP specific' 87 | elif interface == hug.interface.Local: 88 | return 'Local' 89 | 90 | return 'unknown' 91 | 92 | HTTP directive key word parameters 93 | =================== 94 | 95 | Directives are passed the following additional keyword parameters when they are being run through an HTTP interface: 96 | 97 | - `response`: The HTTP response object that will be returned for this request. 98 | - `request`: The HTTP request object that caused this interface to be called. 99 | - `api_version`: The version of the endpoint being hit. 100 | 101 | CLI directive key word parameters 102 | =================== 103 | 104 | Directives get one additional argument when they are run through a command line interface: 105 | 106 | - `argparse`: The argparse instance created to parse command line arguments. 107 | -------------------------------------------------------------------------------- /documentation/TYPE_ANNOTATIONS.md: -------------------------------------------------------------------------------- 1 | Type annotations in hug 2 | ======================= 3 | 4 | hug leverages Python3 type annotations for validation and API specification. Within the context of hug, annotations should be set to one of 4 things: 5 | 6 | - A cast function, built-in, or your own (str, int, etc) that takes a value casts it and then returns it, raising an exception if it is not in a format that can be cast into the desired type 7 | - A hug type (hug.types.text, hug.types.number, etc.). These are essentially built-in cast functions that provide more contextual information, and good default error messages 8 | - A [marshmallow](https://marshmallow.readthedocs.org/en/latest/) type and/or schema. In hug 2.0.0 Marshmallow is a first class citizen in hug, and all fields and schemas defined with it can be used in hug as type annotations 9 | - A string. When a basic Python string is set as the type annotation it is used by hug to generate documentation, but does not get applied during the validation phase 10 | 11 | For example: 12 | 13 | import hug 14 | 15 | 16 | @hug.get() 17 | def hello(first_name: hug.types.text, last_name: 'Family Name', age: int): 18 | print("Hi {0} {1}!".format(first_name, last_name) 19 | 20 | is a valid hug endpoint. 21 | 22 | Any time a type annotation raises an exception during casting of a type, it is seen as a failure. Otherwise the cast is assumed successful with the returned type replacing the passed-in parameter. By default, all errors are collected in an errors dictionary and returned as the output of the endpoint before the routed function ever gets called. To change how errors are returned you can transform them via the `on_invalid` route option, and specify a specific output format for errors by specifying the `output_invalid` route option. Or, if you prefer, you can keep hug from handling the validation errors at all by passing in `raise_on_invalid=True` to the route. 23 | 24 | Built in hug types 25 | ================== 26 | 27 | hug provides several built-in types for common API use cases: 28 | 29 | - `number`: Validates that a whole number was passed in 30 | - `float_number`: Validates that a valid floating point number was passed in 31 | - `decimal`: Validates and converts the provided value into a Python Decimal object 32 | - `uuid`: Validates that the provided value is a valid UUID 33 | - `text`: Validates that the provided value is a single string parameter 34 | - `multiple`: Ensures the parameter is passed in as a list (even if only one value is passed in) 35 | - `boolean`: A basic naive HTTP style boolean where no value passed in is seen as `False` and any value passed in (even if its `false`) is seen as `True` 36 | - `smart_boolean`: A smarter, but more computentionally expensive, boolean that checks the content of the value for common true / false formats (true, True, t, 1) or (false, False, f, 0) 37 | - `delimited_list(delimiter)`: splits up the passed in value based on the provided delimiter and then passes it to the function as a list 38 | - `one_of(values)`: Validates that the passed in value is one of those specified 39 | - `mapping(dict_of_passed_in_to_desired_values)`: Like `one_of`, but with a dictionary of acceptable values, to converted value. 40 | - `multi(types)`: Allows passing in multiple acceptable types for a parameter, short circuiting on the first acceptable one 41 | - `in_range(lower, upper, convert=number)`: Accepts a number within a lower and upper bound of acceptable values 42 | - `less_than(limit, convert=number)`: Accepts a number within a lower and upper bound of acceptable values 43 | - `greater_than(minimum, convert=number)`: Accepts a value above a given minimum 44 | - `length(lower, upper, convert=text)`: Accepts a a value that is within a specific length limit 45 | - `shorter_than(limit, convert=text)`: Accepts a text value shorter than the specified length limit 46 | - `longer_than(limit, convert=text)`: Accepts a value up to the specified limit 47 | - `cut_off(limit, convert=text)`: Cuts off the provided value at the specified index 48 | 49 | Extending and creating new hug types 50 | ==================================== 51 | 52 | The most obvious way to extend a hug type is to simply inherit from the base type defined in `hug.types` and then override `__call__` to override how the cast function, or override `__init__` to override what parameters the type takes: 53 | 54 | import hug 55 | 56 | 57 | class TheAnswer(hug.types.Text): 58 | """My new documentation""" 59 | 60 | def __call__(self, value): 61 | value = super().__call__(value) 62 | if value != 'fourty-two': 63 | raise ValueError('Value is not the answer to everything.') 64 | return value 65 | 66 | If you simply want to perform additional conversion after a base type is finished, or modify its documentation, the most succinct way is the `hug.type` decorator: 67 | 68 | import hug 69 | 70 | 71 | @hug.type(extend=hug.types.number) 72 | def the_answer(value): 73 | """My new documentation""" 74 | if value != 42: 75 | raise ValueError('Value is not the answer to everything.') 76 | return value 77 | 78 | 79 | Marshmallow integration 80 | ======================= 81 | 82 | [Marshmallow](https://marshmallow.readthedocs.org/en/latest/) is an advanced serialization, deserialization, and validation library. Hug supports using marshmallow fields and schemas as input types. 83 | 84 | Here is a simple example of an API that does datetime addition. 85 | 86 | 87 | import datetime as dt 88 | 89 | import hug 90 | from marshmallow import fields 91 | from marshmallow.validate import Range 92 | 93 | 94 | @hug.get('/dateadd', examples="value=1973-04-10&addend=63") 95 | def dateadd(value: fields.Date(), 96 | addend: fields.Int(validate=Range(min=1))): 97 | """Add a value to a date.""" 98 | delta = dt.timedelta(days=addend) 99 | result = value + delta 100 | return {'result': result} 101 | -------------------------------------------------------------------------------- /examples/authentication.py: -------------------------------------------------------------------------------- 1 | """A basic example of authentication requests within a hug API""" 2 | import hug 3 | import jwt 4 | 5 | # Several authenticators are included in hug/authentication.py. These functions 6 | # accept a verify_user function, which can be either an included function (such 7 | # as the basic username/password function demonstrated below), or logic of your 8 | # own. Verification functions return an object to store in the request context 9 | # on successful authentication. Naturally, this is a trivial demo, and a much 10 | # more robust verification function is recommended. This is for strictly 11 | # illustrative purposes. 12 | authentication = hug.authentication.basic(hug.authentication.verify("User1", "mypassword")) 13 | 14 | 15 | @hug.get("/public") 16 | def public_api_call(): 17 | return "Needs no authentication" 18 | 19 | 20 | # Note that the logged in user can be accessed via a built-in directive. 21 | # Directives can provide computed input parameters via an abstraction 22 | # layer so as not to clutter your API functions with access to the raw 23 | # request object. 24 | @hug.get("/authenticated", requires=authentication) 25 | def basic_auth_api_call(user: hug.directives.user): 26 | return "Successfully authenticated with user: {0}".format(user) 27 | 28 | 29 | # Here is a slightly less trivial example of how authentication might 30 | # look in an API that uses keys. 31 | 32 | # First, the user object stored in the context need not be a string, 33 | # but can be any Python object. 34 | class APIUser(object): 35 | """A minimal example of a rich User object""" 36 | 37 | def __init__(self, user_id, api_key): 38 | self.user_id = user_id 39 | self.api_key = api_key 40 | 41 | 42 | def api_key_verify(api_key): 43 | magic_key = "5F00832B-DE24-4CAF-9638-C10D1C642C6C" # Obviously, this would hit your database 44 | if api_key == magic_key: 45 | # Success! 46 | return APIUser("user_foo", api_key) 47 | else: 48 | # Invalid key 49 | return None 50 | 51 | 52 | api_key_authentication = hug.authentication.api_key(api_key_verify) 53 | 54 | 55 | @hug.get("/key_authenticated", requires=api_key_authentication) # noqa 56 | def basic_auth_api_call(user: hug.directives.user): 57 | return "Successfully authenticated with user: {0}".format(user.user_id) 58 | 59 | 60 | def token_verify(token): 61 | secret_key = "super-secret-key-please-change" 62 | try: 63 | return jwt.decode(token, secret_key, algorithm="HS256") 64 | except jwt.DecodeError: 65 | return False 66 | 67 | 68 | token_key_authentication = hug.authentication.token(token_verify) 69 | 70 | 71 | @hug.get("/token_authenticated", requires=token_key_authentication) # noqa 72 | def token_auth_call(user: hug.directives.user): 73 | return "You are user: {0} with data {1}".format(user["user"], user["data"]) 74 | 75 | 76 | @hug.post("/token_generation") # noqa 77 | def token_gen_call(username, password): 78 | """Authenticate and return a token""" 79 | secret_key = "super-secret-key-please-change" 80 | mockusername = "User2" 81 | mockpassword = "Mypassword" 82 | if mockpassword == password and mockusername == username: # This is an example. Don't do that. 83 | return { 84 | "token": jwt.encode({"user": username, "data": "mydata"}, secret_key, algorithm="HS256") 85 | } 86 | return "Invalid username and/or password for user: {0}".format(username) 87 | -------------------------------------------------------------------------------- /examples/cli.py: -------------------------------------------------------------------------------- 1 | """A basic cli client written with hug""" 2 | import hug 3 | 4 | 5 | @hug.cli(version="1.0.0") 6 | def cli(name: "The name", age: hug.types.number): 7 | """Says happy birthday to a user""" 8 | return "Happy {age} Birthday {name}!\n".format(**locals()) 9 | 10 | 11 | if __name__ == "__main__": 12 | cli.interface.cli() 13 | -------------------------------------------------------------------------------- /examples/cli_multiple.py: -------------------------------------------------------------------------------- 1 | import hug 2 | 3 | 4 | @hug.cli() 5 | def add(numbers: list = None): 6 | return sum([int(number) for number in numbers]) 7 | 8 | 9 | if __name__ == "__main__": 10 | add.interface.cli() 11 | -------------------------------------------------------------------------------- /examples/cli_object.py: -------------------------------------------------------------------------------- 1 | import hug 2 | 3 | API = hug.API("git") 4 | 5 | 6 | @hug.object(name="git", version="1.0.0", api=API) 7 | class GIT(object): 8 | """An example of command like calls via an Object""" 9 | 10 | @hug.object.cli 11 | def push(self, branch="master"): 12 | """Push the latest to origin""" 13 | return "Pushing {}".format(branch) 14 | 15 | @hug.object.cli 16 | def pull(self, branch="master"): 17 | """Pull in the latest from origin""" 18 | return "Pulling {}".format(branch) 19 | 20 | 21 | if __name__ == "__main__": 22 | API.cli() 23 | -------------------------------------------------------------------------------- /examples/cors_middleware.py: -------------------------------------------------------------------------------- 1 | import hug 2 | 3 | api = hug.API(__name__) 4 | api.http.add_middleware(hug.middleware.CORSMiddleware(api, max_age=10)) 5 | 6 | 7 | @hug.get("/demo") 8 | def get_demo(): 9 | return {"result": "Hello World"} 10 | -------------------------------------------------------------------------------- /examples/cors_per_route.py: -------------------------------------------------------------------------------- 1 | import hug 2 | 3 | 4 | @hug.get() 5 | def cors_supported(cors: hug.directives.cors = "*"): 6 | return "Hello world!" 7 | -------------------------------------------------------------------------------- /examples/docker_compose_with_mongodb/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6 2 | ADD . /src 3 | WORKDIR /src 4 | RUN pip install -r requirements.txt 5 | 6 | -------------------------------------------------------------------------------- /examples/docker_compose_with_mongodb/README.md: -------------------------------------------------------------------------------- 1 | # mongodb + hug microservice 2 | 3 | 1. Run with `sudo /path/to/docker-compose up --build` 4 | 2. Add data with something that can POST, e.g. `curl http://localhost:8000/new -d name="my name" -d description="a description"` 5 | 3. Visit `localhost:8000/` to see all the current data 6 | 4. Rejoice! 7 | 8 | -------------------------------------------------------------------------------- /examples/docker_compose_with_mongodb/app.py: -------------------------------------------------------------------------------- 1 | from pymongo import MongoClient 2 | import hug 3 | 4 | 5 | client = MongoClient("db", 27017) 6 | db = client["our-database"] 7 | collection = db["our-items"] 8 | 9 | 10 | @hug.get("/", output=hug.output_format.pretty_json) 11 | def show(): 12 | """Returns a list of items currently in the database""" 13 | items = list(collection.find()) 14 | # JSON conversion chokes on the _id objects, so we convert 15 | # them to strings here 16 | for i in items: 17 | i["_id"] = str(i["_id"]) 18 | return items 19 | 20 | 21 | @hug.post("/new", status_code=hug.falcon.HTTP_201) 22 | def new(name: hug.types.text, description: hug.types.text): 23 | """Inserts the given object as a new item in the database. 24 | 25 | Returns the ID of the newly created item. 26 | """ 27 | item_doc = {"name": name, "description": description} 28 | collection.insert_one(item_doc) 29 | return str(item_doc["_id"]) 30 | -------------------------------------------------------------------------------- /examples/docker_compose_with_mongodb/docker-compose.yml: -------------------------------------------------------------------------------- 1 | web: 2 | build: . 3 | command: hug -f app.py 4 | ports: 5 | - "8000:8000" 6 | links: 7 | - db 8 | db: 9 | image: mongo:3.0.2 10 | -------------------------------------------------------------------------------- /examples/docker_compose_with_mongodb/requirements.txt: -------------------------------------------------------------------------------- 1 | hug 2 | pymongo 3 | -------------------------------------------------------------------------------- /examples/docker_nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Python 3.6 2 | FROM python:3.6 3 | 4 | # Set working directory 5 | RUN mkdir /app 6 | WORKDIR /app 7 | 8 | # Add all files to app directory 9 | ADD . /app 10 | 11 | # Install gunicorn 12 | RUN apt-get update && \ 13 | apt-get install -y && \ 14 | pip3 install gunicorn 15 | 16 | # Run setup.py 17 | RUN python3 setup.py install 18 | -------------------------------------------------------------------------------- /examples/docker_nginx/Makefile: -------------------------------------------------------------------------------- 1 | dev: 2 | docker-compose -f docker-compose.dev.yml up --build 3 | 4 | prod: 5 | docker-compose up --build 6 | -------------------------------------------------------------------------------- /examples/docker_nginx/README.md: -------------------------------------------------------------------------------- 1 | # Docker/NGINX with Hug 2 | 3 | Example of a Docker image containing a Python project utilizing NGINX, Gunicorn, and Hug. This example provides a stack that operates as follows: 4 | 5 | ``` 6 | Client <-> NGINX <-> Gunicorn <-> Python API (Hug) 7 | ``` 8 | 9 | ## Getting started 10 | 11 | Clone/copy this directory to your local machine, navigate to said directory, then: 12 | 13 | __For production:__ 14 | This is an "immutable" build that will require restarting of the container for changes to reflect. 15 | ``` 16 | $ make prod 17 | ``` 18 | 19 | __For development:__ 20 | This is a "mutable" build, which enables us to make changes to our Python project, and changes will reflect in real time! 21 | ``` 22 | $ make dev 23 | ``` 24 | 25 | Once the docker images are running, navigate to `localhost:8000`. A `hello world` message should be visible! 26 | -------------------------------------------------------------------------------- /examples/docker_nginx/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugapi/hug/e4a3fa40f98487a67351311d0da659a6c9ce88a6/examples/docker_nginx/api/__init__.py -------------------------------------------------------------------------------- /examples/docker_nginx/api/__main__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=C0111, E0401 2 | """ API Entry Point """ 3 | 4 | import hug 5 | 6 | 7 | @hug.get("/", output=hug.output_format.html) 8 | def base(): 9 | return "

hello world

" 10 | 11 | 12 | @hug.get("/add", examples="num=1") 13 | def add(num: hug.types.number = 1): 14 | return {"res": num + 1} 15 | -------------------------------------------------------------------------------- /examples/docker_nginx/config/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | location / { 5 | proxy_set_header Host $host; 6 | proxy_set_header X-Real-IP $remote_addr; 7 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 8 | proxy_set_header X-Forwarded-Proto $scheme; 9 | proxy_set_header Host $http_host; 10 | 11 | proxy_pass http://api:8000; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/docker_nginx/docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | api: 5 | build: . 6 | command: gunicorn --reload --bind=0.0.0.0:8000 api.__main__:__hug_wsgi__ 7 | expose: 8 | - "8000" 9 | volumes: 10 | - .:/app 11 | working_dir: /app 12 | 13 | nginx: 14 | depends_on: 15 | - api 16 | image: nginx:latest 17 | ports: 18 | - "8000:80" 19 | volumes: 20 | - .:/app 21 | - ./config/nginx:/etc/nginx/conf.d 22 | -------------------------------------------------------------------------------- /examples/docker_nginx/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | api: 5 | build: . 6 | command: gunicorn --bind=0.0.0.0:8000 api.__main__:__hug_wsgi__ 7 | expose: 8 | - "8000" 9 | 10 | nginx: 11 | depends_on: 12 | - api 13 | image: nginx:latest 14 | ports: 15 | - "8000:80" 16 | volumes: 17 | - .:/app 18 | - ./config/nginx:/etc/nginx/conf.d 19 | -------------------------------------------------------------------------------- /examples/docker_nginx/setup.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=C0326 2 | """ Base setup script """ 3 | 4 | from setuptools import setup 5 | 6 | setup( 7 | name="app-name", 8 | version="0.0.1", 9 | description="App Description", 10 | url="https://github.com/CMoncur/nginx-gunicorn-hug", 11 | author="Cody Moncur", 12 | author_email="cmoncur@gmail.com", 13 | classifiers=[ 14 | # 3 - Alpha 15 | # 4 - Beta 16 | # 5 - Production/Stable 17 | "Development Status :: 3 - Alpha", 18 | "Programming Language :: Python :: 3.6", 19 | ], 20 | packages=[], 21 | # Entry Point 22 | entry_points={"console_scripts": []}, 23 | # Core Dependencies 24 | install_requires=["hug"], 25 | # Dev/Test Dependencies 26 | extras_require={"dev": [], "test": []}, 27 | # Scripts 28 | scripts=[], 29 | ) 30 | -------------------------------------------------------------------------------- /examples/document.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Header 5 |

6 |

7 | Contents 8 |

9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/file_upload_example.py: -------------------------------------------------------------------------------- 1 | """A simple file upload example. 2 | 3 | To test, run this server with `hug -f file_upload_example.py` 4 | 5 | Then run the following from ipython 6 | (you may want to replace .wgetrc with some other small text file that you have, 7 | and it's better to specify absolute path to it): 8 | 9 | import requests 10 | with open('.wgetrc', 'rb') as wgetrc_handle: 11 | response = requests.post('http://localhost:8000/upload', files={'.wgetrc': wgetrc_handle}) 12 | print(response.headers) 13 | print(response.content) 14 | 15 | This should both print in the terminal and return back the filename and filesize of the uploaded file. 16 | """ 17 | 18 | import hug 19 | 20 | 21 | @hug.post("/upload") 22 | def upload_file(body): 23 | """accepts file uploads""" 24 | # is a simple dictionary of {filename: b'content'} 25 | print("body: ", body) 26 | return {"filename": list(body.keys()).pop(), "filesize": len(list(body.values()).pop())} 27 | -------------------------------------------------------------------------------- /examples/force_https.py: -------------------------------------------------------------------------------- 1 | """An example of using a middleware to require HTTPS connections. 2 | requires https://github.com/falconry/falcon-require-https to be installed via 3 | pip install falcon-require-https 4 | """ 5 | import hug 6 | from falcon_require_https import RequireHTTPS 7 | 8 | hug.API(__name__).http.add_middleware(RequireHTTPS()) 9 | 10 | 11 | @hug.get() 12 | def my_endpoint(): 13 | return "Success!" 14 | -------------------------------------------------------------------------------- /examples/happy_birthday.py: -------------------------------------------------------------------------------- 1 | """A basic (single function) API written using Hug""" 2 | import hug 3 | 4 | 5 | @hug.get("/happy_birthday", examples="name=HUG&age=1") 6 | def happy_birthday(name, age: hug.types.number): 7 | """Says happy birthday to a user""" 8 | return "Happy {age} Birthday {name}!".format(**locals()) 9 | 10 | 11 | @hug.get("/greet/{event}") 12 | def greet(event: str): 13 | """Greets appropriately (from http://blog.ketchum.com/how-to-write-10-common-holiday-greetings/) """ 14 | greetings = "Happy" 15 | if event == "Christmas": 16 | greetings = "Merry" 17 | if event == "Kwanzaa": 18 | greetings = "Joyous" 19 | if event == "wishes": 20 | greetings = "Warm" 21 | 22 | return "{greetings} {event}!".format(**locals()) 23 | -------------------------------------------------------------------------------- /examples/hello_world.py: -------------------------------------------------------------------------------- 1 | import hug 2 | 3 | 4 | @hug.get() 5 | def hello(request): 6 | """Says hellos""" 7 | return "Hello Worlds for Bacon?!" 8 | -------------------------------------------------------------------------------- /examples/html_serve.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import hug 4 | 5 | 6 | DIRECTORY = os.path.dirname(os.path.realpath(__file__)) 7 | 8 | 9 | @hug.get("/get/document", output=hug.output_format.html) 10 | def nagiosCommandHelp(**kwargs): 11 | """ 12 | Returns command help document when no command is specified 13 | """ 14 | with open(os.path.join(DIRECTORY, "document.html")) as document: 15 | return document.read() 16 | -------------------------------------------------------------------------------- /examples/image_serve.py: -------------------------------------------------------------------------------- 1 | import hug 2 | 3 | 4 | @hug.get("/image.png", output=hug.output_format.png_image) 5 | def image(): 6 | """Serves up a PNG image.""" 7 | return "../artwork/logo.png" 8 | -------------------------------------------------------------------------------- /examples/import_example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugapi/hug/e4a3fa40f98487a67351311d0da659a6c9ce88a6/examples/import_example/__init__.py -------------------------------------------------------------------------------- /examples/import_example/example_resource.py: -------------------------------------------------------------------------------- 1 | def hi(): 2 | return "hi" 3 | -------------------------------------------------------------------------------- /examples/import_example/import_example_server.py: -------------------------------------------------------------------------------- 1 | import example_resource 2 | import hug 3 | 4 | 5 | @hug.get() 6 | def hello(): 7 | return example_resource.hi() 8 | -------------------------------------------------------------------------------- /examples/marshmallow_example.py: -------------------------------------------------------------------------------- 1 | """Example API using marshmallow fields as type annotations. 2 | 3 | 4 | Requires marshmallow and dateutil. 5 | 6 | $ pip install marshmallow python-dateutil 7 | 8 | 9 | To run the example: 10 | 11 | $ hug -f examples/marshmallow_example.py 12 | 13 | Example requests using HTTPie: 14 | 15 | $ http :8000/dateadd value==1973-04-10 addend==63 16 | $ http :8000/dateadd value==2015-03-20 addend==525600 unit==minutes 17 | """ 18 | import datetime as dt 19 | 20 | import hug 21 | from marshmallow import fields 22 | from marshmallow.validate import Range, OneOf 23 | 24 | 25 | @hug.get("/dateadd", examples="value=1973-04-10&addend=63") 26 | def dateadd( 27 | value: fields.DateTime(), 28 | addend: fields.Int(validate=Range(min=1)), 29 | unit: fields.Str(validate=OneOf(["minutes", "days"])) = "days", 30 | ): 31 | """Add a value to a date.""" 32 | value = value or dt.datetime.utcnow() 33 | if unit == "minutes": 34 | delta = dt.timedelta(minutes=addend) 35 | else: 36 | delta = dt.timedelta(days=addend) 37 | result = value + delta 38 | return {"result": result} 39 | -------------------------------------------------------------------------------- /examples/matplotlib/additional_requirements.txt: -------------------------------------------------------------------------------- 1 | matplotlib==3.1.1 2 | -------------------------------------------------------------------------------- /examples/matplotlib/plot.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | import hug 4 | from matplotlib import pyplot 5 | 6 | 7 | @hug.get(output=hug.output_format.png_image) 8 | def plot(): 9 | pyplot.plot([1, 2, 3, 4]) 10 | pyplot.ylabel("some numbers") 11 | 12 | image_output = io.BytesIO() 13 | pyplot.savefig(image_output, format="png") 14 | image_output.seek(0) 15 | return image_output 16 | -------------------------------------------------------------------------------- /examples/multi_file_cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugapi/hug/e4a3fa40f98487a67351311d0da659a6c9ce88a6/examples/multi_file_cli/__init__.py -------------------------------------------------------------------------------- /examples/multi_file_cli/api.py: -------------------------------------------------------------------------------- 1 | import hug 2 | 3 | import sub_api 4 | 5 | 6 | @hug.cli() 7 | def echo(text: hug.types.text): 8 | return text 9 | 10 | 11 | @hug.extend_api(sub_command="sub_api") 12 | def extend_with(): 13 | return (sub_api,) 14 | 15 | 16 | if __name__ == "__main__": 17 | hug.API(__name__).cli() 18 | -------------------------------------------------------------------------------- /examples/multi_file_cli/sub_api.py: -------------------------------------------------------------------------------- 1 | import hug 2 | 3 | 4 | @hug.cli() 5 | def hello(): 6 | return "Hello world" 7 | -------------------------------------------------------------------------------- /examples/multiple_files/README.md: -------------------------------------------------------------------------------- 1 | # Splitting the API into multiple files 2 | 3 | Example of an API defined in multiple Python modules and combined together 4 | using the `extend_api()` helper. 5 | 6 | Run with `hug -f api.py`. There are three API endpoints: 7 | - `http://localhost:8000/` – `say_hi()` from `api.py` 8 | - `http://localhost:8000/part1` – `part1()` from `part_1.py` 9 | - `http://localhost:8000/part2` – `part2()` from `part_2.py` 10 | -------------------------------------------------------------------------------- /examples/multiple_files/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugapi/hug/e4a3fa40f98487a67351311d0da659a6c9ce88a6/examples/multiple_files/__init__.py -------------------------------------------------------------------------------- /examples/multiple_files/api.py: -------------------------------------------------------------------------------- 1 | import hug 2 | import part_1 3 | import part_2 4 | 5 | 6 | @hug.get("/") 7 | def say_hi(): 8 | """This view will be at the path ``/``""" 9 | return "Hi from root" 10 | 11 | 12 | @hug.extend_api() 13 | def with_other_apis(): 14 | """Join API endpoints from two other modules 15 | 16 | These will be at ``/part1`` and ``/part2``, the paths being automatically 17 | generated from function names. 18 | 19 | """ 20 | return [part_1, part_2] 21 | -------------------------------------------------------------------------------- /examples/multiple_files/part_1.py: -------------------------------------------------------------------------------- 1 | import hug 2 | 3 | 4 | @hug.get() 5 | def part1(): 6 | """This view will be at the path ``/part1``""" 7 | return "part1" 8 | -------------------------------------------------------------------------------- /examples/multiple_files/part_2.py: -------------------------------------------------------------------------------- 1 | import hug 2 | 3 | 4 | @hug.get() 5 | def part2(): 6 | """This view will be at the path ``/part2``""" 7 | return "Part 2" 8 | -------------------------------------------------------------------------------- /examples/on_startup.py: -------------------------------------------------------------------------------- 1 | """Provides an example of attaching an action on hug server startup""" 2 | import hug 3 | 4 | data = [] 5 | 6 | 7 | @hug.startup() 8 | def add_data(api): 9 | """Adds initial data to the api on startup""" 10 | data.append("It's working") 11 | 12 | 13 | @hug.startup() 14 | def add_more_data(api): 15 | """Adds initial data to the api on startup""" 16 | data.append("Even subsequent calls") 17 | 18 | 19 | @hug.cli() 20 | @hug.get() 21 | def test(): 22 | """Returns all stored data""" 23 | return data 24 | -------------------------------------------------------------------------------- /examples/override_404.py: -------------------------------------------------------------------------------- 1 | import hug 2 | 3 | 4 | @hug.get() 5 | def hello_world(): 6 | return "Hello world!" 7 | 8 | 9 | @hug.not_found() 10 | def not_found(): 11 | return {"Nothing": "to see"} 12 | -------------------------------------------------------------------------------- /examples/pil_example/additional_requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow==6.2.0 2 | -------------------------------------------------------------------------------- /examples/pil_example/pill.py: -------------------------------------------------------------------------------- 1 | import hug 2 | from PIL import Image, ImageDraw 3 | 4 | 5 | @hug.get("/image.png", output=hug.output_format.png_image) 6 | def create_image(): 7 | image = Image.new("RGB", (100, 50)) # create the image 8 | ImageDraw.Draw(image).text((10, 10), "Hello World!", fill=(255, 0, 0)) 9 | return image 10 | -------------------------------------------------------------------------------- /examples/post_body_example.py: -------------------------------------------------------------------------------- 1 | """A simple post reading server example. 2 | 3 | To test run this server with `hug -f post_body_example` 4 | then run the following from ipython: 5 | import requests 6 | 7 | requests.post('http://localhost:8000/post_here', json={'one': 'two'}).json() 8 | 9 | This should return back the json data that you posted 10 | """ 11 | import hug 12 | 13 | 14 | @hug.post() 15 | def post_here(body): 16 | """This example shows how to read in post data w/ hug outside of its automatic param parsing""" 17 | return body 18 | -------------------------------------------------------------------------------- /examples/quick_server.py: -------------------------------------------------------------------------------- 1 | import hug 2 | 3 | 4 | @hug.get() 5 | def quick(): 6 | return "Serving!" 7 | 8 | 9 | if __name__ == "__main__": 10 | hug.API(__name__).http.serve() 11 | -------------------------------------------------------------------------------- /examples/quick_start/first_step_1.py: -------------------------------------------------------------------------------- 1 | """First API, local access only""" 2 | import hug 3 | 4 | 5 | @hug.local() 6 | def happy_birthday(name: hug.types.text, age: hug.types.number, hug_timer=3): 7 | """Says happy birthday to a user""" 8 | return {"message": "Happy {0} Birthday {1}!".format(age, name), "took": float(hug_timer)} 9 | -------------------------------------------------------------------------------- /examples/quick_start/first_step_2.py: -------------------------------------------------------------------------------- 1 | """First hug API (local and HTTP access)""" 2 | import hug 3 | 4 | 5 | @hug.get(examples="name=Timothy&age=26") 6 | @hug.local() 7 | def happy_birthday(name: hug.types.text, age: hug.types.number, hug_timer=3): 8 | """Says happy birthday to a user""" 9 | return {"message": "Happy {0} Birthday {1}!".format(age, name), "took": float(hug_timer)} 10 | -------------------------------------------------------------------------------- /examples/quick_start/first_step_3.py: -------------------------------------------------------------------------------- 1 | """First hug API (local, command-line, and HTTP access)""" 2 | import hug 3 | 4 | 5 | @hug.cli() 6 | @hug.get(examples="name=Timothy&age=26") 7 | @hug.local() 8 | def happy_birthday(name: hug.types.text, age: hug.types.number, hug_timer=3): 9 | """Says happy birthday to a user""" 10 | return {"message": "Happy {0} Birthday {1}!".format(age, name), "took": float(hug_timer)} 11 | 12 | 13 | if __name__ == "__main__": 14 | happy_birthday.interface.cli() 15 | -------------------------------------------------------------------------------- /examples/redirects.py: -------------------------------------------------------------------------------- 1 | """This example demonstrates how to perform different kinds of redirects using hug""" 2 | import hug 3 | 4 | 5 | @hug.get() 6 | def sum_two_numbers(number_1: int, number_2: int): 7 | """I'll be redirecting to this using a variety of approaches below""" 8 | return number_1 + number_2 9 | 10 | 11 | @hug.post() 12 | def internal_redirection_automatic(number_1: int, number_2: int): 13 | """This will redirect internally to the sum_two_numbers handler 14 | passing along all passed in parameters. 15 | 16 | This kind of redirect happens internally within hug, fully transparent to clients. 17 | """ 18 | print("Internal Redirection Automatic {}, {}".format(number_1, number_2)) 19 | return sum_two_numbers 20 | 21 | 22 | @hug.post() 23 | def internal_redirection_manual(number: int): 24 | """Instead of normal redirecting: You can manually call other handlers, with computed parameters 25 | and return their results 26 | """ 27 | print("Internal Redirection Manual {}".format(number)) 28 | return sum_two_numbers(number, number) 29 | 30 | 31 | @hug.post() 32 | def redirect(redirect_type: hug.types.one_of(("permanent", "found", "see_other")) = None): 33 | """Hug also fully supports classical HTTP redirects, 34 | providing built in convenience functions for the most common types. 35 | """ 36 | print("HTTP Redirect {}".format(redirect_type)) 37 | if not redirect_type: 38 | hug.redirect.to("/sum_two_numbers") 39 | else: 40 | getattr(hug.redirect, redirect_type)("/sum_two_numbers") 41 | 42 | 43 | @hug.post() 44 | def redirect_set_variables(number: int): 45 | """You can also do some manual parameter setting with HTTP based redirects""" 46 | print("HTTP Redirect set variables {}".format(number)) 47 | hug.redirect.to("/sum_two_numbers?number_1={0}&number_2={0}".format(number)) 48 | -------------------------------------------------------------------------------- /examples/return_400.py: -------------------------------------------------------------------------------- 1 | import hug 2 | from falcon import HTTP_400 3 | 4 | 5 | @hug.get() 6 | def only_positive(positive: int, response): 7 | if positive < 0: 8 | response.status = HTTP_400 9 | -------------------------------------------------------------------------------- /examples/secure_auth_with_db_example.py: -------------------------------------------------------------------------------- 1 | from tinydb import TinyDB, Query 2 | import hug 3 | import hashlib 4 | import logging 5 | import os 6 | 7 | 8 | logger = logging.getLogger(__name__) 9 | logger.setLevel(logging.INFO) 10 | db = TinyDB("db.json") 11 | 12 | """ 13 | Helper Methods 14 | """ 15 | 16 | 17 | def hash_password(password, salt): 18 | """ 19 | Securely hash a password using a provided salt 20 | :param password: 21 | :param salt: 22 | :return: Hex encoded SHA512 hash of provided password 23 | """ 24 | password = str(password).encode("utf-8") 25 | salt = str(salt).encode("utf-8") 26 | return hashlib.sha512(password + salt).hexdigest() 27 | 28 | 29 | def gen_api_key(username): 30 | """ 31 | Create a random API key for a user 32 | :param username: 33 | :return: Hex encoded SHA512 random string 34 | """ 35 | salt = str(os.urandom(64)).encode("utf-8") 36 | return hash_password(username, salt) 37 | 38 | 39 | @hug.cli() 40 | def authenticate_user(username, password): 41 | """ 42 | Authenticate a username and password against our database 43 | :param username: 44 | :param password: 45 | :return: authenticated username 46 | """ 47 | user_model = Query() 48 | user = db.get(user_model.username == username) 49 | 50 | if not user: 51 | logger.warning("User %s not found", username) 52 | return False 53 | 54 | if user["password"] == hash_password(password, user.get("salt")): 55 | return user["username"] 56 | 57 | return False 58 | 59 | 60 | @hug.cli() 61 | def authenticate_key(api_key): 62 | """ 63 | Authenticate an API key against our database 64 | :param api_key: 65 | :return: authenticated username 66 | """ 67 | user_model = Query() 68 | user = db.search(user_model.api_key == api_key)[0] 69 | if user: 70 | return user["username"] 71 | return False 72 | 73 | 74 | """ 75 | API Methods start here 76 | """ 77 | 78 | api_key_authentication = hug.authentication.api_key(authenticate_key) 79 | basic_authentication = hug.authentication.basic(authenticate_user) 80 | 81 | 82 | @hug.cli() 83 | def add_user(username, password): 84 | """ 85 | CLI Parameter to add a user to the database 86 | :param username: 87 | :param password: 88 | :return: JSON status output 89 | """ 90 | 91 | user_model = Query() 92 | if db.search(user_model.username == username): 93 | return {"error": "User {0} already exists".format(username)} 94 | 95 | salt = hashlib.sha512(str(os.urandom(64)).encode("utf-8")).hexdigest() 96 | password = hash_password(password, salt) 97 | api_key = gen_api_key(username) 98 | 99 | user = {"username": username, "password": password, "salt": salt, "api_key": api_key} 100 | user_id = db.insert(user) 101 | 102 | return {"result": "success", "eid": user_id, "user_created": user} 103 | 104 | 105 | @hug.get("/api/get_api_key", requires=basic_authentication) 106 | def get_token(authed_user: hug.directives.user): 107 | """ 108 | Get Job details 109 | :param authed_user: 110 | :return: 111 | """ 112 | user_model = Query() 113 | user = db.search(user_model.username == authed_user)[0] 114 | 115 | if user: 116 | out = {"user": user["username"], "api_key": user["api_key"]} 117 | else: 118 | # this should never happen 119 | out = {"error": "User {0} does not exist".format(authed_user)} 120 | 121 | return out 122 | 123 | 124 | # Same thing, but authenticating against an API key 125 | @hug.get(("/api/job", "/api/job/{job_id}/"), requires=api_key_authentication) 126 | def get_job_details(job_id): 127 | """ 128 | Get Job details 129 | :param job_id: 130 | :return: 131 | """ 132 | job = {"job_id": job_id, "details": "Details go here"} 133 | 134 | return job 135 | 136 | 137 | if __name__ == "__main__": 138 | add_user.interface.cli() 139 | -------------------------------------------------------------------------------- /examples/sink_example.py: -------------------------------------------------------------------------------- 1 | """This is an example of a hug "sink", these enable all request URLs that start with the one defined to be captured 2 | 3 | To try this out, run this api with hug -f sink_example.py and hit any URL after localhost:8000/all/ 4 | (for example: localhost:8000/all/the/things/) and it will return the path sans the base URL. 5 | """ 6 | import hug 7 | 8 | 9 | @hug.sink("/all") 10 | def my_sink(request): 11 | return request.path.replace("/all", "") 12 | -------------------------------------------------------------------------------- /examples/smtp_envelope_example.py: -------------------------------------------------------------------------------- 1 | import envelopes 2 | import hug 3 | 4 | 5 | @hug.directive() 6 | class SMTP(object): 7 | def __init__(self, *args, **kwargs): 8 | self.smtp = envelopes.SMTP(host="127.0.0.1") 9 | self.envelopes_to_send = list() 10 | 11 | def send_envelope(self, envelope): 12 | self.envelopes_to_send.append(envelope) 13 | 14 | def cleanup(self, exception=None): 15 | if exception: 16 | return 17 | for envelope in self.envelopes_to_send: 18 | self.smtp.send(envelope) 19 | 20 | 21 | @hug.get("/hello") 22 | def send_hello_email(smtp: SMTP): 23 | envelope = envelopes.Envelope( 24 | from_addr=(u"me@example.com", u"From me"), 25 | to_addr=(u"world@example.com", u"To World"), 26 | subject=u"Hello", 27 | text_body=u"World!", 28 | ) 29 | smtp.send_envelope(envelope) 30 | -------------------------------------------------------------------------------- /examples/sqlalchemy_example/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.5 2 | 3 | ADD requirements.txt / 4 | RUN pip install -r requirements.txt 5 | ADD demo /demo 6 | WORKDIR / 7 | CMD ["hug", "-f", "/demo/app.py"] 8 | EXPOSE 8000 9 | -------------------------------------------------------------------------------- /examples/sqlalchemy_example/demo/api.py: -------------------------------------------------------------------------------- 1 | import hug 2 | 3 | from demo.authentication import basic_authentication 4 | from demo.directives import SqlalchemySession 5 | from demo.models import TestUser, TestModel 6 | from demo.validation import CreateUserSchema, DumpSchema, unique_username 7 | 8 | 9 | @hug.post("/create_user2", requires=basic_authentication) 10 | def create_user2(db: SqlalchemySession, data: CreateUserSchema()): 11 | user = TestUser(**data) 12 | db.add(user) 13 | db.flush() 14 | return dict() 15 | 16 | 17 | @hug.post("/create_user", requires=basic_authentication) 18 | def create_user(db: SqlalchemySession, username: unique_username, password: hug.types.text): 19 | user = TestUser(username=username, password=password) 20 | db.add(user) 21 | db.flush() 22 | return dict() 23 | 24 | 25 | @hug.get("/test") 26 | def test(): 27 | return "" 28 | 29 | 30 | @hug.get("/hello") 31 | def make_simple_query(db: SqlalchemySession): 32 | for word in ["hello", "world", ":)"]: 33 | test_model = TestModel() 34 | test_model.name = word 35 | db.add(test_model) 36 | db.flush() 37 | return " ".join([obj.name for obj in db.query(TestModel).all()]) 38 | 39 | 40 | @hug.get("/hello2") 41 | def transform_example(db: SqlalchemySession) -> DumpSchema(): 42 | for word in ["hello", "world", ":)"]: 43 | test_model = TestModel() 44 | test_model.name = word 45 | db.add(test_model) 46 | db.flush() 47 | return dict(users=db.query(TestModel).all()) 48 | 49 | 50 | @hug.get("/protected", requires=basic_authentication) 51 | def protected(): 52 | return "smile :)" 53 | -------------------------------------------------------------------------------- /examples/sqlalchemy_example/demo/app.py: -------------------------------------------------------------------------------- 1 | import hug 2 | 3 | from demo import api 4 | from demo.base import Base 5 | from demo.context import SqlalchemyContext, engine 6 | from demo.directives import SqlalchemySession 7 | from demo.models import TestUser 8 | 9 | 10 | @hug.context_factory() 11 | def create_context(*args, **kwargs): 12 | return SqlalchemyContext() 13 | 14 | 15 | @hug.delete_context() 16 | def delete_context(context: SqlalchemyContext, exception=None, errors=None, lacks_requirement=None): 17 | context.cleanup(exception) 18 | 19 | 20 | @hug.local(skip_directives=False) 21 | def initialize(db: SqlalchemySession): 22 | admin = TestUser(username="admin", password="admin") 23 | db.add(admin) 24 | db.flush() 25 | 26 | 27 | @hug.extend_api() 28 | def apis(): 29 | return [api] 30 | 31 | 32 | Base.metadata.create_all(bind=engine) 33 | initialize() 34 | -------------------------------------------------------------------------------- /examples/sqlalchemy_example/demo/authentication.py: -------------------------------------------------------------------------------- 1 | import hug 2 | 3 | from demo.context import SqlalchemyContext 4 | from demo.models import TestUser 5 | 6 | 7 | @hug.authentication.basic 8 | def basic_authentication(username, password, context: SqlalchemyContext): 9 | return context.db.query( 10 | context.db.query(TestUser) 11 | .filter(TestUser.username == username, TestUser.password == password) 12 | .exists() 13 | ).scalar() 14 | -------------------------------------------------------------------------------- /examples/sqlalchemy_example/demo/base.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.declarative.api import declarative_base 2 | 3 | Base = declarative_base() 4 | -------------------------------------------------------------------------------- /examples/sqlalchemy_example/demo/context.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.engine import create_engine 2 | from sqlalchemy.orm.scoping import scoped_session 3 | from sqlalchemy.orm.session import sessionmaker, Session 4 | 5 | 6 | engine = create_engine("sqlite:///:memory:") 7 | 8 | 9 | session_factory = scoped_session(sessionmaker(bind=engine)) 10 | 11 | 12 | class SqlalchemyContext(object): 13 | def __init__(self): 14 | self._db = session_factory() 15 | 16 | @property 17 | def db(self) -> Session: 18 | return self._db 19 | # return self.session_factory() 20 | 21 | def cleanup(self, exception=None): 22 | if exception: 23 | self.db.rollback() 24 | return 25 | self.db.commit() 26 | -------------------------------------------------------------------------------- /examples/sqlalchemy_example/demo/directives.py: -------------------------------------------------------------------------------- 1 | import hug 2 | from sqlalchemy.orm.session import Session 3 | 4 | from demo.context import SqlalchemyContext 5 | 6 | 7 | @hug.directive() 8 | class SqlalchemySession(Session): 9 | def __new__(cls, *args, context: SqlalchemyContext = None, **kwargs): 10 | return context.db 11 | -------------------------------------------------------------------------------- /examples/sqlalchemy_example/demo/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.sql.schema import Column 2 | from sqlalchemy.sql.sqltypes import Integer, String 3 | 4 | from demo.base import Base 5 | 6 | 7 | class TestModel(Base): 8 | __tablename__ = "test_model" 9 | id = Column(Integer, primary_key=True) 10 | name = Column(String) 11 | 12 | 13 | class TestUser(Base): 14 | __tablename__ = "test_user" 15 | id = Column(Integer, primary_key=True) 16 | username = Column(String) 17 | password = Column( 18 | String 19 | ) # do not store plain password in the database, hash it, see porridge for example 20 | -------------------------------------------------------------------------------- /examples/sqlalchemy_example/demo/validation.py: -------------------------------------------------------------------------------- 1 | import hug 2 | from marshmallow import fields 3 | from marshmallow.decorators import validates_schema 4 | from marshmallow.schema import Schema 5 | from marshmallow_sqlalchemy import ModelSchema 6 | 7 | from demo.context import SqlalchemyContext 8 | from demo.models import TestUser, TestModel 9 | 10 | 11 | @hug.type(extend=hug.types.text, chain=True, accept_context=True) 12 | def unique_username(value, context: SqlalchemyContext): 13 | if context.db.query( 14 | context.db.query(TestUser).filter(TestUser.username == value).exists() 15 | ).scalar(): 16 | raise ValueError("User with a username {0} already exists.".format(value)) 17 | return value 18 | 19 | 20 | class CreateUserSchema(Schema): 21 | username = fields.String() 22 | password = fields.String() 23 | 24 | @validates_schema 25 | def check_unique_username(self, data): 26 | if self.context.db.query( 27 | self.context.db.query(TestUser).filter(TestUser.username == data["username"]).exists() 28 | ).scalar(): 29 | raise ValueError("User with a username {0} already exists.".format(data["username"])) 30 | 31 | 32 | class DumpUserSchema(ModelSchema): 33 | @property 34 | def session(self): 35 | return self.context.db 36 | 37 | class Meta: 38 | model = TestModel 39 | fields = ("name",) 40 | 41 | 42 | class DumpSchema(Schema): 43 | users = fields.Nested(DumpUserSchema, many=True) 44 | -------------------------------------------------------------------------------- /examples/sqlalchemy_example/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | demo: 4 | build: 5 | context: . 6 | ports: 7 | - 8000:8000 8 | -------------------------------------------------------------------------------- /examples/sqlalchemy_example/requirements.txt: -------------------------------------------------------------------------------- 1 | git+git://github.com/timothycrosley/hug@develop#egg=hug 2 | sqlalchemy 3 | marshmallow 4 | marshmallow-sqlalchemy 5 | -------------------------------------------------------------------------------- /examples/static_serve.py: -------------------------------------------------------------------------------- 1 | """Serves a directory from the filesystem using Hug. 2 | 3 | try /static/a/hi.txt /static/a/hi.html /static/a/hello.html 4 | """ 5 | import tempfile 6 | import os 7 | 8 | import hug 9 | 10 | tmp_dir_object = None 11 | 12 | 13 | def setup(api=None): 14 | """Sets up and fills test directory for serving. 15 | 16 | Using different filetypes to see how they are dealt with. 17 | The tempoary directory will clean itself up. 18 | """ 19 | global tmp_dir_object 20 | 21 | tmp_dir_object = tempfile.TemporaryDirectory() 22 | 23 | dir_name = tmp_dir_object.name 24 | 25 | dir_a = os.path.join(dir_name, "a") 26 | os.mkdir(dir_a) 27 | dir_b = os.path.join(dir_name, "b") 28 | os.mkdir(dir_b) 29 | 30 | # populate directory a with text files 31 | file_list = [ 32 | ["hi.txt", """Hi World!"""], 33 | ["hi.html", """Hi World!"""], 34 | [ 35 | "hello.html", 36 | """ 37 | 38 | pop-up 39 | """, 40 | ], 41 | ["hi.js", """alert('Hi World')"""], 42 | ] 43 | 44 | for f in file_list: 45 | with open(os.path.join(dir_a, f[0]), mode="wt") as fo: 46 | fo.write(f[1]) 47 | 48 | # populate directory b with binary file 49 | image = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\n\x00\x00\x00\n\x08\x02\x00\x00\x00\x02PX\xea\x00\x00\x006IDAT\x18\xd3c\xfc\xff\xff?\x03n\xc0\xc4\x80\x170100022222\xc2\x85\x90\xb9\x04t3\x92`7\xb2\x15D\xeb\xc6\xe34\xa8n4c\xe1F\x120\x1c\x00\xc6z\x12\x1c\x8cT\xf2\x1e\x00\x00\x00\x00IEND\xaeB`\x82" 50 | 51 | with open(os.path.join(dir_b, "smile.png"), mode="wb") as fo: 52 | fo.write(image) 53 | 54 | 55 | @hug.static("/static") 56 | def my_static_dirs(): 57 | """Returns static directory names to be served.""" 58 | global tmp_dir_object 59 | if tmp_dir_object == None: 60 | setup() 61 | return (tmp_dir_object.name,) 62 | -------------------------------------------------------------------------------- /examples/streaming_movie_server/movie.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugapi/hug/e4a3fa40f98487a67351311d0da659a6c9ce88a6/examples/streaming_movie_server/movie.mp4 -------------------------------------------------------------------------------- /examples/streaming_movie_server/movie_server.py: -------------------------------------------------------------------------------- 1 | """A simple streaming movie server example""" 2 | import hug 3 | 4 | 5 | @hug.get(output=hug.output_format.mp4_video) 6 | def watch(): 7 | """Watch an example movie, streamed directly to you from hug""" 8 | return "movie.mp4" 9 | -------------------------------------------------------------------------------- /examples/test_happy_birthday.py: -------------------------------------------------------------------------------- 1 | import hug 2 | import happy_birthday 3 | from falcon import HTTP_400, HTTP_404, HTTP_200 4 | 5 | 6 | def tests_happy_birthday(): 7 | response = hug.test.get(happy_birthday, "happy_birthday", {"name": "Timothy", "age": 25}) 8 | assert response.status == HTTP_200 9 | assert response.data is not None 10 | 11 | 12 | def tests_season_greetings(): 13 | response = hug.test.get(happy_birthday, "greet/Christmas") 14 | assert response.status == HTTP_200 15 | assert response.data is not None 16 | assert str(response.data) == "Merry Christmas!" 17 | response = hug.test.get(happy_birthday, "greet/holidays") 18 | assert str(response.data) == "Happy holidays!" 19 | -------------------------------------------------------------------------------- /examples/unicode_output.py: -------------------------------------------------------------------------------- 1 | """A simple example that illustrates returning UTF-8 encoded data within a JSON outputting hug endpoint""" 2 | import hug 3 | 4 | 5 | @hug.get() 6 | def unicode_response(): 7 | """An example endpoint that returns unicode data nested within the result object""" 8 | return {"data": ["Τη γλώσσα μου έδωσαν ελληνική"]} 9 | -------------------------------------------------------------------------------- /examples/use_socket.py: -------------------------------------------------------------------------------- 1 | """A basic example of using hug.use.Socket to return data from raw sockets""" 2 | import hug 3 | import socket 4 | import struct 5 | import time 6 | 7 | 8 | http_socket = hug.use.Socket(connect_to=("www.google.com", 80), proto="tcp", pool=4, timeout=10.0) 9 | ntp_service = hug.use.Socket(connect_to=("127.0.0.1", 123), proto="udp", pool=4, timeout=10.0) 10 | 11 | 12 | EPOCH_START = 2208988800 13 | 14 | 15 | @hug.get() 16 | def get_time(): 17 | """Get time from a locally running NTP server""" 18 | 19 | time_request = "\x1b" + 47 * "\0" 20 | now = struct.unpack("!12I", ntp_service.request(time_request, timeout=5.0).data.read())[10] 21 | return time.ctime(now - EPOCH_START) 22 | 23 | 24 | @hug.get() 25 | def reverse_http_proxy(length: int = 100): 26 | """Simple reverse http proxy function that returns data/html from another http server (via sockets) 27 | only drawback is the peername is static, and currently does not support being changed. 28 | Example: curl localhost:8000/reverse_http_proxy?length=400""" 29 | 30 | http_request = """ 31 | GET / HTTP/1.0\r\n\r\n 32 | Host: www.google.com\r\n\r\n 33 | \r\n\r\n 34 | """ 35 | return http_socket.request(http_request, timeout=5.0).data.read()[0:length] 36 | -------------------------------------------------------------------------------- /examples/versioning.py: -------------------------------------------------------------------------------- 1 | """A simple example of a hug API call with versioning""" 2 | import hug 3 | 4 | 5 | @hug.get("/echo", versions=1) 6 | def echo(text): 7 | return text 8 | 9 | 10 | @hug.get("/echo", versions=range(2, 5)) # noqa 11 | def echo(text): 12 | return "Echo: {text}".format(**locals()) 13 | 14 | 15 | @hug.get("/unversioned") 16 | def hello(): 17 | return "Hello world!" 18 | 19 | 20 | @hug.get("/echo", versions="6") 21 | def echo(text): 22 | return "Version 6" 23 | -------------------------------------------------------------------------------- /examples/write_once.py: -------------------------------------------------------------------------------- 1 | """An example of writing an API to scrape hacker news once, and then enabling usage everywhere""" 2 | import hug 3 | import requests 4 | 5 | 6 | @hug.local() 7 | @hug.cli() 8 | @hug.get() 9 | def top_post(section: hug.types.one_of(("news", "newest", "show")) = "news"): 10 | """Returns the top post from the provided section""" 11 | content = requests.get("https://news.ycombinator.com/{0}".format(section)).content 12 | text = content.decode("utf-8") 13 | return text.split("")[1].split("")[1].split("<")[0] 14 | -------------------------------------------------------------------------------- /hug/__init__.py: -------------------------------------------------------------------------------- 1 | """hug/__init__.py 2 | 3 | Everyone needs a hug every once in a while. Even API developers. Hug aims to make developing Python driven APIs as 4 | simple as possible, but no simpler. 5 | 6 | Hug's Design Objectives: 7 | 8 | - Make developing a Python driven API as succint as a written definition. 9 | - The framework should encourage code that self-documents. 10 | - It should be fast. Never should a developer feel the need to look somewhere else for performance reasons. 11 | - Writing tests for APIs written on-top of Hug should be easy and intuitive. 12 | - Magic done once, in an API, is better then pushing the problem set to the user of the API. 13 | - Be the basis for next generation Python APIs, embracing the latest technology. 14 | 15 | Copyright (C) 2016 Timothy Edmund Crosley 16 | 17 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 18 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 19 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 20 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all copies or 23 | substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 26 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 27 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 28 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 29 | OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | """ 32 | from __future__ import absolute_import 33 | 34 | from falcon import * 35 | 36 | from hug import ( 37 | directives, 38 | exceptions, 39 | format, 40 | input_format, 41 | introspect, 42 | middleware, 43 | output_format, 44 | redirect, 45 | route, 46 | test, 47 | transform, 48 | types, 49 | use, 50 | validate, 51 | ) 52 | from hug._version import current 53 | from hug.api import API 54 | from hug.decorators import ( 55 | context_factory, 56 | default_input_format, 57 | default_output_format, 58 | delete_context, 59 | directive, 60 | extend_api, 61 | middleware_class, 62 | reqresp_middleware, 63 | request_middleware, 64 | response_middleware, 65 | startup, 66 | wraps, 67 | ) 68 | from hug.route import ( 69 | call, 70 | cli, 71 | connect, 72 | delete, 73 | exception, 74 | get, 75 | get_post, 76 | head, 77 | http, 78 | local, 79 | not_found, 80 | object, 81 | options, 82 | patch, 83 | post, 84 | put, 85 | sink, 86 | static, 87 | trace, 88 | ) 89 | from hug.types import create as type 90 | 91 | # The following imports must be imported last; in particular, defaults to have access to all modules 92 | from hug import authentication # isort:skip 93 | from hug import development_runner # isort:skip 94 | from hug import defaults # isort:skip 95 | 96 | try: # pragma: no cover - defaulting to uvloop if it is installed 97 | import uvloop 98 | import asyncio 99 | 100 | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 101 | except (ImportError, AttributeError): 102 | pass 103 | 104 | __version__ = current 105 | -------------------------------------------------------------------------------- /hug/__main__.py: -------------------------------------------------------------------------------- 1 | import hug 2 | 3 | hug.development_runner.hug.interface.cli() 4 | -------------------------------------------------------------------------------- /hug/_empty.py: -------------------------------------------------------------------------------- 1 | """hug/_empty.py 2 | 3 | Defines a set of empty types for use within hug, to ensure extra memory / processing isn't taken creating empty types 4 | as default values. 5 | 6 | Copyright (C) 2016 Timothy Edmund Crosley 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 9 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 10 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 11 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all copies or 14 | substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 19 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 20 | OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | """ 23 | from __future__ import absolute_import 24 | 25 | from types import MappingProxyType 26 | 27 | list = tuple = () 28 | dict = MappingProxyType({}) 29 | set = frozenset() 30 | -------------------------------------------------------------------------------- /hug/_version.py: -------------------------------------------------------------------------------- 1 | """hug/_version.py 2 | 3 | Defines hug version information 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | from __future__ import absolute_import 23 | 24 | current = "2.6.1" 25 | -------------------------------------------------------------------------------- /hug/defaults.py: -------------------------------------------------------------------------------- 1 | """hug/defaults.py 2 | 3 | Defines and stores Hug's default handlers 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | from __future__ import absolute_import 23 | 24 | import hug 25 | 26 | output_format = hug.output_format.json 27 | cli_output_format = hug.output_format.text 28 | 29 | input_format = { 30 | "application/json": hug.input_format.json, 31 | "application/x-www-form-urlencoded": hug.input_format.urlencoded, 32 | "multipart/form-data": hug.input_format.multipart, 33 | "text/plain": hug.input_format.text, 34 | "text/css": hug.input_format.text, 35 | "text/html": hug.input_format.text, 36 | } 37 | 38 | directives = { 39 | "timer": hug.directives.Timer, 40 | "api": hug.directives.api, 41 | "module": hug.directives.module, 42 | "current_api": hug.directives.CurrentAPI, 43 | "api_version": hug.directives.api_version, 44 | "user": hug.directives.user, 45 | "session": hug.directives.session, 46 | "documentation": hug.directives.documentation, 47 | } 48 | 49 | 50 | def context_factory(*args, **kwargs): 51 | return dict() 52 | 53 | 54 | def delete_context(context, exception=None, errors=None, lacks_requirement=None): 55 | del context 56 | -------------------------------------------------------------------------------- /hug/development_runner.py: -------------------------------------------------------------------------------- 1 | """hug/development_runner.py 2 | 3 | Contains logic to enable execution of hug APIS locally from the command line for development use 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | from __future__ import absolute_import 22 | 23 | import importlib 24 | import os 25 | import subprocess 26 | import sys 27 | import tempfile 28 | import time 29 | from multiprocessing import Process 30 | from os.path import exists 31 | 32 | import _thread as thread 33 | from hug._version import current 34 | from hug.api import API 35 | from hug.route import cli 36 | from hug.types import boolean, number 37 | 38 | INIT_MODULES = list(sys.modules.keys()) 39 | 40 | 41 | def _start_api(api_module, host, port, no_404_documentation, show_intro=True): 42 | API(api_module).http.serve(host, port, no_404_documentation, show_intro) 43 | 44 | 45 | @cli(version=current) 46 | def hug( 47 | file: "A Python file that contains a Hug API" = None, 48 | module: "A Python module that contains a Hug API" = None, 49 | host: "Interface to bind to" = "", 50 | port: number = 8000, 51 | no_404_documentation: boolean = False, 52 | manual_reload: boolean = False, 53 | interval: number = 1, 54 | command: "Run a command defined in the given module" = None, 55 | silent: boolean = False, 56 | ): 57 | """Hug API Development Server""" 58 | api_module = None 59 | if file and module: 60 | print("Error: can not define both a file and module source for Hug API.") 61 | sys.exit(1) 62 | if file: 63 | sys.path.append(os.path.dirname(os.path.abspath(file))) 64 | sys.path.append(os.getcwd()) 65 | api_module = importlib.machinery.SourceFileLoader(file.split(".")[0], file).load_module() 66 | elif module: 67 | sys.path.append(os.getcwd()) 68 | api_module = importlib.import_module(module) 69 | if not api_module or not hasattr(api_module, "__hug__"): 70 | print("Error: must define a file name or module that contains a Hug API.") 71 | sys.exit(1) 72 | 73 | api = API(api_module, display_intro=not silent) 74 | if command: 75 | if command not in api.cli.commands: 76 | print(str(api.cli)) 77 | sys.exit(1) 78 | 79 | flag_index = (sys.argv.index("-c") if "-c" in sys.argv else sys.argv.index("--command")) + 1 80 | sys.argv = sys.argv[flag_index:] 81 | api.cli.commands[command]() 82 | return 83 | 84 | ran = False 85 | if not manual_reload: 86 | thread.start_new_thread(reload_checker, (interval,)) 87 | while True: 88 | reload_checker.reloading = False 89 | time.sleep(1) 90 | try: 91 | _start_api( 92 | api_module, host, port, no_404_documentation, False if silent else not ran 93 | ) 94 | except KeyboardInterrupt: 95 | if not reload_checker.reloading: 96 | sys.exit(1) 97 | reload_checker.reloading = False 98 | ran = True 99 | for name in list(sys.modules.keys()): 100 | if name not in INIT_MODULES: 101 | del sys.modules[name] 102 | if file: 103 | api_module = importlib.machinery.SourceFileLoader( 104 | file.split(".")[0], file 105 | ).load_module() 106 | elif module: 107 | api_module = importlib.import_module(module) 108 | else: 109 | _start_api(api_module, host, port, no_404_documentation, not ran) 110 | 111 | 112 | def reload_checker(interval): 113 | while True: 114 | changed = False 115 | files = {} 116 | for module in list(sys.modules.values()): 117 | path = getattr(module, "__file__", "") 118 | if not path: 119 | continue 120 | if path[-4:] in (".pyo", ".pyc"): 121 | path = path[:-1] 122 | if path and exists(path): 123 | files[path] = os.stat(path).st_mtime 124 | 125 | while not changed: 126 | for path, last_modified in files.items(): 127 | if not exists(path): 128 | print("\n> Reloading due to file removal: {}".format(path)) 129 | changed = True 130 | elif os.stat(path).st_mtime > last_modified: 131 | print("\n> Reloading due to file change: {}".format(path)) 132 | changed = True 133 | 134 | if changed: 135 | reload_checker.reloading = True 136 | thread.interrupt_main() 137 | time.sleep(5) 138 | break 139 | time.sleep(interval) 140 | -------------------------------------------------------------------------------- /hug/directives.py: -------------------------------------------------------------------------------- 1 | """hug/directives.py 2 | 3 | Defines the directives built into hug. Directives allow attaching behaviour to an API handler based simply 4 | on an argument it takes and that arguments default value. The directive gets called with the default supplied, 5 | ther request data, and api_version. The result of running the directive method is then set as the argument value. 6 | Directive attributes are always prefixed with 'hug_' 7 | 8 | Copyright (C) 2016 Timothy Edmund Crosley 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 11 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 12 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 13 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all copies or 16 | substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 20 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 21 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | """ 25 | from __future__ import absolute_import 26 | 27 | from functools import partial 28 | from timeit import default_timer as python_timer 29 | 30 | from hug import introspect 31 | 32 | 33 | def _built_in_directive(directive): 34 | """Marks a callable as a built-in directive""" 35 | directive.directive = True 36 | return directive 37 | 38 | 39 | @_built_in_directive 40 | class Timer(object): 41 | """Keeps track of time surpased since instantiation, outputed by doing float(instance)""" 42 | 43 | __slots__ = ("start", "round_to") 44 | 45 | def __init__(self, round_to=None, **kwargs): 46 | self.start = python_timer() 47 | self.round_to = round_to 48 | 49 | def __float__(self): 50 | time_taken = python_timer() - self.start 51 | return round(time_taken, self.round_to) if self.round_to else time_taken 52 | 53 | def __int__(self): 54 | return int(round(float(self))) 55 | 56 | def __native_types__(self): 57 | return self.__float__() 58 | 59 | def __str__(self): 60 | return str(float(self)) 61 | 62 | def __repr__(self): 63 | return "{}({})".format(self.__class__.__name__, self) 64 | 65 | 66 | @_built_in_directive 67 | def module(default=None, api=None, **kwargs): 68 | """Returns the module that is running this hug API function""" 69 | return api.module if api else default 70 | 71 | 72 | @_built_in_directive 73 | def api(default=None, api=None, **kwargs): 74 | """Returns the api instance in which this API function is being ran""" 75 | return api if api else default 76 | 77 | 78 | @_built_in_directive 79 | def api_version(default=None, api_version=None, **kwargs): 80 | """Returns the current api_version as a directive for use in both request and not request handling code""" 81 | return api_version 82 | 83 | 84 | @_built_in_directive 85 | def documentation(default=None, api_version=None, api=None, **kwargs): 86 | """returns documentation for the current api""" 87 | api_version = default or api_version 88 | if api: 89 | return api.http.documentation(base_url="", api_version=api_version) 90 | 91 | 92 | @_built_in_directive 93 | def session(context_name="session", request=None, **kwargs): 94 | """Returns the session associated with the current request""" 95 | return request and request.context.get(context_name, None) 96 | 97 | 98 | @_built_in_directive 99 | def user(default=None, request=None, **kwargs): 100 | """Returns the current logged in user""" 101 | return request and request.context.get("user", None) or default 102 | 103 | 104 | @_built_in_directive 105 | def cors(support="*", response=None, **kwargs): 106 | """Adds the the Access-Control-Allow-Origin header to this endpoint, with the specified support""" 107 | response and response.set_header("Access-Control-Allow-Origin", support) 108 | return support 109 | 110 | 111 | @_built_in_directive 112 | class CurrentAPI(object): 113 | """Returns quick access to all api functions on the current version of the api""" 114 | 115 | __slots__ = ("api_version", "api") 116 | 117 | def __init__(self, default=None, api_version=None, **kwargs): 118 | self.api_version = api_version 119 | self.api = api(**kwargs) 120 | 121 | def __getattr__(self, name): 122 | function = self.api.http.versioned.get(self.api_version, {}).get(name, None) 123 | if not function: 124 | function = self.api.http.versioned.get(None, {}).get(name, None) 125 | if not function: 126 | raise AttributeError("API Function {0} not found".format(name)) 127 | 128 | accepts = function.interface.arguments 129 | if "hug_api_version" in accepts: 130 | function = partial(function, hug_api_version=self.api_version) 131 | if "hug_current_api" in accepts: 132 | function = partial(function, hug_current_api=self) 133 | 134 | return function 135 | -------------------------------------------------------------------------------- /hug/exceptions.py: -------------------------------------------------------------------------------- 1 | """hug/exceptions.py 2 | 3 | Defines the custom exceptions that are part of, and support 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | from __future__ import absolute_import 23 | 24 | 25 | class InvalidTypeData(Exception): 26 | """Should be raised when data passed in doesn't match a types expectations""" 27 | 28 | def __init__(self, message, reasons=None): 29 | self.message = message 30 | self.reasons = reasons 31 | 32 | 33 | class StoreKeyNotFound(Exception): 34 | """Should be raised when a store key has not been found inside a store""" 35 | 36 | 37 | class SessionNotFound(StoreKeyNotFound): 38 | """Should be raised when a session ID has not been found inside a session store""" 39 | 40 | pass 41 | -------------------------------------------------------------------------------- /hug/format.py: -------------------------------------------------------------------------------- 1 | """hug/format.py 2 | 3 | Defines formatting utility methods that are common both to input and output formatting and aid in general formatting of 4 | fields and content 5 | 6 | Copyright (C) 2016 Timothy Edmund Crosley 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 9 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 10 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 11 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all copies or 14 | substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 19 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 20 | OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | """ 23 | from __future__ import absolute_import 24 | 25 | import re 26 | from cgi import parse_header 27 | 28 | from hug import _empty as empty 29 | 30 | UNDERSCORE = (re.compile("(.)([A-Z][a-z]+)"), re.compile("([a-z0-9])([A-Z])")) 31 | 32 | 33 | def parse_content_type(content_type): 34 | """Separates out the parameters from the content_type and returns both in a tuple (content_type, parameters)""" 35 | if content_type is not None and ";" in content_type: 36 | return parse_header(content_type) 37 | return (content_type, empty.dict) 38 | 39 | 40 | def content_type(content_type): 41 | """Attaches the supplied content_type to a Hug formatting function""" 42 | 43 | def decorator(method): 44 | method.content_type = content_type 45 | return method 46 | 47 | return decorator 48 | 49 | 50 | def underscore(text): 51 | """Converts text that may be camelcased into an underscored format""" 52 | return UNDERSCORE[1].sub(r"\1_\2", UNDERSCORE[0].sub(r"\1_\2", text)).lower() 53 | 54 | 55 | def camelcase(text): 56 | """Converts text that may be underscored into a camelcase format""" 57 | return text[0] + "".join(text.title().split("_"))[1:] 58 | -------------------------------------------------------------------------------- /hug/input_format.py: -------------------------------------------------------------------------------- 1 | """hug/input_formats.py 2 | 3 | Defines the built-in Hug input_formatting handlers 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | from __future__ import absolute_import 23 | 24 | import re 25 | from cgi import parse_multipart 26 | from urllib.parse import parse_qs as urlencoded_converter 27 | 28 | from falcon.util.uri import parse_query_string 29 | 30 | from hug.format import content_type, underscore 31 | from hug.json_module import json as json_converter 32 | 33 | 34 | @content_type("text/plain") 35 | def text(body, charset="utf-8", **kwargs): 36 | """Takes plain text data""" 37 | return body.read().decode(charset) 38 | 39 | 40 | @content_type("application/json") 41 | def json(body, charset="utf-8", **kwargs): 42 | """Takes JSON formatted data, converting it into native Python objects""" 43 | return json_converter.loads(text(body, charset=charset)) 44 | 45 | 46 | def _underscore_dict(dictionary): 47 | new_dictionary = {} 48 | for key, value in dictionary.items(): 49 | if isinstance(value, dict): 50 | value = _underscore_dict(value) 51 | if isinstance(key, str): 52 | key = underscore(key) 53 | new_dictionary[key] = value 54 | return new_dictionary 55 | 56 | 57 | def json_underscore(body, charset="utf-8", **kwargs): 58 | """Converts JSON formatted date to native Python objects. 59 | 60 | The keys in any JSON dict are transformed from camelcase to underscore separated words. 61 | """ 62 | return _underscore_dict(json(body, charset=charset)) 63 | 64 | 65 | @content_type("application/x-www-form-urlencoded") 66 | def urlencoded(body, charset="ascii", **kwargs): 67 | """Converts query strings into native Python objects""" 68 | return parse_query_string(text(body, charset=charset), False) 69 | 70 | 71 | @content_type("multipart/form-data") 72 | def multipart(body, content_length=0, **header_params): 73 | """Converts multipart form data into native Python objects""" 74 | header_params.setdefault("CONTENT-LENGTH", content_length) 75 | if header_params and "boundary" in header_params: 76 | if type(header_params["boundary"]) is str: 77 | header_params["boundary"] = header_params["boundary"].encode() 78 | 79 | form = parse_multipart((body.stream if hasattr(body, "stream") else body), header_params) 80 | for key, value in form.items(): 81 | if type(value) is list and len(value) == 1: 82 | form[key] = value[0] 83 | return form 84 | -------------------------------------------------------------------------------- /hug/introspect.py: -------------------------------------------------------------------------------- 1 | """hug/introspect.py 2 | 3 | Defines built in hug functions to aid in introspection 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | from __future__ import absolute_import 23 | 24 | import inspect 25 | from types import MethodType 26 | 27 | 28 | def is_method(function): 29 | """Returns True if the passed in function is identified as a method (NOT a function)""" 30 | return isinstance(function, MethodType) 31 | 32 | 33 | def is_coroutine(function): 34 | """Returns True if the passed in function is a coroutine""" 35 | return function.__code__.co_flags & 0x0080 or getattr(function, "_is_coroutine", False) 36 | 37 | 38 | def name(function): 39 | """Returns the name of a function""" 40 | return function.__name__ 41 | 42 | 43 | def arguments(function, extra_arguments=0): 44 | """Returns the name of all arguments a function takes""" 45 | if not hasattr(function, "__code__"): 46 | return () 47 | 48 | return function.__code__.co_varnames[: function.__code__.co_argcount + extra_arguments] 49 | 50 | 51 | def takes_kwargs(function): 52 | """Returns True if the supplied function takes keyword arguments""" 53 | return bool(function.__code__.co_flags & 0x08) 54 | 55 | 56 | def takes_args(function): 57 | """Returns True if the supplied functions takes extra non-keyword arguments""" 58 | return bool(function.__code__.co_flags & 0x04) 59 | 60 | 61 | def takes_arguments(function, *named_arguments): 62 | """Returns the arguments that a function takes from a list of requested arguments""" 63 | return set(named_arguments).intersection(arguments(function)) 64 | 65 | 66 | def takes_all_arguments(function, *named_arguments): 67 | """Returns True if all supplied arguments are found in the function""" 68 | return bool(takes_arguments(function, *named_arguments) == set(named_arguments)) 69 | 70 | 71 | def generate_accepted_kwargs(function, *named_arguments): 72 | """Dynamically creates a function that when called with dictionary of arguments will produce a kwarg that's 73 | compatible with the supplied function 74 | """ 75 | if hasattr(function, "__code__") and takes_kwargs(function): 76 | function_takes_kwargs = True 77 | function_takes_arguments = [] 78 | else: 79 | function_takes_kwargs = False 80 | function_takes_arguments = takes_arguments(function, *named_arguments) 81 | 82 | def accepted_kwargs(kwargs): 83 | if function_takes_kwargs: 84 | return kwargs 85 | elif function_takes_arguments: 86 | return {key: value for key, value in kwargs.items() if key in function_takes_arguments} 87 | return {} 88 | 89 | return accepted_kwargs 90 | -------------------------------------------------------------------------------- /hug/json_module.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | HUG_USE_UJSON = bool(os.environ.get("HUG_USE_UJSON", 1)) 4 | try: # pragma: no cover 5 | if HUG_USE_UJSON: 6 | import ujson as json 7 | 8 | class dumps_proxy: # noqa: N801 9 | """Proxies the call so non supported kwargs are skipped 10 | and it enables escape_forward_slashes to simulate built-in json 11 | """ 12 | 13 | _dumps = json.dumps 14 | 15 | def __call__(self, *args, **kwargs): 16 | kwargs.pop("default", None) 17 | kwargs.pop("separators", None) 18 | kwargs.update(escape_forward_slashes=False) 19 | try: 20 | return self._dumps(*args, **kwargs) 21 | except Exception as exc: 22 | raise TypeError("Type[ujson] is not Serializable", exc) 23 | 24 | json.dumps = dumps_proxy() 25 | else: 26 | import json 27 | except ImportError: # pragma: no cover 28 | import json 29 | -------------------------------------------------------------------------------- /hug/redirect.py: -------------------------------------------------------------------------------- 1 | """hug/redirect.py 2 | 3 | Implements convenience redirect methods that raise a redirection exception when called 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | from __future__ import absolute_import 23 | 24 | import falcon 25 | 26 | 27 | def to(location, code=falcon.HTTP_302): 28 | """Redirects to the specified location using the provided http_code (defaults to HTTP_302 FOUND)""" 29 | raise falcon.http_status.HTTPStatus(code, {"location": location}) 30 | 31 | 32 | def permanent(location): 33 | """Redirects to the specified location using HTTP 301 status code""" 34 | to(location, falcon.HTTP_301) 35 | 36 | 37 | def found(location): 38 | """Redirects to the specified location using HTTP 302 status code""" 39 | to(location, falcon.HTTP_302) 40 | 41 | 42 | def see_other(location): 43 | """Redirects to the specified location using HTTP 303 status code""" 44 | to(location, falcon.HTTP_303) 45 | 46 | 47 | def temporary(location): 48 | """Redirects to the specified location using HTTP 307 status code""" 49 | to(location, falcon.HTTP_307) 50 | 51 | 52 | def not_found(*args, **kwargs): 53 | """Redirects request handling to the not found render""" 54 | raise falcon.HTTPNotFound() 55 | -------------------------------------------------------------------------------- /hug/store.py: -------------------------------------------------------------------------------- 1 | """hug/store.py. 2 | 3 | A collecton of native stores which can be used with, among others, the session middleware. 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | from hug.exceptions import StoreKeyNotFound 23 | 24 | 25 | class InMemoryStore: 26 | """ 27 | Naive store class which can be used for the session middleware and unit tests. 28 | It is not thread-safe and no data will survive the lifecycle of the hug process. 29 | Regard this as a blueprint for more useful and probably more complex store implementations, for example stores 30 | which make use of databases like Redis, PostgreSQL or others. 31 | """ 32 | 33 | def __init__(self): 34 | self._data = {} 35 | 36 | def get(self, key): 37 | """Get data for given store key. Raise hug.exceptions.StoreKeyNotFound if key does not exist.""" 38 | try: 39 | data = self._data[key] 40 | except KeyError: 41 | raise StoreKeyNotFound(key) 42 | return data 43 | 44 | def exists(self, key): 45 | """Return whether key exists or not.""" 46 | return key in self._data 47 | 48 | def set(self, key, data): 49 | """Set data object for given store key.""" 50 | self._data[key] = data 51 | 52 | def delete(self, key): 53 | """Delete data for given store key.""" 54 | if key in self._data: 55 | del self._data[key] 56 | -------------------------------------------------------------------------------- /hug/test.py: -------------------------------------------------------------------------------- 1 | """hug/test.py. 2 | 3 | Defines utility function that aid in the round-trip testing of Hug APIs 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | from __future__ import absolute_import 23 | 24 | import ast 25 | import sys 26 | from functools import partial 27 | from io import BytesIO 28 | from unittest import mock 29 | from urllib.parse import urlencode 30 | 31 | from falcon import HTTP_METHODS 32 | from falcon.testing import DEFAULT_HOST, StartResponseMock, create_environ 33 | 34 | from hug import output_format 35 | from hug.api import API 36 | from hug.json_module import json 37 | 38 | 39 | def _internal_result(raw_response): 40 | try: 41 | return raw_response[0].decode("utf8") 42 | except TypeError: 43 | data = BytesIO() 44 | for chunk in raw_response: 45 | data.write(chunk) 46 | data = data.getvalue() 47 | try: 48 | return data.decode("utf8") 49 | except UnicodeDecodeError: # pragma: no cover 50 | return data 51 | except (UnicodeDecodeError, AttributeError): 52 | return raw_response[0] 53 | 54 | 55 | def call( 56 | method, 57 | api_or_module, 58 | url, 59 | body="", 60 | headers=None, 61 | params=None, 62 | query_string="", 63 | scheme="http", 64 | host=DEFAULT_HOST, 65 | **kwargs 66 | ): 67 | """Simulates a round-trip call against the given API / URL""" 68 | api = API(api_or_module).http.server() 69 | response = StartResponseMock() 70 | headers = {} if headers is None else headers 71 | if not isinstance(body, str) and "json" in headers.get("content-type", "application/json"): 72 | body = output_format.json(body) 73 | headers.setdefault("content-type", "application/json") 74 | 75 | params = params if params else {} 76 | params.update(kwargs) 77 | if params: 78 | query_string = "{}{}{}".format( 79 | query_string, "&" if query_string else "", urlencode(params, True) 80 | ) 81 | result = api( 82 | create_environ( 83 | path=url, 84 | method=method, 85 | headers=headers, 86 | query_string=query_string, 87 | body=body, 88 | scheme=scheme, 89 | host=host, 90 | ), 91 | response, 92 | ) 93 | if result: 94 | response.data = _internal_result(result) 95 | response.content_type = response.headers_dict["content-type"] 96 | if "application/json" in response.content_type: 97 | response.data = json.loads(response.data) 98 | return response 99 | 100 | 101 | for method in HTTP_METHODS: 102 | tester = partial(call, method) 103 | tester.__doc__ = """Simulates a round-trip HTTP {0} against the given API / URL""".format( 104 | method.upper() 105 | ) 106 | globals()[method.lower()] = tester 107 | 108 | 109 | def cli(method, *args, api=None, module=None, **arguments): 110 | """Simulates testing a hug cli method from the command line""" 111 | collect_output = arguments.pop("collect_output", True) 112 | if api and module: 113 | raise ValueError("Please specify an API OR a Module that contains the API, not both") 114 | elif api or module: 115 | method = API(api or module).cli.commands[method].interface._function 116 | 117 | command_args = [method.__name__] + list(args) 118 | for name, values in arguments.items(): 119 | if not isinstance(values, (tuple, list)): 120 | values = (values,) 121 | for value in values: 122 | command_args.append("--{0}".format(name)) 123 | if not value in (True, False): 124 | command_args.append("{0}".format(value)) 125 | 126 | old_sys_argv = sys.argv 127 | sys.argv = [str(part) for part in command_args] 128 | 129 | old_outputs = method.interface.cli.outputs 130 | if collect_output: 131 | method.interface.cli.outputs = lambda data: to_return.append(old_outputs(data)) 132 | to_return = [] 133 | 134 | try: 135 | method.interface.cli() 136 | except Exception as e: 137 | to_return = (e,) 138 | 139 | method.interface.cli.outputs = old_outputs 140 | sys.argv = old_sys_argv 141 | if to_return: 142 | result = _internal_result(to_return) 143 | try: 144 | result = json.loads(result) 145 | except Exception: 146 | try: 147 | result = ast.literal_eval(result) 148 | except Exception: 149 | pass 150 | return result 151 | -------------------------------------------------------------------------------- /hug/this.py: -------------------------------------------------------------------------------- 1 | """hug/this.py. 2 | 3 | The Zen of Hug 4 | 5 | Copyright (C) 2019 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | 23 | ZEN_OF_HUG = """ 24 | Simple Things should be easy, complex things should be possible. 25 | Complex things done often should be made simple. 26 | 27 | Magic should be avoided. 28 | Magic isn't magic as soon as its mechanics are universally understood. 29 | 30 | Wrong documentation is worse than no documentation. 31 | Everything should be documented. 32 | 33 | All code should be tested. 34 | All tests should be meaningful. 35 | 36 | Consistency is more important than perfection. 37 | It's okay to break consistency for practicality. 38 | 39 | Clarity is more important than performance. 40 | If we do our job right, there shouldn't need to be a choice. 41 | 42 | Interfaces are one honking great idea -- let's do more of those! 43 | """ 44 | 45 | print(ZEN_OF_HUG) 46 | -------------------------------------------------------------------------------- /hug/transform.py: -------------------------------------------------------------------------------- 1 | """hug/transform.py 2 | 3 | Defines Hug's built-in output transforming functions 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | from __future__ import absolute_import 23 | 24 | from hug.decorators import auto_kwargs 25 | 26 | 27 | def content_type(transformers, default=None): 28 | """Returns a different transformer depending on the content type passed in. 29 | If none match and no default is given no transformation takes place. 30 | 31 | should pass in a dict with the following format: 32 | 33 | {'[content-type]': transformation_action, 34 | ... 35 | } 36 | """ 37 | transformers = { 38 | content_type: auto_kwargs(transformer) if transformer else transformer 39 | for content_type, transformer in transformers.items() 40 | } 41 | default = default and auto_kwargs(default) 42 | 43 | def transform(data, request): 44 | transformer = transformers.get(request.content_type.split(";")[0], default) 45 | if not transformer: 46 | return data 47 | 48 | return transformer(data) 49 | 50 | return transform 51 | 52 | 53 | def suffix(transformers, default=None): 54 | """Returns a different transformer depending on the suffix at the end of the requested URL. 55 | If none match and no default is given no transformation takes place. 56 | 57 | should pass in a dict with the following format: 58 | 59 | {'[suffix]': transformation_action, 60 | ... 61 | } 62 | """ 63 | transformers = { 64 | suffix: auto_kwargs(transformer) if transformer else transformer 65 | for suffix, transformer in transformers.items() 66 | } 67 | default = default and auto_kwargs(default) 68 | 69 | def transform(data, request): 70 | path = request.path 71 | transformer = default 72 | for suffix_test, suffix_transformer in transformers.items(): 73 | if path.endswith(suffix_test): 74 | transformer = suffix_transformer 75 | break 76 | 77 | return transformer(data) if transformer else data 78 | 79 | return transform 80 | 81 | 82 | def prefix(transformers, default=None): 83 | """Returns a different transformer depending on the prefix at the end of the requested URL. 84 | If none match and no default is given no transformation takes place. 85 | 86 | should pass in a dict with the following format: 87 | 88 | {'[prefix]': transformation_action, 89 | ... 90 | } 91 | """ 92 | transformers = { 93 | prefix: auto_kwargs(transformer) if transformer else transformer 94 | for prefix, transformer in transformers.items() 95 | } 96 | default = default and auto_kwargs(default) 97 | 98 | def transform(data, request=None, response=None): 99 | path = request.path 100 | transformer = default 101 | for prefix_test, prefix_transformer in transformers.items(): 102 | if path.startswith(prefix_test): 103 | transformer = prefix_transformer 104 | break 105 | 106 | return transformer(data) if transformer else data 107 | 108 | return transform 109 | 110 | 111 | def all(*transformers): 112 | """Returns the results of applying all passed in transformers to data 113 | 114 | should pass in list of transformers 115 | 116 | [transformer_1, transformer_2...] 117 | """ 118 | transformers = tuple(auto_kwargs(transformer) for transformer in transformers) 119 | 120 | def transform(data, request=None, response=None): 121 | for transformer in transformers: 122 | data = transformer(data, request=request, response=response) 123 | 124 | return data 125 | 126 | return transform 127 | -------------------------------------------------------------------------------- /hug/validate.py: -------------------------------------------------------------------------------- 1 | """hug/validate.py 2 | 3 | Defines hugs built-in validation methods 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | from __future__ import absolute_import 23 | 24 | 25 | def all(*validators): 26 | """Validation only succeeds if all passed in validators return no errors""" 27 | 28 | def validate_all(fields): 29 | for validator in validators: 30 | errors = validator(fields) 31 | if errors: 32 | return errors 33 | 34 | validate_all.__doc__ = " and ".join(validator.__doc__ for validator in validators) 35 | return validate_all 36 | 37 | 38 | def any(*validators): 39 | """If any of the specified validators pass the validation succeeds""" 40 | 41 | def validate_any(fields): 42 | errors = {} 43 | for validator in validators: 44 | validation_errors = validator(fields) 45 | if not validation_errors: 46 | return 47 | errors.update(validation_errors) 48 | return errors 49 | 50 | validate_any.__doc__ = " or ".join(validator.__doc__ for validator in validators) 51 | return validate_any 52 | 53 | 54 | def contains_one_of(*fields): 55 | """Enables ensuring that one of multiple optional fields is set""" 56 | message = "Must contain any one of the following fields: {0}".format(", ".join(fields)) 57 | 58 | def check_contains(endpoint_fields): 59 | for field in fields: 60 | if field in endpoint_fields: 61 | return 62 | 63 | errors = {} 64 | for field in fields: 65 | errors[field] = "one of these must have a value" 66 | return errors 67 | 68 | check_contains.__doc__ = message 69 | return check_contains 70 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.portray] 2 | docs_dir = "documentation" 3 | extra_dirs = ["examples", "artwork"] 4 | 5 | [tool.portray.mkdocs.theme] 6 | favicon = "artwork/koala.png" 7 | logo = "artwork/koala.png" 8 | name = "material" 9 | palette = {primary = "blue grey", accent = "green"} 10 | -------------------------------------------------------------------------------- /requirements/build.txt: -------------------------------------------------------------------------------- 1 | -r build_common.txt 2 | marshmallow==2.18.1 3 | -------------------------------------------------------------------------------- /requirements/build_common.txt: -------------------------------------------------------------------------------- 1 | -r common.txt 2 | flake8==3.5.0 3 | pytest-cov==2.7.1 4 | pytest==4.6.3 5 | python-coveralls==2.9.2 6 | wheel==0.33.4 7 | PyJWT==1.7.1 8 | pytest-xdist==1.29.0 9 | numpy<1.16 10 | -------------------------------------------------------------------------------- /requirements/build_style_tools.txt: -------------------------------------------------------------------------------- 1 | -r build_common.txt 2 | black==19.3b0 3 | isort==4.3.20 4 | pep8-naming==0.8.2 5 | flake8-bugbear==19.3.0 6 | vulture==1.0 7 | bandit==1.6.1 8 | safety==1.8.5 9 | -------------------------------------------------------------------------------- /requirements/build_windows.txt: -------------------------------------------------------------------------------- 1 | -r common.txt 2 | flake8==3.7.7 3 | isort==4.3.20 4 | marshmallow==2.18.1 5 | pytest==4.6.3 6 | wheel==0.33.4 7 | pytest-xdist==1.29.0 8 | numpy==1.15.4 9 | -------------------------------------------------------------------------------- /requirements/common.txt: -------------------------------------------------------------------------------- 1 | falcon==2.0.0 2 | requests==2.22.0 3 | -------------------------------------------------------------------------------- /requirements/development.txt: -------------------------------------------------------------------------------- 1 | bumpversion==0.5.3 2 | Cython==0.29.10 3 | -r common.txt 4 | flake8==3.7.7 5 | ipython==7.5.0 6 | isort==4.3.20 7 | pytest-cov==2.7.1 8 | pytest==4.6.3 9 | python-coveralls==2.9.2 10 | tox==3.12.1 11 | wheel 12 | pytest-xdist==1.29.0 13 | marshmallow==2.18.1 14 | ujson==1.35 15 | numpy<1.16 16 | 17 | -------------------------------------------------------------------------------- /requirements/release.txt: -------------------------------------------------------------------------------- 1 | -r common.txt 2 | -------------------------------------------------------------------------------- /scripts/before_install.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | echo $TRAVIS_OS_NAME 4 | 5 | if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then 6 | 7 | # Travis has an old version of pyenv by default, upgrade it 8 | brew update > /dev/null 2>&1 9 | brew outdated pyenv || brew upgrade pyenv 10 | 11 | pyenv --version 12 | 13 | # Find the latest requested version of python 14 | case "$TOXENV" in 15 | py35) 16 | python_minor=5;; 17 | py36) 18 | python_minor=6;; 19 | py36-marshmallow2) 20 | python_minor=6;; 21 | py36-marshmallow3) 22 | python_minor=6;; 23 | py37) 24 | python_minor=7;; 25 | py38) 26 | python_minor=8;; 27 | esac 28 | latest_version=`pyenv install --list | grep -e "^[ ]*3\.$python_minor" | tail -1` 29 | 30 | pyenv install $latest_version 31 | pyenv local $latest_version 32 | fi 33 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then 4 | 5 | # enable pyenv shims to activate the proper python version 6 | eval "$(pyenv init -)" 7 | 8 | # Log some information on the environment 9 | pyenv local 10 | which python 11 | which pip 12 | python --version 13 | python -m pip --version 14 | fi 15 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | ignore = F401,F403,E502,E123,E127,E128,E303,E713,E111,E241,E302,E121,E261,W391,E731,W503,E305 6 | max-line-length = 120 7 | 8 | [metadata] 9 | license_file = LICENSE 10 | 11 | [aliases] 12 | test=pytest 13 | 14 | [tool:pytest] 15 | addopts = tests 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """setup.py 3 | 4 | Defines the setup instructions for the hug framework 5 | 6 | Copyright (C) 2016 Timothy Edmund Crosley 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 9 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 10 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 11 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all copies or 14 | substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 19 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 20 | OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | """ 23 | import glob 24 | import os 25 | import sys 26 | from os import path 27 | 28 | from setuptools import Extension, setup 29 | 30 | MYDIR = path.abspath(os.path.dirname(__file__)) 31 | CYTHON = False 32 | JYTHON = "java" in sys.platform 33 | 34 | ext_modules = [] 35 | cmdclass = {} 36 | 37 | try: 38 | sys.pypy_version_info 39 | PYPY = True 40 | except AttributeError: 41 | PYPY = False 42 | 43 | if not PYPY and not JYTHON: 44 | if "--without-cython" in sys.argv: 45 | sys.argv.remove("--without-cython") 46 | CYTHON = False 47 | else: 48 | try: 49 | from Cython.Distutils import build_ext 50 | 51 | CYTHON = True 52 | except ImportError: 53 | CYTHON = False 54 | 55 | if CYTHON: 56 | 57 | def list_modules(dirname): 58 | filenames = glob.glob(path.join(dirname, "*.py")) 59 | 60 | module_names = [] 61 | for name in filenames: 62 | module, ext = path.splitext(path.basename(name)) 63 | if module != "__init__": 64 | module_names.append(module) 65 | 66 | return module_names 67 | 68 | ext_modules = [ 69 | Extension("hug." + ext, [path.join("hug", ext + ".py")]) 70 | for ext in list_modules(path.join(MYDIR, "hug")) 71 | ] 72 | cmdclass["build_ext"] = build_ext 73 | 74 | 75 | with open("README.md", encoding="utf-8") as f: # Loads in the README for PyPI 76 | long_description = f.read() 77 | 78 | 79 | setup( 80 | name="hug", 81 | version="2.6.1", 82 | description="A Python framework that makes developing APIs " 83 | "as simple as possible, but no simpler.", 84 | long_description=long_description, 85 | # PEP 566, the new PyPI, and setuptools>=38.6.0 make markdown possible 86 | long_description_content_type="text/markdown", 87 | author="Timothy Crosley", 88 | author_email="timothy.crosley@gmail.com", 89 | # These appear in the left hand side bar on PyPI 90 | url="https://github.com/hugapi/hug", 91 | project_urls={ 92 | "Documentation": "http://www.hug.rest/", 93 | "Gitter": "https://gitter.im/timothycrosley/hug", 94 | }, 95 | license="MIT", 96 | entry_points={"console_scripts": ["hug = hug:development_runner.hug.interface.cli"]}, 97 | packages=["hug"], 98 | requires=["falcon", "requests"], 99 | install_requires=["falcon==2.0.0", "requests"], 100 | tests_require=["pytest", "marshmallow"], 101 | ext_modules=ext_modules, 102 | cmdclass=cmdclass, 103 | python_requires=">=3.5", 104 | keywords="Web, Python, Python3, Refactoring, REST, Framework, RPC", 105 | classifiers=[ 106 | "Development Status :: 6 - Mature", 107 | "Intended Audience :: Developers", 108 | "Natural Language :: English", 109 | "Environment :: Console", 110 | "License :: OSI Approved :: MIT License", 111 | "Programming Language :: Python", 112 | "Programming Language :: Python :: 3", 113 | "Programming Language :: Python :: 3.5", 114 | "Programming Language :: Python :: 3.6", 115 | "Programming Language :: Python :: 3.7", 116 | "Topic :: Software Development :: Libraries", 117 | "Topic :: Utilities", 118 | ], 119 | ) 120 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugapi/hug/e4a3fa40f98487a67351311d0da659a6c9ce88a6/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Configuration for test environment""" 2 | import sys 3 | 4 | from .fixtures import * 5 | -------------------------------------------------------------------------------- /tests/constants.py: -------------------------------------------------------------------------------- 1 | """tests/constants.py. 2 | 3 | Provides constants for the test runner. 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | 23 | import os 24 | 25 | TEST_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) 26 | BASE_DIRECTORY = os.path.realpath(os.path.join(TEST_DIRECTORY, "..")) 27 | -------------------------------------------------------------------------------- /tests/data/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Index Page

4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/fixtures.py: -------------------------------------------------------------------------------- 1 | """Defines fixtures that can be used to streamline tests and / or define dependencies""" 2 | from collections import namedtuple 3 | from random import randint 4 | 5 | import pytest 6 | 7 | import hug 8 | 9 | Routers = namedtuple("Routers", ["http", "local", "cli"]) 10 | 11 | 12 | class TestAPI(hug.API): 13 | pass 14 | 15 | 16 | @pytest.fixture 17 | def hug_api(): 18 | """Defines a dependency for and then includes a uniquely identified hug API for a single test case""" 19 | api = TestAPI("fake_api_{}".format(randint(0, 1000000))) 20 | api.route = Routers( 21 | hug.routing.URLRouter().api(api), 22 | hug.routing.LocalRouter().api(api), 23 | hug.routing.CLIRouter().api(api), 24 | ) 25 | return api 26 | 27 | 28 | @pytest.fixture 29 | def hug_api_error_exit_codes_enabled(): 30 | """ 31 | Defines a dependency for and then includes a uniquely identified hug API 32 | for a single test case with error exit codes enabled. 33 | """ 34 | return TestAPI("fake_api_{}".format(randint(0, 1000000)), cli_error_exit_codes=True) 35 | -------------------------------------------------------------------------------- /tests/module_fake.py: -------------------------------------------------------------------------------- 1 | """Fake HUG API module usable for testing importation of modules""" 2 | import hug 3 | 4 | 5 | class FakeException(BaseException): 6 | pass 7 | 8 | 9 | @hug.directive(apply_globally=False) 10 | def my_directive(default=None, **kwargs): 11 | """for testing""" 12 | return default 13 | 14 | 15 | @hug.default_input_format("application/made-up") 16 | def made_up_formatter(data): 17 | """for testing""" 18 | return data 19 | 20 | 21 | @hug.default_output_format() 22 | def output_formatter(data): 23 | """for testing""" 24 | return hug.output_format.json(data) 25 | 26 | 27 | @hug.get() 28 | def made_up_api(hug_my_directive=True): 29 | """for testing""" 30 | return hug_my_directive 31 | 32 | 33 | @hug.directive(apply_globally=True) 34 | def my_directive_global(default=None, **kwargs): 35 | """for testing""" 36 | return default 37 | 38 | 39 | @hug.default_input_format("application/made-up", apply_globally=True) 40 | def made_up_formatter_global(data): 41 | """for testing""" 42 | return data 43 | 44 | 45 | @hug.default_output_format(apply_globally=True) 46 | def output_formatter_global(data, request=None, response=None): 47 | """for testing""" 48 | return hug.output_format.json(data) 49 | 50 | 51 | @hug.request_middleware() 52 | def handle_request(request, response): 53 | """for testing""" 54 | return 55 | 56 | 57 | @hug.startup() 58 | def on_startup(api): 59 | """for testing""" 60 | return 61 | 62 | 63 | @hug.static() 64 | def static(): 65 | """for testing""" 66 | return ("",) 67 | 68 | 69 | @hug.sink("/all") 70 | def sink(path): 71 | """for testing""" 72 | return path 73 | 74 | 75 | @hug.exception(FakeException) 76 | def handle_exception(exception): 77 | """Handles the provided exception for testing""" 78 | return True 79 | 80 | 81 | @hug.not_found() 82 | def not_found_handler(): 83 | """for testing""" 84 | return True 85 | -------------------------------------------------------------------------------- /tests/module_fake_http_and_cli.py: -------------------------------------------------------------------------------- 1 | import hug 2 | 3 | 4 | @hug.get() 5 | @hug.cli() 6 | def made_up_go(): 7 | return "Going!" 8 | -------------------------------------------------------------------------------- /tests/module_fake_many_methods.py: -------------------------------------------------------------------------------- 1 | """Fake HUG API module usable for testing importation of modules""" 2 | import hug 3 | 4 | 5 | @hug.get() 6 | def made_up_hello(): 7 | """GETting for science!""" 8 | return "hello from GET" 9 | 10 | 11 | @hug.post() 12 | def made_up_hello(): 13 | """POSTing for science!""" 14 | return "hello from POST" 15 | -------------------------------------------------------------------------------- /tests/module_fake_post.py: -------------------------------------------------------------------------------- 1 | """Fake HUG API module usable for testing importation of modules""" 2 | import hug 3 | 4 | 5 | @hug.post() 6 | def made_up_hello(): 7 | """POSTing for science!""" 8 | return "hello from POST" 9 | -------------------------------------------------------------------------------- /tests/module_fake_simple.py: -------------------------------------------------------------------------------- 1 | """Simple 1 endpoint Fake HUG API module usable for testing importation of modules""" 2 | import hug 3 | 4 | 5 | class FakeSimpleException(Exception): 6 | pass 7 | 8 | 9 | @hug.get() 10 | def made_up_hello(): 11 | """for science!""" 12 | return "hello" 13 | 14 | 15 | @hug.get("/exception") 16 | def made_up_exception(): 17 | raise FakeSimpleException("test") 18 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | """tests/test_api.py. 2 | 3 | Tests to ensure the API object that stores the state of each individual hug endpoint works as expected 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | import pytest 23 | 24 | import hug 25 | 26 | api = hug.API(__name__) 27 | 28 | 29 | class TestAPI(object): 30 | """A collection of tests to ensure the hug API object interacts as expected""" 31 | 32 | def test_singleton(self): 33 | """Test to ensure there can only be one hug API per module""" 34 | assert hug.API(__name__) == api 35 | 36 | def test_context(self): 37 | """Test to ensure the hug singleton provides a global modifiable context""" 38 | assert not hasattr(hug.API(__name__), "_context") 39 | assert hug.API(__name__).context == {} 40 | assert hasattr(hug.API(__name__), "_context") 41 | 42 | def test_dynamic(self): 43 | """Test to ensure it's possible to dynamically create new modules to house APIs based on name alone""" 44 | new_api = hug.API("module_created_on_the_fly") 45 | assert new_api.module.__name__ == "module_created_on_the_fly" 46 | import module_created_on_the_fly 47 | 48 | assert module_created_on_the_fly 49 | assert module_created_on_the_fly.__hug__ == new_api 50 | 51 | 52 | def test_from_object(): 53 | """Test to ensure it's possible to rechieve an API singleton from an arbitrary object""" 54 | assert hug.api.from_object(TestAPI) == api 55 | 56 | 57 | def test_api_fixture(hug_api): 58 | """Ensure it's possible to dynamically insert a new hug API on demand""" 59 | assert isinstance(hug_api, hug.API) 60 | assert hug_api != api 61 | 62 | 63 | def test_anonymous(): 64 | """Ensure it's possible to create anonymous APIs""" 65 | assert hug.API() != hug.API() != api 66 | assert hug.API().module == None 67 | assert hug.API().name == "" 68 | assert hug.API(name="my_name").name == "my_name" 69 | assert hug.API(doc="Custom documentation").doc == "Custom documentation" 70 | 71 | 72 | def test_api_routes(hug_api): 73 | """Ensure http API can return a quick mapping all urls to method""" 74 | hug_api.http.base_url = "/root" 75 | 76 | @hug.get(api=hug_api) 77 | def my_route(): 78 | pass 79 | 80 | @hug.post(api=hug_api) 81 | def my_second_route(): 82 | pass 83 | 84 | @hug.cli(api=hug_api) 85 | def my_cli_command(): 86 | pass 87 | 88 | assert list(hug_api.http.urls()) == ["/root/my_route", "/root/my_second_route"] 89 | assert list(hug_api.http.handlers()) == [ 90 | my_route.interface.http, 91 | my_second_route.interface.http, 92 | ] 93 | assert list(hug_api.handlers()) == [ 94 | my_route.interface.http, 95 | my_second_route.interface.http, 96 | my_cli_command.interface.cli, 97 | ] 98 | 99 | 100 | def test_cli_interface_api_with_exit_codes(hug_api_error_exit_codes_enabled): 101 | api = hug_api_error_exit_codes_enabled 102 | 103 | @hug.object(api=api) 104 | class TrueOrFalse: 105 | @hug.object.cli 106 | def true(self): 107 | return True 108 | 109 | @hug.object.cli 110 | def false(self): 111 | return False 112 | 113 | api.cli(args=[None, "true"]) 114 | 115 | with pytest.raises(SystemExit): 116 | api.cli(args=[None, "false"]) 117 | 118 | 119 | def test_cli_interface_api_without_exit_codes(): 120 | @hug.object(api=api) 121 | class TrueOrFalse: 122 | @hug.object.cli 123 | def true(self): 124 | return True 125 | 126 | @hug.object.cli 127 | def false(self): 128 | return False 129 | 130 | api.cli(args=[None, "true"]) 131 | api.cli(args=[None, "false"]) 132 | -------------------------------------------------------------------------------- /tests/test_async.py: -------------------------------------------------------------------------------- 1 | """hug/test.py. 2 | 3 | Tests the support for asynchronous method using asyncio async def 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | 23 | import asyncio 24 | 25 | import hug 26 | 27 | loop = asyncio.get_event_loop() 28 | api = hug.API(__name__) 29 | 30 | 31 | def test_basic_call_async(): 32 | """ The most basic Happy-Path test for Hug APIs using async """ 33 | 34 | @hug.call() 35 | async def hello_world(): 36 | return "Hello World!" 37 | 38 | assert loop.run_until_complete(hello_world()) == "Hello World!" 39 | 40 | 41 | def tested_nested_basic_call_async(): 42 | """Test to ensure the most basic call still works if applied to a method""" 43 | 44 | @hug.call() 45 | async def hello_world(self=None): 46 | return await nested_hello_world() 47 | 48 | @hug.local() 49 | async def nested_hello_world(self=None): 50 | return "Hello World!" 51 | 52 | assert hello_world.interface.http 53 | assert loop.run_until_complete(hello_world()) == "Hello World!" 54 | assert hug.test.get(api, "/hello_world").data == "Hello World!" 55 | 56 | 57 | def test_basic_call_on_method_async(): 58 | """Test to ensure the most basic call still works if applied to a method""" 59 | 60 | class API(object): 61 | @hug.call() 62 | async def hello_world(self=None): 63 | return "Hello World!" 64 | 65 | api_instance = API() 66 | assert api_instance.hello_world.interface.http 67 | assert loop.run_until_complete(api_instance.hello_world()) == "Hello World!" 68 | assert hug.test.get(api, "/hello_world").data == "Hello World!" 69 | 70 | 71 | def test_basic_call_on_method_through_api_instance_async(): 72 | """Test to ensure instance method calling via async works as expected""" 73 | 74 | class API(object): 75 | def hello_world(self): 76 | return "Hello World!" 77 | 78 | api_instance = API() 79 | 80 | @hug.call() 81 | async def hello_world(): 82 | return api_instance.hello_world() 83 | 84 | assert api_instance.hello_world() == "Hello World!" 85 | assert hug.test.get(api, "/hello_world").data == "Hello World!" 86 | 87 | 88 | def test_basic_call_on_method_registering_without_decorator_async(): 89 | """Test to ensure async methods can be used without decorator""" 90 | 91 | class API(object): 92 | def __init__(self): 93 | hug.call()(self.hello_world_method) 94 | 95 | async def hello_world_method(self): 96 | return "Hello World!" 97 | 98 | api_instance = API() 99 | 100 | assert loop.run_until_complete(api_instance.hello_world_method()) == "Hello World!" 101 | assert hug.test.get(api, "/hello_world_method").data == "Hello World!" 102 | -------------------------------------------------------------------------------- /tests/test_coroutines.py: -------------------------------------------------------------------------------- 1 | """hug/test.py. 2 | 3 | Tests the support for asynchronous method using asyncio coroutines 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | import asyncio 23 | 24 | import hug 25 | 26 | loop = asyncio.get_event_loop() 27 | api = hug.API(__name__) 28 | 29 | 30 | def test_basic_call_coroutine(): 31 | """The most basic Happy-Path test for Hug APIs using async""" 32 | 33 | @hug.call() 34 | async def hello_world(): 35 | return "Hello World!" 36 | 37 | assert loop.run_until_complete(hello_world()) == "Hello World!" 38 | 39 | 40 | def test_nested_basic_call_coroutine(): 41 | """The most basic Happy-Path test for Hug APIs using async""" 42 | 43 | @hug.call() 44 | async def hello_world(): 45 | return getattr(asyncio, "ensure_future")(nested_hello_world()) 46 | 47 | @hug.local() 48 | async def nested_hello_world(): 49 | return "Hello World!" 50 | 51 | assert loop.run_until_complete(hello_world()).result() == "Hello World!" 52 | 53 | 54 | def test_basic_call_on_method_coroutine(): 55 | """Test to ensure the most basic call still works if applied to a method""" 56 | 57 | class API(object): 58 | @hug.call() 59 | async def hello_world(self=None): 60 | return "Hello World!" 61 | 62 | api_instance = API() 63 | assert api_instance.hello_world.interface.http 64 | assert loop.run_until_complete(api_instance.hello_world()) == "Hello World!" 65 | assert hug.test.get(api, "/hello_world").data == "Hello World!" 66 | 67 | 68 | def test_basic_call_on_method_through_api_instance_coroutine(): 69 | """Test to ensure the most basic call still works if applied to a method""" 70 | 71 | class API(object): 72 | def hello_world(self): 73 | return "Hello World!" 74 | 75 | api_instance = API() 76 | 77 | @hug.call() 78 | async def hello_world(): 79 | return api_instance.hello_world() 80 | 81 | assert api_instance.hello_world() == "Hello World!" 82 | assert hug.test.get(api, "/hello_world").data == "Hello World!" 83 | 84 | 85 | def test_basic_call_on_method_registering_without_decorator_coroutine(): 86 | """Test to ensure instance method calling via async works as expected""" 87 | 88 | class API(object): 89 | def __init__(self): 90 | hug.call()(self.hello_world_method) 91 | 92 | async def hello_world_method(self): 93 | return "Hello World!" 94 | 95 | api_instance = API() 96 | 97 | assert loop.run_until_complete(api_instance.hello_world_method()) == "Hello World!" 98 | assert hug.test.get(api, "/hello_world_method").data == "Hello World!" 99 | -------------------------------------------------------------------------------- /tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | """tests/test_exceptions.py. 2 | 3 | Tests to ensure custom exceptions work and are formatted as expected 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | import pytest 23 | 24 | import hug 25 | 26 | 27 | def test_invalid_type_data(): 28 | try: 29 | raise hug.exceptions.InvalidTypeData("not a good type") 30 | except hug.exceptions.InvalidTypeData as exception: 31 | error = exception 32 | 33 | assert error.message == "not a good type" 34 | assert error.reasons is None 35 | 36 | try: 37 | raise hug.exceptions.InvalidTypeData("not a good type", [1, 2, 3]) 38 | except hug.exceptions.InvalidTypeData as exception: 39 | error = exception 40 | 41 | assert error.message == "not a good type" 42 | assert error.reasons == [1, 2, 3] 43 | 44 | with pytest.raises(Exception): 45 | try: 46 | raise hug.exceptions.InvalidTypeData() 47 | except hug.exceptions.InvalidTypeData as exception: 48 | pass 49 | -------------------------------------------------------------------------------- /tests/test_full_request.py: -------------------------------------------------------------------------------- 1 | """tests/test_full_request.py. 2 | 3 | Test cases that rely on a command being ran against a running hug server 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | import platform 23 | import sys 24 | import time 25 | from subprocess import Popen 26 | 27 | import pytest 28 | import requests 29 | 30 | import hug 31 | 32 | TEST_HUG_API = """ 33 | import hug 34 | 35 | 36 | @hug.post("/test", output=hug.output_format.json) 37 | def post(body, response): 38 | print(body) 39 | return {'message': 'ok'} 40 | """ 41 | 42 | 43 | @pytest.mark.skipif( 44 | platform.python_implementation() == "PyPy", reason="Can't run hug CLI from travis PyPy" 45 | ) 46 | @pytest.mark.skipif(sys.platform == "win32", reason="CLI not currently testable on Windows") 47 | def test_hug_post(tmp_path): 48 | hug_test_file = tmp_path / "hug_postable.py" 49 | hug_test_file.write_text(TEST_HUG_API) 50 | hug_server = Popen(["hug", "-f", str(hug_test_file), "-p", "3000"]) 51 | time.sleep(5) 52 | requests.post("http://127.0.0.1:3000/test", {"data": "here"}) 53 | hug_server.kill() 54 | -------------------------------------------------------------------------------- /tests/test_global_context.py: -------------------------------------------------------------------------------- 1 | import hug 2 | 3 | 4 | def test_context_global_decorators(hug_api): 5 | custom_context = dict(context="global", factory=0, delete=0) 6 | 7 | @hug.context_factory(apply_globally=True) 8 | def create_context(*args, **kwargs): 9 | custom_context["factory"] += 1 10 | return custom_context 11 | 12 | @hug.delete_context(apply_globally=True) 13 | def delete_context(context, *args, **kwargs): 14 | assert context == custom_context 15 | custom_context["delete"] += 1 16 | 17 | @hug.get(api=hug_api) 18 | def made_up_hello(): 19 | return "hi" 20 | 21 | @hug.extend_api(api=hug_api, base_url="/api") 22 | def extend_with(): 23 | import tests.module_fake_simple 24 | 25 | return (tests.module_fake_simple,) 26 | 27 | assert hug.test.get(hug_api, "/made_up_hello").data == "hi" 28 | assert custom_context["factory"] == 1 29 | assert custom_context["delete"] == 1 30 | assert hug.test.get(hug_api, "/api/made_up_hello").data == "hello" 31 | assert custom_context["factory"] == 2 32 | assert custom_context["delete"] == 2 33 | -------------------------------------------------------------------------------- /tests/test_input_format.py: -------------------------------------------------------------------------------- 1 | """tests/test_input_format.py. 2 | 3 | Tests the input format handlers included with Hug 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | import os 23 | from cgi import parse_header 24 | from io import BytesIO 25 | 26 | import requests 27 | 28 | import hug 29 | 30 | from .constants import BASE_DIRECTORY 31 | 32 | 33 | def test_text(): 34 | """Ensure that plain text input format works as intended""" 35 | test_data = BytesIO(b'{"a": "b"}') 36 | assert hug.input_format.text(test_data) == '{"a": "b"}' 37 | 38 | 39 | def test_json(): 40 | """Ensure that the json input format works as intended""" 41 | test_data = BytesIO(b'{"a": "b"}') 42 | assert hug.input_format.json(test_data) == {"a": "b"} 43 | 44 | 45 | def test_json_underscore(): 46 | """Ensure that camelCase keys can be converted into under_score for easier use within Python""" 47 | test_data = BytesIO(b'{"CamelCase": {"becauseWeCan": "ValueExempt"}}') 48 | assert hug.input_format.json_underscore(test_data) == { 49 | "camel_case": {"because_we_can": "ValueExempt"} 50 | } 51 | 52 | 53 | def test_urlencoded(): 54 | """Ensure that urlencoded input format works as intended""" 55 | test_data = BytesIO(b"foo=baz&foo=bar&name=John+Doe") 56 | assert hug.input_format.urlencoded(test_data) == {"name": "John Doe", "foo": ["baz", "bar"]} 57 | test_data = BytesIO(b"foo=baz,bar&name=John+Doe") 58 | assert hug.input_format.urlencoded(test_data) == {"name": "John Doe", "foo": ["baz", "bar"]} 59 | test_data = BytesIO(b"foo=baz,&name=John+Doe") 60 | assert hug.input_format.urlencoded(test_data) == {"name": "John Doe", "foo": ["baz"]} 61 | 62 | 63 | def test_multipart(): 64 | """Ensure multipart form data works as intended""" 65 | with open(os.path.join(BASE_DIRECTORY, "artwork", "koala.png"), "rb") as koala: 66 | prepared_request = requests.Request( 67 | "POST", "http://localhost/", files={"koala": koala} 68 | ).prepare() 69 | koala.seek(0) 70 | headers = parse_header(prepared_request.headers["Content-Type"])[1] 71 | headers["CONTENT-LENGTH"] = "22176" 72 | file_content = hug.input_format.multipart(BytesIO(prepared_request.body), **headers)[ 73 | "koala" 74 | ] 75 | assert file_content == koala.read() 76 | -------------------------------------------------------------------------------- /tests/test_interface.py: -------------------------------------------------------------------------------- 1 | """tests/test_interface.py. 2 | 3 | Tests hug's defined interfaces (HTTP, CLI, & Local) 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | import pytest 23 | 24 | import hug 25 | 26 | 27 | @hug.http(("/namer", "/namer/{name}"), ("GET", "POST"), versions=(None, 2)) 28 | def namer(name=None): 29 | return name 30 | 31 | 32 | class TestHTTP(object): 33 | """Tests the functionality provided by hug.interface.HTTP""" 34 | 35 | def test_urls(self): 36 | """Test to ensure HTTP interface correctly returns URLs associated with it""" 37 | assert namer.interface.http.urls() == ["/namer", "/namer/{name}"] 38 | 39 | def test_url(self): 40 | """Test to ensure HTTP interface correctly automatically returns URL associated with it""" 41 | assert namer.interface.http.url() == "/namer" 42 | assert namer.interface.http.url(name="tim") == "/namer/tim" 43 | assert namer.interface.http.url(name="tim", version=2) == "/v2/namer/tim" 44 | 45 | with pytest.raises(KeyError): 46 | namer.interface.http.url(undefined="not a variable") 47 | 48 | with pytest.raises(KeyError): 49 | namer.interface.http.url(version=10) 50 | 51 | def test_gather_parameters(self): 52 | """Test to ensure gathering parameters works in the expected way""" 53 | 54 | @hug.get() 55 | def my_example_api(body): 56 | return body 57 | 58 | assert ( 59 | hug.test.get( 60 | __hug__, "my_example_api", body="", headers={"content-type": "application/json"} 61 | ).data 62 | == None 63 | ) 64 | 65 | 66 | class TestLocal(object): 67 | """Test to ensure hug.interface.Local functionality works as expected""" 68 | 69 | def test_local_method(self): 70 | class MyObject(object): 71 | @hug.local() 72 | def my_method(self, argument_1: hug.types.number): 73 | return argument_1 74 | 75 | instance = MyObject() 76 | assert instance.my_method(10) == 10 77 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | """tests/test_main.py. 2 | 3 | Basic testing of hug's `__main__` module 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | import pytest 23 | 24 | 25 | def test_main(capsys): 26 | """Main module should be importable, but should raise a SystemExit after CLI docs print""" 27 | with pytest.raises(SystemExit): 28 | from hug import __main__ 29 | -------------------------------------------------------------------------------- /tests/test_redirect.py: -------------------------------------------------------------------------------- 1 | """tests/test_redirect.py. 2 | 3 | Tests to ensure Hug's redirect methods work as expected 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | import falcon 23 | import pytest 24 | 25 | import hug.redirect 26 | 27 | 28 | def test_to(): 29 | """Test that the base redirect to function works as expected""" 30 | with pytest.raises(falcon.http_status.HTTPStatus) as redirect: 31 | hug.redirect.to("/") 32 | assert "302" in redirect.value.status 33 | 34 | 35 | def test_permanent(): 36 | """Test to ensure function causes a redirect with HTTP 301 status code""" 37 | with pytest.raises(falcon.http_status.HTTPStatus) as redirect: 38 | hug.redirect.permanent("/") 39 | assert "301" in redirect.value.status 40 | 41 | 42 | def test_found(): 43 | """Test to ensure function causes a redirect with HTTP 302 status code""" 44 | with pytest.raises(falcon.http_status.HTTPStatus) as redirect: 45 | hug.redirect.found("/") 46 | assert "302" in redirect.value.status 47 | 48 | 49 | def test_see_other(): 50 | """Test to ensure function causes a redirect with HTTP 303 status code""" 51 | with pytest.raises(falcon.http_status.HTTPStatus) as redirect: 52 | hug.redirect.see_other("/") 53 | assert "303" in redirect.value.status 54 | 55 | 56 | def test_temporary(): 57 | """Test to ensure function causes a redirect with HTTP 307 status code""" 58 | with pytest.raises(falcon.http_status.HTTPStatus) as redirect: 59 | hug.redirect.temporary("/") 60 | assert "307" in redirect.value.status 61 | 62 | 63 | def test_not_found(): 64 | with pytest.raises(falcon.HTTPNotFound) as redirect: 65 | hug.redirect.not_found() 66 | assert "404" in redirect.value.status 67 | -------------------------------------------------------------------------------- /tests/test_store.py: -------------------------------------------------------------------------------- 1 | """tests/test_store.py. 2 | 3 | Tests to ensure that the native stores work correctly. 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | import pytest 23 | 24 | from hug.exceptions import StoreKeyNotFound 25 | from hug.store import InMemoryStore 26 | 27 | stores_to_test = [InMemoryStore()] 28 | 29 | 30 | @pytest.mark.parametrize("store", stores_to_test) 31 | def test_stores_generically(store): 32 | key = "test-key" 33 | data = {"user": "foo", "authenticated": False} 34 | 35 | # Key should not exist 36 | assert not store.exists(key) 37 | 38 | # Set key with custom data, verify the key exists and expect correct data to be returned 39 | store.set(key, data) 40 | assert store.exists(key) 41 | assert store.get(key) == data 42 | 43 | # Expect exception if unknown session key was requested 44 | with pytest.raises(StoreKeyNotFound): 45 | store.get("unknown") 46 | 47 | # Delete key 48 | store.delete(key) 49 | assert not store.exists(key) 50 | -------------------------------------------------------------------------------- /tests/test_test.py: -------------------------------------------------------------------------------- 1 | """tests/test_test.py. 2 | 3 | Test to ensure basic test functionality works as expected. 4 | 5 | Copyright (C) 2019 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | import pytest 23 | 24 | import hug 25 | 26 | api = hug.API(__name__) 27 | 28 | 29 | def test_cli(): 30 | """Test to ensure the CLI tester works as intended to allow testing CLI endpoints""" 31 | 32 | @hug.cli() 33 | def my_cli_function(): 34 | return "Hello" 35 | 36 | assert hug.test.cli(my_cli_function) == "Hello" 37 | assert hug.test.cli("my_cli_function", api=api) == "Hello" 38 | 39 | # Shouldn't be able to specify both api and module. 40 | with pytest.raises(ValueError): 41 | assert hug.test.cli("my_method", api=api, module=hug) 42 | -------------------------------------------------------------------------------- /tests/test_this.py: -------------------------------------------------------------------------------- 1 | """tests/test_this.py. 2 | 3 | Tests the Zen of Hug 4 | 5 | Copyright (C) 2019 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | from hug import this 23 | 24 | 25 | def test_this(): 26 | """Test to ensure this exposes the ZEN_OF_HUG as a string""" 27 | assert type(this.ZEN_OF_HUG) == str 28 | -------------------------------------------------------------------------------- /tests/test_transform.py: -------------------------------------------------------------------------------- 1 | """tests/test_transform.py. 2 | 3 | Test to ensure hugs built in transform functions work as expected 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | import hug 23 | 24 | 25 | def test_content_type(): 26 | """Test to ensure the transformer used can change based on the provided content-type""" 27 | transformer = hug.transform.content_type({"application/json": int, "text/plain": str}) 28 | 29 | class FakeRequest(object): 30 | content_type = "application/json" 31 | 32 | request = FakeRequest() 33 | assert transformer("1", request) == 1 34 | 35 | request.content_type = "text/plain" 36 | assert transformer(2, request) == "2" 37 | 38 | request.content_type = "undefined" 39 | transformer({"data": "value"}, request) == {"data": "value"} 40 | 41 | 42 | def test_suffix(): 43 | """Test to ensure transformer content based on the end suffix of the URL works as expected""" 44 | transformer = hug.transform.suffix({".js": int, ".txt": str}) 45 | 46 | class FakeRequest(object): 47 | path = "hey.js" 48 | 49 | request = FakeRequest() 50 | assert transformer("1", request) == 1 51 | 52 | request.path = "hey.txt" 53 | assert transformer(2, request) == "2" 54 | 55 | request.path = "hey.undefined" 56 | transformer({"data": "value"}, request) == {"data": "value"} 57 | 58 | 59 | def test_prefix(): 60 | """Test to ensure transformer content based on the end prefix of the URL works as expected""" 61 | transformer = hug.transform.prefix({"js/": int, "txt/": str}) 62 | 63 | class FakeRequest(object): 64 | path = "js/hey" 65 | 66 | request = FakeRequest() 67 | assert transformer("1", request) == 1 68 | 69 | request.path = "txt/hey" 70 | assert transformer(2, request) == "2" 71 | 72 | request.path = "hey.undefined" 73 | transformer({"data": "value"}, request) == {"data": "value"} 74 | 75 | 76 | def test_all(): 77 | """Test to ensure transform.all allows chaining multiple transformations as expected""" 78 | 79 | def annotate(data, response): 80 | return {"Text": data} 81 | 82 | assert hug.transform.all(str, annotate)(1, response="hi") == {"Text": "1"} 83 | -------------------------------------------------------------------------------- /tests/test_validate.py: -------------------------------------------------------------------------------- 1 | """tests/test_validate.py. 2 | 3 | Tests to ensure hug's custom validation methods work as expected 4 | 5 | Copyright (C) 2016 Timothy Edmund Crosley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 16 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 18 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | import hug 23 | 24 | TEST_SCHEMA = {"first": "Timothy", "place": "Seattle"} 25 | 26 | 27 | def test_all(): 28 | """Test to ensure hug's all validation function works as expected to combine validators""" 29 | assert not hug.validate.all( 30 | hug.validate.contains_one_of("first", "year"), hug.validate.contains_one_of("last", "place") 31 | )(TEST_SCHEMA) 32 | assert hug.validate.all( 33 | hug.validate.contains_one_of("last", "year"), hug.validate.contains_one_of("first", "place") 34 | )(TEST_SCHEMA) 35 | 36 | 37 | def test_any(): 38 | """Test to ensure hug's any validation function works as expected to combine validators""" 39 | assert not hug.validate.any( 40 | hug.validate.contains_one_of("last", "year"), hug.validate.contains_one_of("first", "place") 41 | )(TEST_SCHEMA) 42 | assert hug.validate.any( 43 | hug.validate.contains_one_of("last", "year"), hug.validate.contains_one_of("no", "way") 44 | )(TEST_SCHEMA) 45 | 46 | 47 | def test_contains_one_of(): 48 | """Test to ensure hug's contains_one_of validation function works as expected to ensure presence of a field""" 49 | assert hug.validate.contains_one_of("no", "way")(TEST_SCHEMA) 50 | assert not hug.validate.contains_one_of("last", "place")(TEST_SCHEMA) 51 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py{35,36,37,38,py3}-marshmallow{2,3}, cython-marshmallow{2,3} 3 | 4 | [testenv] 5 | deps= 6 | -rrequirements/build_common.txt 7 | marshmallow2: marshmallow <3.0 8 | marshmallow3: marshmallow==3.0.0rc6 9 | 10 | whitelist_externals=flake8 11 | commands=py.test --durations 3 --cov-report html --cov hug -n auto tests 12 | 13 | [testenv:py37-black] 14 | deps= 15 | -rrequirements/build_style_tools.txt 16 | marshmallow==3.0.0rc6 17 | 18 | whitelist_externals=flake8 19 | commands=black --check --verbose -l 100 hug 20 | 21 | [testenv:py37-vulture] 22 | deps= 23 | -rrequirements/build_style_tools.txt 24 | marshmallow==3.0.0rc6 25 | 26 | whitelist_externals=flake8 27 | commands=vulture hug --min-confidence 100 --ignore-names req_succeeded 28 | 29 | 30 | [testenv:py37-flake8] 31 | deps= 32 | -rrequirements/build_style_tools.txt 33 | marshmallow==3.0.0rc6 34 | 35 | whitelist_externals=flake8 36 | commands=flake8 hug 37 | 38 | [testenv:py37-bandit] 39 | deps= 40 | -rrequirements/build_style_tools.txt 41 | marshmallow==3.0.0rc6 42 | 43 | whitelist_externals=flake8 44 | commands=bandit -r hug/ -ll 45 | 46 | [testenv:py37-isort] 47 | deps= 48 | -rrequirements/build_style_tools.txt 49 | marshmallow==3.0.0rc6 50 | 51 | whitelist_externals=flake8 52 | commands=isort -c --diff --recursive hug 53 | 54 | [testenv:py37-safety] 55 | deps= 56 | -rrequirements/build_style_tools.txt 57 | marshmallow==3.0.0rc6 58 | 59 | whitelist_externals=flake8 60 | commands=safety check -i 36810 61 | 62 | [testenv:pywin] 63 | deps =-rrequirements/build_windows.txt 64 | basepython = {env:PYTHON:}\python.exe 65 | commands=py.test hug -n auto tests 66 | 67 | [testenv:cython] 68 | deps=Cython 69 | -rrequirements/build.txt 70 | --------------------------------------------------------------------------------