├── .coveragerc ├── .coveralls.yml ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .pyup.yml ├── .readthedocs.yml ├── .travis.yml ├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile └── source │ ├── _static │ ├── favicon.ico │ ├── kobin-example.gif │ ├── kobin.png │ ├── logo.png │ ├── ogp.png │ ├── ogp_ja.png │ ├── twitter_share.png │ └── twitter_share_ja.png │ ├── _templates │ └── layout.html │ ├── api.rst │ ├── conf.py │ ├── devguide.rst │ ├── index.rst │ ├── locale │ └── ja │ │ └── LC_MESSAGES │ │ ├── api.po │ │ ├── devguide.po │ │ ├── index.po │ │ ├── modules.po │ │ ├── sphinx.po │ │ └── tutorial.po │ └── tutorial.rst ├── example ├── helloworld │ ├── README.md │ └── hello.py └── template_and_static_files │ ├── README.md │ ├── app.py │ ├── config.py │ ├── static │ ├── favicon.ico │ └── style.css │ └── templates │ ├── base.html │ └── hello_jinja2.html ├── kobin ├── __init__.py ├── app.py ├── app.pyi ├── requests.py ├── requests.pyi ├── responses.py ├── responses.pyi ├── routes.py └── routes.pyi ├── requirements ├── constraints.txt ├── dev.txt ├── docs.txt └── test.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── dummy_config.py ├── templates │ ├── jinja2.html │ └── this_is_not_file │ │ └── .gitkeep ├── test_apps.py ├── test_requests.py ├── test_responses.py └── test_routes.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = kobin 3 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | repo_token: ZM7iVE7b2JMQEeyYOxuzGFzOrOKMcokqM -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Developer guide 2 | 3 | ## Bug Reports and Feature Requests 4 | 5 | If you have encountered a problem with Kobin or have an idea for a new feature, 6 | please submit it to the issue tracker on Github. 7 | 8 | Including or providing a link to the source files involved may help us fix the issue. If possible, 9 | try to create a minimal project that produces the error and post that instead. 10 | 11 | ## Documentation 12 | 13 | ### Build 14 | 15 | * English: ``make html`` 16 | * Japanese: ``make -e SPHINXOPTS="-D language='ja'" html`` 17 | 18 | 19 | ### Translation 20 | 21 | Updating your po files by new pot files. 22 | 23 | 24 | ```console 25 | $ make gettext 26 | $ sphinx-intl update -p build/locale 27 | # edit doc/source/locale/*.po files 28 | $ make -e SPHINXOPTS="-D language='ja'" html 29 | ``` 30 | 31 | Reference: [Internationalization -- Sphinx documentation](http://www.sphinx-doc.org/en/stable/intl.html) 32 | 33 | 34 | ## Testing 35 | 36 | The following test are running in Kobin project. 37 | If you add the changes to Kobin, Please run tox testing. 38 | 39 | * pytest: ``python setup.py test`` 40 | * mypy: ``mypy --check-untyped-defs --fast-parser --python-version 3.6 kobin`` 41 | * Flake8: ``flake8`` 42 | * doctest: ``cd docs; make doctest`` 43 | * Run all using tox: ``tox`` 44 | 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | 4 | ## Expected Behavior 5 | 6 | ## Actual Behavior 7 | 8 | If possible, provide a minimal reproducible example (We usually don't have time to read hundreds of lines of your code) 9 | 10 | ## Environment 11 | 12 | - Operating System: 13 | - Python version: 14 | - Kobin version: 15 | 16 | ## Related Issue(Optional) 17 | 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | Fix or Enhancement? 4 | 5 | 6 | ## Description 7 | 8 | A few sentences describing the overall goals of the pull request's commits. 9 | 10 | 11 | ## Are there the related PRs or Issues? (optional) 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # Installer logs 27 | pip-log.txt 28 | pip-delete-this-directory.txt 29 | 30 | # Unit test / coverage reports 31 | htmlcov/ 32 | .tox/ 33 | .coverage 34 | .coverage.* 35 | .cache 36 | nosetests.xml 37 | coverage.xml 38 | *,cover 39 | 40 | # Sphinx documentation 41 | docs/build/ 42 | *.mo 43 | # transifex 44 | .tx 45 | 46 | ### intelij 47 | .idea/ 48 | 49 | ### Virtualenv 50 | venv/ 51 | venv35/ 52 | venv36/ 53 | 54 | -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | update: all 2 | schedule: "every week on monday" 3 | search: False 4 | requirements: 5 | - requirements/constraints.txt 6 | label_prs: update 7 | assignnees: c-bata 8 | 9 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | python: 2 | version: 3.6 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - '3.6' 5 | - nightly 6 | matrix: 7 | allow_failures: 8 | - python: nightly 9 | install: pip install -q tox-travis -r ./requirements/test.txt 10 | script: tox 11 | after_success: 12 | - coverage run setup.py test 13 | - coverage report 14 | - coveralls 15 | deploy: 16 | provider: pypi 17 | user: c-bata 18 | password: 19 | secure: P/GoHut6O5rY+eb9jLSsEK3I+cj0fr/Pp7MFhlQ9U/OYVSxw8GHAirfTkIme3boRZnpLLCmadM4TDC0Fnv+CqQgr9Rp2gACPFJLyA8m+L4g4P7n7+jxKLFdDa8EeNfbSMFGO7E1OtI7RsNc0KoC3HoyzO4MLmBDh43q4syRd2HhkFt9ebz8+63+ak8HMlZLYfUlku7Q0C85bQ3Z4bM+ibGLApqON8r40vneHoGZ+OX1FN5jgDYneWznwZs64K/nptPLRDO3VyWxNgi5Vn6lfEvaciGW6JeFzvDu6XUOvsd35S3L2NDH1tyiIY5bLWbKjVz+3HKpLXQZS5XhxMJQfSQhuswEfex3lU0Htkl3yY9+t0sIjXpA+T76M34F9KBW7mschz8Pv1EqaWhC3bCNF8JtjL2u7QCS03I+VxpXSUeN3MhhA/rbaL5b7ngcZLkBNmXctNzC0H/MWw/CUWZQCUGw3Tp9DEU04vq+8JQ1qmkd0v5HAvZy0zIU7W+bhOdWdx0Soxs671t10CKZPGHJNvyVHZlApZ3uxYdAXuJL+xD7JFg0u8ir4wwDa9FoeEqtprXQuCP40D1puH7I2mf8zS+jR5gzcUqmL39hy6jSNi38olo+ga2S0rxXMdoa728b055o51TstIlgvfgXXI97dFECxnvV5hRrb6U0hh2Sc/lU= 20 | distributions: sdist bdist_wheel 21 | on: 22 | tags: true 23 | notifications: 24 | slack: 25 | secure: LOU57CZvAlZ8Pj8JhOk2GWt8TI8hvq8IdRhxbRoPb6B+/ysF8Kdp1wBINlBSmwHW9GxgtpxAj46Ni36O30Hwa/FO/CeFDcmRTRtPadAE45PUEuE8TeN+94LScTufIvfLfFtsqcr1WJoFLyy5PrpYJqhbCgZccGGqwNg2vCBTp5Jif+9fkDf0sHrTsPmbi0tO0pJJKCYif7nnSBxOcQod+BCIwavHxesc2Pkzl/v4T48a53fKK3dkF0t42bMINwUmDGsa5GtK3EHuDKO9sc03m1rpPmH4Y/3fOBa/UJi5/Obu9YQy8UaYhH+t9MAPCgb1vASs3h69heEwJ147jRo0CWRZZGMXQ4PEFAVn7OP4Xcwmb1fjUR5cUp9jewF+s5zPQgc8BH7RhEIvcr2MHM4pLlcGuVud2L87FJmAxrTXNoCT6mpBlzuRhmLd8n+1CieLIms7jW+N502fi4RQ6xwCK4w0ric8Zny+a5b42AVxGogQGByHCT71L+TL/Vxq1wLNuj25KAWFXJdehwbTlN6qL2sv/nkrGYN6FAK14gp08cEdSBG96tRj9ZbY9lO/3k4ZZIxkfcCTsEXN5XI7Z1dPSa1NAdpXtB8jvzRH6JT5o3NAMlZGBWJU/F8ARaYiPq/lHx380AOeAyrmf9iH1D2tw9D2tDU09p8LFKHhXa0eNqo= 26 | email: false 27 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | CHANGES 2 | ======= 3 | 4 | 0.1.10 (2017-03-20) 5 | ------------------ 6 | 7 | * Freezing the app state after running. 8 | 9 | 0.1.9 (2017-03-20) 10 | ------------------ 11 | 12 | * Add logger integration. 13 | 14 | 0.1.8 (2017-01-24) 15 | ------------------ 16 | 17 | * Fix to response 405 when request path found but method not matched. (Thanks @kwatch) 18 | * Fix a load_config does not set TEMPLATE_ENVIRONMENT bug when no arguments are passed. 19 | 20 | 0.1.7 (2017-01-14) 21 | ------------------ 22 | 23 | * Multiple after / before request hook. 24 | * Integrate logging module. 25 | 26 | 0.1.6 (2017-01-03) 27 | ------------------ 28 | 29 | * Fix the critical bug in request.forms. 30 | * Flexible template settings. 31 | 32 | 0.1.5 (2017-01-01) 33 | ------------------ 34 | 35 | * Refactor Response classes. 36 | * Split environs to requests.py and responses.py 37 | * Remove Config class. 38 | 39 | 0.1.4 (2017-01-01) 40 | ------------------ 41 | 42 | Happy New Year! This is a first release in 2017. 43 | We hope kobin helps your web development. 44 | 45 | * Enhancement coverage. 46 | * Add some refactoring changes. 47 | * Set Cookie encryption using `config['SECRET_KEY']` . 48 | 49 | 50 | 0.1.3 (2016-12-30) 51 | ------------------ 52 | 53 | * End of support python3.5 54 | * Add accept_best_match function 55 | * Refactor config object. 56 | * Modify after request hook 57 | 58 | 59 | 0.1.2 (2016-12-18) 60 | ------------------ 61 | 62 | * Support cookie encrypting. 63 | * Add BaseResponse class. 64 | 65 | 0.1.1 (2016-12-17) 66 | ------------------ 67 | 68 | * Response class can return bytes. 69 | * Fix stub files. 70 | 71 | 0.1.0 (2016-12-07) 72 | ------------------ 73 | 74 | * Add before_request / after_request hook 75 | * Update docs. 76 | 77 | 0.0.7 (2016-12-05) 78 | ------------------ 79 | 80 | * headers property in Request object. 81 | * raw_body property in Request object. 82 | * Remove jinja2 from install_requires. 83 | * Update docs. 84 | 85 | 0.0.6 (2016-12-04) 86 | ------------------ 87 | 88 | * Integrating wsgicli. 89 | * Alter sphinx theme. 90 | * Update documentations. 91 | * View functions must return Response or its child class. 92 | * Make Request object to No thread local 93 | * Add Response, JSONResponse, TemplateResponse, RedirectResponse. 94 | * Refactor error handling. 95 | * Add stub files (`.pyi`). 96 | * Python3.6 testing in travis-ci.org. 97 | * Add API documentation. 98 | 99 | 0.0.5 (2016-11-28) 100 | ------------------ 101 | 102 | * Replace regex router with new style router. 103 | * Correspond reverse routing. 104 | * Remove serving static file. Please use wsgi-static-middleware. 105 | * Remove server adapter. 106 | * Support only Jinja2. 107 | * Refactoring. 108 | 109 | 0.0.4 (2016-02-28) 110 | ------------------ 111 | 112 | * Expect the types of routing arguments from type hints. 113 | * Implement template adapter for jinja2. 114 | * Server for static files such as css, images, and so on. 115 | * Manage configuration class. 116 | * Support gunicorn. 117 | * Error handling. 118 | * Fix several bugs. 119 | 120 | 0.0.3 (2016-02-08) 121 | ------------------ 122 | 123 | * Request and Response object. 124 | * tox and Travis-CI Integration. 125 | 126 | 0.0.2 (2015-12-03) 127 | ------------------ 128 | 129 | * Publish on PyPI. 130 | 131 | 0.0.0 (2015-09-14) 132 | ------------------ 133 | 134 | * Create this project. 135 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 MASASHI Shibata 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 CHANGES.rst 3 | recursive-include kobin *.pyi 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Kobin 3 | ===== 4 | 5 | .. image:: https://travis-ci.org/kobinpy/kobin.svg?branch=master 6 | :target: https://travis-ci.org/kobinpy/kobin 7 | 8 | .. image:: https://badge.fury.io/py/kobin.svg 9 | :target: https://badge.fury.io/py/kobin 10 | 11 | .. image:: https://coveralls.io/repos/github/kobinpy/kobin/badge.svg?branch=master 12 | :target: https://coveralls.io/github/kobinpy/kobin?branch=master 13 | 14 | .. image:: https://codeclimate.com/github/c-bata/kobin/badges/gpa.svg 15 | :target: https://codeclimate.com/github/kobinpy/kobin 16 | :alt: Code Climate 17 | 18 | .. image:: https://readthedocs.org/projects/kobin/badge/?version=latest 19 | :target: http://kobin.readthedocs.org/en/latest/?badge=latest 20 | :alt: Documentation Status 21 | 22 | 23 | Type Hints friendly WSGI Framework for Python3. 24 | **This library is a pre-release. Expect missing docs and breaking API changes.** 25 | 26 | Kobin has following features. 27 | 28 | - Decorator based Routing System exploited Type Hints. 29 | - WSGI request and response Wrapper. 30 | - Provide type annotations from stub files. 31 | - and other convenient utilities... 32 | 33 | And Kobin has **NO** following features: 34 | 35 | - *WSGI Server Adapters*: Please use `WSGICLI `_ or Gunicorn CLI. 36 | - *Serving static contents*: Please use `wsgi-static-middleware `_ or Nginx. 37 | - *Template Engine*: But Kobin provides template adapter for Jinja2. 38 | 39 | Requirements 40 | ============ 41 | 42 | Supported python versions are python 3.6 or later. 43 | And Kobin has no required dependencies other than the Python Standard Libraries. 44 | 45 | The following packages are optional: 46 | 47 | * wsgicli - Command Line Interface for developing WSGI application. 48 | * jinja2 - Jinja2 is a full featured template engine for Python. 49 | 50 | Resources 51 | ========= 52 | 53 | * `Documentation (English) `_ 54 | * `Documentation (Japanese) `_ 55 | * `Github `_ 56 | * `PyPI `_ 57 | * `Kobin Todo `_ 58 | 59 | 60 | Kobin's documentation is not yet complete very much. 61 | If you want to know the best practices in Kobin, 62 | Please check `Kobin Todo `_ . 63 | 64 | .. image:: docs/source/_static/kobin-example.gif 65 | :alt: Kobin Todo Demo Animation 66 | :align: center 67 | 68 | License 69 | ======= 70 | 71 | This software is licensed under the MIT License (See `LICENSE <./LICENSE>`_ ). 72 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/kobin.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/kobin.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/kobin" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/kobin" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /docs/source/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kobinpy/kobin/f87a09d071cf8246d3af700581943e881866bf5a/docs/source/_static/favicon.ico -------------------------------------------------------------------------------- /docs/source/_static/kobin-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kobinpy/kobin/f87a09d071cf8246d3af700581943e881866bf5a/docs/source/_static/kobin-example.gif -------------------------------------------------------------------------------- /docs/source/_static/kobin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kobinpy/kobin/f87a09d071cf8246d3af700581943e881866bf5a/docs/source/_static/kobin.png -------------------------------------------------------------------------------- /docs/source/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kobinpy/kobin/f87a09d071cf8246d3af700581943e881866bf5a/docs/source/_static/logo.png -------------------------------------------------------------------------------- /docs/source/_static/ogp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kobinpy/kobin/f87a09d071cf8246d3af700581943e881866bf5a/docs/source/_static/ogp.png -------------------------------------------------------------------------------- /docs/source/_static/ogp_ja.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kobinpy/kobin/f87a09d071cf8246d3af700581943e881866bf5a/docs/source/_static/ogp_ja.png -------------------------------------------------------------------------------- /docs/source/_static/twitter_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kobinpy/kobin/f87a09d071cf8246d3af700581943e881866bf5a/docs/source/_static/twitter_share.png -------------------------------------------------------------------------------- /docs/source/_static/twitter_share_ja.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kobinpy/kobin/f87a09d071cf8246d3af700581943e881866bf5a/docs/source/_static/twitter_share_ja.png -------------------------------------------------------------------------------- /docs/source/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "basic/layout.html" %} 2 | 3 | {%- block doctype -%} 4 | 5 | {%- endblock -%} 6 | 7 | {%- block extrahead -%} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% if language == 'ja' %} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% else %} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% endif %} 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 51 | {%- endblock -%} 52 | 53 | {# put the sidebar before the body #} 54 | {% block sidebar1 %} 55 | Fork me on GitHub 56 | {{ sidebar() }} 57 | {% endblock %} 58 | {% block sidebar2 %}{% endblock %} 59 | 60 | {%- block footer %} 61 | 66 | {%- endblock %} -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | API Documentation 3 | ================= 4 | 5 | This documentations is generated from kobin's source code. 6 | 7 | .. automodule:: kobin.app 8 | :members: 9 | 10 | .. automodule:: kobin.environs 11 | :members: 12 | 13 | .. automodule:: kobin.routes 14 | :members: 15 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import os 6 | import solar_theme 7 | 8 | # If extensions (or modules to document with autodoc) are in another directory, 9 | # add these directories to sys.path here. If the directory is relative to the 10 | # documentation root, use os.path.abspath to make it absolute, like shown here. 11 | SOURCE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__name__)))) 12 | sys.path.insert(0, SOURCE_DIR) 13 | 14 | # -- General configuration ------------------------------------------------ 15 | # Add any Sphinx extension module names here, as strings. 16 | extensions = [ 17 | 'sphinx.ext.autodoc', 18 | 'sphinx.ext.doctest', 19 | 'sphinx.ext.viewcode', 20 | ] 21 | 22 | # Add any paths that contain templates here, relative to this directory. 23 | templates_path = ['_templates'] 24 | 25 | # The suffix(es) of source filenames. 26 | # You can specify multiple suffix as a list of string: 27 | # source_suffix = ['.rst', '.md'] 28 | source_suffix = '.rst' 29 | 30 | # The encoding of source files. 31 | #source_encoding = 'utf-8-sig' 32 | 33 | # The master toctree document. 34 | master_doc = 'index' 35 | 36 | # General information about the project. 37 | project = 'kobin' 38 | copyright = '2016, Masashi Shibata' 39 | author = 'Masashi Shibata' 40 | 41 | # The short X.Y version. 42 | version = '0.1' 43 | release = version 44 | 45 | language = None 46 | 47 | # There are two options for replacing |today|: either, you set today to some 48 | # non-false value, then it is used: 49 | #today = '' 50 | # Else, today_fmt is used as the format for a strftime call. 51 | #today_fmt = '%B %d, %Y' 52 | 53 | # List of patterns, relative to source directory, that match files and 54 | # directories to ignore when looking for source files. 55 | exclude_patterns = [] 56 | 57 | # The reST default role (used for this markup: `text`) to use for all 58 | # documents. 59 | #default_role = None 60 | 61 | # If true, '()' will be appended to :func: etc. cross-reference text. 62 | #add_function_parentheses = True 63 | 64 | # The name of the Pygments (syntax highlighting) style to use. 65 | pygments_style = 'sphinx' 66 | 67 | # A list of ignored prefixes for module index sorting. 68 | #modindex_common_prefix = [] 69 | 70 | # If true, keep warnings as "system message" paragraphs in the built documents. 71 | #keep_warnings = False 72 | 73 | # If true, `todo` and `todoList` produce output, else they produce nothing. 74 | todo_include_todos = False 75 | 76 | 77 | # -- Options for HTML output ---------------------------------------------- 78 | 79 | # The theme to use for HTML and HTML Help pages. See the documentation for 80 | # a list of builtin themes. 81 | html_theme = 'solar_theme' 82 | html_theme_path = [solar_theme.theme_path] 83 | 84 | # Theme options are theme-specific and customize the look and feel of a theme 85 | # further. For a list of options available for each theme, see the 86 | # documentation. 87 | #html_theme_options = {} 88 | 89 | # Add any paths that contain custom themes here, relative to this directory. 90 | #html_theme_path = [] 91 | 92 | # The name for this set of Sphinx documents. If None, it defaults to 93 | # " v documentation". 94 | #html_title = None 95 | 96 | # A shorter title for the navigation bar. Default is the same as html_title. 97 | #html_short_title = None 98 | 99 | # The name of an image file (relative to this directory) to place at the top 100 | # of the sidebar. 101 | html_logo = '_static/kobin.png' 102 | 103 | html_favicon = 'favicon.ico' 104 | 105 | # Add any paths that contain custom static files (such as style sheets) here, 106 | # relative to this directory. They are copied after the builtin static files, 107 | # so a file named "default.css" will overwrite the builtin "default.css". 108 | html_static_path = ['_static'] 109 | 110 | # Add any extra paths that contain custom files (such as robots.txt or 111 | # .htaccess) here, relative to this directory. These files are copied 112 | # directly to the root of the documentation. 113 | #html_extra_path = [] 114 | 115 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 116 | # using the given strftime format. 117 | #html_last_updated_fmt = '%b %d, %Y' 118 | 119 | # If true, SmartyPants will be used to convert quotes and dashes to 120 | # typographically correct entities. 121 | #html_use_smartypants = True 122 | 123 | # Custom sidebar templates, maps document names to template names. 124 | #html_sidebars = {} 125 | 126 | # Additional templates that should be rendered to pages, maps page names to 127 | # template names. 128 | #html_additional_pages = {} 129 | 130 | # If false, no module index is generated. 131 | #html_domain_indices = True 132 | 133 | # If false, no index is generated. 134 | #html_use_index = True 135 | 136 | # If true, the index is split into individual pages for each letter. 137 | #html_split_index = False 138 | 139 | # If true, links to the reST sources are added to the pages. 140 | #html_show_sourcelink = True 141 | 142 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 143 | #html_show_sphinx = True 144 | 145 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 146 | #html_show_copyright = True 147 | 148 | # If true, an OpenSearch description file will be output, and all pages will 149 | # contain a tag referring to it. The value of this option must be the 150 | # base URL from which the finished HTML is served. 151 | #html_use_opensearch = '' 152 | 153 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 154 | #html_file_suffix = None 155 | 156 | # Language to be used for generating the HTML full-text search index. 157 | # Sphinx supports the following languages: 158 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 159 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 160 | #html_search_language = 'en' 161 | 162 | # A dictionary with options for the search language support, empty by default. 163 | # Now only 'ja' uses this config value 164 | #html_search_options = {'type': 'default'} 165 | 166 | # The name of a javascript file (relative to the configuration directory) that 167 | # implements a search results scorer. If empty, the default will be used. 168 | #html_search_scorer = 'scorer.js' 169 | 170 | # Output file base name for HTML help builder. 171 | htmlhelp_basename = 'kobindoc' 172 | 173 | # -- Options for LaTeX output --------------------------------------------- 174 | 175 | latex_elements = {} 176 | 177 | # Grouping the document tree into LaTeX files. List of tuples 178 | # (source start file, target name, title, 179 | # author, documentclass [howto, manual, or own class]). 180 | latex_documents = [ 181 | (master_doc, 'kobin.tex', 'kobin Documentation', 182 | 'Masashi Shibata', 'manual'), 183 | ] 184 | 185 | 186 | # -- Options for manual page output --------------------------------------- 187 | 188 | # One entry per manual page. List of tuples 189 | # (source start file, name, description, authors, manual section). 190 | man_pages = [ 191 | (master_doc, 'kobin', 'kobin Documentation', 192 | [author], 1) 193 | ] 194 | 195 | # -- Options for Texinfo output ------------------------------------------- 196 | 197 | # Grouping the document tree into Texinfo files. List of tuples 198 | # (source start file, target name, title, author, 199 | # dir menu entry, description, category) 200 | texinfo_documents = [ 201 | (master_doc, 'kobin', 'kobin Documentation', 202 | author, 'kobin', 'One line description of project.', 203 | 'Miscellaneous'), 204 | ] 205 | 206 | # -- Options for Internationalization ------------------------------------- 207 | locale_dirs = ['locale/'] 208 | gettext_compat = False 209 | -------------------------------------------------------------------------------- /docs/source/devguide.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Developer guide 3 | =============== 4 | 5 | Bug Reports and Feature Requests 6 | ================================ 7 | 8 | If you have encountered a problem with Kobin or have an idea for a new feature, 9 | please submit it to the issue tracker on Github. 10 | 11 | Including or providing a link to the source files involved may help us fix the issue. If possible, 12 | try to create a minimal project that produces the error and post that instead. 13 | 14 | Documentation 15 | ============= 16 | 17 | Build 18 | ----- 19 | 20 | * English: ``make html`` 21 | * Japanese: ``make -e SPHINXOPTS="-D language='ja'" html`` 22 | 23 | 24 | Translation 25 | ----------- 26 | 27 | Updating your po files by new pot files. 28 | 29 | .. code-block:: console 30 | 31 | $ make gettext 32 | $ sphinx-intl update -p build/locale 33 | # edit doc/source/locale/*.po files 34 | $ make -e SPHINXOPTS="-D language='ja'" html 35 | 36 | Reference: `Internationalization -- Sphinx documentation `_ 37 | 38 | 39 | Testing 40 | ======= 41 | 42 | The following test are running in Kobin project. 43 | If you add the changes to Kobin, Please run tox testing. 44 | 45 | * test: ``python setup.py test`` 46 | * coverage: ``coverage run setup.py test && coverage report`` 47 | * mypy: ``mypy --check-untyped-defs --fast-parser --python-version 3.6 kobin`` 48 | * Flake8: ``flake8`` 49 | * doctest: ``cd docs; make doctest`` 50 | * Run all with tox: ``tox`` 51 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. title:: Welcome to Kobin 2 | 3 | =================== 4 | Kobin Documentation 5 | =================== 6 | 7 | Type Hints friendly WSGI Framework for Python3. 8 | 9 | Kobin has following features. 10 | 11 | - Decorator based Routing System exploited Type Hints. 12 | - WSGI request and response Wrapper. 13 | - Provide type annotations from stub files. 14 | - and other convenient utilities... 15 | 16 | And Kobin has **NO** following features: 17 | 18 | - *WSGI Server Adapters*: Please use WSGICLI or Gunicorn CLI. 19 | - *Serving static contents*: Please use WSGICLI and Nginx. 20 | - *Template Engine*: But Kobin provides template adapter. 21 | 22 | 23 | Statuses 24 | ======== 25 | 26 | .. image:: https://travis-ci.org/kobinpy/kobin.svg?branch=master 27 | :target: https://travis-ci.org/kobinpy/kobin 28 | 29 | .. image:: https://badge.fury.io/py/kobin.svg 30 | :target: https://badge.fury.io/py/kobin 31 | 32 | .. image:: https://coveralls.io/repos/github/kobinpy/kobin/badge.svg?branch=coveralls 33 | :target: https://coveralls.io/github/kobinpy/kobin?branch=master 34 | 35 | .. image:: https://codeclimate.com/github/c-bata/kobin/badges/gpa.svg 36 | :target: https://codeclimate.com/github/kobinpy/kobin 37 | :alt: Code Climate 38 | 39 | .. image:: https://readthedocs.org/projects/kobin/badge/?version=latest 40 | :target: http://kobin.readthedocs.org/en/latest/?badge=latest 41 | :alt: Documentation Status 42 | 43 | 44 | Kobin documentation contents 45 | ============================ 46 | 47 | .. toctree:: 48 | :maxdepth: 2 49 | 50 | tutorial 51 | api 52 | devguide 53 | 54 | 55 | Requirements 56 | ============ 57 | 58 | Supported python versions are Python 3.6 or later. 59 | And Kobin has no required dependencies other than the Python Standard Libraries. 60 | 61 | The following packages are optional: 62 | 63 | * wsgicli - Command Line Interface for developing WSGI application. 64 | * jinja2 - Jinja2 is a full featured template engine for Python. 65 | 66 | 67 | Links 68 | ===== 69 | 70 | * `Documentation (English) `_ 71 | * `Documentation (日本語) `_ 72 | * `Github `_ 73 | * `PyPI `_ 74 | * `Kobin Example `_ 75 | 76 | Indices and tables 77 | ================== 78 | 79 | * :ref:`genindex` 80 | * :ref:`modindex` 81 | * :ref:`search` 82 | -------------------------------------------------------------------------------- /docs/source/locale/ja/LC_MESSAGES/api.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2016, Masashi Shibata 3 | # This file is distributed under the same license as the kobin package. 4 | # FIRST AUTHOR , 2016. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: kobin 0.0.4-alpha.1\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2017-01-03 21:20+0900\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 2.3.4\n" 19 | 20 | #: ../../source/api.rst:3 21 | msgid "API Documentation" 22 | msgstr "API ドキュメント" 23 | 24 | #: ../../source/api.rst:5 25 | msgid "This documentations is generated from kobin's source code." 26 | msgstr "この文書はソースコードから自動生成された、KobinのAPIドキュメントです。" 27 | 28 | #: kobin.app:2 of 29 | msgid "Kobin class" 30 | msgstr "Kobinクラス" 31 | 32 | #: kobin.app:4 of 33 | msgid "The Kobin instance are callable WSGI Application." 34 | msgstr "Kobinクラスのインスタンスが、WSGIのアプリケーションとなります。" 35 | 36 | #: kobin.app:7 of 37 | msgid "Usage" 38 | msgstr "使い方" 39 | 40 | #: kobin.app.Kobin:1 of 41 | msgid "" 42 | "This class is a WSGI application implementation. Create a instance, and " 43 | "run using WSGI Server." 44 | msgstr "WSGI Applicationの実装です。インスタンスを生成して、WSGI Serverで実行しましょう。" 45 | 46 | #: kobin.app.current_config:1 of 47 | msgid "Get the configurations of your Kobin's application." 48 | msgstr "Kobinアプリケーションの設定情報を取得" 49 | 50 | #: kobin.routes:2 of 51 | msgid "Routing" 52 | msgstr "ルーティング" 53 | 54 | #: kobin.routes:4 of 55 | msgid "Kobin's routing system may be slightly distinctive." 56 | msgstr "Kobinのルーティングシステムは少し特徴的かもしれません。" 57 | 58 | #: kobin.routes:7 of 59 | msgid "Rule Syntax" 60 | msgstr "ルーティングの記述ルール" 61 | 62 | #: kobin.routes:9 of 63 | msgid "Kobin use decorator based URL dispatch." 64 | msgstr "" 65 | 66 | #: kobin.routes:11 of 67 | msgid "Dynamic convert URL variables from Type Hints." 68 | msgstr "" 69 | 70 | #: kobin.routes:28 of 71 | msgid "Reverse Routing" 72 | msgstr "URLの逆引き" 73 | 74 | #: kobin.routes:30 of 75 | msgid "`app.router.reverse` function returns URL. The usage is like this:" 76 | msgstr "" 77 | 78 | #: kobin.routes:54 of 79 | msgid "Reverse Routing and Redirecting" 80 | msgstr "" 81 | 82 | #: kobin.routes:56 of 83 | msgid ":class:`RedirectResponse` The usage is like this:" 84 | msgstr "" 85 | -------------------------------------------------------------------------------- /docs/source/locale/ja/LC_MESSAGES/devguide.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2016, Masashi Shibata 3 | # This file is distributed under the same license as the kobin package. 4 | # FIRST AUTHOR , 2016. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: kobin 0.0.4-alpha.1\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2017-01-03 21:20+0900\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 2.3.4\n" 19 | 20 | #: ../../source/devguide.rst:3 21 | msgid "Developer guide" 22 | msgstr "開発者向け情報" 23 | 24 | #: ../../source/devguide.rst:6 25 | msgid "Bug Reports and Feature Requests" 26 | msgstr "バグ報告と機能のリクエスト" 27 | 28 | #: ../../source/devguide.rst:8 29 | msgid "" 30 | "If you have encountered a problem with Kobin or have an idea for a new " 31 | "feature, please submit it to the issue tracker on Github." 32 | msgstr "Kobin に関する問題に遭遇したり、新機能に対するアイデアがある場合は、Github上の issue tracker に提出してください。" 33 | 34 | #: ../../source/devguide.rst:11 35 | msgid "" 36 | "Including or providing a link to the source files involved may help us " 37 | "fix the issue. If possible, try to create a minimal project that produces" 38 | " the error and post that instead." 39 | msgstr "関係するソースファイルを含めるか、リンクを提供することで、私たちが問題を解決する助けになります。可能なら、エラーを生成する最小のプロジェクトを作成して、代わりにそれを送信するようにしてください。" 40 | 41 | #: ../../source/devguide.rst:15 42 | msgid "Documentation" 43 | msgstr "ドキュメント" 44 | 45 | #: ../../source/devguide.rst:18 46 | msgid "Build" 47 | msgstr "ビルド" 48 | 49 | #: ../../source/devguide.rst:20 50 | msgid "English: ``make html``" 51 | msgstr "英語: ``make html``" 52 | 53 | #: ../../source/devguide.rst:21 54 | msgid "Japanese: ``make -e SPHINXOPTS=\"-D language='ja'\" html``" 55 | msgstr "日本語: ``make -e SPHINXOPTS=\"-D language='ja'\" html``" 56 | 57 | #: ../../source/devguide.rst:25 58 | msgid "Translation" 59 | msgstr "翻訳" 60 | 61 | #: ../../source/devguide.rst:27 62 | msgid "Updating your po files by new pot files." 63 | msgstr "``po`` ファイルをアップデートしてください。" 64 | 65 | #: ../../source/devguide.rst:36 66 | msgid "" 67 | "Reference: `Internationalization -- Sphinx documentation `_" 69 | msgstr "" 70 | "ref: `国際化 -- Sphinx documentation `_" 72 | 73 | #: ../../source/devguide.rst:40 74 | msgid "Testing" 75 | msgstr "テスト" 76 | 77 | #: ../../source/devguide.rst:42 78 | msgid "" 79 | "The following test are running in Kobin project. If you add the changes " 80 | "to Kobin, Please run tox testing." 81 | msgstr "Kobinプロジェクトでは以下の項目をテストしています。もし変更を加えた場合、toxコマンドを実行してチェックしてください。" 82 | 83 | #: ../../source/devguide.rst:45 84 | msgid "test: ``python setup.py test``" 85 | msgstr "" 86 | 87 | #: ../../source/devguide.rst:46 88 | msgid "coverage: ``coverage run setup.py test && coverage report``" 89 | msgstr "" 90 | 91 | #: ../../source/devguide.rst:47 92 | msgid "" 93 | "mypy: ``mypy --check-untyped-defs --fast-parser --python-version 3.6 " 94 | "kobin``" 95 | msgstr "" 96 | 97 | #: ../../source/devguide.rst:48 98 | msgid "Flake8: ``flake8``" 99 | msgstr "" 100 | 101 | #: ../../source/devguide.rst:49 102 | msgid "doctest: ``cd docs; make doctest``" 103 | msgstr "" 104 | 105 | #: ../../source/devguide.rst:50 106 | msgid "Run all with tox: ``tox``" 107 | msgstr "" 108 | -------------------------------------------------------------------------------- /docs/source/locale/ja/LC_MESSAGES/index.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2016, Masashi Shibata 3 | # This file is distributed under the same license as the kobin package. 4 | # FIRST AUTHOR , 2016. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: kobin 0.0\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2017-01-03 21:28+0900\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 2.3.4\n" 19 | 20 | #: ../../source/index.rst:5 21 | msgid "Kobin Documentation" 22 | msgstr "Kobin ドキュメント" 23 | 24 | #: ../../source/index.rst:7 25 | msgid "Type Hints friendly WSGI Framework for Python3." 26 | msgstr "Kobinは、型ヒントを活用した小さなWSGIフレームワークです。" 27 | 28 | #: ../../source/index.rst:9 29 | msgid "Kobin has following features." 30 | msgstr "Kobinは次の機能を提供します。" 31 | 32 | #: ../../source/index.rst:11 33 | msgid "Decorator based Routing System exploited Type Hints." 34 | msgstr "Type Hintsを活用したデコレータベースのルーティングシステム" 35 | 36 | #: ../../source/index.rst:12 37 | msgid "WSGI request and response Wrapper." 38 | msgstr "WSGIリクエスト・レスポンスのラッパークラス" 39 | 40 | #: ../../source/index.rst:13 41 | msgid "Provide type annotations from stub files." 42 | msgstr "スタブファイルから型定義情報を提供します。" 43 | 44 | #: ../../source/index.rst:14 45 | msgid "and other convenient utilities..." 46 | msgstr "他にもWebアプリケーション開発に便利なユーティリティを提供します。" 47 | 48 | #: ../../source/index.rst:16 49 | msgid "And Kobin has **NO** following features:" 50 | msgstr "Kobinは次の機能を提供しません。" 51 | 52 | #: ../../source/index.rst:18 53 | msgid "*WSGI Server Adapters*: Please use WSGICLI or Gunicorn CLI." 54 | msgstr "**WSGIサーバのアダプター** : WSGICLIやGunicornのCLIを使用してください。" 55 | 56 | #: ../../source/index.rst:19 57 | msgid "*Serving static contents*: Please use WSGICLI and Nginx." 58 | msgstr "**静的ファイルの配信** : WSGICLIやNginxを使用してください。" 59 | 60 | #: ../../source/index.rst:20 61 | msgid "*Template Engine*: But Kobin provides template adapter." 62 | msgstr "**Template Engine** : Kobinではテンプレートエンジンへのアダプタのみを提供します" 63 | 64 | #: ../../source/index.rst:24 65 | msgid "Statuses" 66 | msgstr "ステータス" 67 | 68 | #: ../../source/index.rst:45 69 | msgid "Kobin documentation contents" 70 | msgstr "目次" 71 | 72 | #: ../../source/index.rst:56 73 | msgid "Requirements" 74 | msgstr "" 75 | 76 | #: ../../source/index.rst:58 77 | msgid "" 78 | "Supported python versions are python 3.6. And Kobin has no required " 79 | "dependencies other than the Python Standard Libraries." 80 | msgstr "Kobinでは、Python 3.6をサポートしています。またKobinはサードパーティのライブラリに依存していません。" 81 | 82 | #: ../../source/index.rst:61 83 | msgid "The following packages are optional:" 84 | msgstr "次のパッケージは、必要に応じてインストールしてください。" 85 | 86 | #: ../../source/index.rst:63 87 | msgid "wsgicli - Command Line Interface for developing WSGI application." 88 | msgstr "" 89 | 90 | #: ../../source/index.rst:64 91 | msgid "jinja2 - Jinja2 is a full featured template engine for Python." 92 | msgstr "" 93 | 94 | #: ../../source/index.rst:68 95 | msgid "Links" 96 | msgstr "リンク" 97 | 98 | #: ../../source/index.rst:70 99 | msgid "`Documentation (English) `_" 100 | msgstr "`ドキュメント (英語) `_" 101 | 102 | #: ../../source/index.rst:71 103 | msgid "`Documentation (日本語) `_" 104 | msgstr "`ドキュメント (日本語) `_" 105 | 106 | #: ../../source/index.rst:72 107 | msgid "`Github `_" 108 | msgstr "" 109 | 110 | #: ../../source/index.rst:73 111 | msgid "`PyPI `_" 112 | msgstr "" 113 | 114 | #: ../../source/index.rst:74 115 | msgid "`Kobin Example `_" 116 | msgstr "" 117 | 118 | #: ../../source/index.rst:77 119 | msgid "Indices and tables" 120 | msgstr "索引表" 121 | 122 | #: ../../source/index.rst:79 123 | msgid ":ref:`genindex`" 124 | msgstr "" 125 | 126 | #: ../../source/index.rst:80 127 | msgid ":ref:`modindex`" 128 | msgstr "" 129 | 130 | #: ../../source/index.rst:81 131 | msgid ":ref:`search`" 132 | msgstr "" 133 | -------------------------------------------------------------------------------- /docs/source/locale/ja/LC_MESSAGES/modules.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2016, Masashi Shibata 3 | # This file is distributed under the same license as the kobin package. 4 | # FIRST AUTHOR , 2016. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: kobin 0.0\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2016-02-27 18:01+0900\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 2.2.0\n" 19 | 20 | #: ../../source/modules.rst:3 21 | msgid "Modules" 22 | msgstr "Kobinモジュール" 23 | 24 | #: ../../source/modules.rst:6 25 | msgid "kobin" 26 | msgstr "" 27 | 28 | #: ../../source/modules.rst:12 29 | msgid "kobin.app" 30 | msgstr "" 31 | 32 | #: ../../source/modules.rst:18 33 | msgid "kobin.environs" 34 | msgstr "" 35 | 36 | #: of kobin.environs.LocalRequest:1 37 | msgid "A thread local subclass of :class:`Request`" 38 | msgstr "" 39 | 40 | #: of kobin.environs.LocalRequest.environ:1 kobin.environs.LocalResponse.body:1 41 | msgid "Thread-local property" 42 | msgstr "" 43 | 44 | #: of kobin.environs.LocalResponse:1 45 | msgid "" 46 | "A thread-local subclass ob :class:`BaseResponse` with a different set of " 47 | "attributes for each thread" 48 | msgstr "" 49 | 50 | #: of kobin.environs.Request:1 51 | msgid "A wrapper for WSGI environment dictionaries." 52 | msgstr "" 53 | 54 | #: of kobin.environs.Request.method:1 55 | msgid "The ``REQUEST_METHOD`` value as an uppercase string." 56 | msgstr "" 57 | 58 | #: of kobin.environs.Request.path:1 59 | msgid "" 60 | "The value of ``PATH_INFO`` with exactly one prefixed slash (to fix broken" 61 | " clients and avoid the \"empty path\" edge case)." 62 | msgstr "" 63 | 64 | #: ../../source/modules.rst:24 65 | msgid "kobin.exceptions" 66 | msgstr "" 67 | 68 | #: ../../source/modules.rst:30 69 | msgid "kobin.routes" 70 | msgstr "" 71 | 72 | #: of kobin.routes.Route:1 73 | msgid "" 74 | "This class wraps a route callback along with route specific metadata. It " 75 | "is also responsible for turing an URL path rule into a regular expression" 76 | " usable by the Router." 77 | msgstr "" 78 | 79 | #: ../../source/modules.rst:36 80 | msgid "kobin.server_adapters" 81 | msgstr "" 82 | 83 | #: ../../source/modules.rst:42 84 | msgid "kobin.static_files" 85 | msgstr "" 86 | 87 | #: ../../source/modules.rst:48 88 | msgid "kobin.templates" 89 | msgstr "" 90 | 91 | #: of kobin.templates.template:1 92 | msgid "Get a rendered template as string iterator." 93 | msgstr "" 94 | 95 | -------------------------------------------------------------------------------- /docs/source/locale/ja/LC_MESSAGES/sphinx.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2016, Masashi Shibata 3 | # This file is distributed under the same license as the kobin package. 4 | # FIRST AUTHOR , 2016. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: kobin 0.0.4\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2016-12-01 17:03+0900\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 2.3.4\n" 19 | 20 | #: ../../source/_templates/layout.html:46 21 | #, python-format 22 | msgid "" 23 | "Created using Sphinx " 24 | "%(sphinx_version)s.Theme by vkvn" 25 | msgstr "" 26 | 27 | -------------------------------------------------------------------------------- /docs/source/locale/ja/LC_MESSAGES/tutorial.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2016, Masashi Shibata 3 | # This file is distributed under the same license as the kobin package. 4 | # Masashi Shibata , 2016. 5 | #, fuzzy 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: kobin 0.0\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2016-12-07 01:31+0900\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: LANGUAGE \n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=utf-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Generated-By: Babel 2.3.4\n" 18 | 19 | #: ../../source/tutorial.rst:3 20 | msgid "Tutorial" 21 | msgstr "チュートリアル" 22 | 23 | #: ../../source/tutorial.rst:6 24 | msgid "Installation" 25 | msgstr "インストール" 26 | 27 | #: ../../source/tutorial.rst:8 28 | msgid "In this tutorial, we will use Python 3.6." 29 | msgstr "このチュートリアルでは、Python3.6を使用します。" 30 | 31 | #: ../../source/tutorial.rst:17 32 | msgid "Kobin: WSGI Framework" 33 | msgstr "" 34 | 35 | #: ../../source/tutorial.rst:18 36 | msgid "WSGICLI: Command line tools for developing your WSGI Application" 37 | msgstr "" 38 | 39 | #: ../../source/tutorial.rst:22 40 | msgid "Your first kobin app" 41 | msgstr "初めてのKobinアプリケーション" 42 | 43 | #: ../../source/tutorial.rst:24 44 | msgid "Let's make Kobin's application. Please create a ``main.py``:" 45 | msgstr "ここからは、実際にKobinのアプリケーションをつくってみます。 ``main.py`` を作成してください。" 46 | 47 | #: ../../source/tutorial.rst:42 48 | msgid "" 49 | "For those who have used the WSGI framework such as Bottle and Flask, it " 50 | "may be familiar with this code. One distinctive feature is the existence " 51 | "of type hints. Kobin casts the URL variable based on the type hinting and" 52 | " passes it to the View function." 53 | msgstr "BottleやFlaskなどのWSGIフレームワークを使用したことのある方にとっては、馴染みのあるものかもしれません。一つ特徴的なのは、型ヒントの存在でしょうか。Kobinでは、引数の型ヒントを元にURL変数をキャストし、View関数に渡します。" 54 | 55 | #: ../../source/tutorial.rst:47 56 | msgid "" 57 | "Let's actually move it. There are several ways to run WSGI's application." 58 | " In the development environment we recommend a command line tool called " 59 | "``wsgicli``." 60 | msgstr "" 61 | "それでは実際に動かしてみましょう。WSGIのアプリケーションを動かす方法はいくつかありますが、開発環境では ``wsgicli`` " 62 | "というコマンドラインツールを推奨しています。" 63 | 64 | #: ../../source/tutorial.rst:56 65 | msgid "When the server starts up successfully, let's access following urls." 66 | msgstr "サーバが無事に起動したら、実際にアクセスしてみましょう。" 67 | 68 | #: ../../source/tutorial.rst:58 69 | msgid "http://localhost:8000/" 70 | msgstr "" 71 | 72 | #: ../../source/tutorial.rst:59 73 | msgid "http://localhost:8000/users/1" 74 | msgstr "" 75 | 76 | #: ../../source/tutorial.rst:61 77 | msgid "Did you see any message? Congratulations!" 78 | msgstr "何かメッセージが表示されましたか?おめでとうございます。" 79 | 80 | #: ../../source/tutorial.rst:65 81 | msgid "Deploy to production" 82 | msgstr "本番環境へのデプロイ" 83 | 84 | #: ../../source/tutorial.rst:67 85 | msgid "" 86 | "In a production, let's use ``gunicorn`` instead of using ``wsgicli`` for " 87 | "a performance reasons." 88 | msgstr "本番環境では、パフォーマンスの観点からこれまで使用してきた ``wsgicli`` ではなく ``gunicorn`` を使いましょう。" 89 | 90 | #: ../../source/tutorial.rst:74 91 | msgid "" 92 | "Then please try accessing your website. If you use the function of static" 93 | " file serving in wsgicli, maybe the layout and styles have gone wrong. " 94 | "Actually, gunicorn doesn't have the function of serving static content " 95 | "such as CSS, JS, and image files. Generally, the reverse proxy server " 96 | "such as Nginx is used for serving static content in production (See " 97 | "`Serving Static Content - Nginx `_ ." 99 | msgstr "" 100 | "それではアクセスしてみてください。大変です、表示がおかしくなってしまいました。実は、gunicornにはCSSやJS、画像ファイルといった静的コンテンツを配信する機能はありません。本番環境では、Nginx等のリバースプロキシを用いて静的コンテンツを配信することが一般的です(参考:" 101 | " `Serving Static Content - Nginx `_ )" 103 | 104 | #: ../../source/tutorial.rst:80 105 | msgid "" 106 | "If you absolutely need to serve static contents in Python's application " 107 | "side (ex: Using Heroku), Please use `kobinpy/wsgi-static-middleware " 108 | "`_ ." 109 | msgstr "" 110 | "もし、HerokuなどどうしてもPythonのアプリケーション側で静的ファイルを配信する必要がある場合は、`kobinpy/wsgi-" 111 | "static-middleware `_ " 112 | "を利用してください。" 113 | 114 | #: ../../source/tutorial.rst:85 115 | msgid "Next Step" 116 | msgstr "次のステップ" 117 | 118 | #: ../../source/tutorial.rst:87 119 | msgid "" 120 | "More practical example is `kobin-example `_ . If you want to know the best practices in Kobin, " 122 | "Please check it." 123 | msgstr "" 124 | "より実践的なアプリケーションとして、 `kobin-example `_ があります。" 126 | "もしKobinの使い方やベストプラクティスについてもう少し学びたい方は、そちらをチェックしてください。" 127 | -------------------------------------------------------------------------------- /docs/source/tutorial.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Tutorial 3 | ======== 4 | 5 | Installation 6 | ============ 7 | 8 | In this tutorial, we will use Python 3.6. 9 | 10 | .. code-block:: console 11 | 12 | $ python --version 13 | Python 3.6.0b4 14 | $ pip install -U pip 15 | $ pip install kobin wsgicli 16 | 17 | * Kobin: WSGI Framework 18 | * WSGICLI: Command line tools for developing your WSGI Application 19 | 20 | 21 | Your first kobin app 22 | ==================== 23 | 24 | Let's make Kobin's application. 25 | Please create a ``main.py``: 26 | 27 | .. code-block:: python 28 | 29 | from kobin import Kobin, Response, JSONResponse 30 | app = Kobin() 31 | 32 | @app.route('/') 33 | def index() -> Response: 34 | return Response("Hello World!") 35 | 36 | @app.route('/users/{user_id}') 37 | def say_hello(user_id: int) -> JSONResponse: 38 | return JSONResponse({ 39 | "message": f"Hello user{user_id}!" 40 | }) 41 | 42 | For those who have used the WSGI framework such as Bottle and Flask, 43 | it may be familiar with this code. 44 | One distinctive feature is the existence of type hints. 45 | Kobin casts the URL variable based on the type hinting and passes it to the View function. 46 | 47 | Let's actually move it. 48 | There are several ways to run WSGI's application. 49 | In the development environment we recommend a command line tool called ``wsgicli``. 50 | 51 | .. code-block:: console 52 | 53 | $ wsgicli run main.py app 54 | Start: 127.0.0.1:8000 55 | 56 | When the server starts up successfully, let's access following urls. 57 | 58 | - http://localhost:8000/ 59 | - http://localhost:8000/users/1 60 | 61 | Did you see any message? Congratulations! 62 | 63 | 64 | Deploy to production 65 | ==================== 66 | 67 | In a production, let's use ``gunicorn`` instead of using ``wsgicli`` for a performance reasons. 68 | 69 | .. code-block:: console 70 | 71 | $ pip install gunicorn 72 | $ gunicorn main:app 73 | 74 | Then please try accessing your website. 75 | If you use the function of static file serving in wsgicli, maybe the layout and styles have gone wrong. 76 | Actually, gunicorn doesn't have the function of serving static content such as CSS, JS, and image files. 77 | Generally, the reverse proxy server such as Nginx is used for serving static content in production 78 | (See `Serving Static Content - Nginx `_ . 79 | 80 | If you absolutely need to serve static contents in Python's application side (ex: Using Heroku), 81 | Please use `kobinpy/wsgi-static-middleware `_ . 82 | 83 | 84 | Next Step 85 | ========= 86 | 87 | More practical example is `kobin-example `_ . 88 | If you want to know the best practices in Kobin, Please check it. 89 | 90 | .. image:: _static/kobin-example.gif 91 | :alt: Kobin Example Demo Animation 92 | :align: center 93 | -------------------------------------------------------------------------------- /example/helloworld/README.md: -------------------------------------------------------------------------------- 1 | # hello example 2 | 3 | ## How to run 4 | 5 | **In Development** 6 | 7 | Please run with wsgicli. 8 | 9 | ```console 10 | $ pip install kobin wsgicli 11 | $ wsgicli hello.py app --reload 12 | ``` 13 | 14 | **In Production** 15 | 16 | And if you want to run in production, please run with gunicorn. 17 | 18 | ```console 19 | $ pip install kobin gunicorn 20 | $ gunicorn -w 1 hello:app 21 | ``` 22 | -------------------------------------------------------------------------------- /example/helloworld/hello.py: -------------------------------------------------------------------------------- 1 | from kobin import Kobin, request, Response, JSONResponse 2 | 3 | app = Kobin() 4 | 5 | 6 | @app.route('/') 7 | def hello() -> Response: 8 | print(request.headers) 9 | return Response("Hello World!!") 10 | 11 | 12 | @app.route('/hello/{name}') 13 | def hello(name: str) -> JSONResponse: 14 | return JSONResponse({ 15 | "name": name 16 | }) 17 | -------------------------------------------------------------------------------- /example/template_and_static_files/README.md: -------------------------------------------------------------------------------- 1 | # Static files and template example 2 | 3 | ## How to run 4 | 5 | **In Development** 6 | 7 | Please run with wsgicli. 8 | 9 | ```console 10 | $ pip install kobin wsgicli jinja2 11 | $ wsgicli app.py app --reload --static --static-root /static/ --static-dirs ./static/ 12 | ``` 13 | 14 | **In Production** 15 | 16 | And if you want to run in production, please run with gunicorn. 17 | 18 | ```console 19 | $ pip install kobin gunicorn 20 | $ gunicorn -w 1 app:app 21 | ``` 22 | 23 | And static files are return with reverse proxy (ex: Nginx). 24 | -------------------------------------------------------------------------------- /example/template_and_static_files/app.py: -------------------------------------------------------------------------------- 1 | from kobin import Kobin, request, Response, TemplateResponse, load_config_from_pyfile 2 | 3 | config = load_config_from_pyfile('config.py') 4 | app = Kobin(config=config) 5 | 6 | 7 | @app.route('/') 8 | def index(): 9 | return TemplateResponse( 10 | 'hello_jinja2.html', name='Kobin', headers={'foo': 'bar'} 11 | ) 12 | 13 | 14 | @app.route('/user/{name}') 15 | def hello(name: str): 16 | body = """ 17 |

Hello {}

18 |

Request Path: {}

19 | """.format(name, request.path) 20 | return Response(body) 21 | -------------------------------------------------------------------------------- /example/template_and_static_files/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.dirname(os.path.abspath(__name__)) 4 | TEMPLATE_DIRS = [os.path.join(BASE_DIR, 'templates')] 5 | -------------------------------------------------------------------------------- /example/template_and_static_files/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kobinpy/kobin/f87a09d071cf8246d3af700581943e881866bf5a/example/template_and_static_files/static/favicon.ico -------------------------------------------------------------------------------- /example/template_and_static_files/static/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | min-height: 100vh; 4 | flex-direction: column; 5 | margin:0; 6 | } 7 | 8 | header { 9 | background-color: #3D90AB; 10 | } 11 | 12 | main { 13 | flex: 1; 14 | } 15 | 16 | footer { 17 | margin: 30px 0; 18 | } 19 | 20 | .container { 21 | max-width: 980px; 22 | width: 100%; 23 | margin: 0 auto; 24 | } 25 | -------------------------------------------------------------------------------- /example/template_and_static_files/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block head %} 6 | {% block title %}{% endblock %} - Kobin 7 | 8 | {% endblock %} 9 | 10 | 11 |
12 | {% block header %} 13 | {% endblock %} 14 |
15 |
16 | {% block main %} 17 | {% endblock %} 18 |
19 |
20 | {% block footer %} 21 | {% endblock %} 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /example/template_and_static_files/templates/hello_jinja2.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Hello!{% endblock %} 4 | 5 | {% block head %} 6 | {{ super() }} 7 | 8 | {% endblock %} 9 | 10 | {% block header %} 11 |
12 |

Kobin is a small and statically-typed web framework.

13 |
14 | {% endblock %} 15 | 16 | {% block main %} 17 |
18 |

Hello {{ name }}! with Jinja2 template.

19 |
20 | {% endblock %} 21 | 22 | {% block footer %} 23 |

Powered by Kobin.

24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /kobin/__init__.py: -------------------------------------------------------------------------------- 1 | from .app import ( 2 | Kobin, load_config, 3 | load_config_from_module, load_config_from_pyfile 4 | ) 5 | from .requests import request 6 | from .responses import ( 7 | BaseResponse, Response, TemplateResponse, 8 | JSONResponse, RedirectResponse, HTTPError 9 | ) 10 | -------------------------------------------------------------------------------- /kobin/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | Kobin class 3 | =========== 4 | 5 | The Kobin instance are callable WSGI Application. 6 | 7 | Usage 8 | ----- 9 | 10 | .. code-block:: python 11 | 12 | from kobin import Kobin, Response 13 | app = Kobin() 14 | 15 | @app.route('/') 16 | def index() -> Response: 17 | return Response('Hello World') 18 | 19 | """ 20 | from importlib.machinery import SourceFileLoader 21 | import logging 22 | import os 23 | import traceback 24 | from urllib.parse import urljoin 25 | import warnings 26 | 27 | from .routes import Router 28 | from .requests import request 29 | from .responses import HTTPError 30 | 31 | 32 | class Kobin: 33 | """ 34 | This class is a WSGI application implementation. 35 | Create a instance, and run using WSGI Server. 36 | """ 37 | def __init__(self, config=None): 38 | self.router = Router() 39 | self.config = load_config(config) 40 | self.before_request_callbacks = [] 41 | self.after_request_callbacks = [] 42 | self.logger = self.config.get('LOGGER') 43 | self._frozen = False 44 | 45 | def __call__(self, environ, start_response): 46 | """It is called when receive http request.""" 47 | if not self._frozen: 48 | self._frozen = True 49 | response = self._handle(environ) 50 | start_response(response.status, response.headerlist) 51 | return response.body 52 | 53 | def __setattr__(self, key, value): 54 | if self.frozen: 55 | warnings.warn("Cannot Change the state of started application!", stacklevel=2) 56 | else: 57 | super().__setattr__(key, value) 58 | 59 | def __delattr__(self, item): 60 | if self.frozen: 61 | warnings.warn("Cannot Delete the state of started application!", stacklevel=2) 62 | else: 63 | super().__setattr__(item) 64 | 65 | @property 66 | def frozen(self): 67 | if '_frozen' not in dir(self): 68 | return False 69 | return self._frozen 70 | 71 | def route(self, rule=None, method='GET', name=None, callback=None): 72 | def decorator(callback_func): 73 | self.router.add(rule, method, name, callback_func) 74 | return callback_func 75 | return decorator(callback) if callback else decorator 76 | 77 | def before_request(self, callback): 78 | def decorator(callback_func): 79 | self.before_request_callbacks.append(callback_func) 80 | return callback_func 81 | return decorator(callback) 82 | 83 | def after_request(self, callback): 84 | def decorator(callback_func): 85 | self.after_request_callbacks.append(callback_func) 86 | return callback_func 87 | return decorator(callback) 88 | 89 | def _handle(self, environ): 90 | environ['kobin.app'] = self 91 | request.bind(environ) 92 | 93 | try: 94 | for before_request_callback in self.before_request_callbacks: 95 | before_request_callback() 96 | 97 | method = environ['REQUEST_METHOD'] 98 | path = environ['PATH_INFO'] or '/' 99 | callback, kwargs = self.router.match(path, method) 100 | response = callback(**kwargs) if kwargs else callback() 101 | 102 | for after_request_callback in self.after_request_callbacks: 103 | wrapped_response = after_request_callback(response) 104 | if wrapped_response: 105 | response = wrapped_response 106 | except HTTPError as e: 107 | response = e 108 | except BaseException as e: 109 | error_message = _get_exception_message(e, self.config.get('DEBUG')) 110 | self.logger.debug(error_message) 111 | response = HTTPError(error_message, 500) 112 | return response 113 | 114 | 115 | def _get_exception_message(e, debug): 116 | if debug: 117 | stacktrace = '\n'.join(traceback.format_tb(e.__traceback__)) 118 | message = f"500: Internal Server Error\n\n" \ 119 | f"Exception:\n {repr(e)}\n\n" \ 120 | f"Stacktrace:\n{stacktrace}\n" 121 | else: 122 | message = 'Internal Server Error' 123 | return message 124 | 125 | 126 | # Following configurations are optional: 127 | # 128 | # * DEBUG 129 | # * SECRET_KEY 130 | # * TEMPLATE_DIRS (default: './templates/') or TEMPLATE_ENVIRONMENT 131 | # * LOG_LEVEL 132 | # * LOG_HANDLER 133 | # 134 | def _current_app(): 135 | # This function exists for unittest.mock.patch. 136 | return request['kobin.app'] 137 | 138 | 139 | def template_router_reverse(name, with_host=False): 140 | url = _current_app().router.reverse(name) 141 | if with_host: 142 | url = urljoin(request.url, url) 143 | 144 | if url is None: 145 | return '' 146 | return url 147 | 148 | 149 | def load_jinja2_env(template_dirs, global_variables=None, global_filters=None, **envoptions): 150 | try: 151 | from jinja2 import Environment, FileSystemLoader 152 | env = Environment(loader=FileSystemLoader(template_dirs), **envoptions) 153 | if global_variables: 154 | env.globals.update(global_variables) 155 | if global_filters: 156 | env.filters.update(global_filters) 157 | return env 158 | except ImportError: 159 | pass 160 | 161 | 162 | def _get_default_logger(debug): 163 | logger = logging.getLogger(__name__) 164 | handler = logging.StreamHandler() 165 | if debug: 166 | logger.setLevel(logging.DEBUG) 167 | else: 168 | logger.setLevel(logging.INFO) 169 | logger.addHandler(handler) 170 | return logger 171 | 172 | 173 | def load_config(config=None): 174 | default_config = { 175 | 'BASE_DIR': os.path.abspath('.'), 176 | 'TEMPLATE_DIRS': [os.path.join(os.path.abspath('.'), 'templates')], 177 | 'DEBUG': False, 178 | } 179 | if config is not None: 180 | default_config.update(config) 181 | 182 | if 'TEMPLATE_ENVIRONMENT' not in default_config: 183 | env = load_jinja2_env(default_config['TEMPLATE_DIRS']) 184 | if env: 185 | default_config['TEMPLATE_ENVIRONMENT'] = env 186 | 187 | if 'LOGGER' not in default_config: 188 | default_config['LOGGER'] = _get_default_logger(default_config.get('DEBUG')) 189 | 190 | return default_config 191 | 192 | 193 | def load_config_from_module(module): 194 | config = {key: getattr(module, key) for key in dir(module) if key.isupper()} 195 | return load_config(config) 196 | 197 | 198 | def load_config_from_pyfile(filepath): 199 | module = SourceFileLoader('config', filepath).load_module() 200 | return load_config_from_module(module) 201 | 202 | 203 | def current_config(key, default=None): 204 | """Get the configurations of your Kobin's application.""" 205 | return request['kobin.app'].config.get(key, default) 206 | -------------------------------------------------------------------------------- /kobin/app.pyi: -------------------------------------------------------------------------------- 1 | from jinja2 import Environment # type: ignore 2 | from logging import Logger, Handler 3 | from typing import Callable, Dict, List, Tuple, Iterable, TypeVar, Any, Union 4 | from types import ModuleType 5 | 6 | from .routes import Router 7 | from .responses import BaseResponse, Response 8 | 9 | 10 | WSGIEnvironValue = TypeVar('WSGIEnvironValue') 11 | WSGIEnviron = Dict[str, WSGIEnvironValue] 12 | StartResponse = Callable[[bytes, List[Tuple[str, str]]], None] 13 | 14 | ViewFunction = Callable[..., BaseResponse] 15 | WSGIResponse = Iterable[bytes] 16 | 17 | 18 | class Kobin: 19 | router: Router 20 | config: Dict[str, Any] 21 | logger: Logger 22 | before_request_callbacks: List[Callable[[], None]] 23 | after_request_callbacks: List[Callable[[BaseResponse], Union[None, BaseResponse]]] 24 | _frozen: bool 25 | 26 | def __init__(self, config: Dict[str, Any] = ...) -> None: ... 27 | def __call__(self, environ: WSGIEnviron, start_response: StartResponse) -> WSGIResponse: ... 28 | 29 | @property 30 | def frozen(self) -> bool: ... 31 | def route(self, rule: str = ..., method: str = ..., name: str = ..., 32 | callback: ViewFunction = ...) -> ViewFunction: ... 33 | def before_request(self, callback: Callable[[], None]) -> Callable[[], None]: ... 34 | def after_request(self, callback: Callable[[BaseResponse], BaseResponse]) -> \ 35 | Callable[[BaseResponse], BaseResponse]: ... 36 | def _handle(self, environ: WSGIEnviron) -> BaseResponse: ... 37 | def wsgi(self, environ: WSGIEnviron, start_response: StartResponse) -> WSGIResponse: ... 38 | 39 | def _get_exception_message(e: BaseException, debug: bool) -> str: ... 40 | 41 | def _current_app() -> Kobin: ... 42 | def load_jinja2_env(template_dirs: List[str], 43 | global_variables: Dict[str, Any] = ..., 44 | global_filters: Dict[str, Any] = ..., 45 | **envoptions: Any) -> Environment: ... 46 | def _get_logger(debug: bool) -> Logger: ... 47 | def load_config(config: Dict[str, Any] = ...) -> Dict[str, Any]: ... 48 | def load_config_from_pyfile(filepath: str) -> Dict[str, Any]: ... 49 | def load_config_from_module(module: ModuleType) -> Dict[str, Any]: ... 50 | def current_app() -> Kobin: ... 51 | def current_config(key: str) -> Any: ... 52 | -------------------------------------------------------------------------------- /kobin/requests.py: -------------------------------------------------------------------------------- 1 | """ 2 | Request 3 | ======= 4 | 5 | When a page is requested, automatically created a :class:`Request` object that 6 | contains metadata about the request. 7 | Since this object is global within the thread, 8 | you can freely import from anywhere and retrieve request information. 9 | """ 10 | import base64 11 | import fnmatch 12 | import hashlib 13 | import hmac 14 | import threading 15 | import cgi 16 | import json 17 | import pickle 18 | from urllib.parse import SplitResult 19 | from http.cookies import SimpleCookie 20 | 21 | 22 | ################################################################################## 23 | # Request Object ################################################################# 24 | ################################################################################## 25 | 26 | 27 | class Request: 28 | """ A wrapper for WSGI environment dictionaries. 29 | """ 30 | __slots__ = ('environ', '_body', '_forms') 31 | 32 | def __init__(self, environ=None): 33 | self.environ = {} if environ is None else environ 34 | self.environ['kobin.request'] = self 35 | self._body = None 36 | self._forms = None 37 | 38 | def get(self, value, default=None): 39 | return self.environ.get(value, default) 40 | 41 | @property 42 | def path(self): 43 | """ The value of ``PATH_INFO`` with exactly one prefixed slash (to fix 44 | broken clients and avoid the "empty path" edge case). """ 45 | return '/' + self.environ.get('PATH_INFO', '').lstrip('/') 46 | 47 | @property 48 | def method(self): 49 | """ The ``REQUEST_METHOD`` value as an uppercase string. """ 50 | return self.environ.get('REQUEST_METHOD', 'GET').upper() 51 | 52 | @property 53 | def headers(self): 54 | return {k[len('HTTP_'):]: v 55 | for k, v in self.environ.items() 56 | if k.startswith('HTTP_')} 57 | 58 | @property 59 | def query(self): 60 | params = cgi.FieldStorage( 61 | environ=self.environ, 62 | keep_blank_values=True, 63 | ) 64 | p = {k: params[k].value for k in params} 65 | return p 66 | 67 | @property 68 | def forms(self): 69 | if self._forms is None: 70 | form = cgi.FieldStorage( 71 | fp=self.environ['wsgi.input'], 72 | environ=self.environ, 73 | keep_blank_values=True, 74 | ) 75 | self._forms = {k: form[k].value for k in form} 76 | return self._forms 77 | 78 | @property 79 | def raw_body(self): 80 | if self._body is not None: 81 | return self._body 82 | 83 | length = self.environ.get('CONTENT_LENGTH') 84 | if length: 85 | self._body = self.environ['wsgi.input'].read(int(length)) 86 | else: 87 | self._body = b'' 88 | return self._body 89 | 90 | @property 91 | def body(self): 92 | return self.raw_body.decode('utf-8') 93 | 94 | @property 95 | def json(self): 96 | return json.loads(self.body) 97 | 98 | @property 99 | def url(self): 100 | protocol = self.get('HTTP_X_FORWARDED_PROTO') or self.get('wsgi.url_scheme', 'http') 101 | host = self.get('HTTP_X_FORWARDED_HOST') or self.get('HTTP_HOST') 102 | query_params = self.get("QUERY_STRING") 103 | url_split_result = SplitResult(protocol, host, self.path, query_params, '') 104 | return url_split_result.geturl() 105 | 106 | @property 107 | def cookies(self): 108 | cookies = SimpleCookie(self.environ.get('HTTP_COOKIE', '')).values() 109 | return {c.key: c.value for c in cookies} 110 | 111 | def get_cookie(self, key, default=None, secret=None, digestmod=hashlib.sha256): 112 | from kobin.app import current_config 113 | if secret is None: 114 | secret = current_config('SECRET_KEY') 115 | 116 | value = self.cookies.get(key) 117 | if secret and value and value.startswith('!') and '?' in value: 118 | # See BaseResponse.set_cookie for details. 119 | if isinstance(secret, str): 120 | secret = secret.encode('utf-8') 121 | sig, msg = map(lambda x: x.encode('utf-8'), value[1:].split('?', 1)) 122 | hash_string = hmac.new(secret, msg, digestmod=digestmod).digest() 123 | if sig == base64.b64encode(hash_string): 124 | key_and_value = pickle.loads(base64.b64decode(msg)) 125 | if key_and_value and key_and_value[0] == key: 126 | return key_and_value[1] 127 | return value or default 128 | 129 | def __getitem__(self, key): 130 | return self.environ[key] 131 | 132 | def __delitem__(self, key): 133 | self[key] = "" 134 | del (self.environ[key]) 135 | 136 | def __setitem__(self, key, value): 137 | """ Change an environ value and clear all caches that depend on it. """ 138 | self.environ[key] = value 139 | todelete = () 140 | 141 | if key == 'wsgi.input': 142 | todelete = ('body', 'forms', 'files', 'params', 'post', 'json') 143 | elif key == 'QUERY_STRING': 144 | todelete = ('query', 'params') 145 | elif key.startswith('HTTP_'): 146 | todelete = ('headers', 'cookies') 147 | 148 | for key in todelete: 149 | self.environ.pop('kobin.request.' + key, None) 150 | 151 | def __len__(self): 152 | return len(self.environ) 153 | 154 | def __repr__(self): 155 | return '<{cls}: {method} {url}>'.format( 156 | cls=self.__class__.__name__, method=self.method, url=self.path 157 | ) 158 | 159 | 160 | # for Accept header. 161 | def _split_into_mimetype_and_priority(x): 162 | """Split an accept header item into mimetype and priority. 163 | 164 | >>> _split_into_mimetype_and_priority('text/*') 165 | ('text/*', 1.0) 166 | 167 | >>> _split_into_mimetype_and_priority('application/json;q=0.5') 168 | ('application/json', 0.5) 169 | """ 170 | if ';' in x: 171 | content_type, priority = x.split(';') 172 | casted_priority = float(priority.split('=')[1]) 173 | else: 174 | content_type, casted_priority = x, 1.0 175 | 176 | content_type = content_type.lstrip().rstrip() # Replace ' text/html' to 'text/html' 177 | return content_type, casted_priority 178 | 179 | 180 | def _parse_and_sort_accept_header(accept_header): 181 | """Parse and sort the accept header items. 182 | 183 | >>> _parse_and_sort_accept_header('application/json;q=0.5, text/*') 184 | [('text/*', 1.0), ('application/json', 0.5)] 185 | """ 186 | return sorted([_split_into_mimetype_and_priority(x) for x in accept_header.split(',')], 187 | key=lambda x: x[1], reverse=True) 188 | 189 | 190 | def accept_best_match(accept_header, mimetypes): 191 | """Return a mimetype best matched the accept headers. 192 | 193 | >>> accept_best_match('application/json, text/html', ['application/json', 'text/plain']) 194 | 'application/json' 195 | 196 | >>> accept_best_match('application/json;q=0.5, text/*', ['application/json', 'text/plain']) 197 | 'text/plain' 198 | """ 199 | for mimetype_pattern, _ in _parse_and_sort_accept_header(accept_header): 200 | matched_types = fnmatch.filter(mimetypes, mimetype_pattern) 201 | if matched_types: 202 | return matched_types[0] 203 | return mimetypes[0] 204 | 205 | 206 | def _local_property(): 207 | ls = threading.local() 208 | 209 | def fget(_): 210 | try: 211 | return ls.var 212 | except AttributeError: 213 | raise RuntimeError("Request context not initialized.") 214 | 215 | def fset(_, value): 216 | ls.var = value 217 | 218 | def fdel(_): 219 | del ls.var 220 | 221 | return property(fget, fset, fdel, 'Thread-local property') 222 | 223 | 224 | class LocalRequest(Request): 225 | bind = Request.__init__ 226 | environ = _local_property() 227 | _body = _local_property() 228 | _forms = _local_property() 229 | 230 | 231 | request = LocalRequest() 232 | -------------------------------------------------------------------------------- /kobin/requests.pyi: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Tuple, Any, Callable, Union 2 | 3 | WSGIEnviron = Dict[str, Any] 4 | 5 | 6 | class Request: 7 | __slots__: List[str] 8 | environ: WSGIEnviron 9 | _body: str 10 | 11 | def __init__(self, environ: Dict = ...) -> None: ... 12 | def get(self, value: str, default: Any = ...): ... 13 | @property 14 | def path(self) -> str: ... 15 | @property 16 | def method(self) -> str: ... 17 | @property 18 | def headers(self) -> Dict[str, str]: ... 19 | @property 20 | def query(self) -> Dict[str, str]: ... 21 | @property 22 | def forms(self) -> Dict[str, str]: ... 23 | @property 24 | def raw_body(self) -> bytes: ... 25 | @property 26 | def body(self) -> str: ... 27 | @property 28 | def json(self) -> Dict[str, Any]: ... 29 | @property 30 | def url(self) -> str: ... 31 | @property 32 | def cookies(self) -> Dict[str, str]: ... 33 | def get_cookie(self, key: str, default: str = ..., secret: Union[str, bytes] = ...) -> str: ... 34 | def __getitem__(self, key: str): ... 35 | def __delitem__(self, key: str): ... 36 | def __setitem__(self, key: str, value: Any): ... 37 | def __len__(self): ... 38 | def __repr__(self): ... 39 | 40 | 41 | def _split_type_and_priority(x: str) -> Tuple[str, float]: ... 42 | 43 | def _parse_and_sort_accept_header(accept_header: str) -> List[Tuple[str, float]]: ... 44 | 45 | def accept_best_match(accept_header: str, mimetypes: List[str]) -> str: ... 46 | 47 | def _local_property() -> Any: ... 48 | 49 | 50 | class LocalRequest(Request): 51 | bind: Callable[[Dict], None] 52 | environ: WSGIEnviron 53 | _body: str 54 | 55 | 56 | request: LocalRequest 57 | -------------------------------------------------------------------------------- /kobin/responses.py: -------------------------------------------------------------------------------- 1 | """ 2 | Response 3 | ======== 4 | 5 | In contrast to :class:`Request` objects, which are created automatically, 6 | :class:`Response` objects are your responsibility. 7 | Each view functions you write is responsible 8 | for instantiating and returning an :class:`Response` or its child classes. 9 | 10 | In addition to the :class:`Response` class, Kobin provides :class:`TemplateResponse` , 11 | :class:`JSONResponse` , :class:`RedirectResponse` and :class:`HTTPError`. 12 | """ 13 | import base64 14 | import hashlib 15 | import hmac 16 | import time 17 | import json 18 | import pickle 19 | import http.client as http_client 20 | from urllib.parse import urljoin 21 | from http.cookies import SimpleCookie 22 | from wsgiref.headers import Headers 23 | 24 | from .requests import request 25 | 26 | 27 | HTTP_CODES = http_client.responses.copy() 28 | _HTTP_STATUS_LINES = dict((k, '%d %s' % (k, v)) for (k, v) in HTTP_CODES.items()) 29 | 30 | 31 | class BaseResponse: 32 | """Base class for Response.""" 33 | default_status = 200 34 | default_content_type = 'text/plain;' 35 | 36 | def __init__(self, body=None, status=None, headers=None): 37 | self._body = body if body else [b''] 38 | self._status_code = status or self.default_status 39 | self.headers = Headers() 40 | self._cookies = SimpleCookie() 41 | 42 | if headers: 43 | for name, value in headers.items(): 44 | self.headers.add_header(name, value) 45 | 46 | @property 47 | def body(self): 48 | return self._body 49 | 50 | @property 51 | def status_code(self): 52 | """ The HTTP status code as an integer (e.g. 404).""" 53 | return self._status_code 54 | 55 | @property 56 | def status(self): 57 | """ The HTTP status line as a string (e.g. ``404 Not Found``).""" 58 | status = _HTTP_STATUS_LINES.get(self._status_code) 59 | return str(status or ('{} Unknown'.format(self._status_code))) 60 | 61 | @status.setter 62 | def status(self, status_code): 63 | if not 100 <= status_code <= 999: 64 | raise ValueError('Status code out of range.') 65 | self._status_code = status_code 66 | 67 | @property 68 | def headerlist(self): 69 | """ WSGI conform list of (header, value) tuples. """ 70 | if 'Content-Type' not in self.headers: 71 | self.headers.add_header('Content-Type', self.default_content_type) 72 | if self._cookies: 73 | for c in self._cookies.values(): 74 | self.headers.add_header('Set-Cookie', c.OutputString()) 75 | return self.headers.items() 76 | 77 | def set_cookie(self, key, value, expires=None, max_age=None, path='/', 78 | secret=None, digestmod=hashlib.sha256): 79 | from kobin.app import current_config 80 | if secret is None: 81 | secret = current_config('SECRET_KEY') 82 | if secret: 83 | if isinstance(secret, str): 84 | secret = secret.encode('utf-8') 85 | encoded = base64.b64encode(pickle.dumps((key, value), pickle.HIGHEST_PROTOCOL)) 86 | sig = base64.b64encode(hmac.new(secret, encoded, digestmod=digestmod).digest()) 87 | value_bytes = b'!' + sig + b'?' + encoded 88 | value = value_bytes.decode('utf-8') 89 | 90 | self._cookies[key] = value 91 | if len(key) + len(value) > 3800: 92 | raise ValueError('Content does not fit into a cookie.') 93 | 94 | if max_age is not None: 95 | if isinstance(max_age, int): 96 | max_age_value = max_age 97 | else: 98 | max_age_value = max_age.seconds + max_age.days * 24 * 3600 99 | self._cookies[key]['max-age'] = max_age_value 100 | if expires is not None: 101 | if isinstance(expires, int): 102 | expires_value = expires 103 | else: 104 | expires_value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", expires.timetuple()) 105 | self._cookies[key]['expires'] = expires_value 106 | if path: 107 | self._cookies[key]['path'] = path 108 | 109 | def delete_cookie(self, key, **kwargs): 110 | kwargs['max_age'] = -1 111 | kwargs['expires'] = 0 112 | self.set_cookie(key, '', **kwargs) 113 | 114 | 115 | class Response(BaseResponse): 116 | """Returns a plain text from unicode object.""" 117 | default_content_type = 'text/plain; charset=UTF-8' 118 | 119 | def __init__(self, body='', status=None, headers=None, charset='utf-8'): 120 | if isinstance(body, str): 121 | body = body.encode(charset) 122 | iterable_body = [body] 123 | super().__init__(iterable_body, status, headers) 124 | self.charset = charset 125 | 126 | 127 | class JSONResponse(BaseResponse): 128 | """Returns a HTML text from dict or OrderedDict.""" 129 | default_content_type = 'application/json; charset=UTF-8' 130 | 131 | def __init__(self, dic, status=200, headers=None, charset='utf-8', **dump_args): 132 | body = [json.dumps(dic, **dump_args).encode(charset)] 133 | super().__init__(body, status=status, headers=headers) 134 | 135 | 136 | class TemplateResponse(BaseResponse): 137 | """Returns a html using jinja2 template engine""" 138 | default_content_type = 'text/html; charset=UTF-8' 139 | 140 | def __init__(self, filename, status=200, headers=None, charset='utf-8', **tpl_args): 141 | from .app import current_config 142 | template_env = current_config('TEMPLATE_ENVIRONMENT') 143 | if template_env is None: 144 | raise HTTPError('TEMPLATE_ENVIRONMENT is not found in your config.') 145 | template = template_env.get_template(filename) 146 | body = [template.render(**tpl_args).encode(charset)] 147 | super().__init__(body, status=status, headers=headers) 148 | 149 | 150 | class RedirectResponse(BaseResponse): 151 | """Redirect the specified url.""" 152 | def __init__(self, url): 153 | status = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302 154 | super().__init__([b''], status=status, headers={'Location': urljoin(request.url, url)}) 155 | 156 | 157 | class HTTPError(Response, Exception): 158 | """Return the error message when raise this class.""" 159 | default_status = 500 160 | 161 | def __init__(self, body, status, headers=None, charset='utf-8'): 162 | super().__init__(body=body, status=status, headers=headers, charset=charset) 163 | -------------------------------------------------------------------------------- /kobin/responses.pyi: -------------------------------------------------------------------------------- 1 | from datetime import timedelta, date, datetime 2 | from http.cookies import SimpleCookie 3 | from typing import Dict, List, Tuple, Any, Iterable, Callable, Union 4 | from wsgiref.headers import Headers # type: ignore 5 | 6 | WSGIEnviron = Dict[str, Any] 7 | 8 | 9 | HTTP_CODES: Dict[int, str] 10 | _HTTP_STATUS_LINES: Dict[int, str] 11 | 12 | 13 | class BaseResponse: 14 | default_status: int 15 | default_content_type: str 16 | headers: Headers 17 | _body: bytes 18 | _status_code: int 19 | _cookies: SimpleCookie 20 | 21 | def __init__(self, body: Iterable[bytes] = ..., status: int = ..., headers: Dict = ...) -> None: ... 22 | @property 23 | def body(self) -> Iterable[bytes]: ... 24 | @property 25 | def status_code(self) -> int: ... 26 | @property 27 | def status(self) -> str: ... 28 | @status.setter 29 | def status(self, status_code: int) -> None: ... 30 | @property 31 | def headerlist(self) -> List[Tuple[str, str]]: ... 32 | def set_cookie(self, key: str, value: str, 33 | expires: Union[date, datetime, int] = ..., max_age: Union[timedelta, int] = ..., 34 | path: str = ..., secret: Union[str, bytes] = ..., 35 | digestmod: Callable[..., bytes] = ...) -> None: ... 36 | def delete_cookie(self, key: str, **kwargs: Any) -> None: ... 37 | 38 | class Response: 39 | def __init__(self, body: Union[str, bytes] = ..., status: int = ..., headers: Dict = ..., 40 | charset: str = ...) -> None: ... 41 | 42 | class JSONResponse: 43 | def __init__(self, dic: Dict, status: int = ..., headers: Dict = ..., 44 | charset: str = ..., **dump_args: Any) -> None: ... 45 | 46 | 47 | class TemplateResponse: 48 | def __init__(self, filename: str, status: int = ..., headers: Dict[str, str] = ..., 49 | charset: str = ..., **tpl_args: Any) -> None: ... 50 | 51 | 52 | class RedirectResponse(Response): 53 | def __init__(self, url: str) -> None: 54 | super().__init__() 55 | ... 56 | 57 | 58 | class HTTPError(Response, Exception): 59 | def __init__(self, body: str, status: int = ..., headers: Dict = ..., 60 | charset: str = ...) -> None: ... 61 | -------------------------------------------------------------------------------- /kobin/routes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Routing 3 | ======= 4 | 5 | Kobin's routing system may be slightly distinctive. 6 | 7 | Rule Syntax 8 | ----------- 9 | 10 | Kobin use decorator based URL dispatch. 11 | 12 | * Dynamic convert URL variables from Type Hints. 13 | 14 | .. code-block:: python 15 | 16 | from kobin import Kobin, Response, RedirectResponse 17 | app = Kobin() 18 | 19 | @app.route('/') 20 | def index() -> Response: 21 | return Response('Hello World') 22 | 23 | @app.route('/users/{user_id}') 24 | def index(user_id: str) -> Response: 25 | return Response('User List') 26 | 27 | 28 | Reverse Routing 29 | --------------- 30 | 31 | `app.router.reverse` function returns URL. 32 | The usage is like this: 33 | 34 | .. code-block:: python 35 | 36 | from kobin import Kobin, Response 37 | app = Kobin() 38 | 39 | @app.route('/', 'top-page') 40 | def index() -> Response: 41 | return Response('Hello World') 42 | 43 | @app.route('/users/{user_id}', 'user-detail') 44 | def user_detail(user_id: int) -> Response: 45 | return Response('Hello User{}'.format(user_id)) 46 | 47 | print(app.router.reverse('top-page')) 48 | # http://hostname/ 49 | 50 | print(app.router.reverse('user-detail', user_id=1)) 51 | # http://hostname/users/1 52 | 53 | 54 | Reverse Routing and Redirecting 55 | ------------------------------- 56 | 57 | :class:`RedirectResponse` 58 | The usage is like this: 59 | 60 | .. code-block:: python 61 | 62 | from kobin import Kobin, Response, RedirectResponse 63 | app = Kobin() 64 | 65 | @app.route('/', 'top-page') 66 | def index() -> Response: 67 | return Response('Hello World') 68 | 69 | @app.route('/404') 70 | def user_detail() -> Response: 71 | top_url = app.router.reverse('top-page') 72 | return RedirectResponse(top_url) 73 | 74 | """ 75 | from typing import get_type_hints 76 | from .responses import HTTPError 77 | 78 | 79 | def split_by_slash(path): 80 | stripped_path = path.lstrip('/').rstrip('/') 81 | return stripped_path.split('/') 82 | 83 | 84 | def match_url_vars_type(url_vars, type_hints): 85 | """ Match types of url vars. 86 | 87 | >>> match_url_vars_type({'user_id': '1'}, {'user_id': int}) 88 | (True, {'user_id': 1}) 89 | >>> match_url_vars_type({'user_id': 'foo'}, {'user_id': int}) 90 | (False, {}) 91 | """ 92 | typed_url_vars = {} 93 | try: 94 | for k, v in url_vars.items(): 95 | arg_type = type_hints.get(k) 96 | if arg_type and arg_type != str: 97 | typed_url_vars[k] = arg_type(v) 98 | else: 99 | typed_url_vars[k] = v 100 | except ValueError: 101 | return False, {} 102 | return True, typed_url_vars 103 | 104 | 105 | def match_path(rule, path): 106 | """ Match path. 107 | 108 | >>> match_path('/foo', '/foo') 109 | (True, {}) 110 | >>> match_path('/foo', '/bar') 111 | (False, {}) 112 | >>> match_path('/users/{user_id}', '/users/1') 113 | (True, {'user_id': '1'}) 114 | >>> match_path('/users/{user_id}', '/users/not-integer') 115 | (True, {'user_id': 'not-integer'}) 116 | """ 117 | split_rule = split_by_slash(rule) 118 | split_path = split_by_slash(path) 119 | url_vars = {} 120 | 121 | if len(split_rule) != len(split_path): 122 | return False, {} 123 | 124 | for r, p in zip(split_rule, split_path): 125 | if r.startswith('{') and r.endswith('}'): 126 | url_vars[r[1:-1]] = p 127 | continue 128 | if r != p: 129 | return False, {} 130 | return True, url_vars 131 | 132 | 133 | class Router: 134 | def __init__(self) -> None: 135 | self.endpoints = [] 136 | 137 | def match(self, path, method): 138 | """ Get callback and url_vars. 139 | 140 | >>> from kobin import Response 141 | >>> r = Router() 142 | >>> def view(user_id: int) -> Response: 143 | ... return Response(f'You are {user_id}') 144 | ... 145 | >>> r.add('/users/{user_id}', 'GET', 'user-detail', view) 146 | 147 | >>> callback, url_vars = r.match('/users/1', 'GET') 148 | >>> url_vars 149 | {'user_id': 1} 150 | >>> response = callback(**url_vars) 151 | >>> response.body 152 | [b'You are 1'] 153 | 154 | >>> callback, url_vars = r.match('/notfound', 'GET') 155 | Traceback (most recent call last): 156 | ... 157 | kobin.responses.HTTPError 158 | """ 159 | if path != '/': 160 | path = path.rstrip('/') 161 | method = method.upper() 162 | 163 | status = 404 164 | for p, n, m in self.endpoints: 165 | matched, url_vars = match_path(p, path) 166 | if not matched: # path: not matched 167 | continue 168 | 169 | if method not in m: # path: matched, method: not matched 170 | status = 405 171 | raise HTTPError(status=status, body=f'Method not found: {path} {method}') # it has security issue?? 172 | 173 | callback, type_hints = m[method] 174 | type_matched, typed_url_vars = match_url_vars_type(url_vars, type_hints) 175 | if not type_matched: 176 | continue # path: not matched (types are different) 177 | return callback, typed_url_vars 178 | raise HTTPError(status=status, body=f'Not found: {path}') 179 | 180 | def add(self, rule, method, name, callback): 181 | """ Add a new rule or replace the target for an existing rule. 182 | 183 | >>> from kobin import Response 184 | >>> r = Router() 185 | >>> def view(user_id: int) -> Response: 186 | ... return Response(f'You are {user_id}') 187 | ... 188 | >>> r.add('/users/{user_id}', 'GET', 'user-detail', view) 189 | >>> path, name, methods = r.endpoints[0] 190 | >>> path 191 | '/users/{user_id}' 192 | >>> name 193 | 'user-detail' 194 | >>> callback, type_hints = methods['GET'] 195 | >>> view == callback 196 | True 197 | >>> type_hints['user_id'] == int 198 | True 199 | """ 200 | if rule != '/': 201 | rule = rule.rstrip('/') 202 | method = method.upper() 203 | 204 | for i, e in enumerate(self.endpoints): 205 | r, n, callbacks = e 206 | if r == rule: 207 | assert name == n and n is not None, ( 208 | "A same path should set a same name for reverse routing." 209 | ) 210 | callbacks[method] = (callback, get_type_hints(callback)) 211 | self.endpoints[i] = (r, name, callbacks) 212 | break 213 | else: 214 | e = (rule, name, {method: (callback, get_type_hints(callback))}) 215 | self.endpoints.append(e) 216 | 217 | def reverse(self, name, **kwargs): 218 | """ Reverse routing. 219 | 220 | >>> from kobin import Response 221 | >>> r = Router() 222 | >>> def view(user_id: int) -> Response: 223 | ... return Response(f'You are {user_id}') 224 | ... 225 | >>> r.add('/users/{user_id}', 'GET', 'user-detail', view) 226 | >>> r.reverse('user-detail', user_id=1) 227 | '/users/1' 228 | """ 229 | for p, n, _ in self.endpoints: 230 | if name == n: 231 | return p.format(**kwargs) 232 | -------------------------------------------------------------------------------- /kobin/routes.pyi: -------------------------------------------------------------------------------- 1 | from typing import Callable, Dict, List, Tuple, Union, Any 2 | 3 | from .responses import BaseResponse 4 | 5 | ViewFunction = Callable[..., BaseResponse] 6 | 7 | def split_by_slash(path: str) -> List[str]: ... 8 | def match_url_vars_type(url_vars: Dict[str, str], 9 | type_hints: Dict[str, Any]) -> Tuple[bool, Dict[str, Any]]: ... 10 | def match_path(rule: str, path: str) -> Tuple[bool, Dict[str, str]]: ... 11 | 12 | class Router: 13 | endpoints: Tuple[str, Dict[str, ViewFunction], Dict[str, Any]] 14 | def __init__(self) -> None: ... 15 | def match(self, path: str, method: str) -> Tuple[ViewFunction, Dict[str, Any]]: ... 16 | def add(self, rule: str, method: str, name: str, callback: ViewFunction) -> None: ... 17 | def reverse(self, name: str, **kwargs: Any) -> str: ... 18 | -------------------------------------------------------------------------------- /requirements/constraints.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.12 2 | appnope==0.1.0 3 | atomicwrites==1.3.0 4 | attrs==19.1.0 5 | Babel==2.7.0 6 | backcall==0.1.0 7 | certifi==2019.3.9 8 | chardet==3.0.4 9 | click==7.0 10 | coverage==4.5.3 11 | coveralls==1.7.0 12 | decorator==4.4.0 13 | docopt==0.6.2 14 | docutils==0.14 15 | flake8==3.7.7 16 | freezegun==0.3.11 17 | idna==2.8 18 | imagesize==1.1.0 19 | ipdb==0.12 20 | ipython==7.5.0 21 | ipython-genutils==0.2.0 22 | jedi==0.13.3 23 | Jinja2==2.10.1 24 | kobin==0.1.10 25 | MarkupSafe==1.1.1 26 | mccabe==0.6.1 27 | more-itertools==7.0.0 28 | mypy==0.701 29 | mypy-extensions==0.4.1 30 | packaging==19.0 31 | parso==0.4.0 32 | pexpect==4.7.0 33 | pickleshare==0.7.5 34 | pkginfo==1.5.0.1 35 | pluggy==0.11.0 36 | prompt-toolkit==2.0.9 37 | ptyprocess==0.6.0 38 | py==1.8.0 39 | pycodestyle==2.5.0 40 | pyflakes==2.1.1 41 | Pygments==2.4.1 42 | pyparsing==2.4.0 43 | pytest==4.5.0 44 | python-dateutil==2.8.0 45 | pytz==2019.1 46 | requests==2.22.0 47 | requests-toolbelt==0.9.1 48 | simplegeneric==0.8.1 49 | six==1.12.0 50 | snowballstemmer==1.2.1 51 | solar-theme==1.3.3 52 | Sphinx==2.0.1 53 | sphinx-intl==1.0.0 54 | sphinxcontrib-websupport==1.1.2 55 | toml==0.10.0 56 | tox==3.12.1 57 | tqdm==4.32.1 58 | traitlets==4.3.2 59 | twine==1.13.0 60 | typed-ast==1.3.5 61 | urllib3==1.25.3 62 | virtualenv==16.6.0 63 | wcwidth==0.1.7 64 | wsgi-lineprof==0.7.0 65 | wsgi-static-middleware==0.1.2 66 | wsgicli==0.4.0 67 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | # for running 2 | wsgicli 3 | jinja2 4 | 5 | # for packaging 6 | twine 7 | wheel 8 | 9 | # for debuging 10 | ipython 11 | ipdb 12 | 13 | # for documentation 14 | sphinx 15 | sphinx-intl 16 | solar-theme 17 | 18 | # for testing 19 | flake8 20 | mypy 21 | mypy_extensions 22 | pytest 23 | tox 24 | freezegun 25 | -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | # Requirements file for read the docs 2 | sphinx 3 | kobin 4 | solar-theme 5 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | # Requirements file for tox 2 | jinja2 3 | pytest 4 | freezegun 5 | coveralls 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | exclude = 4 | .tox/, 5 | venv/, 6 | .eggs, 7 | build/, 8 | dist/, 9 | */__init__.py, 10 | docs/, 11 | example/ 12 | 13 | [tool:pytest] 14 | testpaths = tests 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from setuptools import setup, find_packages 4 | from setuptools.command.test import test as TestCommand 5 | 6 | BASE_PATH = os.path.abspath(os.path.dirname(__file__)) 7 | README = open(os.path.join(BASE_PATH, 'README.rst')).read() 8 | 9 | __version__ = '0.1.10' 10 | __author__ = 'Masashi Shibata ' 11 | __author_email__ = 'contact@c-bata.link' 12 | __license__ = 'MIT License' 13 | __classifiers__ = ( 14 | 'Development Status :: 2 - Pre-Alpha', 15 | 'Intended Audience :: Developers', 16 | 'License :: OSI Approved :: MIT License', 17 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries', 18 | 'Topic :: Internet :: WWW/HTTP :: HTTP Servers', 19 | 'Topic :: Internet :: WWW/HTTP :: WSGI', 20 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', 21 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware', 22 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Server', 23 | 'Topic :: Software Development :: Libraries :: Application Frameworks', 24 | 'Programming Language :: Python', 25 | 'Programming Language :: Python :: 3', 26 | 'Programming Language :: Python :: 3.6', 27 | 'Programming Language :: Python :: 3.7', 28 | 'Programming Language :: Python :: 3 :: Only', 29 | ) 30 | 31 | 32 | class PyTest(TestCommand): 33 | user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")] 34 | 35 | def initialize_options(self): 36 | TestCommand.initialize_options(self) 37 | self.pytest_args = [] 38 | 39 | def finalize_options(self): 40 | TestCommand.finalize_options(self) 41 | self.test_args = [] 42 | self.test_suite = True 43 | 44 | def run_tests(self): 45 | # import here, cause outside the eggs aren't loaded 46 | import pytest 47 | errno = pytest.main(self.pytest_args) 48 | sys.exit(errno) 49 | 50 | 51 | setup( 52 | name='kobin', 53 | version=__version__, 54 | author=__author__, 55 | author_email=__author_email__, 56 | url='https://github.com/kobinpy/kobin', 57 | description='Type Hints friendly WSGI Framework for Python3.', 58 | long_description=README, 59 | classifiers=__classifiers__, 60 | packages=find_packages(exclude=['test*']), 61 | install_requires=[], 62 | keywords='web framework wsgi', 63 | license=__license__, 64 | include_package_data=True, 65 | test_suite='tests', 66 | tests_require=['pytest'], 67 | cmdclass={'test': PyTest}, 68 | ) 69 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kobinpy/kobin/f87a09d071cf8246d3af700581943e881866bf5a/tests/__init__.py -------------------------------------------------------------------------------- /tests/dummy_config.py: -------------------------------------------------------------------------------- 1 | UPPER_CASE = 1 2 | lower_case = 0 3 | -------------------------------------------------------------------------------- /tests/templates/jinja2.html: -------------------------------------------------------------------------------- 1 | Hello {{ var }} World. 2 | -------------------------------------------------------------------------------- /tests/templates/this_is_not_file/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kobinpy/kobin/f87a09d071cf8246d3af700581943e881866bf5a/tests/templates/this_is_not_file/.gitkeep -------------------------------------------------------------------------------- /tests/test_apps.py: -------------------------------------------------------------------------------- 1 | import os 2 | from unittest import TestCase 3 | from unittest.mock import patch 4 | from kobin import ( 5 | Kobin, Response, 6 | load_config_from_module, load_config_from_pyfile 7 | ) 8 | from kobin.app import ( 9 | template_router_reverse, load_jinja2_env, load_config, 10 | _get_exception_message 11 | ) 12 | 13 | 14 | class KobinTests(TestCase): 15 | def setUp(self): 16 | self.app = Kobin() 17 | self.dummy_start_response = lambda x, y: None 18 | 19 | @self.app.route('/') 20 | def dummy_func(): 21 | return Response('hello') 22 | 23 | @self.app.route('/test/{typed_id}') 24 | def typed_url_var(typed_id: int): 25 | body = "type: {}, value: {}".format(type(typed_id), typed_id) 26 | return Response(body) 27 | 28 | @self.app.route('/test/raise500') 29 | def raise500(typed_id: int): 30 | 1 / 0 31 | return Response("Don't reach here") 32 | 33 | def test_route(self): 34 | test_env = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'} 35 | response = self.app._handle(test_env) 36 | actual = response.body 37 | expected = [b'hello'] 38 | self.assertEqual(actual, expected) 39 | 40 | def test_typed_url_var(self): 41 | test_env = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/test/10'} 42 | response = self.app._handle(test_env) 43 | actual = response.body 44 | expected = [b"type: , value: 10"] 45 | self.assertEqual(actual, expected) 46 | 47 | def test_404_not_found(self): 48 | test_env = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/this_is_not_found'} 49 | response = self.app._handle(test_env) 50 | actual = response._status_code 51 | expected = 404 52 | self.assertEqual(actual, expected) 53 | 54 | def test_404_when_cast_error(self): 55 | test_env = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/test/not-integer'} 56 | response = self.app._handle(test_env) 57 | actual = response._status_code 58 | expected = 404 59 | self.assertEqual(actual, expected) 60 | 61 | def test_response_status_when_500_raised(self): 62 | test_env = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/test/raise500'} 63 | response = self.app._handle(test_env) 64 | actual = response._status_code 65 | expected = 500 66 | self.assertEqual(actual, expected) 67 | 68 | def test_response_body_when_500_raised_and_enable_debugging(self): 69 | self.app.config['DEBUG'] = True 70 | test_env = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/test/raise500'} 71 | response = self.app._handle(test_env) 72 | actual = response.body 73 | expected = [b'Internal Server Error'] 74 | self.assertNotEqual(actual, expected) 75 | 76 | def test_response_body_when_500_raised_and_disable_debugging(self): 77 | test_env = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/test/raise500'} 78 | response = self.app._handle(test_env) 79 | actual = response.body 80 | expected = [b'Internal Server Error'] 81 | self.assertEqual(actual, expected) 82 | 83 | def test_handled_body_message_when_404_not_found(self): 84 | test_env = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/this_is_not_found'} 85 | response = self.app._handle(test_env) 86 | actual = response.body 87 | expected = [b"Not found: /this_is_not_found"] 88 | self.assertEqual(actual, expected) 89 | 90 | def test_wsgi(self): 91 | test_env = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'} 92 | actual = self.app(test_env, self.dummy_start_response) 93 | expected = [b'hello'] 94 | self.assertEqual(actual, expected) 95 | 96 | 97 | class KobinHookTests(TestCase): 98 | def setUp(self): 99 | self.app = Kobin() 100 | self.dummy_start_response = lambda x, y: None 101 | self.before_counter = 0 102 | 103 | @self.app.route('/') 104 | def dummy_func(): 105 | return Response('hello') 106 | 107 | @self.app.before_request 108 | def before(): 109 | self.before_counter += 1 110 | 111 | @self.app.before_request 112 | def before2(): 113 | self.before_counter += 1 114 | 115 | @self.app.after_request 116 | def after(response): 117 | response.headers.add_header('Foo1', 'Bar1') 118 | return response 119 | 120 | @self.app.after_request 121 | def after2(response): 122 | response.headers.add_header('Foo2', 'Bar2') 123 | return response 124 | 125 | def test_before_request(self): 126 | test_env = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'} 127 | self.app.before_counter = 0 128 | self.app._handle(test_env) 129 | self.assertEqual(self.before_counter, 2) 130 | 131 | def test_after_request(self): 132 | test_env = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'} 133 | response = self.app._handle(test_env) 134 | self.assertIn(('Foo1', 'Bar1'), response.headerlist) 135 | self.assertIn(('Foo2', 'Bar2'), response.headerlist) 136 | 137 | 138 | class HandleUnexpectedExceptionTests(TestCase): 139 | def test_get_exception_message_when_debugging(self): 140 | try: 141 | 1 / 0 142 | except BaseException as e: 143 | actual = _get_exception_message(e, True) 144 | else: 145 | actual = "Exception is not raised." 146 | cause_of_exception = '1 / 0' 147 | self.assertIn(cause_of_exception, actual) 148 | 149 | def test_get_exception_message_when_not_debugging(self): 150 | try: 151 | 1 / 0 152 | except BaseException as e: 153 | actual = _get_exception_message(e, False) 154 | else: 155 | actual = "Exception is not raised." 156 | expected = 'Internal Server Error' 157 | self.assertEqual(actual, expected) 158 | 159 | 160 | class KobinAfterHookTests(TestCase): 161 | def setUp(self): 162 | self.app = Kobin() 163 | self.dummy_start_response = lambda x, y: None 164 | self.before_counter = 0 165 | 166 | @self.app.route('/') 167 | def dummy_func(): 168 | return Response('hello') 169 | 170 | @self.app.after_request 171 | def after_do_not_return_response(response): 172 | pass 173 | 174 | def test_after_request(self): 175 | test_env = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'} 176 | response = self.app._handle(test_env) 177 | self.assertEqual('200 OK', response.status) 178 | 179 | 180 | class KobinFrozenTests(TestCase): 181 | def setUp(self): 182 | self.app = Kobin(config={'DEBUG': True}) 183 | self.before_counter = 0 184 | 185 | @self.app.route('/') 186 | def dummy_func(): 187 | return Response('hello') 188 | 189 | def test_can_change_state_before_running(self): 190 | test_env = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'} 191 | 192 | def dummy_start_response(s, h): 193 | pass 194 | 195 | self.app(test_env, dummy_start_response) 196 | self.app.config = {'foo': 'bar'} 197 | self.assertNotIn('foo', self.app.config) 198 | 199 | 200 | class ConfigTests(TestCase): 201 | def setUp(self): 202 | self.base_path = os.path.dirname(os.path.abspath(__file__)) 203 | 204 | @patch('kobin.app._current_app') 205 | def test_lazy_reverse_router(self, app_mock): 206 | app = Kobin() 207 | 208 | @app.route('/', 'GET', 'top') 209 | def top(): 210 | return Response('Hello') 211 | 212 | app_mock.return_value = app 213 | actual = template_router_reverse('top') 214 | 215 | expected = '/' 216 | self.assertEqual(actual, expected) 217 | 218 | @patch('kobin.app._current_app') 219 | def test_lazy_reverse_router_not_found(self, app_mock): 220 | app = Kobin() 221 | app_mock.return_value = app 222 | actual = template_router_reverse('top') 223 | expected = '' 224 | self.assertEqual(actual, expected) 225 | 226 | def test_load_jinja2_env_with_globals(self): 227 | env = load_jinja2_env('.', global_variables={'foo': 'bar'}) 228 | self.assertEqual('bar', env.globals['foo']) 229 | 230 | def test_load_jinja2_env_with_filters(self): 231 | def foo_filter(x): 232 | return x * 2 233 | 234 | env = load_jinja2_env('.', global_filters={'foo': foo_filter}) 235 | self.assertEqual(foo_filter, env.filters['foo']) 236 | 237 | def test_constructor(self): 238 | config = load_config() 239 | self.assertIn('DEBUG', config.keys()) 240 | 241 | def test_load_from_module(self): 242 | from tests import dummy_config 243 | config = load_config_from_module(dummy_config) 244 | self.assertIn('UPPER_CASE', config) 245 | 246 | def test_load_from_pyfile(self): 247 | dummy_config = os.path.join(self.base_path, 'dummy_config.py') 248 | config = load_config_from_pyfile(dummy_config) 249 | self.assertIn('UPPER_CASE', config) 250 | 251 | def test_config_has_not_lower_case_variable(self): 252 | dummy_config = os.path.join(self.base_path, 'dummy_config.py') 253 | config = load_config_from_pyfile(dummy_config) 254 | self.assertNotIn('lower_case', config) 255 | 256 | def test_failure_for_loading_config(self): 257 | dummy_config = os.path.join(self.base_path, 'no_exists.py') 258 | self.assertRaises(FileNotFoundError, load_config_from_pyfile, dummy_config) 259 | -------------------------------------------------------------------------------- /tests/test_requests.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta, datetime 2 | import freezegun 3 | import os 4 | from unittest import TestCase 5 | from unittest.mock import MagicMock, patch 6 | 7 | from kobin.requests import ( 8 | Request, _split_into_mimetype_and_priority, _parse_and_sort_accept_header, accept_best_match 9 | ) 10 | from kobin.responses import BaseResponse 11 | 12 | TEMPLATE_DIRS = [os.path.join(os.path.dirname(__file__), 'templates')] 13 | 14 | 15 | class RequestTests(TestCase): 16 | def test_initialized(self): 17 | env = {'hoge': 'HOGE'} 18 | request = Request(env) 19 | self.assertEqual(request['hoge'], 'HOGE') 20 | 21 | def test_get(self): 22 | request = Request({'hoge': 'HOGE'}) 23 | self.assertEqual(request.get('hoge'), 'HOGE') 24 | 25 | def test_getitem(self): 26 | request = Request({'hoge': 'HOGE'}) 27 | self.assertEqual(request['hoge'], 'HOGE') 28 | 29 | def test_get_default_value(self): 30 | request = Request({}) 31 | self.assertEqual(request.get('hoge', 'HOGE'), 'HOGE') 32 | 33 | def test_path_property(self): 34 | request = Request({'PATH_INFO': '/hoge'}) 35 | self.assertEqual(request.path, '/hoge') 36 | 37 | def test_path_property_stripped_last_slash(self): 38 | request = Request({'PATH_INFO': 'hoge'}) 39 | self.assertEqual(request.path, '/hoge') 40 | 41 | def test_method_name_to_uppercase(self): 42 | self.assertEqual(Request({'REQUEST_METHOD': 'get'}).method, 'GET') 43 | self.assertEqual(Request({'REQUEST_METHOD': 'Post'}).method, 'POST') 44 | 45 | def test_POST_a_parameter(self): 46 | wsgi_input_mock = MagicMock() 47 | wsgi_input_mock.read.return_value = b'key1=value1' 48 | 49 | request = Request({ 50 | 'REQUEST_METHOD': 'POST', 51 | 'QUERY_STRING': '', 52 | 'wsgi.input': wsgi_input_mock, 53 | 'CONTENT_TYPE': 'application/x-www-form-urlencoded', 54 | 'CONTENT_LENGTH': len(b'key1=value1'), 55 | }) 56 | 57 | self.assertEqual(request.forms['key1'], 'value1') 58 | 59 | def test_POST_parameters(self): 60 | wsgi_input_mock = MagicMock() 61 | wsgi_input_mock.read.return_value = b'key1=value1&key2=value2' 62 | 63 | request = Request({ 64 | 'REQUEST_METHOD': 'POST', 65 | 'QUERY_STRING': '', 66 | 'wsgi.input': wsgi_input_mock, 67 | 'CONTENT_TYPE': 'application/x-www-form-urlencoded', 68 | 'CONTENT_LENGTH': len(b'key1=value1&key2=value2'), 69 | }) 70 | 71 | self.assertEqual(request.forms['key1'], 'value1') 72 | self.assertEqual(request.forms['key2'], 'value2') 73 | 74 | def test_GET_a_parameter(self): 75 | request = Request({ 76 | 'REQUEST_METHOD': 'GET', 77 | 'QUERY_STRING': 'key1=value1', 78 | 'CONTENT_TYPE': 'text/plain', 79 | 'CONTENT_LENGTH': '', 80 | }) 81 | self.assertEqual(request.query['key1'], 'value1') 82 | 83 | def test_GET_parameters(self): 84 | request = Request({ 85 | 'REQUEST_METHOD': 'GET', 86 | 'QUERY_STRING': 'key1=value1&key2=value2', 87 | 'CONTENT_TYPE': 'text/plain', 88 | 'CONTENT_LENGTH': '', 89 | }) 90 | self.assertEqual(request.query['key1'], 'value1') 91 | self.assertEqual(request.query['key2'], 'value2') 92 | 93 | def test_raw_body(self): 94 | wsgi_input_mock = MagicMock() 95 | wsgi_input_mock.read.return_value = b'{"key1": "value1"}' 96 | request = Request({ 97 | 'REQUEST_METHOD': 'POST', 98 | 'QUERY_STRING': '', 99 | 'wsgi.input': wsgi_input_mock, 100 | 'CONTENT_TYPE': 'application/json', 101 | 'CONTENT_LENGTH': len(b'{"key1": "value1"}'), 102 | }) 103 | self.assertEqual(request.raw_body, b'{"key1": "value1"}') 104 | 105 | def test_raw_body_with_empty_string_content_length(self): 106 | wsgi_input_mock = MagicMock() 107 | wsgi_input_mock.read.return_value = b'' 108 | request = Request({ 109 | 'REQUEST_METHOD': 'POST', 110 | 'QUERY_STRING': '', 111 | 'wsgi.input': wsgi_input_mock, 112 | 'CONTENT_TYPE': 'text/plain', 113 | 'CONTENT_LENGTH': '', 114 | }) 115 | self.assertEqual(request.raw_body, b'') 116 | 117 | def test_body(self): 118 | wsgi_input_mock = MagicMock() 119 | wsgi_input_mock.read.return_value = b'{"key1": "value1"}' 120 | request = Request({ 121 | 'REQUEST_METHOD': 'POST', 122 | 'QUERY_STRING': '', 123 | 'wsgi.input': wsgi_input_mock, 124 | 'CONTENT_TYPE': 'application/json', 125 | 'CONTENT_LENGTH': len(b'{"key1": "value1"}'), 126 | }) 127 | self.assertEqual(request.body, '{"key1": "value1"}') 128 | 129 | def test_json(self): 130 | wsgi_input_mock = MagicMock() 131 | wsgi_input_mock.read.return_value = b'{"key1": "value1"}' 132 | request = Request({ 133 | 'REQUEST_METHOD': 'POST', 134 | 'QUERY_STRING': '', 135 | 'wsgi.input': wsgi_input_mock, 136 | 'CONTENT_TYPE': 'application/json', 137 | 'CONTENT_LENGTH': len(b'{"key1": "value1"}'), 138 | }) 139 | self.assertEqual(request.json["key1"], "value1") 140 | 141 | def test_url(self): 142 | request = Request({ 143 | 'HTTP_X_FORWARDED_PROTO': 'http', 144 | 'QUERY_STRING': 'key1=value1&key2=value2', 145 | 'HTTP_X_FORWARDED_HOST': 'localhost', 146 | 'PATH_INFO': '/hoge', 147 | }) 148 | actual = request.url 149 | self.assertEqual(actual, "http://localhost/hoge?key1=value1&key2=value2") 150 | 151 | def test_headers(self): 152 | request = Request({'HTTP_FOO': 'Bar', 'QUERY_STRING': 'key1=value1'}) 153 | self.assertEqual(request.headers['FOO'], 'Bar') 154 | 155 | 156 | class AcceptBestMatchTests(TestCase): 157 | def test_split_into_mimetype_and_priority_without_priority(self): 158 | item = 'text/*' 159 | actual = _split_into_mimetype_and_priority(item) 160 | expected = ('text/*', 1.0) 161 | self.assertEqual(actual, expected) 162 | 163 | def test_split_into_mimetype_and_priority_with_priority(self): 164 | item = 'application/json;q=0.5' 165 | actual = _split_into_mimetype_and_priority(item) 166 | expected = ('application/json', 0.5) 167 | self.assertEqual(actual, expected) 168 | 169 | def test_parse_and_sort_accept_header(self): 170 | accept_header = 'application/json;q=0.5, text/html' 171 | actual = _parse_and_sort_accept_header(accept_header) 172 | expected = [ 173 | ('text/html', 1.0), 174 | ('application/json', 0.5) 175 | ] 176 | self.assertEqual(actual, expected) 177 | 178 | def test_best_match_without_priority(self): 179 | accept_header = 'application/json, application/xml' 180 | expected = 'application/json' 181 | actual = accept_best_match(accept_header, ['application/json']) 182 | self.assertEqual(actual, expected) 183 | 184 | def test_best_match_with_priority(self): 185 | accept_header = 'text/*;q=0.9, */;q=0.1, audio/mpeg, application/xml;q=0.' 186 | expected = 'application/json' 187 | actual = accept_best_match(accept_header, ['application/json']) 188 | self.assertEqual(actual, expected) 189 | 190 | def test_best_match_with_priority_and_wildcard(self): 191 | accept_header = 'application/json;q=0.5, text/*, */*;q=0.1' 192 | actual = accept_best_match(accept_header, ['application/json', 'text/plain']) 193 | expected = 'text/plain' 194 | self.assertEqual(actual, expected) 195 | 196 | 197 | class CookieTests(TestCase): 198 | # Set Cookie Tests in BaseResponse Class 199 | def test_set_cookie(self): 200 | response = BaseResponse() 201 | response.set_cookie('foo', 'bar') 202 | expected_set_cookie = ('Set-Cookie', 'foo=bar; Path=/') 203 | self.assertIn(expected_set_cookie, response.headerlist) 204 | 205 | def test_set_cookie_with_max_age(self): 206 | response = BaseResponse() 207 | response.set_cookie('foo', 'bar', max_age=timedelta(seconds=10), path=None) 208 | expected_set_cookie = ('Set-Cookie', 'foo=bar; Max-Age=10') 209 | self.assertIn(expected_set_cookie, response.headerlist) 210 | 211 | def test_set_cookie_with_expires(self): 212 | response = BaseResponse() 213 | response.set_cookie('foo', 'bar', expires=datetime(2017, 1, 1, 0, 0, 0), path=None) 214 | expected_set_cookie = ('Set-Cookie', 'foo=bar; expires=Sun, 01 Jan 2017 00:00:00 GMT') 215 | self.assertIn(expected_set_cookie, response.headerlist) 216 | 217 | def test_set_cookie_with_path(self): 218 | response = BaseResponse() 219 | response.set_cookie('foo', 'bar', path='/foo') 220 | expected_set_cookie = ('Set-Cookie', 'foo=bar; Path=/foo') 221 | self.assertIn(expected_set_cookie, response.headerlist) 222 | 223 | # Get Cookie Tests in Request Class 224 | def test_cookies_property_has_nothing(self): 225 | request = Request({}) 226 | self.assertEqual(len(request.cookies), 0) 227 | 228 | def test_cookies_property_has_an_item(self): 229 | request = Request({'HTTP_COOKIE': 'foo="bar"'}) 230 | self.assertEqual(len(request.cookies), 1) 231 | 232 | def test_get_cookie(self): 233 | request = Request({'HTTP_COOKIE': 'foo="bar"'}) 234 | actual = request.get_cookie("foo") 235 | expected = 'bar' 236 | self.assertEqual(actual, expected) 237 | 238 | # Delete Cookie Tests in Request Class 239 | @freezegun.freeze_time('2017-01-01 00:00:00') 240 | def test_delete_cookie(self): 241 | response = BaseResponse() 242 | response.delete_cookie('foo') 243 | expected_set_cookie = ( 244 | 'Set-Cookie', 245 | 'foo=""; expires=Sun, 01 Jan 2017 00:00:00 GMT; Max-Age=-1; Path=/') 246 | self.assertIn(expected_set_cookie, response.headerlist) 247 | 248 | # Get and Set Cookie Tests with secret 249 | def test_set_cookie_with_secret(self): 250 | response = BaseResponse() 251 | response.set_cookie('foo', 'bar', secret='secretkey', path=None) 252 | expected_set_cookie = ('Set-Cookie', 'foo="!VzhGFLGcW+5OMs1s4beLXaqFxAUwgHdWkH5fgapghoI=' 253 | '?gASVDwAAAAAAAACMA2Zvb5SMA2JhcpSGlC4="') 254 | self.assertIn(expected_set_cookie, response.headerlist) 255 | 256 | def test_get_cookie_with_secret(self): 257 | request = Request({'HTTP_COOKIE': 'foo="!VzhGFLGcW+5OMs1s4beLXaqFxAUwgHdWkH5fgapghoI=' 258 | '?gASVDwAAAAAAAACMA2Zvb5SMA2JhcpSGlC4="'}) 259 | actual = request.get_cookie("foo", secret='secretkey') 260 | expected = 'bar' 261 | self.assertEqual(actual, expected) 262 | 263 | @patch('kobin.app.current_config') 264 | def test_set_cookie_with_secret_in_config(self, mock_current_config): 265 | mock_current_config.return_value = "secretkey" 266 | response = BaseResponse() 267 | response.set_cookie('foo', 'bar', path=None) 268 | expected_set_cookie = ('Set-Cookie', 'foo="!VzhGFLGcW+5OMs1s4beLXaqFxAUwgHdWkH5fgapghoI=' 269 | '?gASVDwAAAAAAAACMA2Zvb5SMA2JhcpSGlC4="') 270 | self.assertIn(expected_set_cookie, response.headerlist) 271 | -------------------------------------------------------------------------------- /tests/test_responses.py: -------------------------------------------------------------------------------- 1 | import os 2 | from unittest import TestCase 3 | from unittest.mock import patch 4 | 5 | from kobin import Kobin, load_config 6 | from kobin.responses import ( 7 | BaseResponse, Response, JSONResponse, TemplateResponse, RedirectResponse, 8 | ) 9 | 10 | TEMPLATE_DIRS = [os.path.join(os.path.dirname(__file__), 'templates')] 11 | 12 | 13 | class BaseResponseTests(TestCase): 14 | def test_constructor_body(self): 15 | response = BaseResponse([b'Body']) 16 | self.assertEqual([b'Body'], response.body) 17 | 18 | def test_constructor_status(self): 19 | response = BaseResponse([b'Body'], 200) 20 | self.assertEqual(response.status_code, 200) 21 | 22 | def test_set_status(self): 23 | response = BaseResponse() 24 | response.status = 200 25 | self.assertEqual(response.status, '200 OK') 26 | 27 | def test_set_invalid_status(self): 28 | response = BaseResponse() 29 | 30 | def set_status(status): 31 | response.status = status 32 | 33 | self.assertRaises(ValueError, set_status, -1) 34 | 35 | def test_constructor_headerlist(self): 36 | response = BaseResponse() 37 | expected_content_type = ('Content-Type', 'text/plain;') 38 | self.assertIn(expected_content_type, response.headerlist) 39 | 40 | def test_constructor_headerlist_has_already_content_type(self): 41 | response = BaseResponse() 42 | response.headers.add_header('Content-Type', 'application/json') 43 | expected_content_type = ('Content-Type', 'application/json') 44 | self.assertIn(expected_content_type, response.headerlist) 45 | expected_content_type = ('Content-Type', 'text/html; charset=UTF-8') 46 | self.assertNotIn(expected_content_type, response.headerlist) 47 | 48 | def test_add_header(self): 49 | response = BaseResponse() 50 | response.headers.add_header('key', 'value') 51 | self.assertIn(('key', 'value'), response.headerlist) 52 | 53 | def test_constructor_headerlist_with_add_header(self): 54 | response = BaseResponse(headers={'key1': 'value1'}) 55 | expected_content_type = ('key1', 'value1') 56 | self.assertIn(expected_content_type, response.headerlist) 57 | 58 | 59 | class ResponseTests(TestCase): 60 | def test_constructor_status(self): 61 | response = Response('Body', charset='utf-8') 62 | self.assertEqual(response.status_code, 200) 63 | 64 | def test_constructor_body_when_given_bytes(self): 65 | response = Response(b'Body') 66 | self.assertEqual([b'Body'], response.body) 67 | 68 | def test_constructor_body_when_given_str(self): 69 | response = Response('Body') 70 | self.assertEqual([b'Body'], response.body) 71 | 72 | 73 | class JSONResponseTests(TestCase): 74 | def test_constructor_status(self): 75 | response = JSONResponse({'foo': 'bar'}) 76 | self.assertEqual(response.status_code, 200) 77 | 78 | def test_constructor_headerlist(self): 79 | response = JSONResponse({'foo': 'bar'}) 80 | expected_content_type = ('Content-Type', 'application/json; charset=UTF-8') 81 | self.assertIn(expected_content_type, response.headerlist) 82 | 83 | def test_constructor_headerlist_with_add_header(self): 84 | response = JSONResponse({'foo': 'bar'}, headers={'key1': 'value1'}) 85 | expected_content_type = ('key1', 'value1') 86 | self.assertIn(expected_content_type, response.headerlist) 87 | 88 | 89 | class Jinja2TemplateTests(TestCase): 90 | @patch('kobin.app.current_config') 91 | def test_file(self, mock_config): 92 | """ Templates: Jinja2 file """ 93 | config = load_config({'TEMPLATE_DIRS': TEMPLATE_DIRS}) 94 | mock_config.return_value = config['TEMPLATE_ENVIRONMENT'] 95 | response = TemplateResponse('jinja2.html', var='kobin') 96 | actual = response.body 97 | expected = [b"Hello kobin World."] 98 | self.assertEqual(actual, expected) 99 | 100 | 101 | class RedirectResponseTests(TestCase): 102 | def test_constructor_body(self): 103 | response = RedirectResponse('/') 104 | self.assertEqual(response.body, [b'']) 105 | 106 | def test_constructor_status_when_http10(self): 107 | test_env = {'HTTP_HOST': 'localhost', 'SERVER_PROTOCOL': 'HTTP/1.0'} 108 | app = Kobin() 109 | app(test_env, lambda x, y: None) 110 | response = RedirectResponse('/') 111 | self.assertEqual(response.status_code, 302) 112 | 113 | def test_constructor_status_when_http11(self): 114 | test_env = {'HTTP_HOST': 'localhost', 'SERVER_PROTOCOL': 'HTTP/1.1'} 115 | app = Kobin() 116 | app(test_env, lambda x, y: None) 117 | response = RedirectResponse('/') 118 | self.assertEqual(response.status_code, 303) 119 | 120 | def test_constructor_headerlist_has_location(self): 121 | test_env = {'HTTP_HOST': 'localhost', 'SERVER_PROTOCOL': 'HTTP/1.1'} 122 | app = Kobin() 123 | app(test_env, lambda x, y: None) 124 | response = RedirectResponse('/hello') 125 | expected_content_type = ('Location', 'http://localhost/hello') 126 | self.assertIn(expected_content_type, response.headerlist) 127 | -------------------------------------------------------------------------------- /tests/test_routes.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from kobin.routes import Router 3 | from kobin.responses import HTTPError, Response 4 | 5 | 6 | def view_int(year: int) -> Response: 7 | return Response(f'hello {year}') 8 | 9 | 10 | def view_str(name): 11 | return Response(f'hello {name}') 12 | 13 | 14 | class RouterTests(TestCase): 15 | def test_match_dynamic_routes_with_casted_number(self): 16 | r = Router() 17 | r.add('/tests/{year}', 'GET', 'hoge', view_int) 18 | actual_target, actual_args = r.match('/tests/2015/', 'GET') 19 | self.assertEqual(actual_args, {'year': 2015}) 20 | 21 | def test_match_dynamic_routes_with_string(self): 22 | r = Router() 23 | r.add('/tests/{name}', 'GET', 'hoge', view_str) 24 | actual_target, actual_args = r.match('/tests/kobin/', 'GET') 25 | self.assertEqual(actual_args, {'name': 'kobin'}) 26 | 27 | def test_404_not_found(self): 28 | r = Router() 29 | r.add('/tests/{name}', 'GET', 'hoge', view_str) 30 | with self.assertRaises(HTTPError) as cm: 31 | r.match('/this_is_not_found', 'GET') 32 | self.assertEqual('404 Not Found', cm.exception.status) 33 | 34 | def test_405_method_not_allowed(self): 35 | r = Router() 36 | r.add('/tests/{name}', 'GET', 'hoge', view_str) 37 | with self.assertRaises(HTTPError) as cm: 38 | r.match('/tests/kobin', 'POST') 39 | self.assertEqual('405 Method Not Allowed', cm.exception.status) 40 | 41 | 42 | class ReverseRoutingTests(TestCase): 43 | def setUp(self): 44 | self.router = Router() 45 | 46 | def index() -> Response: 47 | return Response('hello world') 48 | 49 | def user_detail(user_id: int) -> Response: 50 | return Response(f'hello user{user_id}') 51 | 52 | self.router.add('/', 'GET', 'top', index) 53 | self.router.add('/users/{user_id}', 'GET', 'user-detail', user_detail) 54 | 55 | def test_reverse_route_without_url_vars(self): 56 | actual = self.router.reverse('top') 57 | expected = '/' 58 | self.assertEqual(actual, expected) 59 | 60 | def test_reverse_route_with_url_vars(self): 61 | actual = self.router.reverse('user-detail', user_id=1) 62 | expected = '/users/1' 63 | self.assertEqual(actual, expected) 64 | 65 | def test_reverse_not_match(self): 66 | actual = self.router.reverse('foobar', foo=1) 67 | expected = None 68 | self.assertEqual(actual, expected) 69 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py36 4 | py37 5 | flake8 6 | mypy 7 | doctest 8 | 9 | [tox:travis] 10 | 3.6=py36,flake8,mypy,doctest 11 | nightly=py37 12 | 13 | [testenv] 14 | basepython = python3.6 15 | setenv=PYTHONPATH = {toxinidir}:{toxinidir} 16 | deps = -rrequirements/test.txt 17 | commands = python setup.py test 18 | 19 | [testenv:doctest] 20 | deps = pytest 21 | commands = pytest --doctest-module -v kobin 22 | 23 | [testenv:py37] 24 | basepython = python3.7 25 | 26 | [testenv:flake8] 27 | deps = flake8 28 | commands = flake8 29 | 30 | [testenv:mypy] 31 | deps = 32 | mypy 33 | jinja2 34 | commands = mypy --check-untyped-defs kobin 35 | --------------------------------------------------------------------------------